BDH — an independent implementation, assessed against its own artifacts 2026-09-13

Where this architecture actually stands

A genuinely linear-time attention path, a Hebbian state matrix that is written on every step and read by nothing, and a documentation layer that describes the second as the architecture's core contribution.

An independent, from-scratch implementation of a brain-inspired linear-attention language model: a ~4.79M-parameter model with a genuinely linear-time attention path, trained briefly on TinyStories, alongside a Hebbian state matrix whose update is implemented but whose read path was never wired up — so the memory that the documentation describes as the architecture's core contribution does not currently affect any output. The linear-attention property is real; every memory and stability claim around it is unsupported, and two of them are directly contradicted by the repository's own benchmark output.

The scale qualifier matters: the headline TinyStories run's log reaches step 79,200 of 356,657 — about 22% of one epoch — and ends there with no completion marker and no checkpoint saved. measured · run

training_bbpe_tinystories.txt — last logged line. A separate 29.38M configuration did complete: 7,812/7,812 batches, final loss 5.9990, 11.9 minutes.

The Hebbian state is written on every step and read by nothing. The method that would fold it into the output, _combine_states, is defined in three files and called from none of them. So the attention is genuinely linear-time, and the memory is genuinely instrumented — and no output this model produces depends on it. That is a specific, fixable missing read path, not a dead idea: the state update exists, is correct in form, and is computed on every forward pass.

The linear attention is real and it is in the code: no N by N table is ever built. The state update is real too, and it is computed on every forward pass — but it never reaches the output, because the only method that would fold it in is defined in three files and called from none. What is left is a small transformer-style model with unnormalised linear attention and an instrumented state, trained to about a fifth of one epoch on TinyStories. No run in this repository compares it to a transformer at matched parameters and matched tokens, and the memory and stability numbers in the project's own README are contradicted by the project's own benchmark JSON.

The four labels used on this page

measured
An executed script or run produced it and the raw output exists in the repository. Qualifiers: run, code.
project-reported
A number from prose, a to-be-filled placeholder, or arithmetic derived from a stated config or formula. No run behind it. Qualifiers: doc-value, placeholder, derived, assumption.
paper
Published work by someone else. Qualifier: the external citation key.
not reproduced
The artifact contradicts the claim, or no artifact exists at all. Qualifiers: contradicted, uncomputable, unverified, not run.

This page labels 112 numbers and claims: 46 measured, 35 project-reported, 6 paper, 25 not reproduced. The four chips in the key above are counted in that total.

Every number on this page carries exactly one label. Structural claims — statements that are true by construction — carry none, and are followed by the caveat that limits them. The claims this project's documentation makes that its own files contradict are gathered in one place, the claims ledger, and appear nowhere else on the page.

What was built, run, and measured

Everything in this section has a raw artifact in the repository. None of it is a quality comparison, and none of it is a completed training run.

Artifacts in the repository, with the file each value was read from.
What existsWhat the artifact recordsFileLabel
A from-scratch implementation Linear attention as matmul(Kᵀ,V) then matmul(Q,·); no N×N tensor is formed anywhere. implementation/bdh_v2_clean.py
implementation/multiscale_bdh.py
implementation/bdh_recurrent.py
bdh/
measured · code
A real GPU training run Last logged line is step 79,200 of 356,657 — about 22% of one epoch — loss 4.3805, no completion marker, no checkpoint. ~273,000 tok/s and 0.6 GB VRAM are logged earlier in the same run. training_bbpe_tinystories.txt measured · run
Two distillation smoke tests 200 samples: 50 passes over them in 2.9 minutes, ending at loss 2.2710; and 3 passes in 0.2 minutes, ending at loss 5.3599. The first logged loss is 5.9226. training_log_50epochs.txt
training_log.txt
measured · run
A benchmark suite that wrote raw JSON Throughput 151,492.09 and 156,311.11 tok/s at max_seq_len=512. The JSON records no device, so the GPU attribution is an inference; only the 273,182 tok/s figure is provably the RTX 4070. benchmarking/results/benchmark_results.json
benchmarking/results/test_results.json
measured · run
Perplexity, measured on a tiny snippet 276.08 and 290.31 — cross-entropy 5.62 and 5.67, against ln(256) = 5.545 for uniform output. benchmarking/results/benchmark_results.json measured · run
A tokenizer and a distillation pipeline A project-trained BBPE tokenizer, vocabulary 8,192, 8,036 merges, used by more than ten scripts; and the distillation scripts that produced the two smoke-test logs above. tokenizers/bbpe_tokenizer.json
train_bbpe_tinystories.py
train_distillation.py
measured · code

What the training logs do and do not record

The recorded TinyStories run's log ends at about 22% of one epoch; no completed run of that configuration is recorded and no checkpoint was kept. A different, 29.38M configuration did complete: 7,812/7,812 batches, final loss 5.9990, 11.9 minutes. measured · run

The 2.2710 loss comes from a run that passed 200 samples through the model fifty times; it measures memorisation of those 200 samples, not corpus training.

The two perplexity values come from the 4.79M v1 benchmark configuration, on a 7,256-byte Shakespeare snippet, after roughly 33 optimizer steps. They are not comparable with published WikiText-2 numbers and are not placed beside them. measured · run

The test files exist. Whether they pass is not recorded in any artifact this page could find. measured · code

Parameter arithmetic: the three configurations this repository conflates

The formula, with the tied token embedding counted once and the Hebbian state matrices not registered as parameters:

total = vocab·d + L·(4·d² + 2·d·f + 4·d) + 2·d

