Problem
When you need this. Apply a quality gate when successful training alone cannot decide whether a candidate meets the declared validation requirements.
A training process can exit successfully and emit valid parameters while still failing a task-quality requirement. After exporting the exact inference bundle, you will apply a declared validation policy to those bytes and inspect a candidate that must be refused.
Concepts
A quality gate makes a pass/fail decision from task outcomes and a declared policy. It is separate from successful process exit, valid JSON and matching artifact identity. This exercise requires validation accuracy of at least 90% and recall of at least 85% for each Rice class. The values are explicit exercise policy, not inferred business requirements.
Accuracy divides all correct classifications by all validation rows. Recall for Cammeo divides correctly predicted Cammeo rows by all actual Cammeo rows; Osmancik recall uses the other actual-class row. A majority-class predictor can therefore have acceptable aggregate accuracy in some datasets while failing a class-recall requirement.
Architecture
The gate verifies the expected candidate digest and supported training identity, predicts each validation row with the reloaded bundle, and accumulates confusion counts. It binds the candidate digest, validation membership and policy into its result. Registration recomputes this gate rather than accepting a caller-edited passed flag.
Working code
Download the complete lab, extract it, and enter mlops-registry-serving/. Use Python 3.12 or later; there are no third-party dependencies. The ZIP includes the candidate, data, source and tests.
python exercise.py gate
python -m unittest -v test_pipeline.pyDerive the saved decision from its counts
Run python exercise.py gate. It recomputes validation predictions for the saved candidate and then evaluates a deliberately constant-class copy. No training occurs in this exercise.
The candidate gets 547 of 590 rows correct. Cammeo recall is 223 / 248; Osmancik recall is 324 / 342. Compare each fraction with the policy, rather than using a rounded percentage printed for display.
The implementation checks correct × denominator >= total × numerator, using the policy’s integer ratios. For example, the accuracy requirement uses the ratio 90/100. This avoids accepting a candidate because a displayed rounded score happens to reach the threshold.
Refuse a valid but inadequate classifier
The exercise sets every weight to zero and the bias to a positive constant. That copy predicts Cammeo for every row. It remains a structurally valid model but loses all Osmancik recall. Inspect constant_class_candidate, its false gate result and the registration refusal. The failed attempt leaves 0 files in the temporary registry.

