all_lessons/ray/13lesson 14 / 17

Part III - Cluster and platform layer

The ML Platform Bridge

Lesson 12 made the whole thing run on a real cluster: a head node, autoscaling worker groups, KubeRay turning a YAML file into pods. But we still have five libraries that we have only ever met one at a time — Data (09), Train (10), Tune (08), Serve (11). A production ML system is never one of them; it is all of them in a line, and the line is where systems actually break. This lesson reads the book's "Ray AI Runtime" chapter not as a product to learn but as the answer to one question: how do these libraries compose into a platform, and what is the object that crosses every boundary between them? The answer is the checkpoint — and the sharpest hazard in the whole pipeline lives in the handoff it travels through.

Book source
Chapter 10, "Getting Started with the Ray AI Runtime" - AIR concepts, Datasets, Preprocessors, Trainers, Tuners, Checkpoints, offline batch prediction, Deployments, execution, memory, failure, and autoscaling. Calibrated to current Ray: the older "AIR" umbrella is now expressed directly through the library APIs (ray.data, ray.train.torch.TorchTrainer, ray.tune.Tuner, ray.serve), with ray.train.Checkpoint as the shared handoff object and preprocessors fitted as ordinary Ray Data transforms saved alongside it.
Linear position
Prerequisite: Lessons 09–11 — Ray Data (block-streaming transforms), Ray Train (N worker actors producing a checkpoint), Ray Tune (trials racing under a scheduler), Ray Serve (replica actors loading a model behind HTTP) — and lesson 12 (the cluster they all run on).
New capability: Reason about an end-to-end ML pipeline as a composition of Ray libraries joined by one artifact; identify and prevent train/serve skew at the feature-transform boundary; and choose deliberately between one unified runtime and a best-of-breed stack with the blast-radius and coupling trade-offs made explicit.
The plan
Five moves. (1) See the platform as four stages around one runtime, and name the object that crosses every boundary — the checkpoint. (2) Trace that checkpoint through Train → Tune → batch prediction → Serve and watch it be the universal handoff. (3) Meet the critical hazard — train/serve skew — and work the numeric bug where a live-fitted preprocessor silently destroys accuracy. (4) Lay out the platform trade-off: one unified runtime vs a best-of-breed stack, with blast radius and coupling priced in. (5) Sketch the multi-stage pipeline's memory, failure, and autoscaling behavior at altitude, then hand the deep failure model to lesson 14.

1 · A platform is four stages around one runtime

Each library we have met answers a different question about the same model. Put them in the order a model actually moves through and the platform appears:

1Data (09) — read raw input as a stream of blocks, and fit a preprocessor on the training data: compute the feature transform (normalization stats, a vocabulary, an encoder) and apply it.
2Train (10) — N worker actors each hold a model replica, consume sharded preprocessed data, exchange gradients, and emit a checkpoint.
3Tune (08) — run many Train trials with different configs; the scheduler kills losers early; each surviving trial resumes from and produces checkpoints, and you select the best one.
4Serve (11) / batch — load the selected checkpoint into replica actors (online) or into map_batches (offline) and answer requests.

The book calls the umbrella over these "AIR." The durable idea is not the brand — it is that all four run on one Ray runtime, sharing one object store (lesson 04), so a value produced by one stage can be handed to the next without a glue service, a separate scheduler, or a round-trip through external storage you have to wire up by hand. That shared substrate is the platform. And the value that crosses every boundary is a single kind of object.

Define: checkpoint
A checkpoint is the serialized, durable snapshot of everything needed to reconstruct a usable model: the trained weights, plus the metadata and any fitted state required to use them correctly (optimizer state for resuming, and — this is the part everyone forgets — the fitted preprocessor). In Ray it is a ray.train.Checkpoint: a directory of files on durable storage (local, NFS, or S3) that any stage can load. It is the only object designed to cross stage boundaries, which is exactly why it is the handoff contract of the whole platform.