Applied to each configuration in the repository:

  • 256·256 + 6·(4·256² + 2·256·1024 + 4·256) + 2·256 = 4,790,784
  • 256·128 + 4·(4·128² + 2·128·256 + 4·128) + 2·128 = 559,360
  • 32000·512 + 8·(4·512² + 2·512·2048 + 4·512) + 2·512 = 41,567,232

The formula reproduces 4,790,784 exactly — the training run printed the same number, [OK] Student created: 4,790,784 parameters in training_log_50epochs.txt — and it reproduces 41,567,232 exactly, the figure the repository's own deep-dive quotes for a configuration nothing was trained with. That is the evidence the formula is right, and therefore that 559,360 is the benchmark model.

The three configurations. No number on this page belongs to more than one of them.
Configurationvocab / d / L / fParametersWhat it producedLabel
Training config, "the 10M model" 256 / 256 / 6 / 1024 4,790,784 ≈ 4.79M total; 4,725,248 excluding the tied embedding The TinyStories and BBPE training runs, and the distillation student. measured · run
v2 ablation config 256 / 128 / 4 / 256 559,360 ≈ 0.56M total; 526,592 excluding the tied embedding The ablation_*.json files and the small task JSONs — not benchmark_results.json. project-reported · derived
bdh package default 32000 / 512 / 8 / 2048 41,567,232 ≈ 41.57M, embedding-dominated: 16.38M of it is the 32000×512 table Nothing on record. It is a default, not a run; the "41M" the repository's deep-dive quotes matches this config, not any trained model. project-reported · derived

Which script wrote which file: benchmark_results.json and test_results.json are written by the v1 suite, whose config is 256/256/6/1024 = 4,790,784. benchmark_suite_v2.py writes only ablation_*.json and the small task JSONs.

The repository also states a parameter count that no configuration in it yields: the summary's own component list does not sum to its own total. The struck claim and its contradicting values are in the claims ledger. not reproduced · contradicted

The recurrence that is specified, and the one that runs

The design specifies a matrix that is written by co-activation, decayed by time, and read back with a query. The update exists, and runs on every forward pass. The read is not called.

Two equations describe the intended mechanism. The state is written, per head and per scale, as E ← γ·E + η·W, and it is read as y = q·E. In the shipped configuration the write is W = diag(q ⊙ v); the full outer product W = q vᵀ is implemented behind a flag that is off. Neither one is fed back into any line that produces an output.

Three structural properties, each with its limit

  • Structural: the state is a fixed O(d²) per head instead of a cache that grows with context. project-reported · derived

    O(d²) per head can exceed a KV cache at short context; the advantage only appears at long context — and here the state it saves is not used, because nothing reads it.

  • Structural: the memory is an explicit object rather than a hidden activation. measured · code

    Readable is not interpretable: no interpretability experiment was run, and in this code the object is not read at all.

  • Structural: the write rule is local and Hebbian. Backpropagation still trains every weight; the write itself is local. measured · code

    Nobody has shown this scales. That is the open question, not a result.

The configuration that produced every result

In every configuration that produced a result, the outer product is off. The flag defaults to false in both config definitions and the benchmark suite defaults it to false as well, so the diagonal write is what the JSON in this repository describes. The full outer product is implemented and correct in form — torch.einsum("hd,he->hde", …) against torch.diag_embed(Q_flat * V_flat) — and it is never enabled.

The state is also a decayed running average: both implementations average Q and V over batch and time before writing, so a token's own write is diluted by the sequence it arrived in. The write touches D of D² cells in the shipped mode.

And the state is written but never read: _combine_states, the only method that would fold it into the output, is defined at bdh_v2_clean.py:242, multiscale_bdh.py:255 and bdh/attention.py:79, and called from nowhere in the repository. measured · code

implementation/bdh_v2_clean.py:62, :214-224, :278-296 · benchmarking/benchmark_suite_v2.py:97, :730

The repository's own summary of the feed-forward network states that its ReLU leaves about 5% of neurons active. No sparsity histogram exists in the repository, and an analytic estimate from the initialisation puts the zero fraction nearer half. project-reported · doc-value

The core recurrence, in two views

measured · code

Left, the state matrix as it is written and decayed. Below it, the two readers that could consume it: the one the design specifies, and the one the code actually runs — which never touches the state.

Static figure, script disabled or this plate unavailable. A square matrix of the state for one head is drawn at 16 by 16 cells, with three smaller copies beside it for the fast, medium and slow scales. The matrix is drawn in luminance only: a cell's brightness is its magnitude relative to the largest magnitude currently in the matrix, and a cell holding a negative value is left dark with a thin outline.

In the configuration drawn here — the shipped one — only the 16 diagonal cells are ever written, so every off-diagonal cell stays at exactly zero. The three smaller copies show the same matrix under three decay rates.

Above the matrices, a row of boxes carries the computation the shipped code runs: Q, K, V into KᵀV, into Q·(KᵀV), into Wo, into the output. The arrow that would carry the state into that output is dashed and stops 24 pixels short of it, marked not called; beside it sits a greyed box for _combine_states(). Every number the figure uses is in the tables below.

