CS336 // FIELD MAP
← field map
ASSIGNMENT 3 · SCALING55 pts · v1.0.0 · Spring 2025

Scaling Laws

Stanford CS336 · Spring 2025 · assignment 3 of 5 · walkthrough of the official handout, everything linked

Lectures behind it: L09 Scaling laws 1 (opens A3), L11 Scaling laws 2 (closes it) · supporting: L02 resource accounting for C ≈ 6ND, L03 architectures, hyperparameters for which knobs are safe to fix.

TL;DR — Two problems, 55 points, and no code to make green. First you reimplement the Chinchilla IsoFLOPs method offline on a supplied JSON of 72 synthetic runs and extrapolate the compute-optimal model and dataset size out to 1023 and 1024 FLOPs (5 pts). Then you do it for real: you get a hard-capped 2×1018 FLOP allowance to query a hosted training API that returns final training loss for a requested architecture, and you must fit your own law and predict the compute-optimal model size, its hyperparameters and its loss at a target budget of 1019 FLOPs (50 pts). The hard part is experiment design under a budget you cannot refill — every query is spent forever, and part of your grade is how good the predicted configuration actually turns out to be. It is the shortest assignment in the course to code and the easiest to waste: a weekend, most of it thinking, if you plan the sweep before you fire it.

A3 is the odd one out. Every other assignment in CS336 hands you a test suite and asks you to make it pass; this one hands you a budget and asks you to make a decision. The course's argument up to here has been that you can predict the cost of a training run from first principles (L02), that the architecture choices are mostly settled (L03), and that the hardware sets the shape of what is efficient (L05–L08). A3 asks the question those chapters were building toward: given a fixed pile of FLOPs, how do you split it between a bigger model and more tokens? What is forbidden here is not a library call — it is training the model you are reasoning about. You never see the 1019-FLOP run. You get a surrogate, a small allowance of small runs, and a power law you have to trust two orders of magnitude past where you measured it.

Map of the assignment

Two graded problems, 5 + 50 = 55 points. The handout does not print a total, and unlike A1, A2 and A4 there is no tests/ directory, no adapters.py and no pytest run in the repo — the entire spring2025 tree is a README, a pyproject, a reference model.py, one JSON data file and the handout. Everything is graded by a human reading your write-up, plus the accuracy of the configuration you submit to the form.

§ProblemPtsDeliverableGraded byLecture
2.1chinchilla_isoflops5code + two plots + two one-sentence answerswrite-up, over data/isoflops_curves.jsonL09
3scaling_laws50typeset write-up + form entry (model size, hyperparameters, predicted loss)write-up, plus measured quality of your predicted config via the Google formL09, L11

Setup: environment, data, the API key

Clone the repo and let uv handle the environment. The README is four lines long: install uv, uv add <package> whatever you need, uv run <command> to run in the environment, uv run which python if your editor wants an interpreter path. The pyproject pins Python ≥ 3.12 and declares only requests and torch>=2.7.0 — torch is there purely so the reference model.py imports, not because you train anything. You will want to uv add scipy matplotlib yourself. (Two cosmetic oddities in that file, both harmless: the setuptools stanza still says include = ["cs336_basics"], a leftover from A1, and the CHANGELOG stamps its 1.0.0 and 0.0.0 entries with 2024 dates in a 2025 repo.)

There is no test harness to learn. What there is instead:

you must be on the Stanford network to query this API, so you may have to use a VPN.A3 handout §3.1 · cs336_spring2025_assignment3_scaling.pdf

If you are working through this outside the course, that is where the assignment stops being fully reproducible: http://hyperturing.stanford.edu:8000/docs is unreachable from the open internet (it times out rather than refusing), and the host is not maintained between quarters. Problem 1 runs fine on the checked-in JSON. Problem 2 you can only do as a design exercise on paper — or against the 2026 restatement of it, which the repo's main branch has since rolled forward to and which prices experiments in B200-hours instead of FLOPs.

