aiml
SearchMy learning
Menu

Lesson 13 of 19Module 4

Register and select a content-addressed model

Bind a local model selection to separately retained expected bytes.

Bind a local model selection to separately retained expected bytes. A local Rice-classifier lab with explicit failure checks.

Prerequisites

Gate a model candidate on validation results; Python files and SHA-256 content identity.

Preparation and term definitions

By the end, you can

  • Register passing candidate bytes under an explicit content digest.
  • Preserve identical retry behavior without overwriting conflicting or incomplete files.
  • Load against a separately retained expectation and explain the trusted-filesystem boundary.
Runtime
Python 3.12+
Dependencies
Standard library only

Lab code, tests and fixtures (ZIP, 2.4 MB)Quiz check your understanding

Progress is saved in this browser.

Problem

When you need this. Use a retained content identity when the model loaded for inference must be the same bytes that passed review.

A file called latest.json can change between review and use. After checking the exported candidate against a quality policy, you will register passing bytes under their digest and load them using a separately retained expected version.

Concepts

A content-addressed version names bytes by their hash. A mutable alias is a separate name that can point to different versions over time. The lab requires the explicit digest and does not implement an alias. MLflow also distinguishes versions and aliases, but this exercise builds a small local filesystem registry, not an MLflow service.

The expected digest matters only if the consumer retains it independently of the file being checked. Recomputing the expectation from a changed file accepts that change. Hashes identify bytes; they do not authenticate a publisher, authorize a release or make ordinary storage physically immutable.

Architecture

Candidate +expected digestRecompute gatebefore registrationExclusive localdigest-named fileVerify digest +load contractRetained expecteddigestcompareChanged orincomplete bytesrefuseCandidate +expected digestRecompute gatebefore registrationExclusive localdigest-named fileVerify digest +load contractExpecteddigestChanged orincomplete bytes
Registration requires a passing gate and writes a digest-named file exclusively. Loading compares its bytes with the separately retained expected digest; changed or incomplete data is refused.

register() checks the supplied bytes against the expected digest and recomputes the quality gate before creating the digest-named file exclusively. An identical retry succeeds; conflicting bytes are refused. load() reads the selected file once, checks its digest against the retained expectation, then validates its inference contract.

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.

Run the focused registry exercise and its regression testsTerminal · bash
python exercise.py registry
python -m unittest -v test_pipeline.py

Register twice without changing the selected version

Run python exercise.py registry. It uses a temporary directory, registers the saved candidate, registers identical bytes again, and loads the selected model. Inspect candidate_sha256 and the accepted identical retry. The expected digest comes from the separately retained run result; it is not recalculated from the registry file during loading.

Change bytes while retaining the old expectation

The exercise appends a space to the temporary registered JSON. The parsed JSON would still have the same values, but its bytes differ. Loading must refuse it. The exercise also requests latest, which is refused because it is not an explicit digest.

All modifications occur in a temporary directory. The bundled candidate and original training records remain unchanged. In your own integration, investigate a partial or conflicting registration rather than overwriting it to make a retry appear successful.

Actual registry exercise report from the previously trained Rice candidate, including its recorded result and refusal case.
Actual local execution using a previously trained candidate. This exercise does not perform new training. Full-resolution capture · Raw result · Command and hashes.
Read the actual pipeline.py implementation
pipeline.pySource · pythonDownload
"""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 registry output
Recorded local registry exerciseRecorded output · textDownload
{
  "execution_mode": "local exercise using a previously trained candidate",
  "candidate_sha256": "a4692350c5e6d1f231eea8c5eb3354c24276870e73a60922a10abb07abffa5f6",
  "first_registration": "accepted",
  "identical_retry": "accepted",
  "loaded_contract": "rice-measurements-v1",
  "changed_bytes": "registered bytes changed or incomplete",
  "alias": "select an explicit SHA-256 version"
}

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
Changed or incomplete registration test_registration_is_idempotent_and_partial_is_refused substitutes a partial file and requires both loading and retry to fail.
Alias or symlink substitution test_alias_and_symlink_refused requires an explicit version and refuses a substituted symlink.

The focused run records registered bytes changed or incomplete after the whitespace change, and select an explicit SHA-256 version for the alias request.

A crash can leave a partial exclusively created file. This implementation refuses it; it does not provide automatic recovery. The directory and its ancestors are assumed to be trusted during execution. Hostile concurrent edits, ancestor symlink races and distributed transactions are outside this local contract.

Deployment

A consumer loads one expected digest before using the model. Retain that expectation in the consumer’s reviewed configuration, separately from the editable registry file. Do not infer deployment approval from the artifact’s presence; this lesson establishes local selection, not traffic promotion.

Scaling

The local registry keeps one ordinary file per digest and has no remote API, retention manager or distributed coordination. Replacing it with object storage or a registry service requires preserving exact selection and adding that system’s authentication, concurrency and recovery rules. No remote registry has been exercised here.

Key takeaways

A registry file changes. The consumer recomputes its expected digest from that same changed file before loading. What has been lost?

A registry file changes. The consumer recomputes its expected digest from that same changed file before loading. What has been lost?

Choose one answer, then check your reasoning.

Read the explanation

The expected identity must be retained separately; deriving it from the replacement accepts the replacement.

Best practices

Keep expected identities separate from mutable storage. Make identical retries idempotent, refuse conflicting bytes and investigate incomplete writes. Treat integrity, authorization and release policy as separate checks.

References

Written by Arjun Prabhulal.

Module 4

Validate, register and serve a candidate