all_lessons/ray/08lesson 9 / 17

Part II - Applications and libraries

Ray Tune: Experiments and Hyperparameter Search

Lesson 07 gave you RLlib: many cheap rollout actors feeding a few expensive learners, one training run at a time. But which learning rate? Which network width, which batch size, which discount factor? A single training run answers none of that — you need to run dozens of configurations and let the good ones survive. Ray Tune is that layer. The trap is to read it as "random search, but on more machines." It is something sharper: a control plane over a population of experiments that decides, while they run, which configs deserve more compute and which to kill. This lesson builds that control plane from the problem up, and makes the central engineering claim — that early stopping, not parallelism, is where the compute savings live — quantitative.

Book source
Chapter 5, "Hyperparameter Optimization with Ray Tune" - random search, why HPO is hard, Tune architecture, configuration, RLlib, and Keras examples. API wording is calibrated to current Ray: Tuner / TuneConfig / tune.report / tune.with_resources / ASHAScheduler.
Linear position
Prerequisite: Lesson 07 (RLlib) — one configured training run as a task/actor graph, with metrics reported per iteration. Also the task/actor/object-store model from Part I.
New capability: launch and govern a population of training runs, propose configs intelligently, and reallocate cluster compute away from doomed trials toward promising ones — turning "train it 50 times by hand" into one resource-aware experiment.
The plan
Five moves. (1) Establish why hyperparameter optimization (HPO) is genuinely hard — high-dimensional, expensive, black-box, no gradient — so you see why throwing machines at it is not the answer. (2) Decompose Tune into three separated concerns: the trial, the search algorithm (explore the space), and the scheduler (reallocate compute over time). (3) Make ASHA's successive halving quantitative with a worked epoch budget — the multiple-× saving that is the whole point. (4) Resource-aware concurrency and checkpointing — how Tune packs trials onto the cluster and pauses/resumes them. (5) Failure modes, the late-bloomer risk, and the hand-off to Ray Train.