Transport
Steps
What is written
Write matrix W
Write source
d² cells per state; the shipped benchmark head dimension is 32.
Focused scale
The focused scale's decay is the one the γ control below edits.
Presets set the focused scale only. η is fixed at 0.001.
Which view
Reader
Solid means computed and used. Dashed means specified, not enabled, or not called.
Plate 1 — live values — the numbers this plate is drawing
Live values for this plate. Step and token are the simulation's own clock and carry no label. The plate is arithmetic, not a measurement of the model: it computes the update rule on the page and draws it.
Step64
Token shownt64
Cells written this step project-reported · derived 16 of 256 cells — the diagonal
‖E‖ fast scale project-reported · derived 4.081e-04
‖E‖ medium scale project-reported · derived 1.994e-03
‖E‖ slow scale project-reported · derived 2.573e-03
Running max |E| — the shading denominator project-reported · derived 1.633e-03
γ fast / medium / slow measured · code 0.950 / 0.990 / 0.995
η measured · code 0.001
Write matrix W measured · code diagonal — torch.diag_embed(Q_flat * V_flat), the shipped default
Write source measured · code batch-mean — Q.mean(dim=(0,1)), V.mean(dim=(0,1))
Read path measured · code _combine_states defined ×3 · called ×0
Reader drawn measured · code View B — the output is computed from the current sequence only; the state is updated after it
Step history at the plate's current parameters. Every value in this table is the page's own arithmetic on the update rule. project-reported · derived
StepTokenCells written‖E‖ fast‖E‖ medium‖E‖ slow
0016000
16t16161.494e-032.337e-032.478e-03
32t32169.151e-042.276e-032.578e-03
48t48166.993e-042.310e-032.765e-03
64t64164.081e-041.994e-032.573e-03
The same state object, two readers.
PathSpecifiedComputed
State update E ← γE + ηW Implemented. Implemented and executed on every forward pass. measured · code
Query read y = q·E Specified. Not called. _combine_states is defined in three files and called from none of them. measured · code

The matrix is written on every step and decays between writes. In the shipped configuration only the diagonal is written, so D of D² cells receive anything at all. The matrix keeps its values across tokens; the input does not. Nothing in the shipped code reads those values back.

What this does not prove. It does not show that the model remembers anything, and its View A is not a description of the shipped behaviour: View A is the design specification, shown so the missing piece is visible. It does not show a full outer product in the benchmarked configuration, because the flag is false there (implementation/bdh_v2_clean.py:62, benchmarking/benchmark_suite_v2.py:97, :730) and no measured artifact used it. It does not show a per-token write in the default view: the code averages Q and V over batch and time first (:214-215), which is why the batch-mean view exists. And the state drawn here has no effect on any output in this repository: the forward pass computes kv = KᵀV, attn = Q·kv, out = Wo·attn and updates the state afterwards (:278-296) — the state is not read by any line that computes out.
How this is computed

Per head, per scale i: E_i ← γ_i·E_i + η·W. The write is W = diag(q ⊙ v) in the shipped mode, touching the d diagonal entries of a d×d matrix, or W = q vᵀ in the mode that is implemented and off. The read the design specifies is E = Σ w_i E_i with w = [0.2, 0.3, 0.5], then y = q·E.

q and v are signed values in [-1, 1), drawn from a seeded stream (mulberry32, seed 0xBD5). They are not rectified, because the shipped code does not apply ReLU to Q or V — ReLU appears only inside the feed-forward network (implementation/bdh_v2_clean.py:330), and the gate is a sigmoid (:395, :400). No positive-orthant constraint is applied to the state write.

Cell shading is |E| / max(1e-6, runningMaxAbs), mapped to luminance between the plate surround and a light grey. Sign is structural, not coloured: a negative cell is filled with the surround colour and given a 1px inner outline in its own luminance grey. The running maximum is printed in the readout above so the shading is never read as an absolute scale.

In the small multiples each cell is one sampled block, so a negative reads simply as the surround colour; the 1px outline is reserved for the focused matrix, where cells are large enough to carry it. In the outer-product mode every cell is written, so the write flash is drawn as a single inset outline of the whole matrix rather than a per-cell overlay; in diagonal mode the d written cells are outlined. The flash fades over 220 ms and its alpha never exceeds 0.4.

Drawing convention, used across every plate on this page: solid means computed and used; dashed means specified, not enabled, or not called.

The same product, computed in two different orders

Q(KᵀV) equals (QKᵀ)V. The difference is what has to exist at the same time: one order builds an N by N table, the other never does.

Per head, ignoring batch: the quadratic order costs 2N²D multiply–adds and needs an N²-cell intermediate; the linear order costs 2ND² multiply–adds and needs a D²-cell intermediate. The two counts are equal when N equals D. At the benchmark head dimension of 32, that crossover is at 32 tokens; at 2,048 tokens the count ratio is 64.

Operation counts are not time. A count ratio is a statement about arithmetic, not about wall-clock speed, and nothing in it says which order finishes first on a real device. project-reported · derived

The right-hand panel below is what this code runs, and it is not standard attention: there is no softmax, no denominator and no 1/√D scaling anywhere in the attention path. measured · code

No wall-clock measurement as a function of sequence length exists in this repository, so the claim that the linear order is faster is not tested here in either direction. not reproduced · not run

Two matmuls, side by side

project-reported · derived

The same product, computed in the two orders. Left builds an N by N table; right never materialises one. Both counts are computed in closed form on this page and neither is profiled.

Static figure, script disabled or this plate unavailable. Two panels. The left panel is a square grid of blocks standing for the N by N score table that the quadratic order has to hold; each drawn block stands for many real cells and the number it stands for is printed on the canvas beside the grid. The right panel is a small grid of the D by D intermediate this code actually builds, drawn in the same luminance style.

The sweep that fills the left grid in the live version is a drawing device only; it has no timing meaning. Every count the figure displays is computed from N and D in closed form and appears in the tables below.