1 · Assignment overview

The framing the handout gives you is a role-play: you run training for a lab about to spend a small country's GDP on one model, and you get one shot. Concretely there are two budgets and you should keep them straight from the first minute, because most of the confusion in this assignment comes from conflating them.

BudgetFLOPsWhat it is
Target1e19The compute you are pretending to have for the one big run. You never actually spend it. Your job is to predict the model size, hyperparameters and final loss it would produce.
Scaling-laws (fitting)2e1820% of the target. This is real and it is enforced: it is the total FLOPs you may spend on API queries while fitting. Exceed it and the API refuses further requests.

A 20% overhead for the science before the run is not an arbitrary number — it is roughly what a real lab budgets for a scaling study, and the reason the assignment feels tight is that it is supposed to. The relationship tying the two together is the one from L02: C ≈ 6ND, compute equals six times parameters times tokens. Every trade-off in this assignment is a movement along that hyperbola. Fix C, and choosing N chooses D.

2 · Scaling laws review

Section 2 is exposition, not work — but it is the exposition that tells you which of Chinchilla's three methods you are being asked to implement, so do not skim it. Hoffmann et al. (2022) fit their law three different ways; the one here is approach 2, the IsoFLOP profile. The other two (fixed-model training-curve envelopes, and a parametric fit of L(N, D)) are not what problem 1 wants, and a solution that quietly implements one of them will not match the deliverable.

2.1 Scaling laws from IsoFLOPs profiles

The mental model is a family of U-shaped curves. Pick a compute budget C. Train several models of different sizes N against that same budget, letting the token count fall out of D = C/(6N) — a small model gets a lot of tokens, a big model gets few. Plot final loss against model size and you get a convex curve: tiny models are underparameterised and plateau high no matter how much data you pour in; enormous models run out of gradient steps before they converge, and in the limit a model so large that one step exhausts C stops at initialisation loss. Somewhere between the two is a minimum, and that minimum is the compute-optimal model size for that budget.

Do this for a ladder of budgets C1 … Ck and you get a sequence of minima ⟨Ci, Nopt(Ci)⟩, and by C = 6ND a matching ⟨Ci, Dopt(Ci)⟩. Fit power laws Nopt ∝ Ca and Dopt ∝ Cb through those minima, and you can extrapolate to a budget nobody has trained at. That extrapolation — from a handful of small runs to a budget you cannot afford to rehearse — is the entire product.

The handout also makes a deliberate simplification to Hoffmann's recipe, and it is worth knowing you are being let off:

While Hoffmann et al. [2022] fits a quadratic function to each IsoFLOP profile to find its minimum, we instead recommend you simply take the run with the lowest training loss for each compute budget as the minimum.A3 handout, Problem (chinchilla_isoflops) · handout PDF

Problem (chinchilla_isoflops): 5 points

Deliverable: a script that reads data/isoflops_curves.json, plus two plots (model size vs compute, dataset size vs compute) each showing the fitted power law, the data points it was fit from, and the extrapolation out to at least 1024 FLOPs — and, for each plot, a one-sentence answer giving your predicted optimum at 1023 and 1024 FLOPs.

Mechanically this is an hour of work, and the five points are a reading comprehension check on §2.1. Group the records by compute_budget; take the argmin of final_loss within each group; that record's parameters is Nopt(Ci); derive Dopt(Ci) = Ci/(6·Nopt); fit each of the two point sets to a power law; evaluate the fits at 1023 and 1024. The shape of the supplied file, so you can check your parse:

Property of data/isoflops_curves.jsonValue
Total run records72
Distinct compute budgets9 — 6e18, 1e19, 3e19, 6e19, 1e20, 3e20, 6e20, 1e21, 3e21
Runs per budget8, roughly geometric in model size (≈1.6× apart)
Keys per recordparameters, compute_budget, final_loss
Token countsabsent — derive D = C/(6N)