1 · Why HPO is hard (and why machines alone don't fix it)

Stochastic gradient descent tunes a model's weights because the loss is differentiable in the weights — you have a gradient pointing downhill. Hyperparameters are different. Learning rate, batch size, network depth, dropout, weight decay — these sit outside the differentiable graph. You cannot take the gradient of "final validation accuracy" with respect to "learning rate," because evaluating that relationship requires training a whole model. HPO is therefore the optimization of a function with four hostile properties:

black-boxYou only learn the value of a config by running it end to end. No analytic form, no gradient — you observe, you cannot differentiate.
expensiveOne evaluation is one full (or partial) training run: minutes to days of GPU time. You get few samples for the money, so each one must count.
high-dimensionalFive hyperparameters with ten plausible values each is 10⁵ = 100,000 combinations. Grid search is exponential in dimensions; exhaustive coverage is hopeless.
noisyRe-run the same config with a new random seed and the metric moves. You are optimizing through a haze, so a 0.2% "improvement" may be noise.

Now the key insight that separates Tune from a job scheduler. Because evaluation is expensive and the budget is fixed, the question is never "can I run all the configs in parallel?" — you usually cannot afford to. The question is "given a fixed pile of GPU-hours, how do I spend them to find the best config?" That reframes HPO as a resource-allocation problem under uncertainty: partial results arrive at different times, and something must continuously decide which running experiments are worth feeding. That "something" is the control plane Tune provides. Parallelism is table stakes; allocation is the value.

The control-plane framing
"Random search on more machines" buys you a linear speedup: 8 machines, 8× the configs in the same wall-clock. Useful, but bounded by your hardware budget. The Tune framing buys you something parallelism cannot: it spends less compute per config by abandoning losers early, so the same GPU budget explores far more of the space. Section 3 shows this is often a 5–10× effective speedup on top of whatever parallelism you have — and it comes from a scheduler, not from more nodes.

2 · Three separated concerns: trial, search, scheduler

Tune's design cleanly splits three jobs that beginners tend to conflate. Define each at first use:

Trial
One hyperparameter config, trained as a Ray task or actor. A trial owns its config, reports a metric over time via tune.report(...), and writes checkpoints (saved model+optimizer state) so it can pause and resume. It is the unit of parallelism here — exactly the "tuning trial" named in lesson 00.
Search algorithm
Proposes the next config to try — this is exploration of the parameter space. Grid and random are stateless baselines; Bayesian methods (via Optuna or HyperOpt) build a model of the space from past trials and propose where the expected payoff is highest — exploitation of what was learned.
Scheduler
Decides, among already-running trials, which to stop early and which to keep funding — this is allocation of compute over time. ASHA, median-stopping, and PBT live here. The scheduler is where the GPU-hours are actually saved.

The separation matters because the two decisions answer different questions and can be mixed and matched. The search algorithm answers "where in the space should I look next?" The scheduler answers "of the trials I am already running, which are wasting my compute right now?" You can pair random search (cheap, embarrassingly parallel) with an aggressive scheduler, or Bayesian search (sample-efficient but sequential — it must see results before proposing) with a gentle one. The book's chapter walks random search first precisely because it is the robust baseline against which any clever search must justify itself.

Exploration vs exploitation, concretely. Pure random search is all exploration: it never uses what it learned, so it wastes samples re-checking obviously-bad regions, but it parallelizes perfectly and never gets stuck. Bayesian search exploits: after 20 trials it has a surrogate model and concentrates samples near the best region — far fewer trials to a good answer, but each proposal waits on prior results (a serial dependency, recall the Amdahl ceiling from lesson 00), and it can tunnel into a local optimum. The practical answer is usually random/grid for breadth plus a scheduler for the savings, escalating to Bayesian only when each trial is so expensive that sample-efficiency dominates.

3 · Successive halving: where the compute is actually saved

This is the heart of the lesson. ASHA — Asynchronous Successive Halving Algorithm — is the default scheduler worth understanding in full, because its arithmetic is the entire argument for using Tune over a loop.

The idea: do not train every config to completion to find out it was bad. A config that is in the bottom third after 1 epoch is very unlikely to be the winner after 27. So organize training into rungs — checkpoints at increasing budgets — and at each rung promote only the top fraction, killing the rest. A rung is a budget milestone (e.g. "1 epoch trained"); successive halving is the repeated cull at each rung. The grace period is the minimum budget every trial gets before it is eligible to be killed — your protection against murdering a config before it has had a fair chance.

Worked number — 81 configs, reduction factor 3
Take 81 configs, a reduction factor η = 3 (keep the top 1/3 at each rung), a grace period of 1 epoch, and a max of 27 epochs. Rungs sit at 1, 3, 9, 27 epochs.

Full training (no scheduler): 81 configs × 27 epochs = 2,187 epoch-units.

ASHA successive halving:
rung 0run all 81 configs for 1 epoch → 81 epoch-units. Keep top 27.
rung 1run the surviving 27 for 3 epochs (2 more each) → 54 epoch-units. Keep top 9.
rung 2run the surviving 9 for 9 epochs (6 more each) → 54 epoch-units. Keep top 3.
rung 3run the surviving 3 for 27 epochs (18 more each) → 54 epoch-units. One winner.
Total ≈ 81 + 54 + 54 + 54 = 243 epoch-units. That is 2,187 / 243 = 9× less compute than training everything to the end — and crucially, the winner still got its full 27 epochs. The savings come entirely from the 54 configs you killed at rung 0 after a single epoch, and the 18 you killed at rung 1. Stack this on top of parallelism: 8 GPUs running ASHA find the same winner roughly 9× cheaper than 8 GPUs running every config full-length — that is the 5–10× from §1, made of arithmetic, not hardware.

The "A" in ASHA — asynchronous — matters on a real cluster. Classic Hyperband waits for an entire rung to finish before promoting, so one straggler stalls the whole population (a synchronization barrier; recall the shuffle bottleneck of lesson 05). ASHA promotes a trial the moment enough peers have reported at its rung, so GPUs never sit idle waiting for the slowest trial. That is why it is the cluster default.

The late-bloomer risk is real
Successive halving assumes early performance predicts late performance. Sometimes it does not: a config with a low warm-up learning rate, or one needing a long schedule, can sit in the bottom third at epoch 1 and overtake everyone by epoch 27 — but ASHA already killed it. This is the genuine downside of aggressive early stopping, and the lever against it is the grace period: raise it (e.g. 5 epochs before any culling) and you protect slow starters at the cost of spending more compute on eventual losers. Grace period is the exploration-budget knob, and there is no free setting — it trades wasted compute on doomed trials against the risk of killing a winner.

4 · Resource-aware concurrency and checkpointing

A scheduler that kills trials is only useful if the freed compute is immediately reused. Tune is resource-aware: each trial declares what it needs (CPUs, GPUs, fractional GPUs), and Tune packs trials onto the cluster honoring those reservations — the same placement logic from lesson 04, applied to a trial population.

Worked number — packing 4 GPUs
You have a 4-GPU node and 40 configs to explore. If each trial needs a whole GPU, Tune runs 4 at a time (max concurrency = 4) and queues the other 36; as ASHA kills a trial, the next dequeues onto the freed GPU. If your model is small and a trial only needs 0.5 GPU (declared via tune.with_resources(train_fn, {"gpu": 0.5})), Tune packs 8 trials onto the same 4 GPUs — doubling exploration throughput at the cost of two trials sharing each GPU's memory and compute. The trade-off: too-fine fractions cause GPU memory thrash and noisy timings; whole-GPU trials waste capacity on small models. max_concurrent_trials additionally caps concurrency when a Bayesian search needs results to flow back before proposing more.

Checkpointing is what makes pause/resume possible. When ASHA stops a trial at a rung, it does not always discard it — with pause-and-resume schedulers it saves the trial's state to durable storage so it can be reloaded later. More importantly, checkpoints are how Tune survives node failure: a preempted spot instance or a dead worker loses a trial's process, but the trial resumes from its last checkpoint instead of restarting from epoch 0. The rule from the old skeleton holds — checkpoint often enough to lose little work, but cheaply enough not to dominate runtime, and write to shared/durable storage (S3, NFS) not local disk, because trials migrate between nodes.

One scheduler deserves a special note because it blurs the search/scheduler line: Population Based Training (PBT). PBT runs a population, periodically copies the weights of top performers onto poor ones, and then mutates their hyperparameters live — so the config is not fixed for a trial's lifetime, it evolves. This is uniquely powerful for things like learning-rate schedules (it discovers a schedule, not just a constant), at the cost of more bookkeeping and the requirement that trials checkpoint frequently so weights can be cloned mid-flight.

5 · The real code

Putting the three concerns together. The train function is an ordinary loop that calls tune.report each epoch; the Tuner wires up the search space, the search algorithm, and the scheduler:

from ray import tune, train
from ray.tune.schedulers import ASHAScheduler
from ray.tune.search.optuna import OptunaSearch

def train_fn(config):                         # ONE trial
    model = build_model(width=config["width"])
    opt = make_optimizer(lr=config["lr"])
    for epoch in range(27):                    # up to the ASHA max
        loss, acc = train_one_epoch(model, opt)
        # report the metric AND a checkpoint so Tune can rank + resume
        tune.report({"accuracy": acc, "loss": loss})

tuner = tune.Tuner(
    tune.with_resources(train_fn, {"gpu": 0.5}),   # resource-aware: 2 trials / GPU
    param_space={                                  # the search space
        "lr":    tune.loguniform(1e-5, 1e-1),      # continuous, log scale
        "width": tune.choice([64, 128, 256]),      # categorical
    },
    tune_config=tune.TuneConfig(
        metric="accuracy", mode="max",
        search_alg=OptunaSearch(),                 # Bayesian exploration
        scheduler=ASHAScheduler(                    # successive halving
            max_t=27, grace_period=1, reduction_factor=3),
        num_samples=81,                            # 81 configs to draw
    ),
)
results = tuner.fit()
best = results.get_best_result(metric="accuracy", mode="max")
print(best.config, best.metrics["accuracy"])

Read the separation in the code: param_space is the space, search_alg proposes within it (exploration), scheduler reallocates compute across the running trials (the savings), and tune.with_resources governs packing. Swap OptunaSearch() for the default random search, or ASHAScheduler for None, and you can isolate exactly how much each contributes — which is precisely the baseline discipline the book insists on.

6 · Make ASHA's budget tangible

Drag the knobs to see how the number of configs and the reduction factor trade off against compute. The bars compare full training (every config to the max) against ASHA's successive halving over the same population.

ASHA budget — compute saved by early stopping
Pick how many configs you explore (N), how aggressively each rung culls (reduction factor η — keep the top 1/η), and the max epochs a survivor trains. The widget builds rungs at 1, η, η², … epochs, keeps the top 1/η at each, and totals the epoch-units. Compare the green ASHA bar against the grey full-training bar — the gap is the GPU-hours early stopping buys you.
Full training (epoch-units)
ASHA (epoch-units)
Speedup
Rungs
Show the core JS
// rungs at budgets 1, eta, eta^2, ... up to maxEpochs; keep top 1/eta survivors each.
let survivors = N, prevBudget = 0, asha = 0, budget = 1;
while (true) {
  asha += survivors * (budget - prevBudget);   // incremental epochs this rung
  if (budget >= maxEpochs || survivors <= 1) break;
  survivors = Math.max(1, Math.floor(survivors / eta));
  prevBudget = budget;
  budget = Math.min(maxEpochs, budget * eta);
}
const full = N * maxEpochs;                      // train everything to the end
const speedup = full / asha;                     // the early-stopping win

7 · Failure modes & checklist

Failure modes

  • Killing a late bloomer. Grace period too short, so ASHA culls a slow-starting config that would have won. Raise the grace period for schedules with warm-ups; sanity-check against a no-scheduler baseline on a small N.
  • Tuning on noise. A single-seed validation metric wiggles run-to-run; the scheduler then promotes on luck. Average a few seeds or use a less noisy metric before trusting a 0.2% gap.
  • Comparing unequal budgets. Letting a search algorithm rank a 1-epoch trial against a 27-epoch one without a scheduler-aware metric. The scheduler must compare like with like — at the same rung.
  • Local-disk checkpoints. Trials migrate between nodes; a checkpoint on the worker's local disk vanishes when the trial moves or the node dies. Write to shared/durable storage.
  • Sequential search starving the cluster. A purely Bayesian search proposes one config at a time, so 32 GPUs sit idle waiting for results. Allow concurrent proposals or pair with random seeding.

Implementation checklist

  • Did you set metric and mode (max/min) explicitly before launching?
  • Is there a random/grid baseline to justify any clever search algorithm?
  • What is the grace period, and is it long enough for your slowest-starting plausible config?
  • Does each trial declare its per-trial resources so Tune can pack the cluster intentionally?
  • Are checkpoints frequent enough to lose little, cheap enough not to dominate, and on durable storage?
  • Is the search algorithm allowed enough concurrency to keep the cluster busy?

Checkpoint exercise

Try it
You have 64 GPUs and a model that trains in 27 epochs. Plan an ASHA experiment over 5 hyperparameters: choose N (configs), η, and the grace period, and estimate the epoch-unit budget vs full training using §3's arithmetic. Now flip an assumption: your best config is known to use a long warm-up that looks bad for the first 6 epochs. What must change — and what does it cost? (The grace period must rise to at least ~6, which shrinks the early-stopping saving; the "right" plan changes the moment late-blooming becomes plausible.)

Where this points next

Tune governs a population of trials, but it has been treating each trial as a black-box train function. What if one trial is itself too big for a single GPU and must be data-parallel across many workers? That is the recurring distributed-training shape — and Tune composes directly with it: a trial can be a Ray Train run, and the data feeding it comes from Ray Data (lesson 09). Lesson 10 builds Ray Train's N-worker, gradient-exchanging machinery, after which a single Tune trial can itself span a multi-GPU cluster. The control plane you just built sits one layer above the training engine you are about to meet.

Takeaway
Ray Tune is a control plane over a population of experiments, not random search on more machines. HPO is hard because it is black-box, expensive, high-dimensional, and noisy, which reframes it as resource allocation under uncertainty. Tune separates three concerns: a trial (one config as a task/actor reporting a metric and checkpointing), a search algorithm (explore the space — random/grid baselines vs Bayesian exploitation via Optuna/HyperOpt), and a scheduler (reallocate compute over time). The real savings live in the scheduler: ASHA's successive halving trains 81 configs to a winner for ~243 epoch-units instead of 2,187 — a 9× win on top of parallelism — by killing the bottom 1/η at each rung. Its cost is the late-bloomer risk, tuned by the grace period; its enablers are resource-aware packing (tune.with_resources, fractional GPUs, max_concurrent_trials) and durable checkpointing for pause/resume and fault tolerance. Tune composes with Train (10) and RLlib (07) — it sits one layer above the engine that runs each trial.

Interview prompts