Intuition What this page is
The parent topic — Contributing to open-source ML (the note this page hangs under, linked here as Contributing to open-source ML ) — told you the lifecycle of a contribution: find an issue, fork, branch, code, submit, review, merge. This page does something narrower and more useful: it enumerates every kind of situation you will actually hit — the tiny doc fix, the sneaky bug, the feature that needs a tradeoff argument, and the awkward "your PR got rejected" case — and works each one all the way through.
Think of it like a chef's mise en place: before you cook, you lay out every ingredient category so nothing surprises you mid-service. We do the same for contributions.
Before any symbol or term appears, we build it from zero. If you have never opened a terminal, you can still follow from line one. Any name in double brackets like Testing ML Systems is a link to another note in this vault — you can safely read this page without clicking any of them; they are there for later.
Intuition How to read the coloured boxes and the checks (read once)
This page uses a few labelled boxes. They are just visual containers, not code:
a definition box pins down a term, a formula box holds one equation, an example box works a scenario, a mistake box flags a common trap, a mnemonic box gives a memory hook, and a recall box is a collapsible self-quiz.
Every numeric claim ends with "checked in VERIFY". VERIFY is a machine-checked appendix attached to this page: for each number I assert, a tiny standalone arithmetic program recomputes it and confirms it is true, so you are never trusting my mental maths. You do not need to run anything — the point is that the numbers on this page are independently verifiable , and the exact snippet exists in the page's VERIFY block.
Every open-source contribution you will ever make falls into one of a small number of cells . A cell here just means "a category of situation" — the way a chessboard has squares. If you can handle every cell, nothing can ambush you.
Let me define the two axes of the table before drawing it.
(D1) The two axes
Effort axis — how much of the codebase you must understand:
Trivial — you change only words (a docstring, a comment). No program logic moves.
Local — you change the inside of one function , and nobody calling that function needs to change how they call it. The blast radius is a single file.
Structural — you add new behaviour that spans several files or introduces a new option/API , so other parts of the codebase (and its callers) must be aware of it. Precisely: a change is structural when it touches more than one file, or adds a public name (class, function, argument) that users can now rely on and maintainers must support forever.
Outcome axis — what happens to your work: Merged (accepted), Revised (accepted after changes), Rejected (politely declined).
The figure below places every worked example of this page onto that grid, so you can see at a glance which corner each one lives in. Read it like a map: horizontal = how deep into the code you must go (Trivial → Local → Structural, left to right), vertical = what became of your work (Merged bottom, Revised middle, Rejected top). Each pastel tile carries an example label; the top-left tile reads "(empty)" because a trivial change almost never gets outright rejected , which is exactly why doc fixes (Ex 1, bottom-left) are the safest first contribution. The two arrows on the left and bottom edges show the direction each axis increases. In words: doc fixes sit lower-left (easy, welcomed), feature work sits right (structural), and the only rejection tile (Ex 6, the plugin) sits upper-middle.
Here is the same matrix in full detail. Every worked example below is tagged with the cell it fills.
Cell
Effort
Situation
Worked example
A
Trivial
Fix a typo / clarify docs
Ex 1
B
Local
Fix a crash on a degenerate input (empty / 1-D)
Ex 2
C
Local
Fix a silent wrong-number bug
Ex 3
D
Structural
Add a feature with a compute/memory tradeoff
Ex 4
E
Structural
The zero/edge guard everyone forgets
Ex 5
F
Any → Rejected
Your PR is declined — recover gracefully
Ex 6
G
Word problem
A real user opens a vague issue; turn it into a PR
Ex 7
H
Exam twist
Reviewer asks "prove your change is equivalent"
Ex 8
The "degenerate input" row (cell B/E) is the analogue of checking every quadrant in geometry: the case that looks trivial but breaks everything. We give it two full examples because it is where beginners lose PRs. There are eight cells and eight worked examples — one for each — so nothing on the matrix is left unworked.
Recall Why enumerate cells at all?
Question: Why lay out a matrix before writing examples? ::: So no class of situation is left unshown — the reader never meets a scenario we skipped, exactly like covering all four quadrants of an angle.
Every code snippet on this page is Python . When a snippet uses .sum(dim=1), .norm(...), .unsqueeze(...) or .ndim, it is operating on a PyTorch tensor — PyTorch's multi-dimensional array type (see Software Engineering for ML ). A plain Python list like [1, 2, 3] is not a tensor; the examples show the moment a list is turned into a tensor and given the extra "batch" dimension the library expects. Where we do pure hand-arithmetic we write it as maths in $…$, not as code.
Every term the examples lean on, defined once, plainly.
We also use one piece of maths repeatedly — cosine similarity — so define it now.
Worked example The typo that teaches internals
A user files: "The n_estimators docstring in a random forest just says 'number of trees'. What's a sane default?" You must improve the docstring.
Forecast: Guess — is the "trivial" fix actually trivial? What must you read before you can write one true sentence?
Step 1 — Read the source, not the docs. Open the Python function and find where n_estimators is used: it's the length of a loop building trees.
Why this step? You cannot clarify what you don't understand; the docstring must match the code's actual behaviour, not folklore.
Step 2 — State the tradeoff, not just the definition.
n_estimators : int, default=100
Number of trees. More trees → lower variance (predictions
stabilise) but linearly more compute and memory. Returns
diminish past ~100–500 for most datasets.
Why this step? A good doc answers the decision the user faces ("how many?"), following Technical Writing for ML principles.
Step 3 — One-line PR title, link the issue. docs: clarify n_estimators tradeoff (#1234).
Verify: Does variance really drop with more trees? Here σ (the Greek letter sigma ) is the standard deviation of one tree's prediction — a measure of how much a single tree's answer wobbles from run to run — and σ 2 is its variance (the square of that wobble). Averaging n independent estimators each with variance σ 2 gives variance σ 2 / n — so 100 trees cut variance to 1% of a single tree. We check this number in VERIFY.
This is the "quadrant II where the naive formula fails" of coding: the input the author never pictured.
Worked example cosine_similarity dies on 1-D vectors
A user runs cosine similarity (formula F1 ) on two flat lists and gets RuntimeError: expected 2D. The Python code assumes a batch dimension (an outer wrapper list of many vectors) that a lone vector doesn't have.
Forecast: Where exactly does a plain list [ 1 , 2 , 3 ] differ from a batch [[ 1 , 2 , 3 ]] , and which line trips?
Step 1 — Reproduce the crash. Feed a = [ 1 , 2 , 3 ] , b = [ 4 , 5 , 6 ] . The PyTorch line (a*b).sum(dim=1) asks for dimension 1, but a 1-D tensor only has dimension 0. Crash.
Why this step? A bug you cannot reproduce, you cannot prove you fixed (core Software Engineering for ML discipline).
Step 2 — Fix by promoting 1-D to 2-D. If the input is 1-D, wrap it: [ 1 , 2 , 3 ] → [[ 1 , 2 , 3 ]] (in PyTorch this is x.unsqueeze(0)), giving it the batch dimension the code expects.
Why this step? We handle the degenerate case (missing dimension) without touching the normal path — smallest safe change.
Step 3 — Compute the real answer by hand using F1 .
a ⋅ b = 1 ⋅ 4 + 2 ⋅ 5 + 3 ⋅ 6 = 32 , ∥ a ∥ = 14 , ∥ b ∥ = 77
cos θ = 14 77 32 = 1078 32 ≈ 0.9746
Why this step? The test must check the number , not merely "doesn't crash" — a fix that returns a wrong number silently is worse than a crash.
Verify: 0.9746 is close to 1 , meaning the two vectors point almost the same way (small θ ) — sensible, since [ 4 , 5 , 6 ] ≈ 2 ⋅ [ 2 , 2.5 , 3 ] , roughly parallel to [ 1 , 2 , 3 ] . VERIFY confirms 0.9746 .
The nastiest bug: no crash, just quietly wrong output.
Worked example Off-by-one in a moving average
A Python metrics helper computes a moving average over the last k values but accidentally averages the last k + 1 . No error — just biased logging.
Forecast: If you average k + 1 points when you meant k , is the result too high, too low, or unpredictable?
Step 1 — Nail the intended maths. For window k = 3 over [ 2 , 4 , 6 , 8 ] , the last-3 average is 3 4 + 6 + 8 = 6 .
Why this step? You need the ground-truth number before you can call anything "wrong".
Step 2 — Show the buggy result. Averaging the last 4 (the bug): 4 2 + 4 + 6 + 8 = 5 .
Why this step? Demonstrating the gap (6 vs 5 ) proves the bug is real and quantifies it — reviewers need evidence, per Code Review Best Practices .
Step 3 — Fix the slice index and add a regression test asserting the answer is exactly 6 .
Why this step? A regression test freezes the correct number so nobody re-breaks it later.
Verify: Both 6 (correct) and 5 (buggy) are checked in VERIFY. The bug pulls the average down here because the extra element 2 sits below the true window mean of 6 — a specific, explainable direction, not random noise.
Worked example Gradient checkpointing: prove the trade is worth it
You add gradient checkpointing: store activations only every k -th layer, recompute the rest during the backward pass. This trades compute for memory.
Forecast: If you checkpoint every k = 4 layers out of 24 , what fraction of memory do you keep ? Guess before computing.
Step 1 — Model normal memory. Storing every layer's activation costs
M normal = L ⋅ B ⋅ H
with L layers, batch B , hidden size H . Here L = 24 .
Why this step? You need a baseline; a tradeoff claim is meaningless without "compared to what?".
Step 2 — Model checkpointed memory. Keep only every k -th activation:
M ckpt = k L B H ⇒ M normal M ckpt = k 1
For k = 4 : keep 4 1 = 25% of stored activations.
Why this step? Reducing to a clean ratio makes the benefit undeniable and dimension-free.
Step 3 — Model the compute cost honestly. One normal forward pass costs L layer-evaluations. During the backward pass we must recompute the activations we didn't keep: that is the L − k L dropped layers, redone once. So total layer-evaluations are
C = forward L + recompute ( L − k L ) = L ( 2 − k 1 ) .
For k = 4 the factor is 2 − 4 1 = 1.75 × , not a full 2 × ; the full 2 × is only the limiting case as k → ∞ (keep almost nothing). Always quote the exact factor for your chosen k .
Why this step? Every gain has a cost; overstating it as "double" when it is really 1.75 × is the kind of sloppy claim Code Review Best Practices reviewers reject.
Verify: memory ratio = 1/4 = 0.25 and compute factor = 2 − 1/4 = 1.75 (with the 2 × as the k → ∞ bound). Both checked in VERIFY. The honest claim — "spend 1.75 × compute to keep 25% memory" — is now a defensible argument, in line with ML Research Paper Implementation .
The "what if the input is zero?" case — the coding twin of dividing by zero .
Worked example cosine_similarity of a zero vector
What is cos θ (formula F1 ) when one vector is all zeros, a = [ 0 , 0 , 0 ] ?
Forecast: A zero vector has no direction (no arrow, so no angle θ ). What should the function return, and what does the naive formula do?
Step 1 — Trace the maths. ∥ a ∥ = 0 + 0 + 0 = 0 , so
cos θ = 0 ⋅ ∥ b ∥ a ⋅ b = 0 0
undefined — a division by zero.
Why this step? You must see the degeneracy before you can guard it.
Step 2 — Choose a defined behaviour: floor the length , not zero. Introduce a tiny positive number called ε (the Greek letter epsilon , standing for "a very small number"). We pick ε = 1 0 − 8 because that is near the smallest gap a 32-bit float can reliably represent around 1 (about 1 0 − 7 ), so it is large enough to avoid division-by-zero yet small enough to never distort a genuine result. The correct guard replaces each length by at least ε :
cos θ = m a x (∥ a ∥ , ε ) ⋅ m a x (∥ b ∥ , ε ) a ⋅ b .
With ∥ a ∥ = 0 this becomes ε ⋅ ∥ b ∥ 0 = 0 (note: writing max ( 0 , ε ) would collapse to just ε and lose the guard for the normal case where the length is a big positive number — so we must floor the actual length ∥ a ∥ , not the constant 0 ).
Why this step? Returning 0 ("no agreement") is safe and never crashes training; flooring the length is the standard fix and keeps normal inputs untouched.
Step 3 — Add a test for exactly this input asserting the result is 0 , not NaN (the "Not a Number" value a computer produces from 0/0 ).
Why this step? The zero case must be shown in the test suite so a future refactor can't reintroduce a NaN that silently poisons a model.
Verify: with length-flooring, cos θ = 0 for a zero vector — confirmed in VERIFY.
Common mistake The most common rejected-PR reason
Handling the "normal" case but forgetting the zero / empty / 1-D case. Reviewers test these first. Always ask: what is the smallest, weirdest, emptiest input?
Worked example "Thanks, but we won't merge this"
Your feature PR is closed: "This adds an API surface we'd have to maintain forever; it belongs in a plugin." Not a bug in your code — a scope decision.
Forecast: Is a rejection a failure? What is still salvageable?
Step 1 — Separate correctness from scope. Your code may be perfect; the maintainer is rejecting inclusion , not quality .
Why this step? Confusing the two makes you defensive; separating them keeps you learning.
Step 2 — Extract the reusable value. Publish the code as a small standalone package and link it — great for Building ML Portfolios .
Why this step? Rejected-from-core ≠ worthless; a clean plugin still proves your skill.
Step 3 — Ask what would be accepted. Often a smaller doc or test PR from the same work is welcome.
Why this step? Turns a closed door into a merged commit — the spirit of Collaborative Machine Learning .
Verify: sanity check — a rejection changes zero lines of your correctness ; your tests still pass. Nothing numeric here; the "check" is that the code's behaviour is unchanged by the social outcome. This example is the one Rejected tile (upper-middle) on the s01 map.
Worked example "It's slow!" — turn a complaint into a change
A user writes: "Loading my 20 GB dataset eats all my RAM." No stack trace, no numbers.
Forecast: What is the actual technical claim hiding inside "it's slow / eats RAM"?
Step 1 — Quantify. Ask for size and behaviour: the Python DataLoader reads all rows into memory at once. Memory used ≈ file size = 20 GB.
Why this step? A vague issue must become a number before it becomes a fix — you cannot design a solution against the word "slow".
Step 2 — Propose lazy loading. Read in chunks of size c rows; peak memory drops to O ( c ) instead of O ( N ) , where N is the total number of rows. For N = 1 0 7 rows and c = 1 0 4 , that's a 1000 × reduction in peak rows held.
Why this step? A concrete design that turns the measured problem (O ( N ) memory) into a measured improvement (O ( c ) ) — a reviewer can now judge the tradeoff instead of a vibe.
Step 3 — Ship with a benchmark table showing before/after peak memory on the user's real file.
Why this step? Evidence, not adjectives, gets merged; a number the reviewer can reproduce is the whole argument (see Code Review Best Practices ).
Verify: peak-rows ratio = N / c = 1 0 7 /1 0 4 = 1000 . The claimed "1000 × less memory" is exactly this ratio — checked in VERIFY.
Worked example Reviewer: "Show your fast path gives the same answer."
You replaced a slow Python loop that sums 1 + 2 + ⋯ + n with the closed form 2 n ( n + 1 ) . The reviewer wants a proof it's identical, not just "trust me."
Forecast: Are a loop-sum and the formula exactly equal, or only approximately?
Step 1 — State both. Loop: S = ∑ i = 1 n i . Formula: S = 2 n ( n + 1 ) .
Why this step? You can't prove equality without writing both sides explicitly.
Step 2 — The pairing argument (the picture). Pair first with last: 1 + n , 2 + ( n − 1 ) , … — each pair sums to n + 1 , and there are 2 n pairs, giving 2 n ( n + 1 ) .
Why this step? A geometric/pairing argument is a proof of exact equivalence , the gold standard for refactors — it shows the two are equal for every n , not just the ones you tested.
Step 3 — Also add a numeric equivalence test over many values of n .
Why this step? Reviewers trust a proof plus a machine check more than either alone; the test guards against a typo in the fast path even after the proof convinces everyone.
Verify: for n = 100 , both give 2 100 ⋅ 101 = 5050 . Checked in VERIFY.
Recall Self-check
Which cell is "cosine similarity of a zero vector"? ::: Cell E — the degenerate zero-input guard.
Why is the PR quality a product not a sum? ::: Any factor at zero (untested, unclear, unjustified) makes the whole PR unmergeable — multiplication makes one zero fatal.
What compute factor does gradient checkpointing cost with k = 4 ? ::: 2 − 4 1 = 1.75 × , to keep 25% of stored activations (2 × only as k → ∞ ).
Mnemonic The one habit that gets PRs merged
S.Z.T. — S how the normal case, guard the Z ero/degenerate case, and T est the exact number. Miss any letter and a reviewer will find it first.
Prerequisites & neighbours: Version Control with Git · Testing ML Systems · Code Review Best Practices · Software Engineering for ML · ML Research Paper Implementation · Building ML Portfolios · Technical Writing for ML · Collaborative Machine Learning · parent Contributing to open-source ML .