Four places people lose time on this, in rough order of frequency. First, fitting in linear space. Handing scipy.optimize.curve_fit a model like N = k * C**a with raw values spanning 1018 to 1021 on one axis and 108 to 1010 on the other gives you a least-squares problem whose residuals are dominated entirely by the largest budget, and an optimiser that stalls or returns nonsense depending on your initial guess. Take logs first and fit a straight line: log N = a·log C + const. The power law is a line in log-log; fit it as one. curve_fit is what the handout suggests and it is fine, but on log-transformed data a plain least-squares fit is enough.

Second, forgetting that D is derived. There is no token count in the JSON, and the second plot is not a second lookup — it is the first answer pushed through C = 6ND. Which gives you the best free correctness check in the assignment: because Dopt is defined as C/(6·Nopt), the two exponents must satisfy a + b = 1 exactly. If you fit them independently and they do not sum to one, you have a bug — you computed D from something other than C/(6N). For reference, Chinchilla's own reported exponents are a ≈ 0.49 and b ≈ 0.51; a fit on this synthetic set lands in the same neighbourhood of ½, with an R² around 0.98. If yours are far from a half, look for the bug before you look for a story.

Third, treating the argmin as clean. Eight sizes per budget is a coarse grid, and the minima are noisy enough to be non-monotone — in this file the argmin model size at 1021 is actually slightly smaller than at 6×1020, which is physically impossible and is pure grid-and-noise artefact. The handout tells you to take the raw min anyway, so take it; but say in your write-up that you noticed, and note that fitting a quadratic in log N per profile (what Hoffmann actually did) is the standard fix, because it interpolates a minimum that need not sit on a grid point. Knowing why the shortcut is a shortcut is most of the five points.

Fourth, over-reading the numbers. This data is synthetic and it is not the same model family as the API in problem 2. Its optimal token-to-parameter ratios sit in the low single digits, nowhere near Chinchilla's famous ≈20:1. Do not carry any absolute number from problem 1 into problem 2 — carry the method.

3 · Constructing scaling laws

Now the same method, but you generate the data, and the data costs money. Your target is a compute-optimal configuration at 1019 FLOPs; your instrument is a GET /loss endpoint that returns the final training loss for an architecture and a FLOP count; your constraint is that the FLOP counts you request are summed and capped.

you may not query more than 2e18 FLOPs worth of experiments for fitting your scaling law. This is hard cap that will be enforced by the API.A3 handout, Problem (scaling_laws) · handout PDF (typo in the original)

Problem (scaling_laws): 50 points

Deliverable: a typeset write-up describing your method for fitting the scaling law and for turning it into a prediction — complete enough to reproduce, with commentary on why you made each design decision — plus a submission to the Google form of (1) predicted optimal model size, (2) the training hyperparameters to use, including a batch size of 128 or 256, and (3) the predicted training loss. Part of the 50 points rides on how well that configuration actually performs.

The handout seeds your write-up with five questions it wants answered: how you decided which runs to query, how you fit the law, how well it fits, what optimum and loss it predicts at 1019, and what hyperparameters you would use at that size. Treat them as the section headings of the report — they are as close to a rubric as you are given.

The real content of this problem is allocation, and the arithmetic is small enough to do before you write any code. The API's most expensive single run is 1018 FLOPs, so your entire budget is exactly two of them. Here is what each price tier buys you:

train_flopsShare of the 2e18 budgetRuns you could afford (if you spent it all here)
1e130.0005%200,000
1e150.05%2,000
1e160.5%200
1e175%20
3e1715%6
6e1730%3
1e1850%2

Two consequences fall straight out of that table. The cheap end is effectively free — anything at 1015 or below costs a rounding error, so there is no excuse for not doing your hyperparameter exploration down there. And the expensive end is where your extrapolation lever lives, because a power law fit through minima at 1015, 1016 and 1017 is reaching two decades to 1019, while one that includes a 1018 profile is reaching only one. The tension is that a single 1018 profile with enough sizes in it to have a credible minimum would blow the whole budget on its own. Resolving that tension, explicitly, is the assignment.