Transport
Sweep
Arrow keys move through 16, 32, 64, 128, 256, 512, 1024, 2048, 4096.
Head dimension D
Plate 2 — live values — the numbers this plate is drawing
Live counts, computed in closed form from N and D. Nothing here is profiled.
Multiply–adds, quadratic order 2N²D project-reported · derived 268,435,456
Multiply–adds, linear order 2ND² project-reported · derived 4,194,304
Count ratio N/D project-reported · derived 64
Peak intermediate, quadratic project-reported · derived 4,194,304 cells
Peak intermediate, linear project-reported · derived 1,024 cells
Bytes at fp32, quadratic project-reported · derived 16,777,216 bytes — 16 MiB
Bytes at fp32, linear project-reported · derived 4,096 bytes — 4 KiB
Cells drawn this sweep — a drawing device, not a measurement project-reported · derived 0
Crossover project-reported · derived The two counts are equal when N equals D. At D = 32 that is 32 tokens.
The four quantities at D = 32, for every sequence length the control offers. Values in this table are the page's own arithmetic. project-reported · derived
NQuadratic multiply–adds 2N²DLinear multiply–adds 2ND²Peak intermediate, quadraticPeak intermediate, linear
1616,38432,768256 cells1,024 cells
3265,53665,5361,024 cells1,024 cells
64262,144131,0724,096 cells1,024 cells
1281,048,576262,14416,384 cells1,024 cells
2564,194,304524,28865,536 cells1,024 cells
51216,777,2161,048,576262,144 cells1,024 cells
1,02467,108,8642,097,1521,048,576 cells1,024 cells
2,048268,435,4564,194,3044,194,304 cells1,024 cells
4,0961,073,741,8248,388,60816,777,216 cells1,024 cells

The two orders compute the same product. The counts are equal when N equals D. The peak intermediate is the memory difference: it is the largest object that has to exist at one time, and in the quadratic order it grows with the square of the sequence length.

What this does not prove. At the context lengths this repository trained and evaluated, at most 2,048 tokens, the crossover is irrelevant, and dense quadratic attention is faster in absolute wall-clock terms on GPUs. No wall-clock measurement as a function of sequence length exists in this repository. This attention also has no softmax and no 1/√D scaling, so the right-hand panel is not equivalent to standard attention — it is what the code runs.
How this is computed

Quadratic, per head, ignoring batch: S = QKᵀ at N²D multiply–adds and N² cells, then O = SV at N²D, giving 2N²D and a peak intermediate of N² cells, 4N² bytes at fp32.

Linear, the path this code runs (implementation/bdh_v2_clean.py:278-281): Z = KᵀV at ND² and D² cells, then O = QZ at ND², giving 2ND² and a peak intermediate of D² cells, 4D² bytes at fp32.

Every number is computed in closed form and never by enumerating cells: the left grid draws at most 128 by 128 blocks, each block standing for (N/128)² real cells, and the number of real cells a block stands for is printed on the canvas beside the grid. Nothing here is timed. The sweep exists so that the size of the table is visible rather than asserted, and it runs at 30 columns per second for that reason — a per-frame sweep would read as a strobe, not as a measurement.

Three decay rates, and what they would carry

γ^Δ is exact: a write made Δ steps ago survives in scale i at exactly γᵢ^Δ times its original size. That is arithmetic of the update rule, not a measurement of what the model remembers — in the shipped code the state is not read by anything, so it is not a description of the model's behaviour either.

With the code's own rates, the arithmetic horizon is: at 500 tokens, γ^Δ is 7.3e-12 for the fast scale, 6.6e-03 for the medium and 8.2e-02 for the slow. At 2,000 tokens only the slow scale is still above 4.4e-05, and none of the three is above the 1e-3 floor this page assumes.

The measured counterpoint. The one executed measurement in this repository's benchmark JSON reports 6.91e-05 for the baseline arm and 7.20e-05 for the arm labelled multi-scale — but that number comes from an untrained 4.79M model, its "multi-scale" arm is only a different single decay constant, and the quantity is a state-norm proxy rather than a fraction kept. It is not the percentage gap the documentation states, and it is not a measurement of memory at all. The design extends the arithmetic horizon; nothing on record shows the extension being used, and nothing on record shows it could be. measured · run

benchmarking/results/benchmark_results.json:34 · the same file's create_multiscale_config() is state_decay=0.995 with the comment "future: multiple decay rates", and the suite benchmarks freshly constructed, untrained models.

Three decay rates, and what they would carry

project-reported · derived

Three exact decay curves, one cue fired into them, and the three state matrices it would leave behind. Nothing here is what the model does with them, because nothing reads them.

Static figure, script disabled or this plate unavailable. A log-scale plot of γ^Δ for the three decay rates over Δ from 1 to 2,000. The vertical axis spans nine decades; the horizontal axis is logarithmic in Δ. A horizontal rule marks the floor below which this page treats a value as gone, and the three curves cross it at different distances — which is the whole of what the three rates change.

Below the plot are three small matrices drawn in the same luminance style as the recurrence plate: one per decay rate, each carrying the pattern written by a cue fired at step 0 and then decayed for Δ steps. In the shipped diagonal mode only the diagonal of each matrix receives anything, and every off-diagonal cell stays at exactly zero.

The top-left corner of the plot area carries the label conceptual — not read by the shipped model, on the canvas and not only in the caption, because a decay curve is the easiest thing here to misread as behaviour. Every value it plots is in the table below.

The cue
Fire the cue
The cue writes a marked pattern at step 0 and the plate then steps forward to the current Δ.
Log-mapped: the slider's full travel spans 1 to 2,000 steps.
Log-mapped 1e-9 to 1. An assumption this page makes, not a repository value.
Write matrix W
Affects the matrix view only; the curves are pure arithmetic and do not depend on it.
Decay rates

The three rates are clamped on input so that fast < medium < slow; the clamp is applied when a control moves, and the values shown are always the values in force.