2 · The checkpoint is the universal handoff

Watch one checkpoint cross every boundary. The same object that Train writes is the object Tune resumes from, the object batch prediction loads, and the artifact a Serve deployment opens. Nothing else travels the full length of the pipeline.

Train writes itAt the end of (or periodically during) training, the workers call train.report(metrics, checkpoint=Checkpoint.from_directory(d)). The checkpoint lands on durable storage. This is also the fault-tolerance boundary: a worker dying mid-run resumes from the last checkpoint, not from epoch 0 (lesson 10).
Tune resumes from itA Tune trial is a Train run. The scheduler (ASHA, lesson 08) pauses a promising trial and later resumes it — from its checkpoint. When the search ends, result_grid.get_best_result().checkpoint is the one object you carry forward. Tune does not invent a new artifact; it selects among checkpoints.
Batch prediction loads itOffline scoring is a Ray Data job: ds.map_batches(Predictor, ...) where Predictor.__init__ loads the checkpoint once per actor and __call__ scores a batch. Same object, no online machinery — just the checkpoint inside a data pipeline.
Serve loads itAn online deployment's replica calls Checkpoint.from_directory(uri) in its __init__ (lesson 11's "load the expensive thing once"). The artifact a deployment loads is the artifact Train produced — byte-for-byte the same directory.

This is why the platform is not five systems bolted together: a single object type, written once, flows the whole length. The handoff contract is "produce a checkpoint; consume a checkpoint." Everything else — schedulers, replicas, blocks — is local to a stage. But notice the quiet assumption hiding in that contract: the checkpoint must contain everything the next stage needs. If it carries the weights but not the feature transform, the boundary leaks. That leak has a name.

3 · Train/serve skew — the hazard at the feature boundary

The model never sees raw input. Between raw data and the model sits a preprocessor: a learned feature transform — a normalization (subtract mean, divide by standard deviation), a tokenizer, a categorical encoder, a learned vocabulary. It is fitted on data: the normalizer learns the training mean and standard deviation; the encoder learns which categories exist. The model's weights were tuned assuming inputs arrive already passed through that specific fitted transform.

Define: preprocessor & train/serve skew
A preprocessor is the fitted feature transform applied to inputs before the model. Train/serve skew is the failure where the transform applied at serving time differs — even subtly — from the one fitted at training time, so the model sees a different input distribution online than it learned from. It produces no error, no exception, no crash. The service returns 200 OK with confident, wrong answers. It is the single most common silent ML-production bug, and it lives precisely at the Data→Serve boundary the checkpoint is supposed to span.

The bug, worked in numbers

Suppose one feature is a user's account age in days. On the training set it has mean = 50, standard deviation = 30. The normalizer fitted at training time computes z = (x − 50) / 30. The model learned its weights against z-scores in roughly the [−2, +3] range — a typical account is age 50, which maps to z = 0; a 6-month account, age 180, maps to z = (180 − 50)/30 ≈ 4.3, a meaningful "this is an unusually old account" signal.

Now the serving code makes the classic mistake: instead of loading the saved training mean and standard deviation, it normalizes each incoming request batch by that batch's own statistics. A burst of long-tenured "power users" arrives — a request batch with mean = 200, standard deviation = 40. Trace the same age-180 user through the wrong transform:

The silent collapse, step by step
Training-time (correct) transform for the age-180 user: z = (180 − 50) / 30 ≈ 4.3 — the model reads "very old account," exactly the signal it trained on.

Serving-time (skewed) transform, normalizing by the live batch's mean 200 / std 40: z = (180 − 200) / 40 = −0.5 — the model reads "slightly below average account."

The very same raw input (age 180) becomes +4.3 at training and −0.5 at serving: a swing of nearly 5 standard deviations on one feature. The model was never trained on this mapping, so its output is meaningless — yet every request returns a confident probability and a 200 status code. Accuracy on this slice silently collapses from, say, 0.93 to near chance (0.50), and the only symptom is a downstream metric drifting two weeks later with no exception in any log. The bug is not in the model; it is in the boundary.

