Phase 16 / Workload operations
Changes, rollback, and artifact provenance
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 95 minutes · CPU practice
Separate content identity, validation evidence and active selection. Reject corrupt or failed artifacts before a pointer change.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Failure recovery and operational runbooks
Hardware: CPU
Setup help: Python, notebook, or Cloud TPU VM
Use the tested CPU packages (the recorded environment uses Python 3.14.3). Other Python versions and accelerator environments require separate checks. The optional Cloud TPU specialization explains the target-specific execution boundaries. Logical CPU devices practice partitioning; they do not simulate TPU performance.
Download the repository ZIP or use your existing checkout. Extract it and open a terminal in the folder containing requirements-cpu.txt. The beginner ZIP is separate; these lessons use the full repository folder.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-cpu.txtpy -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-cpu.txtAfter activation, run the complete worked example from that same folder:
python phases/16-operations/04-changes-rollback-and-artifact-provenance/code/main.py.\.venv\Scripts\python.exe phases/16-operations/04-changes-rollback-and-artifact-provenance/code/main.pyFor an incremental build, create main.py in your own practice folder, append each build block in order, and run python main.py with this environment active. Keep that file separate from the downloaded reference solution.
Download this lesson’s notebook. Open it with a Python kernel using the course environment. Run cells from the top after restarting the kernel; experiments depend on earlier definitions.
For local Jupyter, first install the course requirements using the Local Python steps, then use your environment’s Python to start Jupyter:
python -m pip install jupyterlab
python -m jupyter lab.\.venv\Scripts\python.exe -m pip install jupyterlab
.\.venv\Scripts\python.exe -m jupyter labSelect the downloaded operations-04.ipynb file in JupyterLab. The notebook contains reference solutions; make your prediction and attempt each practice task before opening them. Restart the kernel before changing logical device setup.
The worked reference for this lesson is CPU-validated. Some references explicitly configure logical CPU devices; setting JAX_PLATFORMS does not adapt them to a TPU. Follow the execution matrix and reviewed multi-host example before attempting a target run.
For accelerator preparation, study TPU setup, observed runtime receipts, checkpointed jobs and precision and retained traces. These target paths remain unqualified on physical TPU hardware until an actual receipt is recorded. Cloud provisioning is optional for CPU learning.
Check: Publishing leaves the active pointer unchanged. A failed validation gate cannot change it, and rollback selects the first verified content ID again.
Revisit the guided CPU setup · Open 19: TPU Setup. Package instructions are CPU instructions; Windows and accelerator environments need their own validation.
What you will be able to do
- Separate content identity, validation evidence and active selection.
- Reject corrupt or failed artifacts before a pointer change.
- Rehearse a reversible local artifact selection and document its limits.
The problem
A new artifact is available, but which exact bytes are currently selected, and can we return to the previous valid version after a bad change? We will create content-addressed artifacts from an actual trained checkpoint, test a validation gate, atomically change one local selection pointer and rehearse rollback.
The idea
A candidate artifact can exist without serving users. Validation, activation and rollback are separate release transitions. Bind each decision to immutable artifact identity so rejected candidates cannot accidentally change the active system.
Publishing is different from activating
Imagine candidate B is uploaded while artifact A remains active. If B fails evaluation, the correct transition leaves A active. The presence of B in storage is not permission to select it.
Rollback should select a known compatible artifact together with its processor and configuration. Restoring only weights while leaving an incompatible tokenizer or schema in place can create another failure.
The artifact-selection bars summarize discrete identities; their numeric height is not a quality score. Read them as a sequence of accepted or rejected transitions. A state diagram makes the unchanged active identity on rejection easier to verify.
Rejected candidates do not become active
Predict: Should a rejected candidate change the active artifact pointer?
Candidate B can exist while A remains active. Only the passing branch permits activation under the policy; failure keeps A selected. Letters identify artifacts, not scores. Preserve compatible processor and configuration identities with the selected model.
A name is not a content identity
Filenames such as latest or final can be overwritten without revealing that the bytes changed. Our artifact ID is the SHA-256 digest of a canonical JSON encoding with sorted keys and nonfinite values rejected. Reordering dictionary insertion alone does not change that encoding. Changing a parameter, source hash or even a release note produces a different ID. This identity detects changed bytes; it does not prove that the artifact is correct or authorized.
Attach enough provenance to reproduce the question
The artifact includes worker-source hash, training-configuration hash, data hash, full state and a validation record. These fields tie a claimed result to a concrete experiment. The local validation flag is a demonstration gate supplied by this trusted authoring workflow; it is not a signed attestation or protection against an adversarial publisher. A production registry needs access controls, trusted validators and policies appropriate to its use. Activation also independently evaluates the two model parameters on fixed held-out inputs , using the known relationship . It requires mean squared error no larger than . This intentionally permissive fixture gate checks that a changed model is actually evaluated; it is not a production quality threshold.
Publish without affecting the current reader
publish writes a complete content-addressed file but does not create or change active.json. This permits inspection before use. activate reads that file, recomputes its digest and checks the gate before replacing the pointer. A failed gate leaves the current pointer byte-for-byte unchanged. The pointer contains current and previous IDs so an operator can identify what changed.
Keep selection atomic and scope the guarantee
The atomic helper writes to a temporary sibling file, flushes and fsyncs it, then replaces the selected filename. The exercise has one writer and a local filesystem. It does not provide compare-and-swap against concurrent deployments, distributed consensus, atomic updates across multiple files or guaranteed power-loss durability. A multi-writer service must add a concurrency contract rather than assuming this example already supplies one.
Rollback is another checked selection
Rollback reads the previous ID and calls the same activation path. It therefore rejects a previous artifact that has become corrupt. Selecting older weights does not undo external side effects, database migrations, changed data schemas or requests already served. Here the only changed operational state is a local pointer to a compatible JSON artifact. Describe that bounded result precisely.
A change record should support diagnosis
Retain previous and candidate IDs, source/config/data hashes, the gate result, and why the change or rollback occurred. In this exercise the second valid artifact differs only in reviewed metadata, which makes the byte-level identity test unambiguous. The separate degraded-model experiment changes actual parameters and is rejected by the measured held-out gate; the accepted metadata revision is not claimed to improve predictions.
The selected artifact changes only after a passing gate
Predict before looking: Should publishing or rejecting a candidate move the active selection?
Read the figure
The horizontal categories are actions in the executed local release drill. The vertical values are categorical codes: means artifact A and means artifact B. They are identifiers, not quality scores. The selected ID remains A after publishing B, changes to B after activation, stays B after a rejected candidate, and returns to A after rollback.
Connect it to the computation
The flat pair around the rejected gate is the important invariant: failure must not partially change the active pointer. Every transition in the diagram is backed by a read of the local pointer in the figure experiment. The result proves a single-writer local selection workflow, not a multi-host rollout or rollback of external side effects.
Open full-size figureDownload data and provenance
The notebook contains the data experiment, plotting code, and recorded figure output. Rerun it to test your changes.
Where to put & run this code: Work in phases/16-operations/04-changes-rollback-and-artifact-provenance/code/main.py (or downloaded exercises/operations-04.py / exercises/operations-04.ipynb) and execute from the repository root with python3 phases/16-operations/04-changes-rollback-and-artifact-provenance/code/main.py.
1 Create the local artifact helpers
Create main.py with this block. Run python3 main.py in the CPU course environment.
# Step 1: Create the local artifact helpers
"""Bounded local JAX subprocess supervisor and lifecycle verifier."""
from pathlib import Path
import hashlib
import json
import math
import os
import subprocess
import sys
import tempfile
import time
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()).hexdigest()
def atomic_json(path, value):
"""Replace one local JSON file only after its bytes have been flushed."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=path.name+'.', suffix='.tmp', dir=path.parent)
# Run the boundary check and catch the expected exception:
try:
# Enter `os.fdopen(fd, 'w')` context block:
with os.fdopen(fd, 'w') as stream:
json.dump(value, stream, sort_keys=True, allow_nan=False)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)Code building blocks in this step
digest(...)— Calldigestwith your updated parameters or inputs from this lesson's workspace.hashlib.sha256(...)— Callhashlib.sha256with your updated parameters or inputs from this lesson's workspace.
The only filesystem writes are to the caller-selected run directory. Stable JSON encoding makes hashes reproducible.
2 Define the supervised worker
Append this block to main.py. Run python3 main.py in the CPU course environment.
# This file is materialized only inside the caller-owned temporary run folder.
WORKER = r'''
import hashlib, json, os, sys, time
from pathlib import Path
import jax
import jax.numpy as jnp
import numpy as np
root = Path(sys.argv[1])
cfg = json.loads((root/'config.json').read_text())
worker_hash = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
def emit(event, **fields):
print(json.dumps(dict(event=event, monotonic_s=time.monotonic(), **fields)), flush=True)
def digest(value):
return hashlib.sha256(json.dumps(value,sort_keys=True,separators=(',', ':'),allow_nan=False).encode()).hexdigest()
def write_checkpoint(state):
target = root/'checkpoint.json'
temporary = root/'checkpoint.pending'
with temporary.open('w') as stream:
json.dump(dict(state,checksum=digest(state)),stream,sort_keys=True,allow_nan=False)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary,target)
emit('checkpoint',step=state['step'],state_hash=digest(state))
try:
emit('started',pid=os.getpid(),backend=jax.default_backend(),device_count=jax.device_count())
if jax.default_backend() != cfg['backend'] or jax.device_count() < cfg['min_devices']:
raise ValueError('runtime backend/device contract failed')
x = jnp.linspace(-1.,1.,64)
y = 2*x+1
data_hash = digest(dict(x=np.asarray(x).tolist(),y=np.asarray(y).tolist()))
training = {name:cfg[name] for name in ('seed','learning_rate','momentum','batch_size')}
config_hash = digest(training)
if cfg['resume']:
saved = json.loads((root/'checkpoint.json').read_text())
checksum = saved.pop('checksum',None)
if checksum != digest(saved) or saved.get('worker_hash') != worker_hash:
raise ValueError('checkpoint checksum/source mismatch')
if saved['schema_version'] != 1 or saved['config_hash'] != config_hash or saved['data_hash'] != data_hash:
raise ValueError('checkpoint provenance mismatch')
state = saved
step = saved['step']
params=jnp.array(saved['params'])
velocity=jnp.array(saved['velocity'])
key=jnp.array(saved['key'],dtype=jnp.uint32)
if not isinstance(step,int) or isinstance(step,bool) or step < 0 or step > cfg['steps'] or params.shape != (2,) or velocity.shape != (2,) or key.shape != (2,):
raise ValueError('invalid checkpoint state shape or step')
if not bool(jnp.all(jnp.isfinite(params))) or not bool(jnp.all(jnp.isfinite(velocity))):
raise ValueError('nonfinite checkpoint state')
emit('restored',step=step,state_hash=digest(saved))
else:
step=0
params=jnp.zeros(2)
velocity=jnp.zeros(2)
key=jax.random.PRNGKey(cfg['seed'])
def update(params,velocity,key):
key, sample = jax.random.split(key)
indices=jax.random.choice(sample,64,(cfg['batch_size'],),replace=False)
objective=lambda p:jnp.mean((p[0]*x[indices]+p[1]-y[indices])**2)
loss, grad=jax.value_and_grad(objective)(params)
velocity=cfg['momentum']*velocity+grad
params=params-cfg['learning_rate']*velocity
return params,velocity,key,loss
compiled=jax.jit(update)
# Compile and synchronize once without consuming actual training state.
begin=time.perf_counter()
warm=compiled(params,velocity,key)
jax.block_until_ready(warm)
emit('ready',compile_warmup_s=time.perf_counter()-begin,step=step)
while step < cfg['steps']:
if cfg.get('stall_at') == step:
emit('stall_injected',step=step)
time.sleep(cfg.get('stall_seconds',10.))
begin=time.perf_counter()
params,velocity,key,loss=compiled(params,velocity,key)
jax.block_until_ready((params,velocity,key,loss))
elapsed=time.perf_counter()-begin
step+=1
state=dict(schema_version=1,worker_hash=worker_hash,step=step,params=np.asarray(params).tolist(),velocity=np.asarray(velocity).tolist(),key=np.asarray(key).tolist(),config_hash=config_hash,data_hash=data_hash)
emit('progress',step=step,loss=float(loss),examples=cfg['batch_size'],update_s=elapsed,state_hash=digest(state))
if step % cfg['checkpoint_every'] == 0 or step == cfg['steps']:
write_checkpoint(state)
if cfg.get('fail_after') == step:
raise RuntimeError('controlled failure after update')
write_checkpoint(state)
emit('completed',step=step,state_hash=digest(state))
except Exception as error:
emit('failed',kind=type(error).__name__,message=str(error))
raise SystemExit(23)
'''Code building blocks in this step
jnp.linspace(start, stop, num)— Createsnumevenly spaced float points across the closed interval[start, stop].jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.jnp.zeros / jnp.ones(shape, dtype=...)— Allocates a tensor of the givenshapeinitialized with constants.x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).
The string is a real Python program launched in a separate process. It emits structured events, synchronizes JAX updates and preserves complete state.
3 Add bounded launch and result collection
Append this block to main.py. Run python3 main.py in the CPU course environment.
# Step 3 — Add bounded launch and result collection: The supervisor owns the child handle, captures its output and...
def default_config(**changes):
cfg=dict(seed=3,learning_rate=.04,momentum=.8,batch_size=8,steps=8,
checkpoint_every=2,backend='cpu',min_devices=1,resume=False,
fail_after=None,stall_at=None,stall_seconds=10.)
# Update state in place with the new values.
cfg.update(changes)
for name in ('steps','checkpoint_every','batch_size','min_devices'):
# Guard input contract (`not isinstance(cfg[name], int) or isinstance(cfg[name], bool) or cfg[name] < 1`) and fail fast if violated.
if not isinstance(cfg[name],int) or isinstance(cfg[name],bool) or cfg[name] < 1:
raise ValueError(name+' must be a positive integer')
# Guard input contract (`cfg['batch_size'] > 64 or not 0 <= cfg['momentum'] < 1 or (not 0 < cfg['learning_rate'] < 1)`) and fail fast if violated.
if cfg['batch_size'] > 64 or not 0 <= cfg['momentum'] < 1 or not 0 < cfg['learning_rate'] < 1:
raise ValueError('invalid training configuration')
return cfg
def launch(root, config=None, timeout=10.):
root=Path(root)
root.mkdir(parents=True,exist_ok=True)
cfg=default_config(**(config or {}))
# Guard input contract (`timeout <= 0`) and fail fast if violated.
if timeout <= 0:
raise ValueError('timeout must be positive')
atomic_json(root/'config.json',cfg)
worker=root/'worker.py'
worker.write_text(WORKER)
# Configure environment variable before initializing the runtime.
env=dict(os.environ,JAX_PLATFORMS=cfg['backend'],PYTHONUNBUFFERED='1')
started=time.perf_counter()
process=subprocess.Popen([sys.executable,str(worker),str(root)],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,env=env)
timed_out=False
try:
stdout,stderr=process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
timed_out=True
process.terminate()
try:
stdout,stderr=process.communicate(timeout=1.)
except subprocess.TimeoutExpired:
process.kill()
stdout,stderr=process.communicate(timeout=1.)
wall=time.perf_counter()-started
events=[]
malformed=[]
for line in stdout.splitlines():
if not line.strip():
continue
try:
event=json.loads(line)
if not isinstance(event,dict) or 'event' not in event:
raise ValueError('not an event')
events.append(event)
except (ValueError,TypeError):
malformed.append(line)
status='timed_out' if timed_out else 'completed' if process.returncode == 0 and not malformed and events and events[-1]['event']=='completed' else 'failed'
# Compute deterministic cryptographic digest `result` for provenance verification.
result=dict(status=status,returncode=process.returncode,wall_s=wall,events=events,stderr=stderr,
worker_hash=hashlib.sha256(WORKER.encode()).hexdigest(),config=cfg,malformed_stdout=malformed)
atomic_json(root/'run.json',result)
return resultCode building blocks in this step
default_config(...)— Calldefault_configwith your updated parameters or inputs from this lesson's workspace.cfg.update(...)— Callcfg.updatewith your updated parameters or inputs from this lesson's workspace.
The supervisor owns the child handle, captures its output and always waits for termination after a timeout.
4 Add content-addressed publication and selection
Append this block to main.py. Run python3 main.py in the CPU course environment.
# Step 4 — Add content-addressed publication and selection: Publication and activation are separate; rollback uses the same...
def publish(store, artifact):
"""Content-address one JSON artifact. Publishing does not activate it."""
store=Path(store)
identifier=digest(artifact)
atomic_json(store/'artifacts'/(identifier+'.json'),artifact)
return identifier
def read_artifact(store, identifier):
# Guard input contract (`len(identifier) != 64 or any((c not in '0123456789abcdef' for c in identifier))`) and fail fast if violated.
if len(identifier) != 64 or any(c not in '0123456789abcdef' for c in identifier):
raise ValueError('invalid artifact identifier')
artifact=json.loads((Path(store)/'artifacts'/(identifier+'.json')).read_text())
# Guard input contract (`digest(artifact) != identifier`) and fail fast if violated.
if digest(artifact) != identifier:
raise ValueError('artifact digest mismatch')
return artifact
def artifact_metrics(artifact):
"""Independent held-out arithmetic for this fixed linear-model release contract."""
params=artifact.get('state',{}).get('params',[])
# Guard input contract (`len(params) != 2 or any((not math.isfinite(float(v)) for v in params))`) and fail fast if violated.
if len(params)!=2 or any(not math.isfinite(float(v)) for v in params):
raise ValueError('invalid model parameters')
inputs=(-1.5,-.37,.22,1.5)
mse=sum((params[0]*x+params[1]-(2*x+1))**2 for x in inputs)/len(inputs)
return dict(held_out_mse=mse,acceptance_limit=1.,passed=mse<=1.)
def activate(store, identifier):
artifact=read_artifact(store,identifier)
# Guard input contract (`artifact.get('validation', {}).get('passed') is not True`) and fail fast if violated.
if artifact.get('validation',{}).get('passed') is not True:
raise ValueError('artifact has no passing validation evidence')
required=['worker_hash','config_hash','data_hash','state']
# Guard input contract (`any((name not in artifact for name in required))`) and fail fast if violated.
if any(name not in artifact for name in required):
raise ValueError('artifact provenance incomplete')
# Guard input contract (`not artifact_metrics(artifact)['passed']`) and fail fast if violated.
if not artifact_metrics(artifact)['passed']:
raise ValueError('held-out model validation failed')
previous=None
pointer=Path(store)/'active.json'
if pointer.exists():previous=json.loads(pointer.read_text())['current']
atomic_json(pointer,dict(current=identifier,previous=previous))
return identifier
def rollback(store):
pointer=json.loads((Path(store)/'active.json').read_text())
# Guard input contract (`not pointer.get('previous')`) and fail fast if violated.
if not pointer.get('previous'):raise ValueError('no previous version')
return activate(store,pointer['previous'])Code building blocks in this step
publish(...)— Callpublishwith your updated parameters or inputs from this lesson's workspace.Path(...)— CallPathwith your updated parameters or inputs from this lesson's workspace.
Publication and activation are separate; rollback uses the same integrity and validation checks.
5 Rehearse a local release and rollback
Append this block to main.py. Run python3 main.py in the CPU course environment.
# Step 5 — Rehearse a local release and rollback: The artifact contains a checkpoint from an actual successful JAX job.
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory(prefix='ops-release-') as folder:
base=Path(folder)
run=launch(base/'run')
state=json.loads((base/'run'/'checkpoint.json').read_text())
store=base/'registry'
artifact=dict(worker_hash=run['worker_hash'],config_hash=state['config_hash'],data_hash=state['data_hash'],state=state,validation={'passed':True,'check':'completed CPU fixture and checksum'})
version_a=publish(store,artifact)
assert not (store/'active.json').exists()
activate(store,version_a)
version_b=publish(store,dict(artifact,release_note='reviewed metadata revision'))
activate(store,version_b)
rejected=publish(store,dict(artifact,validation={'passed':False}))
before=(store/'active.json').read_bytes()
try:
activate(store,rejected)
except ValueError:
rejected_without_change=(store/'active.json').read_bytes()==before
else:
raise AssertionError('bad release activated')
assert rejected_without_change
assert rollback(store)==version_a
chosen=json.loads((store/'active.json').read_text())
assert chosen['current']==version_a
print('first / second content IDs:',version_a,version_b)
print('failed gate kept selection:',rejected_without_change,'rollback restored first:',chosen['current']==version_a)Code building blocks in this step
tempfile.TemporaryDirectory(...)— Calltempfile.TemporaryDirectorywith your updated parameters or inputs from this lesson's workspace.Path(...)— CallPathwith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
The artifact contains a checkpoint from an actual successful JAX job. The pointer changes only after its gate passes.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1: Create the local artifact helpers
"""Bounded local JAX subprocess supervisor and lifecycle verifier."""
from pathlib import Path
import hashlib
import json
import math
import os
import subprocess
import sys
import tempfile
import time
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()).hexdigest()
def atomic_json(path, value):
"""Replace one local JSON file only after its bytes have been flushed."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=path.name+'.', suffix='.tmp', dir=path.parent)
# Run the boundary check and catch the expected exception:
try:
# Enter `os.fdopen(fd, 'w')` context block:
with os.fdopen(fd, 'w') as stream:
json.dump(value, stream, sort_keys=True, allow_nan=False)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
# This file is materialized only inside the caller-owned temporary run folder.
WORKER = r'''
import hashlib, json, os, sys, time
from pathlib import Path
import jax
import jax.numpy as jnp
import numpy as np
root = Path(sys.argv[1])
cfg = json.loads((root/'config.json').read_text())
worker_hash = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
def emit(event, **fields):
print(json.dumps(dict(event=event, monotonic_s=time.monotonic(), **fields)), flush=True)
def digest(value):
return hashlib.sha256(json.dumps(value,sort_keys=True,separators=(',', ':'),allow_nan=False).encode()).hexdigest()
def write_checkpoint(state):
target = root/'checkpoint.json'
temporary = root/'checkpoint.pending'
with temporary.open('w') as stream:
json.dump(dict(state,checksum=digest(state)),stream,sort_keys=True,allow_nan=False)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary,target)
emit('checkpoint',step=state['step'],state_hash=digest(state))
try:
emit('started',pid=os.getpid(),backend=jax.default_backend(),device_count=jax.device_count())
if jax.default_backend() != cfg['backend'] or jax.device_count() < cfg['min_devices']:
raise ValueError('runtime backend/device contract failed')
x = jnp.linspace(-1.,1.,64)
y = 2*x+1
data_hash = digest(dict(x=np.asarray(x).tolist(),y=np.asarray(y).tolist()))
training = {name:cfg[name] for name in ('seed','learning_rate','momentum','batch_size')}
config_hash = digest(training)
if cfg['resume']:
saved = json.loads((root/'checkpoint.json').read_text())
checksum = saved.pop('checksum',None)
if checksum != digest(saved) or saved.get('worker_hash') != worker_hash:
raise ValueError('checkpoint checksum/source mismatch')
if saved['schema_version'] != 1 or saved['config_hash'] != config_hash or saved['data_hash'] != data_hash:
raise ValueError('checkpoint provenance mismatch')
state = saved
step = saved['step']
params=jnp.array(saved['params'])
velocity=jnp.array(saved['velocity'])
key=jnp.array(saved['key'],dtype=jnp.uint32)
if not isinstance(step,int) or isinstance(step,bool) or step < 0 or step > cfg['steps'] or params.shape != (2,) or velocity.shape != (2,) or key.shape != (2,):
raise ValueError('invalid checkpoint state shape or step')
if not bool(jnp.all(jnp.isfinite(params))) or not bool(jnp.all(jnp.isfinite(velocity))):
raise ValueError('nonfinite checkpoint state')
emit('restored',step=step,state_hash=digest(saved))
else:
step=0
params=jnp.zeros(2)
velocity=jnp.zeros(2)
key=jax.random.PRNGKey(cfg['seed'])
def update(params,velocity,key):
key, sample = jax.random.split(key)
indices=jax.random.choice(sample,64,(cfg['batch_size'],),replace=False)
objective=lambda p:jnp.mean((p[0]*x[indices]+p[1]-y[indices])**2)
loss, grad=jax.value_and_grad(objective)(params)
velocity=cfg['momentum']*velocity+grad
params=params-cfg['learning_rate']*velocity
return params,velocity,key,loss
compiled=jax.jit(update)
# Compile and synchronize once without consuming actual training state.
begin=time.perf_counter()
warm=compiled(params,velocity,key)
jax.block_until_ready(warm)
emit('ready',compile_warmup_s=time.perf_counter()-begin,step=step)
while step < cfg['steps']:
if cfg.get('stall_at') == step:
emit('stall_injected',step=step)
time.sleep(cfg.get('stall_seconds',10.))
begin=time.perf_counter()
params,velocity,key,loss=compiled(params,velocity,key)
jax.block_until_ready((params,velocity,key,loss))
elapsed=time.perf_counter()-begin
step+=1
state=dict(schema_version=1,worker_hash=worker_hash,step=step,params=np.asarray(params).tolist(),velocity=np.asarray(velocity).tolist(),key=np.asarray(key).tolist(),config_hash=config_hash,data_hash=data_hash)
emit('progress',step=step,loss=float(loss),examples=cfg['batch_size'],update_s=elapsed,state_hash=digest(state))
if step % cfg['checkpoint_every'] == 0 or step == cfg['steps']:
write_checkpoint(state)
if cfg.get('fail_after') == step:
raise RuntimeError('controlled failure after update')
write_checkpoint(state)
emit('completed',step=step,state_hash=digest(state))
except Exception as error:
emit('failed',kind=type(error).__name__,message=str(error))
raise SystemExit(23)
'''
# Step 3 — Add bounded launch and result collection: The supervisor owns the child handle, captures its output and...
def default_config(**changes):
cfg=dict(seed=3,learning_rate=.04,momentum=.8,batch_size=8,steps=8,
checkpoint_every=2,backend='cpu',min_devices=1,resume=False,
fail_after=None,stall_at=None,stall_seconds=10.)
# Update state in place with the new values.
cfg.update(changes)
for name in ('steps','checkpoint_every','batch_size','min_devices'):
# Guard input contract (`not isinstance(cfg[name], int) or isinstance(cfg[name], bool) or cfg[name] < 1`) and fail fast if violated.
if not isinstance(cfg[name],int) or isinstance(cfg[name],bool) or cfg[name] < 1:
raise ValueError(name+' must be a positive integer')
# Guard input contract (`cfg['batch_size'] > 64 or not 0 <= cfg['momentum'] < 1 or (not 0 < cfg['learning_rate'] < 1)`) and fail fast if violated.
if cfg['batch_size'] > 64 or not 0 <= cfg['momentum'] < 1 or not 0 < cfg['learning_rate'] < 1:
raise ValueError('invalid training configuration')
return cfg
def launch(root, config=None, timeout=10.):
root=Path(root)
root.mkdir(parents=True,exist_ok=True)
cfg=default_config(**(config or {}))
# Guard input contract (`timeout <= 0`) and fail fast if violated.
if timeout <= 0:
raise ValueError('timeout must be positive')
atomic_json(root/'config.json',cfg)
worker=root/'worker.py'
worker.write_text(WORKER)
# Configure environment variable before initializing the runtime.
env=dict(os.environ,JAX_PLATFORMS=cfg['backend'],PYTHONUNBUFFERED='1')
started=time.perf_counter()
process=subprocess.Popen([sys.executable,str(worker),str(root)],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,env=env)
timed_out=False
try:
stdout,stderr=process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
timed_out=True
process.terminate()
try:
stdout,stderr=process.communicate(timeout=1.)
except subprocess.TimeoutExpired:
process.kill()
stdout,stderr=process.communicate(timeout=1.)
wall=time.perf_counter()-started
events=[]
malformed=[]
for line in stdout.splitlines():
if not line.strip():
continue
try:
event=json.loads(line)
if not isinstance(event,dict) or 'event' not in event:
raise ValueError('not an event')
events.append(event)
except (ValueError,TypeError):
malformed.append(line)
status='timed_out' if timed_out else 'completed' if process.returncode == 0 and not malformed and events and events[-1]['event']=='completed' else 'failed'
# Compute deterministic cryptographic digest `result` for provenance verification.
result=dict(status=status,returncode=process.returncode,wall_s=wall,events=events,stderr=stderr,
worker_hash=hashlib.sha256(WORKER.encode()).hexdigest(),config=cfg,malformed_stdout=malformed)
atomic_json(root/'run.json',result)
return result
# Step 4 — Add content-addressed publication and selection: Publication and activation are separate; rollback uses the same...
def publish(store, artifact):
"""Content-address one JSON artifact. Publishing does not activate it."""
store=Path(store)
identifier=digest(artifact)
atomic_json(store/'artifacts'/(identifier+'.json'),artifact)
return identifier
def read_artifact(store, identifier):
# Guard input contract (`len(identifier) != 64 or any((c not in '0123456789abcdef' for c in identifier))`) and fail fast if violated.
if len(identifier) != 64 or any(c not in '0123456789abcdef' for c in identifier):
raise ValueError('invalid artifact identifier')
artifact=json.loads((Path(store)/'artifacts'/(identifier+'.json')).read_text())
# Guard input contract (`digest(artifact) != identifier`) and fail fast if violated.
if digest(artifact) != identifier:
raise ValueError('artifact digest mismatch')
return artifact
def artifact_metrics(artifact):
"""Independent held-out arithmetic for this fixed linear-model release contract."""
params=artifact.get('state',{}).get('params',[])
# Guard input contract (`len(params) != 2 or any((not math.isfinite(float(v)) for v in params))`) and fail fast if violated.
if len(params)!=2 or any(not math.isfinite(float(v)) for v in params):
raise ValueError('invalid model parameters')
inputs=(-1.5,-.37,.22,1.5)
mse=sum((params[0]*x+params[1]-(2*x+1))**2 for x in inputs)/len(inputs)
return dict(held_out_mse=mse,acceptance_limit=1.,passed=mse<=1.)
def activate(store, identifier):
artifact=read_artifact(store,identifier)
# Guard input contract (`artifact.get('validation', {}).get('passed') is not True`) and fail fast if violated.
if artifact.get('validation',{}).get('passed') is not True:
raise ValueError('artifact has no passing validation evidence')
required=['worker_hash','config_hash','data_hash','state']
# Guard input contract (`any((name not in artifact for name in required))`) and fail fast if violated.
if any(name not in artifact for name in required):
raise ValueError('artifact provenance incomplete')
# Guard input contract (`not artifact_metrics(artifact)['passed']`) and fail fast if violated.
if not artifact_metrics(artifact)['passed']:
raise ValueError('held-out model validation failed')
previous=None
pointer=Path(store)/'active.json'
if pointer.exists():previous=json.loads(pointer.read_text())['current']
atomic_json(pointer,dict(current=identifier,previous=previous))
return identifier
def rollback(store):
pointer=json.loads((Path(store)/'active.json').read_text())
# Guard input contract (`not pointer.get('previous')`) and fail fast if violated.
if not pointer.get('previous'):raise ValueError('no previous version')
return activate(store,pointer['previous'])
# Step 5 — Rehearse a local release and rollback: The artifact contains a checkpoint from an actual successful JAX job.
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory(prefix='ops-release-') as folder:
base=Path(folder)
run=launch(base/'run')
state=json.loads((base/'run'/'checkpoint.json').read_text())
store=base/'registry'
artifact=dict(worker_hash=run['worker_hash'],config_hash=state['config_hash'],data_hash=state['data_hash'],state=state,validation={'passed':True,'check':'completed CPU fixture and checksum'})
version_a=publish(store,artifact)
assert not (store/'active.json').exists()
activate(store,version_a)
version_b=publish(store,dict(artifact,release_note='reviewed metadata revision'))
activate(store,version_b)
rejected=publish(store,dict(artifact,validation={'passed':False}))
before=(store/'active.json').read_bytes()
try:
activate(store,rejected)
except ValueError:
rejected_without_change=(store/'active.json').read_bytes()==before
else:
raise AssertionError('bad release activated')
assert rejected_without_change
assert rollback(store)==version_a
chosen=json.loads((store/'active.json').read_text())
assert chosen['current']==version_a
print('first / second content IDs:',version_a,version_b)
print('failed gate kept selection:',rejected_without_change,'rollback restored first:',chosen['current']==version_a)Run the example
python3 phases/16-operations/04-changes-rollback-and-artifact-provenance/code/main.pyAbout this command
This shortcut uses an activated macOS/Linux course environment in the repository folder. For Windows, use the explicit interpreter command in “Setup help” above. The script includes worked examples and reference solutions.
Inspect the complete assembled example
# Complete runnable example (operations-04)
"""Bounded local JAX subprocess supervisor and lifecycle verifier."""
from pathlib import Path
import hashlib
import json
import math
import os
import subprocess
import sys
import tempfile
import time
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()).hexdigest()
def atomic_json(path, value):
"""Replace one local JSON file only after its bytes have been flushed."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=path.name+'.', suffix='.tmp', dir=path.parent)
# Run the boundary check and catch the expected exception:
try:
# Enter `os.fdopen(fd, 'w')` context block:
with os.fdopen(fd, 'w') as stream:
json.dump(value, stream, sort_keys=True, allow_nan=False)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
# This file is materialized only inside the caller-owned temporary run folder.
WORKER = r'''
import hashlib, json, os, sys, time
from pathlib import Path
import jax
import jax.numpy as jnp
import numpy as np
root = Path(sys.argv[1])
cfg = json.loads((root/'config.json').read_text())
worker_hash = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
def emit(event, **fields):
print(json.dumps(dict(event=event, monotonic_s=time.monotonic(), **fields)), flush=True)
def digest(value):
return hashlib.sha256(json.dumps(value,sort_keys=True,separators=(',', ':'),allow_nan=False).encode()).hexdigest()
def write_checkpoint(state):
target = root/'checkpoint.json'
temporary = root/'checkpoint.pending'
with temporary.open('w') as stream:
json.dump(dict(state,checksum=digest(state)),stream,sort_keys=True,allow_nan=False)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary,target)
emit('checkpoint',step=state['step'],state_hash=digest(state))
try:
emit('started',pid=os.getpid(),backend=jax.default_backend(),device_count=jax.device_count())
if jax.default_backend() != cfg['backend'] or jax.device_count() < cfg['min_devices']:
raise ValueError('runtime backend/device contract failed')
x = jnp.linspace(-1.,1.,64)
y = 2*x+1
data_hash = digest(dict(x=np.asarray(x).tolist(),y=np.asarray(y).tolist()))
training = {name:cfg[name] for name in ('seed','learning_rate','momentum','batch_size')}
config_hash = digest(training)
if cfg['resume']:
saved = json.loads((root/'checkpoint.json').read_text())
checksum = saved.pop('checksum',None)
if checksum != digest(saved) or saved.get('worker_hash') != worker_hash:
raise ValueError('checkpoint checksum/source mismatch')
if saved['schema_version'] != 1 or saved['config_hash'] != config_hash or saved['data_hash'] != data_hash:
raise ValueError('checkpoint provenance mismatch')
state = saved
step = saved['step']
params=jnp.array(saved['params'])
velocity=jnp.array(saved['velocity'])
key=jnp.array(saved['key'],dtype=jnp.uint32)
if not isinstance(step,int) or isinstance(step,bool) or step < 0 or step > cfg['steps'] or params.shape != (2,) or velocity.shape != (2,) or key.shape != (2,):
raise ValueError('invalid checkpoint state shape or step')
if not bool(jnp.all(jnp.isfinite(params))) or not bool(jnp.all(jnp.isfinite(velocity))):
raise ValueError('nonfinite checkpoint state')
emit('restored',step=step,state_hash=digest(saved))
else:
step=0
params=jnp.zeros(2)
velocity=jnp.zeros(2)
key=jax.random.PRNGKey(cfg['seed'])
def update(params,velocity,key):
key, sample = jax.random.split(key)
indices=jax.random.choice(sample,64,(cfg['batch_size'],),replace=False)
objective=lambda p:jnp.mean((p[0]*x[indices]+p[1]-y[indices])**2)
loss, grad=jax.value_and_grad(objective)(params)
velocity=cfg['momentum']*velocity+grad
params=params-cfg['learning_rate']*velocity
return params,velocity,key,loss
compiled=jax.jit(update)
# Compile and synchronize once without consuming actual training state.
begin=time.perf_counter()
warm=compiled(params,velocity,key)
jax.block_until_ready(warm)
emit('ready',compile_warmup_s=time.perf_counter()-begin,step=step)
while step < cfg['steps']:
if cfg.get('stall_at') == step:
emit('stall_injected',step=step)
time.sleep(cfg.get('stall_seconds',10.))
begin=time.perf_counter()
params,velocity,key,loss=compiled(params,velocity,key)
jax.block_until_ready((params,velocity,key,loss))
elapsed=time.perf_counter()-begin
step+=1
state=dict(schema_version=1,worker_hash=worker_hash,step=step,params=np.asarray(params).tolist(),velocity=np.asarray(velocity).tolist(),key=np.asarray(key).tolist(),config_hash=config_hash,data_hash=data_hash)
emit('progress',step=step,loss=float(loss),examples=cfg['batch_size'],update_s=elapsed,state_hash=digest(state))
if step % cfg['checkpoint_every'] == 0 or step == cfg['steps']:
write_checkpoint(state)
if cfg.get('fail_after') == step:
raise RuntimeError('controlled failure after update')
write_checkpoint(state)
emit('completed',step=step,state_hash=digest(state))
except Exception as error:
emit('failed',kind=type(error).__name__,message=str(error))
raise SystemExit(23)
'''
# Step 3 — Add bounded launch and result collection: The supervisor owns the child handle, captures its output and...
def default_config(**changes):
cfg=dict(seed=3,learning_rate=.04,momentum=.8,batch_size=8,steps=8,
checkpoint_every=2,backend='cpu',min_devices=1,resume=False,
fail_after=None,stall_at=None,stall_seconds=10.)
# Update state in place with the new values.
cfg.update(changes)
for name in ('steps','checkpoint_every','batch_size','min_devices'):
# Guard input contract (`not isinstance(cfg[name], int) or isinstance(cfg[name], bool) or cfg[name] < 1`) and fail fast if violated.
if not isinstance(cfg[name],int) or isinstance(cfg[name],bool) or cfg[name] < 1:
raise ValueError(name+' must be a positive integer')
# Guard input contract (`cfg['batch_size'] > 64 or not 0 <= cfg['momentum'] < 1 or (not 0 < cfg['learning_rate'] < 1)`) and fail fast if violated.
if cfg['batch_size'] > 64 or not 0 <= cfg['momentum'] < 1 or not 0 < cfg['learning_rate'] < 1:
raise ValueError('invalid training configuration')
return cfg
def launch(root, config=None, timeout=10.):
root=Path(root)
root.mkdir(parents=True,exist_ok=True)
cfg=default_config(**(config or {}))
# Guard input contract (`timeout <= 0`) and fail fast if violated.
if timeout <= 0:
raise ValueError('timeout must be positive')
atomic_json(root/'config.json',cfg)
worker=root/'worker.py'
worker.write_text(WORKER)
# Configure environment variable before initializing the runtime.
env=dict(os.environ,JAX_PLATFORMS=cfg['backend'],PYTHONUNBUFFERED='1')
started=time.perf_counter()
process=subprocess.Popen([sys.executable,str(worker),str(root)],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,env=env)
timed_out=False
try:
stdout,stderr=process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
timed_out=True
process.terminate()
try:
stdout,stderr=process.communicate(timeout=1.)
except subprocess.TimeoutExpired:
process.kill()
stdout,stderr=process.communicate(timeout=1.)
wall=time.perf_counter()-started
events=[]
malformed=[]
for line in stdout.splitlines():
if not line.strip():
continue
try:
event=json.loads(line)
if not isinstance(event,dict) or 'event' not in event:
raise ValueError('not an event')
events.append(event)
except (ValueError,TypeError):
malformed.append(line)
status='timed_out' if timed_out else 'completed' if process.returncode == 0 and not malformed and events and events[-1]['event']=='completed' else 'failed'
# Compute deterministic cryptographic digest `result` for provenance verification.
result=dict(status=status,returncode=process.returncode,wall_s=wall,events=events,stderr=stderr,
worker_hash=hashlib.sha256(WORKER.encode()).hexdigest(),config=cfg,malformed_stdout=malformed)
atomic_json(root/'run.json',result)
return result
# Step 4 — Add content-addressed publication and selection: Publication and activation are separate; rollback uses the same...
def publish(store, artifact):
"""Content-address one JSON artifact. Publishing does not activate it."""
store=Path(store)
identifier=digest(artifact)
atomic_json(store/'artifacts'/(identifier+'.json'),artifact)
return identifier
def read_artifact(store, identifier):
# Guard input contract (`len(identifier) != 64 or any((c not in '0123456789abcdef' for c in identifier))`) and fail fast if violated.
if len(identifier) != 64 or any(c not in '0123456789abcdef' for c in identifier):
raise ValueError('invalid artifact identifier')
artifact=json.loads((Path(store)/'artifacts'/(identifier+'.json')).read_text())
# Guard input contract (`digest(artifact) != identifier`) and fail fast if violated.
if digest(artifact) != identifier:
raise ValueError('artifact digest mismatch')
return artifact
def artifact_metrics(artifact):
"""Independent held-out arithmetic for this fixed linear-model release contract."""
params=artifact.get('state',{}).get('params',[])
# Guard input contract (`len(params) != 2 or any((not math.isfinite(float(v)) for v in params))`) and fail fast if violated.
if len(params)!=2 or any(not math.isfinite(float(v)) for v in params):
raise ValueError('invalid model parameters')
inputs=(-1.5,-.37,.22,1.5)
mse=sum((params[0]*x+params[1]-(2*x+1))**2 for x in inputs)/len(inputs)
return dict(held_out_mse=mse,acceptance_limit=1.,passed=mse<=1.)
def activate(store, identifier):
artifact=read_artifact(store,identifier)
# Guard input contract (`artifact.get('validation', {}).get('passed') is not True`) and fail fast if violated.
if artifact.get('validation',{}).get('passed') is not True:
raise ValueError('artifact has no passing validation evidence')
required=['worker_hash','config_hash','data_hash','state']
# Guard input contract (`any((name not in artifact for name in required))`) and fail fast if violated.
if any(name not in artifact for name in required):
raise ValueError('artifact provenance incomplete')
# Guard input contract (`not artifact_metrics(artifact)['passed']`) and fail fast if violated.
if not artifact_metrics(artifact)['passed']:
raise ValueError('held-out model validation failed')
previous=None
pointer=Path(store)/'active.json'
if pointer.exists():previous=json.loads(pointer.read_text())['current']
atomic_json(pointer,dict(current=identifier,previous=previous))
return identifier
def rollback(store):
pointer=json.loads((Path(store)/'active.json').read_text())
# Guard input contract (`not pointer.get('previous')`) and fail fast if violated.
if not pointer.get('previous'):raise ValueError('no previous version')
return activate(store,pointer['previous'])
# Step 5 — Rehearse a local release and rollback: The artifact contains a checkpoint from an actual successful JAX job.
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory(prefix='ops-release-') as folder:
base=Path(folder)
run=launch(base/'run')
state=json.loads((base/'run'/'checkpoint.json').read_text())
store=base/'registry'
artifact=dict(worker_hash=run['worker_hash'],config_hash=state['config_hash'],data_hash=state['data_hash'],state=state,validation={'passed':True,'check':'completed CPU fixture and checksum'})
version_a=publish(store,artifact)
assert not (store/'active.json').exists()
activate(store,version_a)
version_b=publish(store,dict(artifact,release_note='reviewed metadata revision'))
activate(store,version_b)
rejected=publish(store,dict(artifact,validation={'passed':False}))
before=(store/'active.json').read_bytes()
try:
activate(store,rejected)
except ValueError:
rejected_without_change=(store/'active.json').read_bytes()==before
else:
raise AssertionError('bad release activated')
assert rejected_without_change
assert rollback(store)==version_a
chosen=json.loads((store/'active.json').read_text())
assert chosen['current']==version_a
print('first / second content IDs:',version_a,version_b)
print('failed gate kept selection:',rejected_without_change,'rollback restored first:',chosen['current']==version_a)Expected result: Publishing leaves the active pointer unchanged. A failed validation gate cannot change it, and rollback selects the first verified content ID again.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
first / second content IDs: 810b956e3376228ff3c1f03f469feb3ab26653b488ba51271e82277672590b80 aed1378a7842598237e59c5d189b13d486068b6284d9c1ca3d4e865866ba6990
failed gate kept selection: True rollback restored first: True
first / second content IDs: 810b956e3376228ff3c1f03f469feb3ab26653b488ba51271e82277672590b80 aed1378a7842598237e59c5d189b13d486068b6284d9c1ca3d4e865866ba6990
failed gate kept selection: True rollback restored first: True
PASS: operations-04
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Identity follows content rather than key insertion order
# Experiment — Identity follows content rather than key insertion order: Canonical encoding removes an incidental representation...
assert digest({'a':1,'b':2})==digest({'b':2,'a':1})
assert digest({'a':1,'b':2})!=digest({'a':1,'b':3})Expected: Equivalent mappings share an ID; changed values do not.
Canonical encoding removes an incidental representation difference but preserves meaningful payload changes.
Detect corruption before activation
# Experiment — Detect corruption before activation: A filename containing a hash is not enough: the reader must...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
identifier=publish(folder,artifact)
# Write the serialized artifact payload to disk.
(Path(folder)/'artifacts'/(identifier+'.json')).write_text('{}')
# Run the boundary check and catch the expected exception:
try:
activate(folder,identifier)
except ValueError as error:
assert 'digest' in str(error)
else:
raise AssertionError('corruption accepted')
assert not (Path(folder)/'active.json').exists()Expected: The corrupted artifact is rejected before any pointer appears.
A filename containing a hash is not enough: the reader must recompute and compare the payload digest.
Reject an actually degraded model
# Experiment — Reject an actually degraded model: Measured validation and claimed validation are distinct.
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
stable=publish(folder,artifact)
activate(folder,stable)
degraded=dict(artifact,state=dict(artifact['state'],params=[100.,-100.]))
candidate=publish(folder,degraded)
assert artifact_metrics(degraded)['held_out_mse'] > 1.
before=(Path(folder)/'active.json').read_bytes()
try:activate(folder,candidate)
except ValueError as error:assert 'held-out' in str(error)
else:raise AssertionError('degraded model activated')
assert before==(Path(folder)/'active.json').read_bytes()Expected: The known held-out relationship detects the changed model despite its passing claim.
Measured validation and claimed validation are distinct. The local gate recomputes the prediction error before selection.
Make it yours
Where to put your code: Append your solution at the bottom of phases/16-operations/04-changes-rollback-and-artifact-provenance/code/main.py (or inside the Make it yours cell of exercises/operations-04.ipynb) and run python3 phases/16-operations/04-changes-rollback-and-artifact-provenance/code/main.py to verify your prediction before opening the reference solution.
Publish two compatible validated artifacts that differ in an explicit release note. Activate both in sequence, then rollback and verify the exact selected ID and content.
Show a writing guide and starter scaffold
How to write this exercise code: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
tempfile.TemporaryDirectory(...)— Calltempfile.TemporaryDirectorywith your updated parameters or inputs from this lesson's workspace.publish(...)— Callpublishwith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
2. Step-by-step implementation plan:
- Create an isolated temporary directory to run and inspect artifacts safely:
# Exercise solution: Publish two compatible validated artifacts that differ in an explicit...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
one = publish(...) # TODO: compute one
two = publish(...) # TODO: compute two
activate(folder,one)
activate(folder,two)
assert rollback(folder) # TODO: complete assertion check
assert read_artifact(folder,one)['release_note'] # TODO: complete assertion check
Reveal the reference solution
# Exercise solution: Publish two compatible validated artifacts that differ in an explicit...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
one=publish(folder,dict(artifact,release_note='candidate one'))
two=publish(folder,dict(artifact,release_note='candidate two'))
activate(folder,one)
activate(folder,two)
assert rollback(folder)==one
assert read_artifact(folder,one)['release_note']=='candidate one'Reject an incomplete provenance record
Transfer / diagnosis
Construct a payload claiming passing validation but lacking the worker-source hash. Verify that it cannot become active.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
record(...)— Callrecordwith your updated parameters or inputs from this lesson's workspace.tempfile.TemporaryDirectory(...)— Calltempfile.TemporaryDirectorywith your updated parameters or inputs from this lesson's workspace.
2. Step-by-step implementation plan:
- Create an isolated temporary directory to run and inspect artifacts safely:
- Run the boundary check and catch the expected exception:
# Reject an incomplete provenance record (Transfer / diagnosis): The gate establishes only the declared local contract; it...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
incomplete = dict(...) # TODO: compute incomplete
del incomplete['worker_hash']
identifier = publish(...) # TODO: compute identifier
# Run the boundary check and catch the expected exception:
try:
activate(folder,identifier)
except ValueError:
pass
else:
raise AssertionError('missing provenance accepted')
Show a hint
A boolean flag does not replace required source/config/data evidence.
Reveal reference solution and reasoning
# Reject an incomplete provenance record (Transfer / diagnosis): The gate establishes only the declared local contract; it...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
incomplete=dict(artifact)
del incomplete['worker_hash']
identifier=publish(folder,incomplete)
# Run the boundary check and catch the expected exception:
try:
activate(folder,identifier)
except ValueError:
pass
else:
raise AssertionError('missing provenance accepted')The gate establishes only the declared local contract; it still does not authenticate who supplied those claims.
Check rollback without history
Transfer / diagnosis
Attempt rollback before any previous artifact exists, then verify the first activation remains selected.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
history(...)— Callhistorywith your updated parameters or inputs from this lesson's workspace.tempfile.TemporaryDirectory(...)— Calltempfile.TemporaryDirectorywith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
2. Step-by-step implementation plan:
- Create an isolated temporary directory to run and inspect artifacts safely:
- Run the boundary check and catch the expected exception:
# Check rollback without history (Transfer / diagnosis): A runbook needs a defined failure path when no compatible...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
identifier = publish(...) # TODO: compute identifier
activate(folder,identifier)
before = ... # TODO: compute before
# Run the boundary check and catch the expected exception:
try:
rollback(folder)
except ValueError:
pass
else:
raise AssertionError('invented rollback history')
assert before # TODO: complete assertion check
Show a hint
A failed rollback must not clear the current selection.
Reveal reference solution and reasoning
# Check rollback without history (Transfer / diagnosis): A runbook needs a defined failure path when no compatible...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
identifier=publish(folder,artifact)
activate(folder,identifier)
before=(Path(folder)/'active.json').read_bytes()
# Run the boundary check and catch the expected exception:
try:
rollback(folder)
except ValueError:
pass
else:
raise AssertionError('invented rollback history')
assert before==(Path(folder)/'active.json').read_bytes()A runbook needs a defined failure path when no compatible previous version is available.
Diagnose the result
If a candidate cannot activate, distinguish a digest mismatch, missing provenance and a failed quality gate. Do not repair a corrupt payload by renaming it to its newly computed hash without understanding the change. If rollback fails, verify that previous content still exists and remains compatible before editing the selection pointer.
Carry forward
- Content identity, validation and activation answer separate questions.
- Rollback must be rehearsed against the actual selection and compatibility contracts.
Keep your evidence
Keep source/configuration/data hashes, actual held-out candidate predictions, the rejected degraded release and unchanged active pointer, followed by verified rollback.
Save your modified code, its output, and an explanation of your prediction. Checkpoint answers and exercise evidence are tracked separately.
Saved in this browser. Export or back up your portfolio anytime in My learning.
Verified on local CPU. Stage to a Cloud TPU VM via Cloud TPU setup to record accelerator execution receipts (TODO: add automated Cloud TPU CI execution receipts).