Plate 3 — live values — the numbers this plate is drawing
Exact γ^Δ at the code's three rates, computed on this page with Math.pow and redrawn whenever a rate changes. Every value is arithmetic on the update rule, not a measurement of the model. project-reported · derived
Δγ = 0.95γ = 0.99γ = 0.995
19.500e-019.900e-019.950e-01
105.987e-019.044e-019.511e-01
507.694e-026.050e-017.783e-01
1005.921e-033.660e-016.058e-01
2003.505e-051.340e-013.670e-01
5007.274e-126.570e-038.157e-02
1,0005.292e-234.317e-056.654e-03
2,0002.800e-451.864e-094.428e-05
Which scales carry the cue at the current Δ and floor. This is arithmetic against a threshold this page chose, not a measurement of the model. project-reported · assumption
Fast scale, γ = 0.95007.274e-12below floor
Medium scale, γ = 0.99006.570e-03above floor
Slow scale, γ = 0.99508.157e-02above floor
Cue distance Δ500the fast scale is out; the medium and slow scales are in

At Δ = 2000 with the default floor, no scale carries the cue above the floor. The floor is an assumption control: it is the smallest surviving signal a readout could plausibly use, and the repository contains no measurement of this threshold. The measured counterpoint in the repository's own benchmark JSON is 0.0000691 for the baseline arm and 0.0000720 for the arm labelled multi-scale at 500 tokens — a state-norm proxy from an untrained model, not a fraction, and not comparable with the arithmetic above. measured · run

What this does not prove, and what it is labelled with. The canvas carries the label conceptual — not read by the shipped model, and that label is the point of the plate. It does not show that the model uses whatever survives — nothing in the shipped code reads the state at all. It does not show that multi-scale decay improved anything: the only executed measurement is a state-norm proxy from an untrained model whose "multi-scale" arm is a single different decay constant, so it is not a result in either direction and cannot be compared with the improvement factor the documentation states. It does not show learned behaviour: γ^Δ is exact arithmetic for the update rule, and no experiment on the multi-scale model's memory was run. The floor line is an assumption this page invented for legibility; nothing in the repository measures it.
How this is computed

Each scale is updated as E_i ← γ_i·E_i + η·W_t, so the contribution of a write made at t−Δ to E_i at t is exactly γ_i^Δ · W_{t−Δ}. The combined state the design specifies is E = Σ w_i·E_i with w = [0.2, 0.3, 0.5], the code's own values; they are shown and not editable.

The curves are drawn from Math.pow(gamma, delta) and cached; they are rebuilt only when a rate, the floor, the canvas size or the device pixel ratio changes. The matrices below the plot are 24 by 24 downsampled views of the full state, written once per step into an offscreen buffer.

Drawing convention, used across every plate on this page: solid means computed and used; dashed means specified, not enabled, or not called. The dashed connector at the conceptual banner is the second kind.

The unit that motivated the design

A leaky integrate-and-fire neuron with a threshold that rises as it fires. It integrates, spikes, resets, tires — and under sustained input it settles into a rhythm instead of saturating.

This repository contains no spiking unit

Searching the implementation and package code for integrate-and-fire, membrane, refractory, threshold-spike and fatigue logic returns nothing. The nearest things in shipped code are a ReLU threshold inside the feed-forward network and a sigmoid gate on the layer output. The diagram below models the biological unit; it does not model this code. measured · code

implementation/bdh_v2_clean.py:330 — ReLU in the feed-forward network · :395, :400 — the sigmoid gate. Grep across implementation/, bdh/ and benchmarking/ returns no match for integrate-and-fire, membrane, refractory or fatigue.

Why the lineage matters to the argument on this page: the Hebbian programme's unit is local, unlabelled and cheap. A threshold that adapts, and a write that depends only on the pre- and post-synaptic pair at one moment, is a different kind of object from a global gradient step. Whether that buys anything at scale is the open question this page keeps open — and nothing in this repository tests it, because the unit itself was never implemented here.

The fatiguing spiking unit

project-reported · derived measured · code

One model neuron, integrating a drive and firing when it reaches a threshold that its own firing raises. The equations are textbook; the parameters are normalised for legibility and fitted to nothing.

Static figure, script disabled or this plate unavailable. A scrolling trace of one model neuron: the membrane potential rising toward its threshold, and the threshold itself as a second, dashed trace that steps up at every spike and then decays back.

Where the potential reaches the threshold, the trace resets to zero, the threshold jumps, and a short refractory band is drawn: a fixed interval during which no second spike can be recorded. That is the whole of the unit — integration, threshold, reset, refractoriness, and a fatigue that recovers.

Along the bottom runs a strip of twelve units with staggered phases, drawn as marks rather than a network, so no connection is implied between them.

The static frame below is a precomputed 200 ms window with θ_a = 0.30 and a sustained drive of 1.80, both printed on the canvas: fourteen spikes at a mean interval of 14.0 ms, the threshold visibly elevated above its resting value, refractory time drawn between them — the values in the readout table below. This figure models the idea from the lineage. This repository contains no spiking unit, and no fatigue, spiking or threshold-dynamics mechanism exists in it to measure.

Transport
Drive
Trace
Resting drive sits below threshold, as in a neuron that is not being stimulated.
Unit parameters
Plate 4 — live values — the numbers this plate is drawing
Live values over the visible 500 ms window. The simulation is arithmetic on the equations below and is fitted to nothing. project-reported · derived
Rate over the visible window70.0 Hz
Mean inter-spike interval14.0 ms
Spikes since reset14
Threshold θ now1.295
Refractory milliseconds in window98
The last six spike times, regenerated when the reader acts and never while the trace is running. project-reported · derived
SpikeTime (ms)Interval since previous (ms)
9121.014.0
10135.014.0
11149.014.0
12163.014.0
13177.014.0
14191.014.0

