Repository navigation
Add RMS Norm Backward challenge (medium #142) - #351
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude
Bot
requested review from
ishaan-arya,
kunal-mansukhani and
shxjames
as code owners
October 16, 2026 07:01
Contributor
There was a problem hiding this comment.
✅ Review bot: low risk, safe to merge. Lint and structure checks passed; no sensitive paths or grader edits; 161 lines changed.
- ID 142 is unused. The open PR's 134–139 are not in the base list, and the topic (RMSNorm backward with a cross-row dweight reduction) does not duplicate any existing challenge (forward-only 50/83/113/131, 111 is GEMM-heavy).
- challenge.py defines the 5 required methods, has no init, and sets name/atol/rtol/num_gpus/access_tier. It adds only two private helpers (_outputs, _make). Imports are limited to ctypes, typing, torch and the challenge base, with nothing running at import time.
- reference_impl asserts shapes and dtypes with no device checks, and uses only standard torch ops. I verified the example by hand (dx and dweight match the html), and the formulas match the html.
- All tensor allocations use device=self.device. Outputs are pre-filled with 123.0, which forces solutions to write every element.
- Functional tests cover N=1/C=1, a single row, all-zero x, large eps, zero dy, negative weights, large x magnitude, non-power-of-two shapes, a ragged row count and a realistic shape. A 1e-3 tolerance is reasonable for float32.
- The performance test (N=16384, C=4096) uses about 805 MB for x, dy and dx plus small buffers, well under the 3.2 GB limit.
- The starters compile as stubs with only the parameter comment, and the Mojo starter uses max.gpu imports and MutAnyOrigin. The html constraints and example match the signature, and the diff is complete with no injection or obfuscation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds RMS Norm Backward (
challenges/medium/142_rms_norm_backward), the RMSNorm training kernel used in every LLaMA/Mistral/Qwen block. Givenx (N, C),weight (C,)and upstreamdy (N, C), the solver computesdx (N, C)anddweight (C,). The per-rowrstdis not saved from the forward pass, so the solver has to recompute it.Number 142 is the next one after the merged 140/141. 134–139 belong to the open PR #349, and #236 covers multi-GPU vector add, so neither number nor topic overlaps.
What a solver learns
dxneeds per-row reductions (Σx²andΣ dy·w·x) anddweightneeds a reduction down each column over allNtokens. Doing both in a single pass over memory is the core difficulty of norm-backward kernels in Apex, Liger and Triton's layer-norm tutorial.dweightis a reduction over the whole batch. Solvers choose between per-element atomics, a column-per-thread kernel, and per-block register partials followed by a small second-stage reduce.Σ g·x̂ = r·Σ g·x, sodx = r·g − x·r³·Σ(g·x)/C.Novelty compared with the closest challenges
dxdepends onΣ_k g_k x̂_k) and, more importantly, a reduction over the batch axis that crosses blocks.Naive vs. optimized
rstdand the projection and writesdx. Kernel B, one thread per column, loops over allNrows, recomputes or reloadsrstd, and accumulatesdy·x̂. Alternatively, kernel AatomicAdds intodweightfor every element.x/dyin registers. One fused block reduction givesΣx²andΣ dy·w·x, then the block writesdxand accumulatesdy·x̂into per-thread register partials for the columns that thread owns. At the end each block writes oneC-length partial, and a tiny kernel sums theG × Cpartials. Results match the naive version up to fp32 summation order.How the benchmark exposes it
Performance is measured with
N = 16,384,C = 4,096(batch 4 × seq 4,096 at LLaMA-7B hidden size). That is 256 MB per tensor and 768 MB in total, which fits 5× in 16 GB. The op is purely memory-bound: about 2 FLOPs per byte.xanddyonce and writingdxonce (3·N·C·4 B ≈ 805 MB). The two-kernel naive version readsxanddya second time (+537 MB, about 1.7× the traffic).C = 4,096columns, a column-per-threaddweightkernel launches just 4,096 threads, about 3 warps per SM on a 40-SM T4, and each runs a serial loop of 16,384 iterations. That leaves the GPU latency-bound and badly underused.atomicAddputs 67M atomics on only 4,096 addresses. Per-block register partials need justG·C(about 0.6M) writes plus a reduction smaller than 3 MB.dyis correlated withxin the benchmark, so the projection term is O(1) and can't be skipped without failing correctness.Functional tests and the mistakes they catch
12 cases. Outputs are pre-filled with
123.0, so every element has to be written. Each mistake below was checked locally by running it as a mutant against the reference.− x̂·mean(g·x̂)(treating RMSNorm as a pure scale)N=C=1case (exact dx ≈ 0), correlated-dycases, almost all othersdyinstead ofdy·weightindxxinstead ofx̂indweightx(σ=50) case, most othersrstdfactor indxeps, or adding it outside the sqrtxcase (otherwise inf/nan),eps = 1.0caseNinstead ofC100×30,255×1000and othersdweightwithout initializing it (atomics onto an assumed-zero buffer)4099×256(ragged multi-block) and1024×2048CC = 1, 3, 30, 1000Other coverage: zero inputs (all-zero
x, all-zerody, which must give exactly 0), negative weights and values, powers of two (16×64, 32×128, 256, 1024×2048) and non-powers of two.Independent check: the example values were worked out by hand (eps≈0:
dx = [[1.375, 1.625, 0.875, 0.625], [0, 0, 0, 1]],dweight = [3, -1, 1, -1]), checked with float64 scalar loops, and matched by the reference. Every functional case was also compared against float64torch.autogradof the forward pass. The largest absolute deviation was 8.3e-4, on the 4,099-row case, wheredweightvalues are about 4e3, so it is well within the relative tolerance.Tolerance (
atol = rtol = 1e-3): fp32 reordering error is about 1e-6 relative, while each mistake in the table shifts results by at least 1e-2 on its targeted case.Validation
scripts/run_challenge.py --language cudaon NVIDIA Tesla T4:--action rungave ✓ Test passed, and--action submitgave ✓ All tests passed, including the performance test. The solution file is not committed.pre-commit run --all-filespasses, andscripts/validate_challenges.pyreports 0 errors.<p>, has<h2>sections, includes an SVG on#222, the bmatrix example matchesgenerate_example_test(), and the perf bullet matches. All 6 starters have exactly one parameter comment (no parenthetical, since this is medium); the JAX starter also has# return output tensor directly. Allocations usedevice=self.device, and the Mojo starter uses 1.1 style.🤖 Generated with Claude Code