Training / Serving Parity
A model is a function fitted to one feature distribution and then run against
whatever the serving path produces. When those two differ, nothing errors. The
model returns a confident, well-formed, wrong prediction, the offline metrics
stay excellent, and the gap belongs to no team: the training pipeline is
correct, the serving pipeline is correct, and the composition is broken.
The invariant, and it is the whole skill:
features_train(x, t) == features_serve(x, t) — for the same entity at
the same moment, both paths must produce the same vector. Not similar. The
same.
This is parser-differential-hunting with different vocabulary. There too, two
implementations read one input and disagree; there too, each is locally
reasonable; there too, the finding is that the component which decides is not
the component which acts. Here the deciding component is the training run
that fixed the model's weights, and the acting component is the request path
months later.
Composes with the library:
- parser-differential-hunting — the same structure, the same harness, the
same fix ladder; read it for the general method
- data-leakage-hunting — the sibling failure at the other end: that skill
catches the offline number being too good, this one catches the online
outcome being worse
- honest-degradation — the silent default in Part 4 is its archetype
- training-run-provenance — the model and its transform are one versioned
artifact, not two
- model-evaluation-discipline — the offline evaluation cannot see any of
this; Part 5 says what can
- forensic-logging-design — the serving feature vector is the telemetry
that makes all of this diagnosable
Part 1 — The four skew families
- Implementation skew. The feature is computed twice, in two languages, by
two teams, months apart — Spark SQL or pandas for training, Go or Java in
the request path. Every one of these is a real, recurring divergence:
null handling (
mean() skipping nulls offline, treating them as zero
online), rounding and float width, string normalization and case folding,
timezone and date-boundary handling, unseen-category encoding, one-hot
column order, clipping bounds, and the default that each side chose
independently for a missing value.
- Point-in-time skew. Training used a value as it looks in the warehouse —
complete, settled, backfilled. Serving sees it partial. A daily aggregate is
a full day offline and six hours online; a counter is final offline and
still incrementing online; a status field is the corrected version offline
and the provisional one online. The training row is not wrong, and it is not
reproducible at request time — which is the definition of this family.
- Pipeline-state skew. Vocabularies, embedding tables, scaler statistics,
category mappings, and bucket boundaries shipped separately from the
model. The model is redeployed and the transform is not, or the feature
store is backfilled with a newer transform than the one the model was fitted
under. Everything versions independently until one of them moves.
- Distribution skew. The serving population is not the training
population — a new integration, a new country, a new client that dominates
traffic, a seasonal shift. And the feedback loop: the model changes which
requests it later receives, so the drift is partly self-inflicted.
Families 1–3 are defects and are fixable by construction. Family 4 is a fact
about the world and is managed by monitoring and retraining. Separate them
before proposing anything, because the fixes have nothing in common.
Part 2 — Fix by construction, ranked
The point fix ("we corrected the null handling in the Go service") loses for
the same reason it loses in parser differentials: the next feature reintroduces
it. Rank the remediation:
- One implementation, called by both paths. The transform is a single
artifact — one library, one function, one version — imported by the training
job and by the serving path. This deletes families 1 and 3 outright.
- Log-and-train. Compute the feature vector once, at serving time, log
it, and train on the logged vectors. Serving becomes the source of truth and
implementation skew becomes structurally impossible. It costs a bootstrap
problem for a brand-new model and is worth it from the second model onward.
- Ship preprocessing with the model as one atomic artifact. The weights
and their transform are a single versioned unit that cannot be deployed
independently (
training-run-provenance). If they can be deployed
separately, one day they will be.
- A feature store with point-in-time correctness for family 2 — training
reads values "as of the event", never "as of now", and serving reads the
same view. If you do not have one, the substitute is to build training rows
from the logged online values.
- If two implementations genuinely must exist (a latency budget the
training stack cannot meet), then the differential harness of Part 3 is
mandatory and belongs in CI. Two implementations without a differential test
is not a tradeoff, it is an unmonitored bug.
- Only then: drift monitoring, which detects rather than prevents.
Part 3 — The differential harness
Cheap, offline, and it finds real defects on the first run in most systems.
- Take N real serving requests — sampled from production logs, including
the ugly ones: missing fields, new categories, extreme values, and the
slowest 1%.
- Replay through both paths and compare feature by feature.
- State the tolerance per type, in advance. Exact equality for categorical,
integer, boolean and encoded columns. An explicit epsilon for floats — and
epsilon is a decision to write down, not a default to inherit.
- Report per-feature mismatch rates, never an aggregate. "97% of features
match" hides that the 3% is the top-importance feature. One broken column out
of two hundred is a broken model.
- Rank mismatches by feature importance. A skewed feature the model barely
uses is a cleanup ticket; a skewed top-five feature is an incident.
- Freeze a golden set — those N requests with their expected vectors — and
run it in CI on every change to either path.
Same discipline as any differential: compare the projection that matters, and
triage each mismatch by whether it reaches the thing that acts.
Part 4 — The silent default is the failure you will actually hit
A feature service times out. The lookup returns nothing. The code fills in
0, or the training-set mean, or an empty embedding — and the model returns a
confident prediction on a vector it was never fitted on. Nothing logs an error.
The latency graph looks fine. This is the single most common way a deployed ML
system degrades, and honest-degradation names it exactly: a plausible answer
produced from data that was not there.
What to do:
- Count and export the default rate per feature. It is the highest-value
metric in ML serving and almost nobody emits it. A default rate that moves
from 0.1% to 8% is an outage, whatever the prediction distribution says.
- Decide the degraded behavior deliberately, per feature. Fail the request,
abstain, or fall back to a simpler model that does not use that feature —
each is defensible. Silently imputing and answering anyway is not.
- Make abstention a first-class output, and measure coverage alongside
accuracy (
model-evaluation-discipline Part 2).
- Log the served feature vector, sampled. Without it, a wrong prediction
cannot be reconstructed — you have the input and the output and none of the
middle. With it, every incident in this skill is diagnosable in minutes.
Part 5 — Monitor features, not just predictions
The prediction distribution is a lagging and weak signal: it moves late, and it
moves for many reasons. Watch the inputs.
Per feature, alert on: null and default rate, cardinality (new categories
appearing, old ones vanishing), range and quantile shift, and freshness — the
age of the value at the moment it was used. Compare each against the training
distribution, which means the training distribution has to be stored as an
artifact alongside the model.
And for the final honest number: evaluate on logged serving features joined
to real outcomes, not on a rebuilt offline feature set. That is the only
evaluation that has seen the serving path, and it is the one that would have
caught every family above.
Deliverable
## Parity review — <model> <version>
Feature paths: training <impl> | serving <impl> | shared code: yes/no
Transform artifact shipped with weights: yes/no
Point-in-time correctness: <how training values are made reproducible online>
### Differential run (N = <n> replayed production requests)
feature | type | tolerance | mismatch rate | importance rank | family
### Silent-default exposure
feature | default value | current default rate | degraded behavior chosen
### Fixes (ranked, structural first)
### Monitoring added: per-feature null/default/cardinality/range/freshness
### Not covered
Anti-patterns
- Two implementations of one feature and no differential test between them.
- Reporting an aggregate match rate instead of per-feature mismatches.
- Comparing floats with an inherited default tolerance nobody chose.
- Deploying the model and its preprocessing as independently versioned things.
- Building training rows from warehouse values that the request path cannot
reproduce, and calling the gap "drift".
- Imputing a failed feature lookup and answering confidently.
- Not emitting a per-feature default rate, so degradation is invisible until
someone notices the business metric.
- Monitoring only the prediction distribution.
- Never logging the served feature vector, leaving wrong predictions
permanently undiagnosable.
- Retraining to fix what is an implementation bug — it hides the skew inside
new weights and the gap comes back with the next feature.
1---2name: training-serving-parity3description: Training / Serving Parity4---56# Training / Serving Parity78A model is a function fitted to one feature distribution and then run against9whatever the serving path produces. When those two differ, nothing errors. The10model returns a confident, well-formed, wrong prediction, the offline metrics11stay excellent, and the gap belongs to no team: the training pipeline is12correct, the serving pipeline is correct, and the composition is broken.1314The invariant, and it is the whole skill:1516> **`features_train(x, t) == features_serve(x, t)`** — for the same entity at17> the same moment, both paths must produce the same vector. Not similar. The18> same.1920This is `parser-differential-hunting` with different vocabulary. There too, two21implementations read one input and disagree; there too, each is locally22reasonable; there too, the finding is that the component which *decides* is not23the component which *acts*. Here the deciding component is the training run24that fixed the model's weights, and the acting component is the request path25months later.2627Composes with the library:2829- **parser-differential-hunting** — the same structure, the same harness, the30 same fix ladder; read it for the general method31- **data-leakage-hunting** — the sibling failure at the other end: that skill32 catches the offline number being too good, this one catches the online33 outcome being worse34- **honest-degradation** — the silent default in Part 4 is its archetype35- **training-run-provenance** — the model and its transform are one versioned36 artifact, not two37- **model-evaluation-discipline** — the offline evaluation cannot see any of38 this; Part 5 says what can39- **forensic-logging-design** — the serving feature vector is the telemetry40 that makes all of this diagnosable4142---4344## Part 1 — The four skew families45461. **Implementation skew.** The feature is computed twice, in two languages, by47 two teams, months apart — Spark SQL or pandas for training, Go or Java in48 the request path. Every one of these is a real, recurring divergence:49 null handling (`mean()` skipping nulls offline, treating them as zero50 online), rounding and float width, string normalization and case folding,51 timezone and date-boundary handling, unseen-category encoding, one-hot52 column *order*, clipping bounds, and the default that each side chose53 independently for a missing value.542. **Point-in-time skew.** Training used a value as it looks in the warehouse —55 complete, settled, backfilled. Serving sees it partial. A daily aggregate is56 a full day offline and six hours online; a counter is final offline and57 still incrementing online; a status field is the corrected version offline58 and the provisional one online. The training row is not wrong, and it is not59 reproducible at request time — which is the definition of this family.603. **Pipeline-state skew.** Vocabularies, embedding tables, scaler statistics,61 category mappings, and bucket boundaries shipped *separately* from the62 model. The model is redeployed and the transform is not, or the feature63 store is backfilled with a newer transform than the one the model was fitted64 under. Everything versions independently until one of them moves.654. **Distribution skew.** The serving population is not the training66 population — a new integration, a new country, a new client that dominates67 traffic, a seasonal shift. And the feedback loop: the model changes which68 requests it later receives, so the drift is partly self-inflicted.6970Families 1–3 are defects and are fixable by construction. Family 4 is a fact71about the world and is managed by monitoring and retraining. Separate them72before proposing anything, because the fixes have nothing in common.7374## Part 2 — Fix by construction, ranked7576The point fix ("we corrected the null handling in the Go service") loses for77the same reason it loses in parser differentials: the next feature reintroduces78it. Rank the remediation:79801. **One implementation, called by both paths.** The transform is a single81 artifact — one library, one function, one version — imported by the training82 job and by the serving path. This deletes families 1 and 3 outright.832. **Log-and-train.** Compute the feature vector once, *at serving time*, log84 it, and train on the logged vectors. Serving becomes the source of truth and85 implementation skew becomes structurally impossible. It costs a bootstrap86 problem for a brand-new model and is worth it from the second model onward.873. **Ship preprocessing with the model as one atomic artifact.** The weights88 and their transform are a single versioned unit that cannot be deployed89 independently (`training-run-provenance`). If they can be deployed90 separately, one day they will be.914. **A feature store with point-in-time correctness** for family 2 — training92 reads values "as of the event", never "as of now", and serving reads the93 same view. If you do not have one, the substitute is to build training rows94 from the *logged* online values.955. **If two implementations genuinely must exist** (a latency budget the96 training stack cannot meet), then the differential harness of Part 3 is97 mandatory and belongs in CI. Two implementations without a differential test98 is not a tradeoff, it is an unmonitored bug.996. Only then: drift monitoring, which detects rather than prevents.100101## Part 3 — The differential harness102103Cheap, offline, and it finds real defects on the first run in most systems.1041051. **Take N real serving requests** — sampled from production logs, including106 the ugly ones: missing fields, new categories, extreme values, and the107 slowest 1%.1082. **Replay through both paths** and compare **feature by feature**.1093. **State the tolerance per type, in advance.** Exact equality for categorical,110 integer, boolean and encoded columns. An explicit epsilon for floats — and111 epsilon is a decision to write down, not a default to inherit.1124. **Report per-feature mismatch rates, never an aggregate.** "97% of features113 match" hides that the 3% is the top-importance feature. One broken column out114 of two hundred is a broken model.1155. **Rank mismatches by feature importance.** A skewed feature the model barely116 uses is a cleanup ticket; a skewed top-five feature is an incident.1176. **Freeze a golden set** — those N requests with their expected vectors — and118 run it in CI on every change to either path.119120Same discipline as any differential: compare the projection that matters, and121triage each mismatch by whether it reaches the thing that acts.122123## Part 4 — The silent default is the failure you will actually hit124125A feature service times out. The lookup returns nothing. The code fills in126`0`, or the training-set mean, or an empty embedding — and the model returns a127confident prediction on a vector it was never fitted on. Nothing logs an error.128The latency graph looks fine. This is the single most common way a deployed ML129system degrades, and `honest-degradation` names it exactly: a plausible answer130produced from data that was not there.131132What to do:133134- **Count and export the default rate per feature.** It is the highest-value135 metric in ML serving and almost nobody emits it. A default rate that moves136 from 0.1% to 8% is an outage, whatever the prediction distribution says.137- **Decide the degraded behavior deliberately, per feature.** Fail the request,138 abstain, or fall back to a simpler model that does not use that feature —139 each is defensible. Silently imputing and answering anyway is not.140- **Make abstention a first-class output**, and measure coverage alongside141 accuracy (`model-evaluation-discipline` Part 2).142- **Log the served feature vector, sampled.** Without it, a wrong prediction143 cannot be reconstructed — you have the input and the output and none of the144 middle. With it, every incident in this skill is diagnosable in minutes.145146## Part 5 — Monitor features, not just predictions147148The prediction distribution is a lagging and weak signal: it moves late, and it149moves for many reasons. Watch the inputs.150151Per feature, alert on: null and default rate, cardinality (new categories152appearing, old ones vanishing), range and quantile shift, and freshness — the153age of the value at the moment it was used. Compare each against the *training*154distribution, which means the training distribution has to be stored as an155artifact alongside the model.156157And for the final honest number: **evaluate on logged serving features joined158to real outcomes**, not on a rebuilt offline feature set. That is the only159evaluation that has seen the serving path, and it is the one that would have160caught every family above.161162---163164## Deliverable165166```167## Parity review — <model> <version>168Feature paths: training <impl> | serving <impl> | shared code: yes/no169Transform artifact shipped with weights: yes/no170Point-in-time correctness: <how training values are made reproducible online>171172### Differential run (N = <n> replayed production requests)173feature | type | tolerance | mismatch rate | importance rank | family174175### Silent-default exposure176feature | default value | current default rate | degraded behavior chosen177178### Fixes (ranked, structural first)179### Monitoring added: per-feature null/default/cardinality/range/freshness180### Not covered181```182183## Anti-patterns184185- Two implementations of one feature and no differential test between them.186- Reporting an aggregate match rate instead of per-feature mismatches.187- Comparing floats with an inherited default tolerance nobody chose.188- Deploying the model and its preprocessing as independently versioned things.189- Building training rows from warehouse values that the request path cannot190 reproduce, and calling the gap "drift".191- Imputing a failed feature lookup and answering confidently.192- Not emitting a per-feature default rate, so degradation is invisible until193 someone notices the business metric.194- Monitoring only the prediction distribution.195- Never logging the served feature vector, leaving wrong predictions196 permanently undiagnosable.197- Retraining to fix what is an implementation bug — it hides the skew inside198 new weights and the gap comes back with the next feature.