Phase 16 / Workload operations
Health, logs, and workload observability
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 85 minutes · CPU practice
Build useful summaries from structured events emitted by a real worker. Define throughput with explicit numerator, denominator and synchronization boundary.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Accelerator jobs and runtime lifecycle
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/02-health-logs-and-workload-observability/code/main.py.\.venv\Scripts\python.exe phases/16-operations/02-health-logs-and-workload-observability/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-02.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: The healthy run records processed examples. The failure occurs at step , with checkpoint step , so one completed update must be replayed.
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
- Build useful summaries from structured events emitted by a real worker.
- Define throughput with explicit numerator, denominator and synchronization boundary.
- Estimate uncheckpointed work from progress and checkpoint events.
The problem
A process is alive and prints messages, yet useful training may have stopped. What signals distinguish real progress from noise, and what could be lost if the process fails now? We will measure synchronized updates, count examples and compare progress with the latest committed checkpoint using events emitted by actual runs.
The idea
Logs tell us what a process reported; accepted checkpoints tell us what can be resumed. Monitoring should connect these signals without treating them as interchangeable. Their difference helps estimate the consequences of a failure.
Keep observed progress separate from recoverable progress
If the live process reports step 42 and the newest accepted checkpoint is step 40, there are two completed steps beyond that recovery boundary. Whether they must be replayed depends on the recovery contract and recorded input state.
Name metric units and update points: examples processed, valid tokens, optimizer steps and accepted checkpoint steps are different counters. A sudden flat line can indicate stalled computation, blocked logging or a failed collector; inspect another signal before assigning the cause.
Read the two progress curves at the same time coordinate and locate the failure. The gap describes lag in recoverable progress, while the slope of live progress describes reported work rate. Neither alone proves hardware utilization.
An event needs meaning and units
Each event has a monotonic timestamp and an event name. A progress event adds completed step, scalar loss, examples processed, synchronized update seconds and a hash of the resulting complete state. Checkpoint events record the step that was actually replaced on disk. A heartbeat would show liveness only; it must not increment examples or claim training progress. Monotonic time is useful for local durations but is not a globally comparable timestamp across unrelated hosts.
Time completed computation
JAX may return control before an operation finishes. The worker calls block_until_ready on the result before stopping its update timer. That timer includes dispatch and completion of one compiled minibatch update, but excludes JSON encoding, checkpoint writes, process startup and compilation. The ready event records a separate compile/warmup duration. A benchmark that times dispatch alone would understate service time.
Two throughputs answer different questions
For updates of examples, the numerator is examples. Divide by the sum of synchronized update durations to describe the update path. Divide by supervisor wall time to describe the short job end to end. The latter is lower here because this tiny workload spends much of its lifetime outside the compiled updates. Neither rate should be relabeled as a device hardware utilization percentage.
Freshness is about committed progress
The controlled incident fails after update , while the checkpoint cadence is every updates. Its last committed checkpoint is step , leaving one completed but uncommitted update. This is a count of potential replay work, not a time-based recovery objective. To express time at risk, use appropriate measured durations and explicitly distinguish completed work from work that might still be in flight.
A declining loss is not a health guarantee
The fixture fits a line with minibatch momentum updates. Loss may rise between batches because different examples are sampled, and momentum can overshoot. A lower recent loss does not prove input freshness, checkpoint durability or correct evaluation. Conversely, a noisy loss sequence does not prove a stalled process. Diagnose loss behavior using the optimization lessons; diagnose missing progress using event and process evidence.
Scope of this collector
This bounded lab captures stdout and stderr in memory and analyzes events after the child exits. That is appropriate for a few dozen events. A long-running service needs streamed, bounded logs, a durable sink, timestamps aligned for its fleet, and monitored queue/backpressure behavior. The lesson provides measurement contracts and actual failure evidence, not an operational telemetry backend.
Observed progress and committed progress diverge before failure
Predict before looking: Will the checkpoint line always equal the progress line?
Read the figure
The horizontal axis is completed update number for the failed run. The vertical axis is step index. The progress line reaches , while the checkpoint line advances at steps and , ending at . The vertical gap at the final point is one update. These values come from actual emitted events, not a hypothetical fleet trace.
Connect it to the computation
The staircase records when state becomes available for recovery. A gap is expected between checkpoints; it becomes operationally relevant when the process fails. Resuming from step repeats update using its restored optimizer and random state. This plot does not show elapsed recovery time or prove power-loss durability.
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/02-health-logs-and-workload-observability/code/main.py (or downloaded exercises/operations-02.py / exercises/operations-02.ipynb) and execute from the repository root with python3 phases/16-operations/02-health-logs-and-workload-observability/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 Summarize event boundaries
Append this block to main.py. Run python3 main.py in the CPU course environment.
# Step 4 — Summarize event boundaries: The summary retains work counts, timing boundaries and checkpoint...
def summarize(result):
updates=[e for e in result['events'] if e['event']=='progress']
checkpoints=[e['step'] for e in result['events'] if e['event']=='checkpoint']
count=sum(e['examples'] for e in updates)
update_s=sum(e['update_s'] for e in updates)
latest=max((e['step'] for e in updates),default=0)
checkpoint=max(checkpoints,default=0)
return dict(status=result['status'],updates=len(updates),examples=count,last_step=latest,checkpoint_step=checkpoint,
uncheckpointed_updates=max(0,latest-checkpoint),update_s=update_s,
update_examples_per_s=count/update_s if update_s else 0.,
job_examples_per_s=count/result['wall_s'] if result['wall_s'] else 0.,
update_duty_fraction=update_s/result['wall_s'] if result['wall_s'] else 0.)Code building blocks in this step
summarize(...)— Callsummarizewith your updated parameters or inputs from this lesson's workspace.sum(...)— Callsumwith your updated parameters or inputs from this lesson's workspace.
The summary retains work counts, timing boundaries and checkpoint lag separately.
5 Run a healthy job and a controlled incident
Append this block to main.py. Run python3 main.py in the CPU course environment.
# Step 5 — Run a healthy job and a controlled incident: Both summaries are derived from real child output; failures are...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory(prefix='ops-signals-') as folder:
observed=launch(Path(folder)/'healthy',dict(steps=12))
interrupted=launch(Path(folder)/'failed',dict(steps=12,fail_after=5))
healthy=summarize(observed)
incident=summarize(interrupted)
progress=[e for e in observed['events'] if e['event']=='progress']
assert healthy['examples']==96 and healthy['updates']==12
assert incident['last_step']==5 and incident['checkpoint_step']==4
assert incident['uncheckpointed_updates']==1
assert healthy['job_examples_per_s'] < healthy['update_examples_per_s']
print('healthy:',healthy)
print('incident:',incident)Code building blocks in this step
tempfile.TemporaryDirectory(...)— Calltempfile.TemporaryDirectorywith your updated parameters or inputs from this lesson's workspace.launch(...)— Calllaunchwith 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.
Both summaries are derived from real child output; failures are not invented status rows.
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 — Summarize event boundaries: The summary retains work counts, timing boundaries and checkpoint...
def summarize(result):
updates=[e for e in result['events'] if e['event']=='progress']
checkpoints=[e['step'] for e in result['events'] if e['event']=='checkpoint']
count=sum(e['examples'] for e in updates)
update_s=sum(e['update_s'] for e in updates)
latest=max((e['step'] for e in updates),default=0)
checkpoint=max(checkpoints,default=0)
return dict(status=result['status'],updates=len(updates),examples=count,last_step=latest,checkpoint_step=checkpoint,
uncheckpointed_updates=max(0,latest-checkpoint),update_s=update_s,
update_examples_per_s=count/update_s if update_s else 0.,
job_examples_per_s=count/result['wall_s'] if result['wall_s'] else 0.,
update_duty_fraction=update_s/result['wall_s'] if result['wall_s'] else 0.)
# Step 5 — Run a healthy job and a controlled incident: Both summaries are derived from real child output; failures are...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory(prefix='ops-signals-') as folder:
observed=launch(Path(folder)/'healthy',dict(steps=12))
interrupted=launch(Path(folder)/'failed',dict(steps=12,fail_after=5))
healthy=summarize(observed)
incident=summarize(interrupted)
progress=[e for e in observed['events'] if e['event']=='progress']
assert healthy['examples']==96 and healthy['updates']==12
assert incident['last_step']==5 and incident['checkpoint_step']==4
assert incident['uncheckpointed_updates']==1
assert healthy['job_examples_per_s'] < healthy['update_examples_per_s']
print('healthy:',healthy)
print('incident:',incident)Run the example
python3 phases/16-operations/02-health-logs-and-workload-observability/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-02)
"""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 — Summarize event boundaries: The summary retains work counts, timing boundaries and checkpoint...
def summarize(result):
updates=[e for e in result['events'] if e['event']=='progress']
checkpoints=[e['step'] for e in result['events'] if e['event']=='checkpoint']
count=sum(e['examples'] for e in updates)
update_s=sum(e['update_s'] for e in updates)
latest=max((e['step'] for e in updates),default=0)
checkpoint=max(checkpoints,default=0)
return dict(status=result['status'],updates=len(updates),examples=count,last_step=latest,checkpoint_step=checkpoint,
uncheckpointed_updates=max(0,latest-checkpoint),update_s=update_s,
update_examples_per_s=count/update_s if update_s else 0.,
job_examples_per_s=count/result['wall_s'] if result['wall_s'] else 0.,
update_duty_fraction=update_s/result['wall_s'] if result['wall_s'] else 0.)
# Step 5 — Run a healthy job and a controlled incident: Both summaries are derived from real child output; failures are...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory(prefix='ops-signals-') as folder:
observed=launch(Path(folder)/'healthy',dict(steps=12))
interrupted=launch(Path(folder)/'failed',dict(steps=12,fail_after=5))
healthy=summarize(observed)
incident=summarize(interrupted)
progress=[e for e in observed['events'] if e['event']=='progress']
assert healthy['examples']==96 and healthy['updates']==12
assert incident['last_step']==5 and incident['checkpoint_step']==4
assert incident['uncheckpointed_updates']==1
assert healthy['job_examples_per_s'] < healthy['update_examples_per_s']
print('healthy:',healthy)
print('incident:',incident)Expected result: The healthy run records processed examples. The failure occurs at step , with checkpoint step , so one completed update must be replayed.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
healthy: {'status': 'completed', 'updates': 12, 'examples': 96, 'last_step': 12, 'checkpoint_step': 12, 'uncheckpointed_updates': 0, 'update_s': 0.0003859163261950016, 'update_examples_per_s': 248758.58698834025, 'job_examples_per_s': 176.72946936655072, 'update_duty_fraction': 0.0007104457036284513}
incident: {'status': 'failed', 'updates': 5, 'examples': 40, 'last_step': 5, 'checkpoint_step': 4, 'uncheckpointed_updates': 1, 'update_s': 0.00017587607726454735, 'update_examples_per_s': 227432.86421969283, 'job_examples_per_s': 69.44720752337886, 'update_duty_fraction': 0.0003053525609047209}
healthy: {'status': 'completed', 'updates': 12, 'examples': 96, 'last_step': 12, 'checkpoint_step': 12, 'uncheckpointed_updates': 0, 'update_s': 0.0003807488828897476, 'update_examples_per_s': 252134.68591527935, 'job_examples_per_s': 174.31857639191733, 'update_duty_fraction': 0.0006913708669599339}
incident: {'status': 'failed', 'updates': 5, 'examples': 40, 'last_step': 5, 'checkpoint_step': 4, 'uncheckpointed_updates': 1, 'update_s': 0.00018524983897805214, 'update_examples_per_s': 215924.61413550316, 'job_examples_per_s': 70.63820657474201, 'update_duty_fraction': 0.0003271429098417335}
examples / synchronized seconds: 96 0.0003807488828897476
measured update duty fraction, not hardware utilization: 0.0006913708669599339
PASS: operations-02
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Recompute throughput independently
# Experiment — Recompute throughput independently: Summing work and time weights slow steps appropriately; an...
total_examples=sum(e['examples'] for e in progress)
total_time=sum(e['update_s'] for e in progress)
assert abs(total_examples/total_time-healthy['update_examples_per_s']) < 1e-6
assert all(e['update_s']>0 for e in progress)
print('examples / synchronized seconds:',total_examples,total_time)Expected: The summary matches the independently accumulated numerator and denominator.
Summing work and time weights slow steps appropriately; an unweighted mean of rates generally answers a different question.
Locate the incident boundary
# Experiment — Locate the incident boundary: A progress log is not a committed state artifact.
names=[e['event'] for e in interrupted['events']]
assert names[-1]=='failed'
assert [e['step'] for e in interrupted['events'] if e['event']=='checkpoint']==[2,4]
assert [e['step'] for e in interrupted['events'] if e['event']=='progress'][-1]==5Expected: The event order exposes exactly one completed update after the latest checkpoint.
A progress log is not a committed state artifact. Recovery must select the committed boundary.
Make it yours
Where to put your code: Append your solution at the bottom of phases/16-operations/02-health-logs-and-workload-observability/code/main.py (or inside the Make it yours cell of exercises/operations-02.ipynb) and run python3 phases/16-operations/02-health-logs-and-workload-observability/code/main.py to verify your prediction before opening the reference solution.
Compute the fraction of supervisor wall time spent inside synchronized updates, and state precisely what that fraction excludes.
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:
sum(...)— Callsumwith your updated parameters or inputs from this lesson's workspace.abs(...)— Callabswith 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:
- Set up the changed input or configuration parameters requested in the prompt.
- Run the target function or transformation and assert that the output matches your prediction.
# Exercise solution: Compute the fraction of supervisor wall time spent inside synchronized...
duty = sum(...) # TODO: compute duty
assert 0 # TODO: complete assertion check
assert abs(duty-healthy['update_duty_fraction']) # TODO: complete assertion check
print('measured update duty fraction, not hardware utilization:',duty)
Reveal the reference solution
# Exercise solution: Compute the fraction of supervisor wall time spent inside synchronized...
duty=sum(e['update_s'] for e in progress)/observed['wall_s']
assert 0 < duty < 1
assert abs(duty-healthy['update_duty_fraction']) < 1e-9
print('measured update duty fraction, not hardware utilization:',duty)Catch an average-of-rates error
Transfer / diagnosis
Two updates process examples each in and seconds. Compare the average rate with the rate of all completed work.
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:
error(...)— Callerrorwith your updated parameters or inputs from this lesson's workspace.sum(...)— Callsumwith 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:
- Set up the changed input or configuration parameters requested in the prompt.
- Run the target function or transformation and assert that the output matches your prediction.
# Catch an average-of-rates error (Transfer / diagnosis): The short step gets excessive influence when rates are...
durations = ... # TODO: compute durations
counts = ... # TODO: compute counts
wrong = sum(...) # TODO: compute wrong
right = sum(...) # TODO: compute right
assert abs(wrong-16/3) # TODO: complete assertion check
Show a hint
Sum examples and seconds first.
Reveal reference solution and reasoning
# Catch an average-of-rates error (Transfer / diagnosis): The short step gets excessive influence when rates are...
durations=[1.,3.]
counts=[8,8]
wrong=sum(n/t for n,t in zip(counts,durations))/2
right=sum(counts)/sum(durations)
assert abs(wrong-16/3)<1e-12 and right==4.The short step gets excessive influence when rates are averaged without accounting for their duration.
Change the checkpoint cadence
Transfer / diagnosis
Run with checkpoint cadence and fail after update . Predict the committed step and replay count.
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:
cadence(...)— Callcadencewith 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:
# Change the checkpoint cadence (Transfer / diagnosis): The run tests a changed operational policy rather than...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
changed = launch(...) # TODO: compute changed
stats = summarize(...) # TODO: compute stats
assert stats['checkpoint_step'] # TODO: complete assertion check
Show a hint
Expect the most recent multiple of the checkpoint interval before the failure.
Reveal reference solution and reasoning
# Change the checkpoint cadence (Transfer / diagnosis): The run tests a changed operational policy rather than...
# Create an isolated temporary directory to run and inspect artifacts safely:
with tempfile.TemporaryDirectory() as folder:
changed=launch(folder,dict(checkpoint_every=3,fail_after=5))
stats=summarize(changed)
assert stats['checkpoint_step']==3 and stats['uncheckpointed_updates']==2The run tests a changed operational policy rather than assuming every failure loses exactly one update.
Diagnose the result
If throughput is implausibly high, check synchronization and whether examples were counted twice. If checkpoint freshness looks perfect during a failure, verify that checkpoint events are emitted only after replacement completes. If timestamps appear out of order across hosts, do not subtract unrelated monotonic clocks.
Carry forward
- Choose metrics that answer a concrete operational decision.
- Specify the timing boundary before reporting a throughput or utilization-like ratio.
Keep your evidence
Keep raw structured events, observed work counts, synchronized timing units and checkpoint lag. Derive job and update throughput with different boundaries.
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).