Integration is leaky and linear, the threshold resets upward by a fixed increment at each spike and relaxes back with its own time constant, and a refractory interval forbids a second spike immediately after the first. The unit therefore fires in a rhythm rather than saturating. This repository contains no spiking unit; the nearest analogues in shipped code are the ReLU threshold in the feed-forward network (implementation/bdh_v2_clean.py:330) and the sigmoid gate (:395, :400).

What this does not prove. It does not show that BDH is biologically plausible, and it does not show that a spiking unit would help language modelling. It does not reproduce any experiment: no fatigue, spiking or threshold-dynamics mechanism exists in this repository to measure. It models the idea from the lineage, not the code.
How this is computed

k = 1 − exp(−dt/τ_m)
V = V + (I − V)·k
if V ≥ θ and t ≥ refractoryUntil:
  record spike at t; V = 0; θ = θ₀ + θ_a; refractoryUntil = t + t_ref
θ = θ₀ + (θ − θ₀)·exp(−dt/τ_θ)

Normalised units, dt = 1 ms, three simulated milliseconds per frame at 60 frames per second, which is roughly 0.18 times real time. Threshold 1.0 is arbitrary. With the baseline drive alone the unit never reaches threshold; spiking requires a pulse, a train, or the hold-drive button.

These are textbook leaky-integrate-and-fire dynamics with adaptation, parameterised in normalised units for legibility. They are not fitted to any recorded neuron and nothing in this repository implements them.

The trace is a ring buffer of 500 samples; the threshold is drawn dashed and the refractory bands are filled at low opacity. The bottom strip shows twelve units with staggered phases as marks, deliberately not as a graph, so there is no node-and-edge structure to read as a network.

Drawing convention, used across every plate on this page: solid means computed and used; dashed means specified, not enabled, or not called. The dashed threshold trace and the not-implemented label are the second kind.

Claims this project makes that its own artifacts contradict

Each row is a sentence from the project's own documentation, followed by what the project's own files say. The struck text is the claim. The text after it is the artifact.

Ten documentation claims

Multi-scale retains 15% at 500 tokens vs baseline 0.6%, a 24× improvement
Reproduced to four different values in four places: 15%, 8%, 10.2%, and a measured 7.2e-05. The only executed measurement is a different quantity — a state-norm proxy, not a percentage — produced by a suite whose "multiscale" arm is just state_decay=0.995 ("future: multiple decay rates") on untrained weights. No measurement can yield four values for one quantity.
not reproduced · contradicted
Baseline 0.6% at 500 tokens is a measurement
baseline_metrics.json metadata calls its values expected/placeholder; 0.0065 is the arithmetic of 0.99^500.
project-reported · placeholder
95% stable runs vs 40% baseline
Measured stability is 0.7644 / 0.7546, and the placeholder file the claim was drawn from says stability 0.7 → 0.85. The row contradicts both the measurement and its own source.
not reproduced · contradicted
2.75× faster training with BBPE (27.5K vs 10K tok/s)
The completed 29.38M run is logged at 273,182 tok/s on the RTX 4070; the 10K baseline is a placeholder. The 27.5K figure originated as a hardcoded literal in a docx-generating script.
not reproduced · contradicted
10.23M parameters
No configuration in the repository yields it. The training config computes to 4,790,784 and the run printed that number; the benchmark config computes to 559,360. The summary's own component list does not sum to its stated total.
not reproduced · contradicted
Perplexity competitive with GPT-2-scale models; ppl_wikitext2_100m: 23.5
23.5 is the placeholder multiscale value in a template file, promoted into a SOTA table beside GPT-2's 28.5. The only executed perplexity is 276.08 / 290.31 for the 4.79M config on a 7,256-byte Shakespeare snippet after about 33 optimizer steps — near-uniform output.
not reproduced · contradicted
The first architecture to combine true Hebbian outer-product state updates, multi-scale decay, positive-orthant constraints and multiplicative gating
No literature review supporting priority exists in the repository, and the outer-product flag is false in every config that produced a result.
not reproduced · unverified
230× compression by distilling a 1.1B teacher
1,100,048,384 ÷ 4,790,784 ≈ 229.6, so the arithmetic is right — but it is a parameter ratio from a 200-sample smoke test, presented as a distillation result. The run passed 200 samples through the model 50 times.
project-reported · doc-value
Trained from scratch; 50 epochs
The recorded TinyStories run's log ends at step 79,200 of 356,657 — about 22% of one epoch — with loss 4.3805 and no completion marker (and the log is provably incomplete: zero checkpoint lines where the code would print seven). The separately completed run reached loss 5.9990 after 7,812/7,812 batches. The "50 epochs" log is 200 synthetic samples, 25 batches, 50 passes in 2.9 minutes, ending at loss 2.2710 by memorising 200 samples.
not reproduced · contradicted
Memory retention improves with multi-scale decay
Not computable from the shipped forward pass: the state is written and never read, and the one method that would fold it into the output is defined three times and called from none. Every claim of this kind is downstream of a read path that does not exist.
not reproduced · uncomputable

The same drift inside the code

Six findings from reading the implementation itself. The fifth is the section's only positive row and is set apart: the linear-attention property is genuinely present in the code, unlike everything above it.