The fix: fit on training data, save with the checkpoint, reuse byte-for-byte

The transform must be fitted once on the training data and then frozen and saved alongside the model in the checkpoint, so Serve loads the identical mean = 50 / std = 30 and applies it to every request, regardless of what the live batch looks like. The checkpoint's job is to be the single source of truth for both the weights and the transform that the weights expect. If both ride in the same checkpoint, the boundary cannot leak — there is only one transform in existence and every stage reads it.

import ray
from ray import serve
from ray.train.torch import TorchTrainer
from ray.train import ScalingConfig, Checkpoint
from ray.data.preprocessors import StandardScaler

# --- STAGE 1+2: Data fits the preprocessor on TRAINING data, Train consumes it ---
train_ds = ray.data.read_parquet("s3://data/train")

scaler = StandardScaler(columns=["account_age_days"])   # the feature transform
scaler.fit(train_ds)                  # learns mean=50, std=30 from TRAINING data ONLY
train_ds = scaler.transform(train_ds) # apply the FITTED transform to training data

def train_loop(config):
    shard = ray.train.get_dataset_shard("train")
    model = build_and_fit(shard)      # weights learn against the scaled features
    # CRITICAL: save the fitted scaler INSIDE the checkpoint, with the weights.
    ckpt = save_model_and_preprocessor(model, scaler)   # -> Checkpoint directory
    ray.train.report({"val_acc": evaluate(model)}, checkpoint=ckpt)

trainer = TorchTrainer(
    train_loop,
    scaling_config=ScalingConfig(num_workers=4, use_gpu=True),
    datasets={"train": train_ds},
)
result = trainer.fit()
best_ckpt = result.checkpoint            # the universal handoff object

# --- STAGE 4: Serve loads the SAME checkpoint AND the SAME fitted preprocessor ---
@serve.deployment(num_replicas=2, ray_actor_options={"num_gpus": 1})
class OnlineModel:
    def __init__(self, ckpt: Checkpoint):
        self.model, self.scaler = load_model_and_preprocessor(ckpt)  # mean=50, std=30
    async def __call__(self, request):
        row = await request.json()
        row = self.scaler.transform_batch(row)   # SAME transform as training — no skew
        return {"score": self.model(row)}

serve.run(OnlineModel.bind(best_ckpt))

The whole defense is two lines of discipline: scaler.fit runs on training data only, and the fitted scaler is serialized into the checkpoint so the deployment's __init__ reconstructs the exact same transform. Never call fit on request data; never recompute statistics online. The preprocessor is part of the model, not part of the request handler.

4 · The platform trade-off: one runtime vs best-of-breed

Why build the whole pipeline on one Ray runtime at all? The alternative is a best-of-breed stack: a dedicated feature store, a separate training service, and a Kubernetes-native serving system (KServe, Triton) — each the best tool for its stage, wired together with glue. Both are real, defensible architectures. The choice turns on a single tension: a shared substrate buys you frictionless handoffs but concentrates your operational risk.