Read the actual pipeline.py implementation
"""Local Rice inference export, validation gate and digest-selected registry.
No external packages. This is a trusted local filesystem exercise, not MLflow.
"""
import hashlib
import json
import math
import re
from pathlib import Path
from vendor import baseline
HERE = Path(__file__).resolve().parent
CONTRACT = 'rice-measurements-v1'
POLICY = {'accuracy': [90, 100], 'class_recall': [85, 100]}
def encode(value):
return (json.dumps(value, sort_keys=True, separators=(',', ':'),
allow_nan=False) + '\n').encode()
def digest(data):
return hashlib.sha256(data).hexdigest()
def pairs(items):
result = {}
for key, value in items:
if key in result:
raise ValueError('duplicate JSON key')
result[key] = value
return result
def decode(data):
return json.loads(data, object_pairs_hook=pairs,
parse_constant=lambda _: (_ for _ in ()).throw(ValueError('nonfinite JSON')))
def number(value):
try:
finite = type(value) in (float, int) and math.isfinite(value)
except OverflowError:
finite = False
if not finite:
raise ValueError('expected a finite non-Boolean number')
return value
def validate(bundle):
if not isinstance(bundle, dict):
raise ValueError('expected a bundle object')
if (type(bundle.get('format')) is not int or bundle.get('format') != 1 or bundle.get('contract') != CONTRACT
or bundle.get('features') != baseline.FEATURES
or bundle.get('labels') != baseline.LABELS
or bundle.get('positive') != baseline.POSITIVE):
raise ValueError('unsupported bundle contract')
for field in ('weights', 'mean', 'std'):
values = bundle.get(field)
if not isinstance(values, list) or len(values) != len(baseline.FEATURES):
raise ValueError('wrong parameter dimensions')
for value in values:
number(value)
if any(value <= 0 for value in bundle['std']):
raise ValueError('nonpositive standard deviation')
number(bundle.get('bias'))
return bundle
def predict(bundle, request, version):
if not isinstance(request, dict) or set(request) != {'contract', 'features'}:
raise ValueError('expected contract and features')
features = request['features']
if (request['contract'] != bundle['contract'] or not isinstance(features, dict)
or set(features) != set(bundle['features'])):
raise ValueError('feature contract mismatch')
values = [number(features[name]) for name in bundle['features']]
scaled = [(v - m) / s for v, m, s in zip(values, bundle['mean'], bundle['std'])]
z = bundle['bias'] + sum(w * v for w, v in zip(bundle['weights'], scaled))
if not all(math.isfinite(v) for v in scaled) or not math.isfinite(z):
raise ValueError('nonfinite computation')
probability = baseline.sigmoid(z)
label = baseline.POSITIVE if z >= 0 else baseline.LABELS[1]
return {'version': version, 'contract': CONTRACT, 'label': label,
'positive_probability': probability}
def request_for(row):
return {'contract': CONTRACT, 'features': dict(zip(baseline.FEATURES, row['x']))}
def export():
parts = baseline.split_rows(baseline.load_rows())
scaler = baseline.fit_scaler(parts['training'])
model = baseline.train_logistic(parts['training'], scaler)
bundle = {'format': 1, 'contract': CONTRACT, 'features': baseline.FEATURES,
'labels': baseline.LABELS, 'positive': baseline.POSITIVE,
'weights': model['weights'], 'bias': model['bias'], **scaler,
'training': {'dataset_sha256': baseline.DATA_SHA256,
'split_salt': baseline.SALT, 'epochs': baseline.EPOCHS,
'learning_rate': baseline.LEARNING_RATE,
'trainer_sha256': digest(Path(baseline.__file__).read_bytes()),
'exporter_sha256': digest(Path(__file__).read_bytes())}}
validate(bundle)
# Match the source predictor on every validation row before retaining bytes.
version = digest(encode(bundle))
for row in parts['validation']:
if predict(bundle, request_for(row), version)['label'] != baseline.predict_logistic(model, scaler, row):
raise ValueError('export prediction mismatch')
return encode(bundle)
def quality_gate(data, expected):
if digest(data) != expected:
raise ValueError('candidate digest mismatch')
bundle = validate(decode(data))
training = bundle.get('training', {})
required = {'dataset_sha256': baseline.DATA_SHA256, 'split_salt': baseline.SALT,
'epochs': baseline.EPOCHS, 'learning_rate': baseline.LEARNING_RATE,
'trainer_sha256': digest(Path(baseline.__file__).read_bytes()),
'exporter_sha256': digest(Path(__file__).read_bytes())}
if training != required:
raise ValueError('unsupported training identity')
rows = baseline.split_rows(baseline.load_rows())['validation']
matrix = {label: {other: 0 for other in baseline.LABELS} for label in baseline.LABELS}
for row in rows:
result = predict(bundle, request_for(row), expected)
matrix[row['label']][result['label']] += 1
correct = sum(matrix[label][label] for label in matrix)
enough = lambda a, b, ratio: b > 0 and a * ratio[1] >= b * ratio[0]
passed = enough(correct, len(rows), POLICY['accuracy']) and all(
enough(matrix[label][label], sum(matrix[label].values()), POLICY['class_recall'])
for label in matrix)
return {'candidate_sha256': expected, 'partition': 'validation', 'policy': POLICY,
'membership_sha256': digest(encode(sorted(row['key'] for row in rows))),
'rows': len(rows), 'correct': correct, 'confusion': matrix, 'passed': passed}
def registered_file(root, version):
if not isinstance(version, str) or not re.fullmatch('[0-9a-f]{64}', version):
raise ValueError('select an explicit SHA-256 version')
root = Path(root)
if root.is_symlink():
raise ValueError('symlink registry root')
return root / (version + '.json')
def register(root, data, expected):
record = quality_gate(data, expected)
if not record['passed']:
raise ValueError('candidate failed quality policy')
path = registered_file(root, expected)
path.parent.mkdir(parents=True, exist_ok=True)
if path.is_symlink():
raise ValueError('symlink registration')
try:
with path.open('xb') as stream:
stream.write(data)
except FileExistsError:
if path.read_bytes() != data:
raise ValueError('conflicting or incomplete registration') from None
return record
def load(root, expected):
path = registered_file(root, expected)
if path.is_symlink():
raise ValueError('symlink registration')
data = path.read_bytes()
if digest(data) != expected:
raise ValueError('registered bytes changed or incomplete')
return validate(decode(data))
def batch(bundle, requests, version):
if not isinstance(requests, list) or not 1 <= len(requests) <= 64:
raise ValueError('batch size must be 1..64')
# Return only a complete list. A bad row raises before any output is returned.
return [predict(bundle, request, version) for request in requests]Read the complete recorded gate output
{
"execution_mode": "local exercise using a previously trained candidate",
"candidate_sha256": "a4692350c5e6d1f231eea8c5eb3354c24276870e73a60922a10abb07abffa5f6",
"candidate": {
"candidate_sha256": "a4692350c5e6d1f231eea8c5eb3354c24276870e73a60922a10abb07abffa5f6",
"partition": "validation",
"policy": {
"accuracy": [
90,
100
],
"class_recall": [
85,
100
]
},
"membership_sha256": "7dd5f31b4d981bb1400c5dee0b845d14929a5936eb4516b6ad6c881870e4d544",
"rows": 590,
"correct": 547,
"confusion": {
"Cammeo": {
"Cammeo": 223,
"Osmancik": 25
},
"Osmancik": {
"Cammeo": 18,
"Osmancik": 324
}
},
"passed": true
},
"constant_class_candidate": {
"candidate_sha256": "e0fd221b389939213c0a1a15cb8d7b56220ca765c422981c3fff2b6ffdbf4868",
"partition": "validation",
"policy": {
"accuracy": [
90,
100
],
"class_recall": [
85,
100
]
},
"membership_sha256": "7dd5f31b4d981bb1400c5dee0b845d14929a5936eb4516b6ad6c881870e4d544",
"rows": 590,
"correct": 248,
"confusion": {
"Cammeo": {
"Cammeo": 248,
"Osmancik": 0
},
"Osmancik": {
"Cammeo": 342,
"Osmancik": 0
}
},
"passed": false
},
"registration_refusal": "candidate failed quality policy",
"files_after_failed_gate": 0
}The exercise driver shows each operation and cleanup. The fresh-training driver and training run record document how the saved candidate was created. Adding --record to an exercise deliberately replaces its result and generated report; it does not refresh the screenshot automatically.
Failure cases
| Trigger | Required boundary and test |
|---|---|
| Failed class recall | test_bad_model_cannot_register requires the constant-class model to fail without creating a registry file. |
| Candidate digest mismatch | test_gate_uses_exact_counts_and_binds_candidate changes the candidate bytes while retaining the old expectation and requires refusal. |
The recorded registration refusal is candidate failed quality policy. Valid dimensions and a newly computed digest cannot repair the failed task-quality check.
This is a fixed validation gate. Repeatedly selecting candidates or changing thresholds after reading its results can overfit the validation partition. The workflow performs no held-out test fitting, tuning or scoring. Passing does not establish calibration, independent test performance or a safe production release.
Deployment
Use the gate result to decide whether this candidate may enter the local registry. Keep its candidate identity, policy and validation membership together. A passing gate does not start a model server or authorize a traffic rollout; the registry lesson handles selection next.
Scaling
The current gate predicts every validation row synchronously. A larger evaluator may distribute predictions, but it must preserve row identity, complete coverage and failure accounting. Averages over only completed predictions can conceal failed cases. No distributed evaluator is implemented here.
Key takeaways
The candidate digest matches and its overall accuracy passes, but one class recall fails. What should registration do?
Read the explanation
Every required quality condition must pass for registration.
Best practices
Declare the policy before selecting candidates. Compare exact counts against thresholds. Keep validation selection separate from a final held-out assessment, and retain the failed candidate’s result for diagnosis.
References
- MLflow model-registry workflows explain versions and mutable aliases. This lab uses its own local registry.
- scikit-learn common pitfalls explains fitting preprocessing on training data and applying it consistently during inference.
- Python HTTP server documentation describes the adapter and its production limitations.
- UCI Rice dataset supplies the measurement data under CC BY 4.0.