The outer-product flag is true in the shipped configuration
It defaults to False in both config definitions and to False again in the benchmark suite's model factory and ablation configs, so the diagonal write is what every JSON in benchmarking/results/ describes. With the flag off the write is torch.diag_embed(Q_flat * V_flat), touching D of D² entries; with it on, torch.einsum("hd,he->hde", …) is a genuine per-head outer product, and it is never enabled.
measured · code
The state is written per token, in arrival order
Both implementations collapse the sequence before the write: Q.mean(dim=(0,1)) and V.mean(dim=(0,1)). The state is a decayed running average of per-batch mean co-activations, not a per-token sum, and it carries no token order.
measured · code
All three implementations write a full Hebbian matrix
implementation/multiscale_bdh.py has no outer product at all: hebbian = Q_flat * V_flat then reshape(C), a [C] vector added to a [C,C] matrix, so every row of that matrix stays identical. In bdh_gpu_10m.py the state is a length-C vector whose "outer product" is torch.einsum('hd,hd->hd', …), an element-wise product, despite the comment above it saying outer product.
measured · code
Q and V are rectified, so the state write lives in the positive orthant
Q and V are not ReLU'd. ReLU appears only in the feed-forward hidden layer; the gate is a sigmoid. Nothing in the state write constrains the sign of its inputs, and the recurrence plate draws signed values for that reason.
measured · code
The retrieval path never materialises an N×N table
attn = matmul(K_T, V) gives [B,H,D,D], and matmul(Q, attn) gives the output. This one is real, and it is the property the whole architecture rests on. Its limit is stated with it: the same lines apply no softmax and no 1/√D scaling, so the output is not normalised the way standard attention is.
measured · code
Rotary position encoding is active in the shipped model
RoPE is disabled. In bdh_v2_clean.py it is gated by use_rope=False; in multiscale_bdh.py the forward pass has it commented out with # TEMPORARILY DISABLED - causing shape mismatch errors. Whatever order the attention carries is whatever the matmuls give it.
measured · code

Every number this page quotes, and the model it belongs to

The repository conflates three configurations. No number on this page belongs to more than one of them.

Attribution of the measured numbers this page uses.
NumberConfigurationWhat it measuredLabel
4,790,784 parameters The training configuration, printed by the run. Its own size. The checkpoint is gone. measured · run
559,360 parameters The v2 benchmark and ablation configuration. Computed from the config; no JSON records a parameter count. project-reported · derived
151,492.09 / 156,311.11 tok/s The 4.79M v1 suite at max_seq_len=512. Throughput. The JSON records no device, so the GPU attribution is an inference. measured · run
~273,000 tok/s, 0.6 GB VRAM The 29.38M BBPE TinyStories run, on an RTX 4070. One line of the same log, at step 79,190 of 356,657. measured · run
PPL 276.08 / 290.31 (CE 5.62 / 5.67) The 4.79M v1 configuration. A 7,256-byte Shakespeare snippet after about 33 optimizer steps; never placed beside GPT-2 WikiText-2 numbers. measured · run
retention@500: 6.91e-05 / 7.20e-05 The 4.79M v1 configuration, untrained weights. A state-norm proxy on a 7 KB snippet, not a percentage kept and not a property of the multi-scale arm. measured · run
stability 0.7644 / 0.7546 · final loss 3.464 / 3.504 · convergence 741 / 1000 The 4.79M v1 configuration, untrained weights. The same benchmark run. measured · run
200 samples · 25 batches · 50 passes · 2.9 min · loss 2.2710 The 4,790,784-parameter student, distilled from a 1.1B teacher. A smoke test. The loss is memorisation of those 200 samples, not corpus training. measured · run
1B parameters The published BDH-GPU work of Kosowski et al., arXiv:2509.26507. The paper's own statement of the scale it tested. Third-party work; not reproduced here. paper · arXiv:2509.26507

What the code does not do

Eight things this repository does not establish. The first is not a missing experiment; it is a missing line of code.

  1. The memory does not function at all. The state is written on every forward pass and read by nothing; _combine_states has three definitions and zero call sites. Every memory and context claim in this project is downstream of this, and none of them is computable from the shipped forward pass. measured · code
  2. No head-to-head against a transformer at matched parameters and tokens. The comparison table is curated literals and hand-assigned 1–5 scores, on two of which the project awards itself 5/5. It is not an experiment. not reproduced · not run
  3. No scale, and no completed run recorded at the headline configuration. ~4.79M parameters on TinyStories, with the captured log ending at about 22% of one epoch and no checkpoint kept. A different 29.38M configuration did complete — 7,812/7,812 batches, final loss 5.9990 — and it is not the configuration the documentation describes. not reproduced · unverified
  4. No long-context evaluation. Nothing beyond about 2k tokens; the O(N) advantage is structural, never demonstrated on a task. not reproduced · not run
  5. The retrieval tests were run, and the model failed them. Needle-in-haystack scored 0.07 / 0.18 / 0.32 / 0.34 at 32/64/128/256 tokens, and exact associative recall scored 0.0 for every configuration tried — both from the 0.56M v2 configuration on synthetic data, 100 samples. For fixed-state linear attention this is the decisive test, and the honest statement is the result rather than its absence — a result produced by a model whose state is not called anywhere in its forward pass. measured · run
  6. No checkpoints survive, so nothing can be re-run from the repository as it stands. The absence is verifiable: there is no checkpoints/ directory and no .pt or .pth file anywhere in the tree. not reproduced · contradicted
  7. The paper is third-party work. Kosowski et al., arXiv:2509.26507; this repository is an independent implementation plus extensions, and nothing here reproduces its results. paper · arXiv:2509.26507
  8. The compression figure is a parameter ratio from a 200-sample smoke test, not a capability or quality result: 1,100,048,384 ÷ 4,790,784 ≈ 229.6. project-reported · doc-value

None of these is an argument that the approach cannot work. They are the list of things that would have to be true before any of the project's quality claims could be.

Where this sits, by evidence rather than by capability

Two axes: the size of the largest model in a reported result, and the strength of the evidence behind the reported quality. Published systems are plotted from their papers. This project is plotted twice, and the gap between its two markers is what this page is about.