One rule makes planning much more forgiving than it first looks:

Note that querying this endpoint with a previously-queried experimental configuration does not incur extra FLOPs used toward your budget.A3 handout §3.1 · handout PDF

The cap is on distinct configurations, not on HTTP requests. A crashed script that re-runs its first twenty queries costs nothing, and GET /previous_runs gives you back everything you have ever asked for, so you can rebuild your dataset from the server rather than from a local file you forgot to flush. Cache locally anyway — but the failure mode people fear here (losing budget to a bug) is not actually the one that gets them. The one that gets them is a well-organised sweep of the wrong thing.

The competing demands. You are fitting two different objects with one budget. The scaling law Nopt(C) needs several distinct compute tiers, each with several model sizes bracketing a minimum — that is breadth. The hyperparameters at the predicted optimum — learning rate above all, then the depth/width split and head count — need sweeps at fixed size, which is depth. The write-up's fifth question is explicitly about the second, and the form asks for the hyperparameters as a graded field, so a solution that spends 100% of the budget mapping Nopt and then guesses a learning rate for the final configuration has left points on the table. The natural resolution is sequential: use the near-free tiers (1013–1015) to establish a hyperparameter recipe — a learning rate that works, an aspect ratio you will hold fixed, a head count rule — then hold that recipe constant while you spend the expensive tiers on IsoFLOP profiles. Constant-recipe profiles are also what makes the fit interpretable: if the architecture rule changes between tiers, the trend you fit is a mixture of two effects and the extrapolation inherits both.

The search box is small, and its top corner matters. The API bounds d_model to [64, 1024] and num_layers to [2, 24], and the handout gives you the bridge from those knobs to N:

To estimate the number of non-embedding parameters for a given model hyperparameter configuration, use 12·nlayer·dmodel2.A3 handout, Problem (scaling_laws) · handout PDF

Run that formula against the corners and the box is: minimum 12·2·64² ≈ 98K non-embedding parameters, maximum 12·24·1024² ≈ 302M. Now apply Chinchilla's rule of thumb in the other direction — if the optimum sits near 20 tokens per parameter then C = 6·20·N² and Nopt = √(C/120), which at C = 1019 is about 289M. That is within a few percent of the box's ceiling. Two things follow. It is a hint that the problem was sized to be solvable inside the allowed ranges. And it is a warning: if your law predicts an optimum near the top of the box, you cannot bracket it from above with a real query, so the claim rests entirely on the fit rather than on a measurement. Say so in the write-up rather than hoping nobody checks. (The corresponding token count, D = C/6N, is ≈5.5×109 tokens at N = 302M — and at context length 512 with batch 128, that is roughly 84,000 optimizer steps, which is the sanity check that the configuration you are reporting is a real training run and not an arithmetic artefact.)

Treat the API as a smooth surrogate, not an oracle. The CHANGELOG records that version 1.0.0 switched the API from a scikit-learn decision-tree regressor to an XGBoost one. Whatever it returns is therefore a fitted model over a grid of real runs, not a fresh training run: broad trends are trustworthy, small differences between neighbouring configurations may be regressor artefacts, and the surface can have kinks no real optimiser would produce. The practical consequence is not to chase third-decimal-place improvements in loss, and to be suspicious of any hyperparameter trend that is within the noise of a flat line — which is exactly the caution L11 draws out of the DeepSeek scaling paper.

The one constraint that is not negotiable in the reported answer:

We place essentially no constraints on the hyperparameters you may report under the FLOPs budget of 1e19, other than the following requirement: your batch size must be either 128 or 256.A3 handout, Problem (scaling_laws) · handout PDF

The stated reason is model FLOPs utilisation — smaller batches would leave the GPUs idle enough that the FLOP accounting stops meaning what it is supposed to mean. If the staff hit OOM running your configuration they will use gradient accumulation or add data-parallel GPUs rather than change your batch size, so you do not need to reason about memory. It is also the only knob where the API's accepted range and the report's allowed range coincide exactly: batch_size is already restricted to {128, 256} on the endpoint.

