08. Implementations
An implementation is code that a reader can read, run and copy. It is written to be read first, to be correct second, and to be fast third. In algorithm, one problem contract checks every language. Here, one NumPy reference written from first principles checks every framework: the PyTorch and JAX versions must agree with it on shared fixtures, and gradients must agree with finite differences.
8.1 What an implementation is
- An implementation belongs to one node and one framework. It lives in
impl/<framework>/in the node directory (04.4). - The kinds that have implementations (04.2):
method,architectureandmetricalways when the kind's targets ask for it (04.10);concept,findingandriskoptionally, as a demonstration;taskthrough its contract adapter (10.2);softwarethrough usage examples, which are not reference implementations and have no X level. - Each implementation has one reference version. It can also have labelled versions (8.5).
- It has its own tests next to it, in the test framework of its language (8.3). The shared fixtures of the node (8.7) run through every framework, so one set of numbers checks them all.
- It follows the equations of the page, and marks each block of code with the id of the equation that it computes (
# eq: attn-scores), so that the page, the scene and the code can point at the same thing (8.3, 12). - The NumPy reference writes the backward pass by hand for every method and architecture whose page derives the gradient (06.5). The PyTorch and JAX versions use automatic differentiation, and their gradients are tested against the NumPy backward and against finite differences (8.7.4). This is how the derivation on the page is tested.
- The build computes its level X0 to X4 (8.10). Nobody writes the level.
8.2 The framework register and the tiers
8.2.1 The register
registers/frameworks.yaml lists each framework. A framework is in the register before any implementation in it can merge (rule X12). Each entry has the fields of this example:
- id: pytorch # the directory name under impl/
name: PyTorch
tier: 1
language: python
ext: [py]
file_case: snake # scaled_dot_product_attention.py
packages: pytorch-ref # the pinned package set of 8.4
test: {framework: pytest, file: "test_<name>.py"}
format: {tool: ruff-format}
lint: {tool: ruff, args: ["--select", "E,F,W,B,UP,NPY"]}
types: {tool: pyright, mode: basic}
mutation: {tool: mutmut}
image: aik-runner-pytorch # 8.9
default_dtype: float64 # in tests; float32 in experiments (09)
determinism: ["torch.use_deterministic_algorithms(True)", "CUBLAS_WORKSPACE_CONFIG=:4096:8"]
highlight: python
since: 2026-12
8.2.2 The tiers
| Tier | Frameworks | Required for | Count |
|---|---|---|---|
| 1 | numpy (NumPy from scratch, the reference), pytorch, jax |
each method, architecture and metric node of the core; each node of the long tail that has an implementation at all has at least numpy |
3 |
| 2 | c (C99, no dependencies), cpp (C++20 with Eigen only), rust (stable, ndarray only), julia (Base and the standard library only), typescript (no framework, typed arrays; it also powers the in-browser notebooks of 12.8) |
the core architectures and metrics only, from M3 (Q-36) | 5 |
| 3 | open: for example TensorFlow and Keras, MLX, tinygrad, Flax NNX layers, Swift, Go, R, Mojo | nothing; a contributor adds them | any |
Rules for the tiers:
- NumPy is always first. No framework version merges before the NumPy reference of the same node is at X1 (rule X01). The NumPy reference is the oracle of the differential tests (8.7.3), as the Lean model is the oracle in algorithm.
- Tier 3 has the same harness, the same fixtures and the same ladder. It is not in the "complete" test of 04.10 and not in the coverage levels of 18.2. The site shows tier-3 code in the framework menu under "More frameworks".
softwarenodes may show usage examples in any framework, inexamples/, with no X level.
8.2.3 The evidence for the choice
| Signal | What it says (2026) |
|---|---|
| research code | PyTorch is the framework of most papers with code on arXiv and at NeurIPS, ICML, ICLR, ACL and CVPR (01.5); JAX is the second, and the main one at Google DeepMind and in much of the large-scale training literature |
| teaching | the canon (5.10) teaches from scratch in NumPy (d2l, Prince's notebooks, CS231n-style assignments) and then in PyTorch; D2L ships PyTorch, JAX, TensorFlow and MXNet versions of each section |
| inference at the edge | llama.cpp and ggml (C and C++), candle and burn (Rust), transformers.js and ONNX Runtime Web (TypeScript) carry much of local and in-browser inference; these are the reasons for the tier-2 list |
| reader need | a reader who wants to understand writes NumPy; a reader who wants to use writes PyTorch or JAX; a reader who wants to run in the browser needs TypeScript |
8.2.4 The pinned toolchains
manifests/frameworks.yaml pins each framework's toolchain. The values below are the Default for M0, as of 2026-11; M0 sets the exact versions to the latest stable releases and records them.
| Framework | Language and toolchain | Packages of the reference set |
|---|---|---|
numpy |
CPython 3.13 | NumPy 2.x |
pytorch |
CPython 3.13 | PyTorch 2.x (CPU wheel in tests), NumPy 2.x |
jax |
CPython 3.13 | JAX and jaxlib 0.x (CPU), NumPy 2.x |
c |
GCC 15 and Clang 21, -std=c99 -O2 -Wall -Wextra -Werror |
libc and libm |
cpp |
GCC 15 and Clang 21, -std=c++20 |
Eigen 3.4 or 5.x |
rust |
stable Rust of M0 | ndarray, approx in tests |
julia |
Julia 1.12 | Base, LinearAlgebra, Random, Test |
typescript |
Node 24 LTS, TypeScript 5.x | none; vitest in tests |
The test packages (pytest, hypothesis, scipy for reference special functions in tests only) are in the images and are listed in the same manifest. A new pin is a pull request that moves the version and carries the full test matrix report of the framework with it (8.9).
8.3 Conventions
8.3.1 Rules for every framework
- The header. Every file starts with the licence header and the link to its page (8.12).
- The docstring names the node and the equations. The module docstring says what the code computes in one sentence, names the page, and lists the equation ids that it implements.
- Equation markers. Each block that computes an equation of the page has a comment
eq: <id>with an id that exists on the page (rule X11). The page highlights the block when the reader hovers the equation, and the reverse. - Shape comments. Every array that the code creates or reshapes has a shape comment in the axis names of
shapes.yaml(8.6):# (B, T, H, D). The NumPy and PyTorch versions use the same names. Rule X13 checks that each name in a comment is an axis of the node'sshapes.yaml. - Notation. Variable names follow the notation register (06.4):
W_q,W_k,W_v,d_model,n_heads,logits,log_probs. A name that differs from the page's symbol has a comment that maps it. - No hidden state. No global random state, no module-level caches, no mutation of inputs. Randomness enters through an explicit generator argument:
rng: np.random.Generatorin NumPy,generator: torch.Generatorin PyTorch,key: jax.Arrayin JAX. - Pure forward and backward in the reference. The NumPy reference has
init(rng, config) -> params,forward(params, x, ...) -> (y, cache)and, where the page derives the gradient,backward(params, cache, dy) -> (dx, dparams). The PyTorch reference is atorch.nn.Modulewith the sameconfigfields and afrom_params(params)constructor, so the fixtures can load the same weights. The JAX reference is a pair of pure functionsinitandapplywith the same parameter tree. - Readable first. The reference is the plainest code that matches the page. A version that is faster but harder to read is a labelled version (8.5). A reference over 200 lines raises a warning (rule X14).
- Numerically careful. Softmax and log-softmax subtract the maximum; log-sum-exp is used for log-likelihoods; division by a standard deviation adds an
epsthat the page names; losses on logits use the logits form (8.11).
8.3.2 The table of tools
| Framework | Test | Format | Lint | Types | Mutation |
|---|---|---|---|---|---|
numpy, pytorch, jax |
pytest, with hypothesis for property tests | ruff format | ruff | pyright (basic) | mutmut |
c |
a small aik test runner over assert.h |
clang-format | clang-tidy | the compiler | Mull |
cpp |
doctest | clang-format | clang-tidy | the compiler | Mull |
rust |
built-in #[test] |
rustfmt | clippy -D warnings |
the compiler | cargo-mutants |
julia |
Test | JuliaFormatter | Aqua.jl and JET.jl | JET.jl | aik mutate (8.8) |
typescript |
vitest | prettier | eslint | tsc --strict |
StrykerJS |
8.3.3 The interface of each kind
Each kind with code has one interface, in schema/impl.v1.yaml, so that the fixtures and the harness can call every framework the same way.
| Kind | Entry points | Example |
|---|---|---|
method (a layer, a regularizer, an attention variant) |
init, forward, backward (reference), and config |
dropout, layer normalization, LoRA, rotary position embedding |
method (a training method: an optimizer, a loss, a schedule) |
init_state(params), step(params, grads, state, hparams) -> (params, state), or loss(logits, targets, ...) |
Adam, label smoothing, cosine schedule with warmup, DPO loss |
method (an inference method) |
decode(model_fn, prompt, rng, hparams) -> tokens against a fixed toy model |
top-p sampling, beam search, speculative decoding with a fixed draft model |
architecture |
init(rng, config), forward(params, x, ...), param_count(config), flops(config, inputs) |
Transformer, ResNet-18, LSTM, Mamba block, U-Net |
metric |
score(predictions, references, **options) -> float or dict |
BLEU, ROUGE-L, F1, mAP, pass@k, expected calibration error |
concept, finding, risk (demo) |
demo(rng) -> dict that produces the numbers of the page's worked example |
the bias-variance decomposition on a toy problem; a prompt-injection string against a toy agent |
For metrics the interface is the contract of the node (contract.yaml, 04.4): the signature, the input and output types, the options that change the score (tokenization, smoothing, casing), and the tolerance.
8.3.4 Backward passes
- The NumPy reference writes the backward pass by hand for each method and architecture whose page has a gradient derivation (06.5). Each step of the backward pass carries the
eq:id of the derivation step that it computes. - When the page has no derivation (a large architecture made of derived parts), the NumPy reference composes the backward passes of its parts, and the test checks the composition.
- The PyTorch version does not write
backwardby hand unless the page is about a custom gradient (straight-through estimators, gradient reversal, the reparameterization trick, a custom attention kernel). Then it usestorch.autograd.Function, and the JAX version usesjax.custom_vjp.
8.4 The pinned package sets
An implementation imports only the packages of its framework's pinned set (rule X07). The sets live in manifests/packages.yaml with exact versions and hashes, and the runner image contains exactly them.
| Set | Allowed in impl/<framework>/*.py (not tests) |
Allowed in tests |
|---|---|---|
numpy-ref |
numpy, the Python standard library |
the set, plus pytest, hypothesis, scipy (for special functions and reference statistics) |
pytorch-ref |
torch, numpy, the standard library |
the set, plus the test packages, plus the NumPy reference of the same node (imported through the fixture loader) |
jax-ref |
jax, jax.numpy, numpy, the standard library |
as for pytorch-ref |
| tier 2 | the packages of 8.2.4 | the test framework of 8.3.2 |
The no self-import rule (X08): an implementation may not call the framework's own version of the thing that the node is about. impl/pytorch/dropout.py may not call torch.nn.Dropout or torch.nn.functional.dropout; impl/pytorch/attention.py may not call torch.nn.functional.scaled_dot_product_attention or torch.nn.MultiheadAttention; impl/jax/adam.py may not import optax. The tests may and should call them, as a second oracle (8.7.3). registers/self-import.yaml lists, for each node, the framework symbols that its code may not use; the curate step fills it, and the critic role reviews it.
Higher-level libraries (Flax, Optax, Equinox, Hugging Face Transformers, timm, torchvision models) are not in the reference sets. They appear in labelled library versions (8.5) and in experiments (09.4), where their own pinned sets apply.
8.5 Versions with labels
| Label | What it is | Rules |
|---|---|---|
reference |
the plain version that matches the page, and the one whose level is the X level | required; one per framework |
efficient |
a faster version that a page discusses: a fused kernel in plain PyTorch, a tiled attention that shows the FlashAttention idea, a vectorized beam search | it must pass every test of the reference; rule X16 needs it to be at least 1.3 times faster on the largest fixture, or to use at most 0.7 times the peak memory |
library |
how to do the same thing with the framework's library: torch.nn.functional.scaled_dot_product_attention, optax.adam, torchmetrics |
it must agree with the reference on the fixtures; it shows the reader the API and its defaults, and the page lists the differences of default (an eps, a bias-correction choice, a smoothing method) |
historical |
the form of the original paper when it differs from the modern one (post-norm Transformer, the original LSTM without forget gate, Adam without bias correction) | it must pass its own fixtures, and the page says how it differs |
Each version is a file with the label in its name (attention_efficient.py) and a versions.yaml entry in impl/<framework>/ with the label, a one-line note and the page section that discusses it.
8.6 The shape contract of an architecture
An architecture node has shapes.yaml. It states the tensor shapes of each block, the parameter count and the FLOP count as formulas in the config variables, and the build checks every formula against every framework's code.
# nodes/deep-learning-architectures/transformers/transformer/shapes.yaml (part)
config:
d_model: {meaning: "width of the residual stream", example: 512}
n_heads: {meaning: "number of attention heads", example: 8}
d_head: {meaning: "width of each head", formula: "d_model / n_heads"}
d_ff: {meaning: "width of the MLP hidden layer", example: 2048}
n_layers: {example: 6}
vocab: {example: 32000}
axes:
B: batch
T: sequence length
H: n_heads
D: d_head
M: d_model
F: d_ff
V: vocab
blocks:
- id: attn
eq: [attn-scores, attn-softmax, attn-output]
inputs: {x: "(B, T, M)"}
outputs: {y: "(B, T, M)"}
internals: {q: "(B, H, T, D)", k: "(B, H, T, D)", v: "(B, H, T, D)", scores: "(B, H, T, T)"}
params: "4 * M * M + 4 * M" # W_q, W_k, W_v, W_o with biases
flops_forward: "8 * B * T * M * M + 4 * B * T * T * M"
- id: mlp
eq: [mlp]
inputs: {x: "(B, T, M)"}
outputs: {y: "(B, T, M)"}
params: "2 * M * F + F + M"
flops_forward: "4 * B * T * M * F"
- id: block
of: [attn, mlp, ln1, ln2]
params: "4 * M * M + 2 * M * F + 9 * M + F"
model:
params: "n_layers * (4 * M * M + 2 * M * F + 9 * M + F) + V * M" # tied embeddings
flops_per_token_forward: "2 * params_non_embedding + 2 * n_layers * T * M"
sources:
- {work: vaswani-2017-attention, locator: "Section 3.3 and Table 1"}
- {work: kaplan-2020-scaling-laws, locator: "Section 2.1, Table 1"}
The checks (rule X09):
- Shapes. The test harness runs
forwardof each framework on random inputs for three configs (the example config and two small random ones fromaik shapes sample) and checks the shape of each output and of each internal that the code exposes throughcache. - Parameter count.
param_count(config)of the code, the count of the actual parameter arrays, and the formula ofshapes.yamlagree exactly for the three configs. - FLOPs.
flops(config, inputs)of the code and the formula agree exactly. The build also counts the multiply-adds of the NumPy forward with an instrumented array type (aik flopcount) on the small configs and checks that it agrees with the formula within the conventions ofschema/flops.md(a multiply-add is 2 FLOPs; softmax, normalization and activations are counted separately and listed underflops_minor). - The page and the shapes agree. Every parameter count and FLOP formula that the page prints is rendered from
shapes.yaml, never typed in the text (rule X20). The calculators of 16.14 use the same formulas.
8.7 Tests
8.7.1 Own tests
Each implementation has tests in its framework (8.3.2) for:
- shapes of outputs for the configs of 8.6, and for the edge configs: batch of one, sequence of one, one head, odd sizes;
- the worked example of the page: the same small input gives the numbers that the page prints (06.6); the page's numbers are rendered from this test's fixture, so they cannot drift;
- edge cases of the kind: an all-masked attention row, a zero-variance input to normalization,
p = 0andpclose to 1 for dropout, empty hypothesis and no n-gram overlap for BLEU, an empty reference set; - invariants stated on the page, as property tests with hypothesis: softmax is invariant to adding a constant to every logit; attention without positions is equivariant to permuting the sequence; layer normalization output has zero mean and unit variance per position up to
eps; a metric is symmetric when the page says it is.
8.7.2 Shared fixtures
fixtures/ in the node directory holds the shared inputs, made by fixtures/make.py with a fixed seed, and stored as small .npz files (at most 1 MB per node, rule X15):
fixtures/
make.py deterministic; writes the files below
params-small.npz a parameter tree in the reference's layout
input-small.npz inputs, masks, targets
expected-small.npz the NumPy reference's outputs and gradients, in float64
manifest.yaml seeds, configs, the hash of each file, the NumPy version that made it
Every framework loads the same parameters, runs the same inputs, and compares with expected-small.npz. When the NumPy reference changes, aik fixtures make rewrites the expected files, and the pull request shows the diff of every number that changed.
8.7.3 Differential tests
| Comparison | Inputs | Tolerance | Needed for |
|---|---|---|---|
| framework reference vs NumPy reference, forward | shared fixtures, and 50 random inputs per PR run (500 nightly) from aik fixtures sample |
float64: rtol=1e-10, atol=1e-12; float32: rtol=1e-5, atol=1e-6; the node's contract.yaml or shapes.yaml may loosen it with a reason |
X2 |
| framework reference vs NumPy reference, gradients | the same | float64: rtol=1e-8, atol=1e-10 |
X2 |
framework reference vs the framework's own library version (torch.nn.functional.layer_norm, jax.nn.softmax, optax.adam) |
the same | float32 tolerances; known differences of default are set explicitly in the test and listed in versions.yaml |
X2 when such a library version exists |
| labelled versions vs reference | the same | float32 tolerances | for each labelled version |
| tier 2 vs NumPy reference | shared fixtures, read from .npz through a small loader in each language |
float64 tolerances | X2 for tier 2 |
Tests run on CPU, in float64 unless the node is about precision (mixed precision, quantization), where the contract names the dtypes and tolerances. GPU numerics are the business of experiments (09.8), not of X levels.
8.7.4 Gradient checks
For each backward of the NumPy reference and for the autodiff gradients of each framework:
- central finite differences in float64,
h = 1e-6scaled by the magnitude of each input, on the small fixtures, for every parameter and input entry when there are at most 2,000 of them, otherwise for 2,000 entries sampled with a fixed seed; - the check passes when the relative error
|g - g_fd| / max(|g|, |g_fd|, 1e-8)is under1e-6for every checked entry; - non-differentiable points (ReLU at 0, max pooling ties,
absat 0) are avoided by the sampler, which moves inputs away from the kinks the contract lists inkinks:; - stochastic layers are checked with a fixed mask or a fixed key, so that the function is deterministic for the check.
A gradient check failure in the NumPy reference blocks X1 (the derivation and the code disagree; rule X04). In a framework version, it blocks X2.
8.7.5 Metric test vectors
A metric node has vectors/ (04.4):
official.jsonl: inputs and outputs computed by the official or canonical implementation, with its name, version and options (sacreBLEU with its signature string for BLEU and chrF;rouge-scorefor ROUGE;pycocotoolsfor COCO mAP; thehuman-evalpackage for pass@k;jiwerfor WER; the scikit-learn functions for classification metrics;torchmetricsandclean-fidwhere the field uses them).aik vectors make <node>runs the official implementation in a separate image,aik-vectors, that has those packages, and writes the file. The official package never enters the reference sets.edge.jsonl: the edge cases of the contract, with the expected outputs computed by the official implementation where it accepts them, and by hand with a note where it does not.variants.yaml: the options that change the score (tokenization, casing, smoothing, the interpolation of the precision-recall curve) and the vector files for each one. The page shows the options as a table and says which ones the main papers use (10.5).
Every framework's score must reproduce every vector within the contract's tolerance (rule X05). This is how a reader knows that our BLEU is the BLEU of the papers, or exactly how it differs.
8.7.6 The learning smoke test
Each architecture and each training method has one test that the code can learn: the model fits a tiny fixed batch (for example 8 examples of a copy task or 32 points of a two-moons set) to a loss under a threshold within a fixed number of steps on CPU. It catches wrong signs, detached gradients and broken masks that shape and gradient tests can miss. It runs in at most 30 seconds per framework, and it is part of X2. It is not an experiment: it makes no claim about the method (09.1).
8.8 Mutation testing
As in algorithm 09.6, with the operators adapted to numerical code:
- Tools. mutmut for the Python frameworks, Mull for C and C++, cargo-mutants for Rust, StrykerJS for TypeScript, and
aik mutate(the generic mutator on the tree-sitter grammar, from algorithm) for Julia and as the common operator set for all. - Operators. The algorithm set (relational and arithmetic operator changes, constants by plus or minus one, boolean negation, statement deletion, argument swaps) plus numerical operators: swap two axes in a
transpose,sumormean; change the axis of a reduction; drop akeepdims; changesqrt(d)tod; remove the max subtraction of a softmax; changeepsby a factor of 1,000; swapmeanandsum; drop a mask; change a+ 1in a bias correction; flip the sign of a gradient term. - What kills a mutant. The own tests, the shared fixtures, the differential tests and the gradient checks. A mutant that crashes or times out counts as killed.
- Equivalent mutants. Listed in
impl/<framework>/mutants.ignorewith a reason, at most 5% (rule X10). Example: removing the max subtraction in a softmax is equivalent on the fixtures but not on large logits, so the right answer is a test with large logits, not an ignore entry. - The threshold. At least 80% of the non-equivalent mutants killed for X3 (rule X06). The threshold is lower than algorithm's 85% because numerical mutants near tolerances are harder to kill without making tests brittle. M2 measures the real distribution and may raise it (Default).
- The budget. At most 150 mutants per file in PR runs, sampled with a fixed seed. The nightly run tests all mutants of files that changed since the last release.
8.9 Runner images and the sandbox
8.9.1 Images
- Each framework has one CPU image,
ghcr.io/tamnd/aik-runner-<framework>, built fromai-agent/images/<framework>/Dockerfile. It holds the toolchain, the pinned package set, the test, format, lint, type and mutation tools. - Experiments use separate images,
ghcr.io/tamnd/aik-exp-<stack>(for exampleaik-exp-pytorch-cu), with GPU builds of the frameworks and the pinned experiment packages (09.4). aik-vectorsholds the official metric implementations (8.7.5). It runs onlyaik vectors make, never the code of a pull request.manifests/images.yamlin the content repository pins each image by digest, as in algorithm. A new image is a pull request that moves the digest and carries the full test matrix report of the framework.- Images are built for
linux/amd64andlinux/arm64. The nightly run uses both; a test that passes on one and fails on the other is a finding (rule X17).
8.9.2 The sandbox
| Limit | Value |
|---|---|
| network | none (--network none) |
| file system | the build tree read-only; a tmpfs of 1 GiB |
| user | unprivileged, no new privileges, the default seccomp profile |
| CPU | 2 cores; OMP_NUM_THREADS, MKL_NUM_THREADS and XLA_FLAGS set to match, so that results do not depend on the machine |
| memory | 4 GiB per test run (PyTorch and JAX need more than algorithm's 2 GiB) |
| time | 120 s for the own tests of one framework of one node, 60 s for the differential and gradient tests, 30 s for the learning smoke test, 900 s for a mutation run of one node |
| processes | at most 256 |
Code from a pull request runs only inside the sandbox, on CI runners or the build server (rule X18: aik test refuses to run outside a container unless -unsafe-local is given).
8.9.3 The cache
A test result is cached under the hash of the implementation files, the fixtures, the vectors, shapes.yaml, contract.yaml, the test files and the image digest. A change to the NumPy reference invalidates every framework of that node, because it is their oracle.
8.9.4 Scale (Estimate)
About 3,000 nodes with code at M5, times 3 tier-1 frameworks, plus about 400 core nodes in 5 tier-2 frameworks, gives about 9,000 implementations, the number of 02.4 once versions are not counted. A cold full run on the build server (32 cores) is estimated at 4 to 6 hours, most of it mutation testing; a warm nightly run at under 40 minutes. M2 measures both (21).
8.10 The execution ladder X0 to X4
The build computes one level for each node and each framework, from the reference version (04.10).
| Level | It needs | Rules |
|---|---|---|
| X0 | no reference version, or one that fails a check below X1 | |
| X1 | it runs in its pinned image with no network; it imports only its package set; it passes its own tests (shapes, the worked example, edge cases, invariants); for the NumPy reference, the hand-written backward passes the gradient check | X01, X03, X04, X07, X08, X11, X12 |
| X2 | X1, and it agrees with the NumPy reference on the shared fixtures and the random differential inputs within the tolerance; autodiff gradients pass the finite-difference check; metrics reproduce the official test vectors; architectures pass the shape contract; the learning smoke test passes | X02, X04, X05, X09 |
| X3 | X2, and format, lint and type checks are clean; the conventions of 8.3 hold (shape comments, equation markers, no hidden state); its tests kill at least 80% of the mutants | X06, X10, X13 |
| X4 | X3, and a person who is not the author reviewed this exact file hash (15.5) |
For the NumPy reference, X2 means: it passes the shared fixtures that it made itself, the official vectors (metrics) and the shape contract (architectures), and it agrees with at least one framework's library version where one exists (the second oracle).
A node is "runs everywhere" (15.1) when numpy and pytorch are at X3 and jax at X2, the target of 04.10.
8.11 Numerics
| Topic | Rule |
|---|---|
| dtype in tests | float64 for every comparison, unless the node is about precision |
| dtype in the reference | the code is generic in dtype; it never casts to float32 silently |
| softmax, log-softmax, log-sum-exp | subtract the maximum; tests include logits of magnitude 1e4 |
| cross-entropy | computed from logits with log-softmax; the probability form is a labelled historical or teaching version |
| normalization | the eps of the page, inside the square root as the page writes it; the page says where frameworks differ (PyTorch's LayerNorm and JAX's defaults) |
| masks | additive masks use the dtype's minimum or -inf with a guard for all-masked rows; the edge test covers the all-masked row |
| random numbers | explicit generators; the fixtures store the masks and noise, so frameworks with different random number generators compare on the same draws |
| NaN and Inf | the tests fail on any NaN or Inf in outputs or gradients unless the contract says the output may be infinite |
8.12 The licence header
# SPDX-License-Identifier: MIT
# Part of https://ai.tamnd.com/dropout/ (ai://3KD9QX). The authors are listed in prov/3KD9QX.jsonl.
Rule X19 checks the header. The code is MIT (19.2). Code adapted from a source with a compatible licence keeps the source's notice in the file and in prov, and the copy check (13.7) compares every implementation with the code corpora of the raw store.
8.13 A worked example: scaled dot-product attention
The NumPy reference, with its backward pass:
# SPDX-License-Identifier: MIT
# Part of https://ai.tamnd.com/scaled-dot-product-attention/ (ai://7HQ2MV). The authors are listed in prov/7HQ2MV.jsonl.
"""Scaled dot-product attention, forward and backward.
Implements eq: attn-scores, attn-softmax, attn-output and the gradients of derivations/gradient.md.
"""
import numpy as np
def forward(q, k, v, mask=None):
"""q: (B, H, T, D), k: (B, H, S, D), v: (B, H, S, D), mask: (B, 1, T, S) bool, True = keep."""
d = q.shape[-1]
scores = q @ np.swapaxes(k, -1, -2) / np.sqrt(d) # eq: attn-scores (B, H, T, S)
if mask is not None:
scores = np.where(mask, scores, -np.inf)
m = np.max(scores, axis=-1, keepdims=True) # eq: attn-softmax (B, H, T, 1)
m = np.where(np.isfinite(m), m, 0.0) # all-masked rows
e = np.exp(scores - m) # (B, H, T, S)
z = np.sum(e, axis=-1, keepdims=True) # (B, H, T, 1)
a = np.divide(e, z, out=np.zeros_like(e), where=z > 0) # (B, H, T, S)
y = a @ v # eq: attn-output (B, H, T, D)
return y, (q, k, v, a)
def backward(cache, dy):
"""dy: (B, H, T, D). Returns dq, dk, dv."""
q, k, v, a = cache
d = q.shape[-1]
dv = np.swapaxes(a, -1, -2) @ dy # eq: grad-v (B, H, S, D)
da = dy @ np.swapaxes(v, -1, -2) # eq: grad-a (B, H, T, S)
ds = a * (da - np.sum(da * a, axis=-1, keepdims=True)) # eq: grad-softmax (B, H, T, S)
dq = ds @ k / np.sqrt(d) # eq: grad-q (B, H, T, D)
dk = np.swapaxes(ds, -1, -2) @ q / np.sqrt(d) # eq: grad-k (B, H, S, D)
return dq, dk, dv
The PyTorch reference computes the same three equations with torch operations and leaves the gradient to autograd. Its test loads fixtures/, compares the forward and the gradients with expected-small.npz, and compares with torch.nn.functional.scaled_dot_product_attention as the second oracle. The JAX reference is the same in jax.numpy, tested with jax.grad against the same file. The own tests check the shapes for S != T, the all-masked row, the invariance of the output to adding a constant to all scores of a row, and the permutation equivariance of the output when q, k and v are permuted together with no mask.
8.14 Who writes implementations
The implementer role (14.2) writes the NumPy reference from the page and its derivations, then each framework version, then the tests, in that order, under the gate of 14.6. The tester role adds tests that kill surviving mutants. A different model family audits the code against the page (15.4). A person reviews core implementations for X4.
8.15 The rules of this document
The rules are listed in 15.3 with their severities. In short:
| Rule | Checks |
|---|---|
| X01 | the NumPy reference exists and is at X1 before any other framework version of the node merges |
| X02 | the framework reference agrees with the NumPy reference on the fixtures and the random inputs, forward and gradients, within the tolerance |
| X03 | it runs in its pinned image with no network and passes its own tests |
| X04 | the hand-written backward (NumPy) and the autodiff gradients (frameworks) pass the finite-difference check |
| X05 | a metric reproduces every official and edge test vector within the contract's tolerance |
| X06 | its tests kill at least 80% of the non-equivalent mutants |
| X07 | it imports only the packages of its framework's pinned set |
| X08 | it does not call the framework's own version of the node's subject (the self-import register) |
| X09 | an architecture passes its shape contract: shapes, parameter count and FLOP formulas agree with the code |
| X10 | at most 5% of mutants are marked equivalent, each with a reason |
| X11 | every eq: marker names an equation id of the page, and every equation in the page's implementation section has a marker |
| X12 | the framework is in the register, and file names follow its conventions |
| X13 | format, lint and type checks are clean; shape comments use the axes of shapes.yaml |
| X14 | warning: a reference version is longer than 200 lines |
| X15 | fixtures are deterministic, at most 1 MB per node, and the manifest hashes match |
| X16 | an efficient version is at least 1.3 times faster or uses at most 0.7 times the peak memory on the largest fixture |
| X17 | results are the same on amd64 and arm64 within the tolerance |
| X18 | tests of a pull request run only in the sandbox |
| X19 | the licence header is present and names the node's page and tag |
| X20 | the numbers that the page prints from a worked example come from the test fixture, and its parameter and FLOP formulas from shapes.yaml, never from the text |