The vertical axis is ordinal, in four levels, and it is this page's judgement — but each level is defined by a checkable criterion, so every placement can be audited:

  1. Level 1 — contradicted by its own artifact. A file in the repository says otherwise.
  2. Level 2 — reported in prose, no artifact. The number appears in a document and nowhere else.
  3. Level 3 — artifact in this repository, reproducible from source. The raw output exists.
  4. Level 4 — published result from a third party, with paper and released weights. Not reproduced here.

The horizontal axis is model size in parameters on a log scale, because size is a fact about a result that can be checked against a config or a paper. It is not a performance axis, and the vertical axis is not a measurement of quality.

Where this sits, by evidence

project-reported · judgement

Eight marks. Two of them are the same project at the same size, two levels apart — and the stem between them is the argument of this page drawn as a distance.

Plate 5 — live values — the numbers this plate is drawing
  1. This project — engineering artifacts: throughput, VRAM, training-loss logging. x ≈ 4.79M parameters · y = level 3, artifact in this repository. The state those runs write is never read. measured · run
  2. This project — benchmark measurements: PPL 276.08 / 290.31, from a forward pass that does not read the state. exact associative recall 0.0 (the state is not called into any output); needle-in-haystack 0.07–0.34. x ≈ 4.79M parameters · y = level 3, artifact in this repository. The perplexity is a 7 KB Shakespeare snippet after about 33 steps; the retrieval figures come from the 0.56M v2 config on synthetic data. measured · run
  3. This project — README quality claims: memory and stability. x ≈ 4.79M parameters · y = level 1, contradicted by its own artifact. The contradicting values are in the ledger above. not reproduced · contradicted
  4. This project — the ~5% active-neuron figure. x ≈ 4.79M parameters · y = level 2, reported in prose, no artifact. No sparsity histogram exists in the repository. project-reported · doc-value
  5. The repository's stated parameter count. x = 1.023e7 as stated · y = level 1, contradicted by its own artifact. Hollow marker: no configuration produces that count. not reproduced · contradicted
  6. The paper's BDH-GPU, Kosowski et al., third party. x = 1e9 parameters · y = level 4, published third-party result. arXiv:2509.26507; the paper states the scale it tested. paper · arXiv:2509.26507
  7. Transformer-class published systems. x = a band from 1e9 to 1e10 parameters · y = level 4. A band, not a point: no specific number is invented here, and any specific figure would need its own external citation — the band's citation key cite:transformer is in the source list. paper · cite:transformer
  8. Mamba-2 / RWKV / RetNet-class published systems. x = a band from 1e9 to 1e10 parameters · y = level 4. Published linear-attention and state-space results at 1B and above, plotted as a range for the same reason — citation keys cite:mamba2, cite:rwkv and cite:retnet are in the source list. paper · cite:mamba2 · cite:rwkv · cite:retnet

Published systems are plotted as ranges from their papers. This project is plotted twice because two different things are true of it: its engineering measurements exist as artifacts, and its headline quality claims are contradicted by those same artifacts. Markers 1 and 3 sit on the vertical stem; markers 2 and 4 are drawn 16 pixels off it, one to each side, so that no two marks overlap — their true x is the same 4.79M.

The selected entry, so that a keyboard reader gets what hover shows.
Marker
Coordinates
Label
What this does not prove. This map is a judgement about evidence quality, not a measurement of capability. No head-to-head run against a transformer at matched parameters and tokens exists in this repository. Nothing here is a ranked comparison, and the horizontal axis is not a performance axis. Published positions are the papers' own claims at the scales they tested, not results reproduced here.
How this is drawn

The horizontal axis maps parameter count logarithmically: x = 300 + (log10(P) − 6)/4 × 580 for P from 1e6 to 1e10. The vertical axis is ordinal: level 1 at the bottom, level 4 at the top, with each level's criterion listed in full above the figure.

Drawing convention, used across every plate on this page: solid means computed and used; dashed means specified, not enabled, or not called. A hollow marker is the fourth case: contradicted. Markers 7 and 8 are horizontal brackets rather than points, because a specific parameter count for a third-party system would need its own citation and none is claimed here.

The figure is static markup: it has no animation beyond a one-time entrance, no zoom, no pan and no filter control. Every mark is repeated in the list above with its coordinates in words, and that list is the accessible representation — the drawing itself is hidden from assistive technology.

The architecture is a specification with a partially realised implementation. The state update is written and correct in form; the read path that would make it a memory was never called. That is a specific, fixable gap — one method, _combine_states, and its use in the attention block — not a research dead end. Until it is wired up, no claim about memory, context or retention in this project can be true of the code, since the state is not read by anything the code does — including the ones the repository's own documentation makes.

What would move any of this

Each item converts one claim from unproven to measured, or kills it. Nothing on this list has been run.

  1. Wire the read path. Call _combine_states and use the combined state in the attention block, so that a token's output depends on what earlier tokens wrote. Then re-run the retention protocol. This is the only item here that is a code change rather than an experiment, and it decides whether any other memory claim can exist — today the method is not called at all. not reproduced · not run
  2. A matched-parameter, matched-token transformer baseline on the same corpus and the same tokenizer. not reproduced · not run
  3. One long-context task beyond 2k tokens with a memory measurement attached, so that the O(N) claim meets a task rather than a shape. not reproduced · not run
  4. An associative-recall evaluation — MQAR or needle-in-haystack — which is the decisive test for fixed-state linear attention, and which the shipped code's state is not called into at all. not reproduced · not run
  5. The outer-product ablation the flag already exists for. Train both variants and publish both JSONs. not reproduced · not run
  6. Preserved checkpoints with a hash, so that any of the above can be re-run rather than re-derived. not reproduced · not run
  7. A sparsity histogram to replace the ~5% doc value with a measurement. not reproduced · not run
  8. A re-run of the retention protocol with the values published as they come out, whatever they are — which the shipped forward pass does not read today. not reproduced · not run