Define: blast radius
Blast radius is the set of things that fail when one component fails. A large blast radius means one fault takes down many stages; a small one means failures stay contained. It is the operational price of coupling — the more stages share a substrate, the wider any single failure spreads.
DimensionOne unified runtime (Ray AIR-style)Best-of-breed stack
HandoffsOne checkpoint object crosses every boundary; data stays in the shared object store in-cluster — no glue services, no re-serialization between stages.Every boundary is a network + format contract: feature store → training service → serving system, each with its own artifact format and a translation step you maintain.
Skew riskOne runtime can carry the same fitted preprocessor through every stage, so train/serve skew is preventable by construction (§3).Skew risk at every boundary: the feature store, the trainer, and the serving system can each apply a subtly different transform unless you enforce one externally.
Blast radiusConcentrated. The shared cluster layer (head node / GCS, lesson 12) is one fault domain: a control-plane failure can take down data, training, and serving at once.Contained. A serving outage need not touch training; stages fail independently, which is exactly what you want for an SLO-bound online tier.
CouplingTighter: stages share a Ray version, a Python environment, and a scheduler. An upgrade ripples; a noisy training job can contend for the cluster's object store.Looser: each stage versions and scales on its own clock — at the cost of integration work and N systems to operate, monitor, and on-call.
Worked number — when each wins
Picture a 200-node cluster. Unified: data, training, and a small serving tier all run on it, sharing the object store; a checkpoint hands off in-cluster with zero external round-trips. But the head node is a single fault domain — if it falls over, the worked answer is brutal: 100% of stages go dark, including the online tier serving live users. Best-of-breed: put serving on its own KServe cluster. Now a training-cluster failure takes down 0% of online serving — the blast radius shrinks from "everything" to "the offline pipeline" — but you pay for it: every model promotion now crosses a boundary where, in §3's terms, a mismatched transform can re-introduce a 5σ skew unless you ship the preprocessor across that boundary too. The rule of thumb: unify when your team is small and the pipeline is Python-native end to end (the glue would cost more than the blast radius); split out the online tier first when an SLO makes serving's blast radius unacceptable.

Neither answer is universally right. The honest reading of AIR is that it optimizes the handoff-and-skew axis hard, and asks you to accept a concentrated blast radius — which is why lesson 12's head-node SPOF and lesson 14's failure model are not side topics but the bill that comes due for unification.

5 · Memory, failure, and autoscaling of the multi-stage pipeline

A pipeline that chains four stages has behavior no single stage shows. At altitude — the deep failure model is lesson 14 — three properties matter:

Memory
Stages contend for one shared object store (lesson 04). A Data stage materializing large blocks while Train holds model replicas and Tune runs several trials can together exceed object-store memory, forcing spilling to disk — a latency cliff that slows every stage at once. The checkpoint, by contrast, lives on durable storage, not the object store, so it survives memory pressure and node loss.
Failure
The checkpoint is the recovery boundary for the whole line. A Train worker dies → resume from its last checkpoint (not epoch 0). A Tune trial dies → the scheduler restarts it from its checkpoint. A Serve replica dies → a new one re-loads the same checkpoint. One artifact, written to durable storage, is what makes every stage individually recoverable.
Autoscaling
Stages have opposite resource shapes, so they scale on different axes (lesson 12's demand-based autoscaler). Data preprocessing wants many cheap CPU workers; Train wants a few expensive GPU workers held as a gang; Serve wants GPU replicas that grow with live traffic. Putting them in one cluster means heterogeneous worker groups, not one uniform pool.
Worked number — CPU/GPU balance across the boundary
Suppose Train needs 4 GPUs fed at 8,000 preprocessed rows/sec to keep them busy, and each CPU preprocessing worker does 500 rows/sec. Then you need 8000 / 500 = 16 CPU workers feeding 4 GPU workers — a 4:1 CPU:GPU ratio, sized so the GPUs never starve (lesson 09's producer/consumer balance). Provision one uniform pool instead and you either starve the GPUs (CPU-bound, expensive accelerators idle) or over-buy CPU. The platform lesson: the cluster is heterogeneous because the pipeline is, and the autoscaler's per-worker-group min/max is how you encode that 4:1 shape.

6 · Failure modes & checklist

Failure modes

  • Live-fitted preprocessor (train/serve skew). Normalizing each request batch by its own statistics instead of the saved training mean/std. Silent — 200 OK, confident wrong answers, accuracy collapse on shifted slices (§3). The single most common silent ML-prod bug.
  • Checkpoint without its preprocessor. Saving the weights but not the fitted transform. The deployment loads weights and reinvents the transform — and reinvents it wrong. The checkpoint must carry both.
  • Picking by metric, losing lineage. Tune returns the best-metric checkpoint, but if you cannot reproduce which data/config/code/preprocessor produced it, you cannot retrain or debug it. Carry the lineage with the artifact.
  • Object-store contention across stages. Data + Train + Tune materializing at once overflows the shared object store and triggers spilling, slowing every stage. Stage materialization, or isolate heavy stages.
  • Unbounded blast radius for the online tier. Running an SLO-bound service on the same control plane as batch training: one head-node fault takes down live users. Consider isolating the serving tier (§4).

Implementation checklist

  • Is the preprocessor fit on training data only, and saved inside the checkpoint?
  • Does the serving path load that exact saved transform — never recomputing statistics from request data?
  • Is the checkpoint the single artifact every stage consumes (Train, Tune-resume, batch, Serve)?
  • Are dataset version, config, code, environment, and preprocessor recorded with the checkpoint as lineage?
  • Are checkpoints on durable storage (S3/NFS), not just the object store?
  • Did you size the CPU:GPU worker-group ratio so preprocessing never starves training?
  • For an SLO-bound online tier, did you decide deliberately whether to share the cluster or isolate it (blast radius)?

Checkpoint exercise

Try it
You have a tabular fraud model. One feature is transaction amount, with training mean = $40 and std = $120. Tonight a marketing push sends a batch of high-value transactions: a live request batch with mean = $500, std = $200. (a) A $300 transaction arrives. Compute its normalized value under the correct (training-fitted) transform and under a live-batch-fitted transform; report the gap in standard deviations. (b) The team proposes isolating the serving tier onto its own cluster to shrink blast radius. State exactly what new boundary that creates and what must cross it to keep §3's skew from reappearing. (c) Now flip the assumption: the model is retrained nightly and traffic distribution is genuinely drifting week to week. Does freezing the training-time mean/std stay right, or does the correct answer change — and what does that tell you about when a fixed preprocessor is itself the bug? (The point: the fix in §3 is "use the transform the weights were trained against"; if the weights are stale relative to the world, the real remedy is retraining, not re-fitting the transform online.)

Where this points next

We have leaned on one phrase repeatedly — "a worker dies and resumes from its checkpoint" — as if that recovery were automatic and free. It is neither. How does Ray know an object was lost, and when can it rebuild it versus when is it gone for good? What does "at-least-once" execution mean for a stage with side effects? Lesson 14 opens the failure model the platform quietly relies on: lineage-based reconstruction of lost objects, where it works and where it can't (actor state, side effects, a dead owner's ray.put), the retry-vs-idempotency contract, and the observability — dashboard, timeline, ray memory — you need to see the object-store contention and stragglers this lesson only named. The platform composes cleanly; lesson 14 is what happens when a piece of it dies.

Takeaway
A Ray ML platform is four stages — Data, Train, Tune, Serve — running on one runtime and one shared object store, joined by a single object that crosses every boundary: the checkpoint. The same checkpoint Train writes is the one Tune resumes from, batch prediction loads, and a Serve replica opens. The critical hazard lives in the feature boundary it spans: train/serve skew, where a preprocessor fitted differently at serving time (e.g. normalizing by a live batch's mean 200 instead of the saved training mean 50) silently swings a single input by ~5 standard deviations and collapses accuracy with no error in any log. The fix is discipline, not API: fit the preprocessor on training data only and save it inside the checkpoint, so every stage applies the identical transform. The platform trade-off is unification vs best-of-breed: one runtime buys frictionless handoffs and skew-prevention-by-construction but concentrates blast radius and tightens coupling; a best-of-breed stack contains failure per stage at the cost of glue and a skew risk at every boundary. Unify when small and Python-native; split out the online tier first when an SLO makes its blast radius unacceptable.

Interview prompts