3.1 Training API

Three GET endpoints on http://hyperturing.stanford.edu:8000, all taking api_key (your SSH public key, no newlines) as an ordinary query parameter. Interactive docs at /docs. Parameters and their accepted ranges, from the handout:

Parameter of GET /lossAccepted values
d_modelinteger in [64, 1024]
num_layersinteger in [2, 24]
num_headsinteger in [2, 16]
batch_sizeone of {128, 256}
learning_ratefloat in [1e-3, 1e-4]
train_flopsone of {1e13, 3e13, 6e13, 1e14, 3e14, 6e14, 1e15, 3e15, 6e15, 1e16, 3e16, 6e16, 1e17, 3e17, 6e17, 1e18}
api_keyyour key, as a string
EndpointReturnsFailure mode
GET /lossJSON with loss (float) and total_flops_used (float)404 with a message naming the out-of-range parameter, e.g. d_model must be in range [64, 1024], got 9999
GET /total_flops_useda single float — everything this key has ever spent422 if the key has no queries yet; a message for an invalid key
GET /previous_runsJSON with previous_runs: every config you have queried, with its lossmessage for an invalid key

Two details the handout does not spell out but which fall out of the reference model. num_heads is bounded independently of d_model, but the model splits d_model across heads — its own docstring says d_model must be evenly divisible by num_heads — so a combination like d_model=100, num_heads=16 is nonsense even if the range check lets it through. Pick head counts that divide your width. And train_flops is a discrete ladder, not a continuous knob: your IsoFLOP tiers must come from those sixteen values, which is what makes the budget arithmetic in the table above exact rather than approximate. Before your first real query, hit /total_flops_used as a key sanity check; the handout suggests exactly this.

3.2 Training run details

The runs behind the API were produced with a Transformer close to the one you built in A1, but not identical — and the differences matter, because the loss surface you are fitting is this model's, not yours. Everything below is fixed by the staff; you cannot query any of it.

SettingA3 training runsvs A1
Position embeddingslearned absoluteA1 used RoPE
NormalisationLayerNormA1 used RMSNorm
FeedforwardLinear → GeLU → Linear, d_ff = 4·d_modelA1 used SwiGLU (3 linear layers)
Dropoutattention and residual, p = 0.1none in A1
Embeddingsuntied input and output—
DataSlimPajamaA1 used TinyStories / OpenWebText
Tokenizerbyte-level BPE, 32K vocab, trained on SlimPajamasame family, different corpus and size
Context length512—
OptimizerAdamW, weight decay 0.01, gradient clipping 1.0—
LR schedulecosine decay by 10×, cycle length = total steps, no warm-upA1's schedule includes warm-up

The one to actually think about is the last row. With no warm-up and a cosine cycle pinned to the total number of steps, the learning rate you request interacts with the run length — a rate that is stable for a long 1018-FLOP run may diverge on a short 1014-FLOP one, and vice versa. That is a real reason a learning rate tuned in the cheap tiers may not transfer upward cleanly, and it is the concrete version of the caveat µP exists to address. The dropout is the second: 0.1 on both attention and residual is unusual for a modern pretraining run and it shifts the optimal token-to-parameter ratio somewhat, so do not expect this model family to land exactly on Chinchilla's 20:1.

What you hand in

Two files to Gradescope and one form. The form is not optional and it is not a formality — it is where the graded prediction lives.

There is no compute budget to report and no leaderboard repository for A3 — a check of stanford-cs336/assignment3-scaling-leaderboard returns 404, unlike the A1, A2 and A4 leaderboards. The form is the competitive surface here, and the only enforcement mechanism you will actually feel during the work is the API refusing you once total_flops_used crosses 2×1018.

Materials, verified

Next: A4 Data · Back to the assignments.