RESONATE 🎼 ELI15 mode

Spectral knowledge-graph embeddings · ogbl-biokg🎼 Finding new uses for old drugs — with music math

ResonatE

Entities are band-limited spectra. Relations are transfer functions. Multi-hop reasoning becomes signal processing — and the model is the smallest on the board by 3.5×, 7× smaller than the 188M-parameter baselines it beats.Every drug, disease and gene is a melody. Relations like “treats” are machines that transform melodies. Asking “what else might this drug cure?” means playing its tune through a machine and listening for the closest match — with an orchestra 3.5× smaller than any other on the board, and better scores than most of them.

0.8505 ±0.0004
Test MRR · distilled + retrieval · 10 runsScore · a band taught by ten, with a memory
0.8425 ±0.0007
Test MRR · + retrieval · 10 runsScore · one band with a memory
5th / 12
Would place on the ogbl-biokg board · 4th single modelWould place on the public board · 4th among solo acts
27M
Params (next smallest on the board: 94M)Model size · smallest orchestra on the board by 3.5×

Flip the 🎼 switch above and the whole page rewrites itself in plain language — the model explained through music.

The model in four sentencesThe idea, without the math

Each entity is a unit-norm complex vector of k² = 144 Fourier modes (k=12). Each relation is a block-diagonal complex transfer function (4×4 blocks) applied mode-wise; a hop is “apply blocks, renormalize.” Scoring is an inner product against the entity table, so an n-hop query costs n tiny matmuls plus one readout — flat in depth, unlike graph traversal. Diagonal (commuting) relations provably cannot express order-sensitive chains, which is why blocks of at least 2×2 exist at all — the architecture was designed backwards from that limitation.Every entity is a melody made of 144 notes (that’s k=12). Every relation — “treats”, “binds”, “causes” — is a machine that transforms melodies, built from small 4×4 note-mixers. A reasoning step means: play the melody through the machine, then re-tune it to standard volume. A three-step question is just three machines in a row, and the answer is whichever entity’s melody sounds closest to what comes out. One rule drove the whole design: simpler machines that shift each note independently provably can’t tell “A then B” from “B then A” — and real reasoning cares about order — so the note-mixers exist precisely to fix that.

Headline resultHow well does it work?

On ogbl-biokg (OGB’s biomedical link-prediction benchmark: 93,773 entities, 51 relations, official split and Evaluator, 500 type-matched negatives per direction), ten seeds of the final recipe give test MRR 0.8118 ± 0.0013 at 27M parameters. Each of those ten models blended with four label-free retrieval features computed from the training graph (two analogy, two edge-overlap — below) gives 0.8425 ± 0.0007 — ten independent runs, one test shot each. Distilling the ten checkpoints into one fresh 27M table at training time (the student matches their averaged scores on each batch’s own candidates; it sees only training triples and plays alone at inference; alone it scores 0.8287 ± 0.0006 on validation) and giving that table its own features gives test MRR 0.8505 ± 0.0004, again over ten students with one shot each — above ComplEx-RP and TripleRE, which would place fifth on the twelve-row board behind the RelEns ensemble, ComplEx², UniBi and AutoBLM (182–192M parameters each), at the smallest parameter count on it. Both retrieval rows pass the score average of the ten checkpoints (0.8428, one test shot), which is why no ensemble appears in the headline: one table plus a lookup over the training graph is the deployable unit. Every protocol’s expected range was written down before any test number existed, and every run landed inside it.The test is ogbl-biokg, the standard public exam for this field: a biomedical graph of 93,773 entities where the model must guess the hidden end of a fact against 500 decoys. We trained the same model ten times from ten random starts — ten bands learning the same album independently — and the average score of 0.8118 ± 0.0013 proves it isn’t one lucky take. Then each band was allowed to consult its memory of the training graph before answering: 0.8425 ± 0.0007, ten bands, one real exam each (how, below). Last, ten fresh bands were each taught by listening to the ten others rehearse — each still plays alone on exam day — and with its memory a taught band scores 0.8505 ± 0.0004. That would be fifth on the public board, behind one ensemble and three orchestras about 7× bigger, with the smallest orchestra on it. Both beat letting all ten bands play at once and averaging their votes (0.8428), so the choir is retired. To keep it honest, every rule was chosen on the practice exam, the expected score was written down before each real exam, and each real exam was sat exactly once.

Test MRR on the live ogbl-biokg leaderboard (twelve rows, fetched 2026-09-02) with each entry’s published parameter count; the three ResonatE rows are ten-run means and are not yet listed. ResonatE is the smallest model on the board by 3.5× (AutoSF is 94M; every other entry is 182M or more); the single model beats every 188M-param classic baseline, the 27M model plus retrieval passes TripleRE at 470M, and the distilled 27M table plus retrieval passes ComplEx-RP — fifth overall, fourth among single models.Exam scores of everyone on the public leaderboard, with model sizes where known; our three rows are ten-run averages. Ours is the smallest orchestra on the board by 3.5× — the single model beats every classic 188M-size rival, one band with a memory overtakes a model 17× its size, and the taught band would sit fifth overall, behind one choir and three orchestras about 7× larger.
Reproduce these numbers
# 10-seed campaign (final recipe: k=12, 4x4 blocks, neg 4096, 50k steps, lr 5e-3 cosine)
cd resonate
for s in 0 1 2 3 4 5 6 7 8 9; do
  uv run python train_ogb.py --device cuda --steps 50000 --block-size 4 \
    --seed $s --save campaign_final/model_final_seed$s.pt --eval both \
    > final_seed$s.log 2>&1
done

# ensemble (the retired row): rule chosen on valid, then ONE test shot
uv run python ensemble_eval.py --device cuda --eval valid --rule mean \
  --models campaign_final/model_final_seed*.pt
uv run python ensemble_eval.py --device cuda --eval test  --rule mean \
  --models campaign_final/model_final_seed*.pt

# the two retrieval rows (0.8425 / 0.8505): see "Reproduce the retrieval result" below

Logs: final_seed*.log, ens_*.log · training ~4 min/seed on an RTX 5090, ~17 min on a GTX 1080 Ti, plus the two evaluations

Every test shot behind the three rows: ten seeds of the single model (σ=0.0013), the same ten with their own retrieval features (σ=0.0007), and ten distilled students with theirs (σ=0.0004). The three clusters never overlap — each step is an order of magnitude larger than the seed noise — and the spread is tight enough that it retroactively validates every single-seed “within noise” call made during exploration.Each dot is one real exam. Top row: the ten bands alone. Middle: the same bands with their memory. Bottom: the ten taught bands with theirs. The groups never touch — each improvement is far bigger than the luck of the draw — and no result is one lucky take.

One model plus retrieval beats the ensembleOne band with a good memory beats the choir

Ten checkpoints is a deployment cost, so the next question was whether a single model could reach the ensemble. It can, using the training graph itself as a second opinion. Four label-free retrieval features are scored next to the model. Analogy: for a candidate answer, take the training entities that already hold that answer under the queried relation and direction, and score the maximum cosine³ between their spectra and the query entity’s — case-based reasoning in the model’s own geometry. Its top-3 mean is the second feature. Jaccard repeats the pair (max and top-3 mean) with raw edge overlap instead of spectra, so it shares none of the embedding’s failure modes. Every feature reads training edges only. The blend is one small weight vector per (relation, direction), fitted on half the validation split and checked on the other half; groups with too few rows fall back to their relation family (all drug–drug relations pooled, say), then to a global setting. The recipe was found with single-model shots (each configuration pre-registered and scored on test once); the two rows that go on the leaderboard were then re-run as ten-seed protocols, each seed with its own features, its own weights fit on validation, and one test shot. The leaderboard numbers below refit the weights on the full validation split before the shot (in-sample validation reported alongside).Ten bands is expensive to keep on stage. Can one band do the job? Yes — if it may consult its memory. Before answering “what does this drug treat?”, the model looks up which known drugs have the most similar melody and borrows their answers; a second lookup does the same using raw shared connections instead of melodies, so it can’t make the same mistakes the melodies make. No answers leak in — the lookups only ever read the training graph. How much to trust the lookup versus the band is settled separately for each kind of question, on the practice exam. The set-ups were found one band at a time, each sitting the real exam exactly once; the two that go on the public board were then repeated with ten bands each.

ConfigurationSet-upValid (held-out half)Practice examTest (one shot)Real exam
single model, k=12, 27Mone band0.81350.8118 (10-seed mean)
+ 3 analogy features — shot #1+ a memory of similar melodies0.83700.8368
+ Jaccard pair (analogy features still using three seeds’ spectra)+ a memory of shared connections0.8441
27M model + its own analogy, top-3 and Jaccard pair — shot #3 (seed 0)the same band, both memories, nothing borrowed0.84340.8433
leaderboard row: the same, all ten campaign seeds — 10 runs, one shot each (in-sample valid 0.8434 ± 0.0006)public-board row: the same, for all ten bands0.8429 ± 0.00070.8425 ± 0.0007
stronger single (k=16, 16,384 negatives, per-entity bias, 48M) + its own features — shot #2a bigger band with both memories0.84740.8472
48M distilled from 12 checkpoints at training time (KL on each batch’s own candidates; alone 0.8343 valid) + its own features — shot #4; exploratory, one shot, not submittedone big band taught by twelve, playing alone, with both memories (a one-off, not on the public board)0.85400.8535
27M distilled from the ten campaign checkpoints (same recipe as the campaign otherwise; alone 0.8297 valid) + its own features — shot #5 (student seed 0)a small band taught by ten, playing alone, with both memories0.85160.8509
leaderboard row: the same, ten student seeds on the same ten teachers — 10 runs, one shot each (alone 0.8287 ± 0.0006 valid; in-sample valid 0.8516 ± 0.0004)public-board row: ten taught bands, each with its memory0.8512 ± 0.00050.8505 ± 0.0004
ten-seed ensemble (score mean, no fitted weights), retiredthe ten-band choir, retired0.8436 (full valid)0.8428

Two levers rejected in the table below returned once the recipe was stronger: 16,384 negatives and k=16 were each flat alone but give 0.8171 together, and a per-entity score bias — dropped at the 12,500-step screen — adds +0.003 on top of that. What did not help, all measured held-out and logged before the shot: k=24 (parity at twice the parameters), a confidence gate on the blend weights (−0.0015), cross-relation neighbourhood overlap (+0.0002), compositional training (−0.005). The remaining errors have a shape: 36% are drug→side-effect tails, where even a 22-member ensemble scores 0.17 — a floor set by the data, not the model. Seeds disagree on which near-tied candidate wins, not on what they know: across ten seeds the truth-vs-runner-up margin has a constant spread of 0.28 and only its mean moves, so “try more seeds” was never going to close the gap.Two ideas that flopped in the auditions below came good once the band was better: more decoys and more notes per melody did nothing on their own, but together they helped, and letting each entity carry a “popularity” volume knob helped a little more on top. Bigger melodies (k=24), a confidence dial on the memory, a wider memory across every kind of connection, and chain drills all failed to move the score — every one checked on the practice exam and written down before the real one. The mistakes that remain mostly have one shape: guessing a drug’s side effects, where even a 22-model choir scores 0.17 — the data simply doesn’t contain the answer. And the ten bands don’t disagree about facts, only about coin-flips between near-identical candidates — so more bands were never going to fix it.

Reproduce the retrieval result
# the edge-overlap features are model-free: built once from training edges
uv run python jaccard_member.py --split valid                                         # + --split test

# leaderboard row 1 — 27M + retrieval, ten runs: campaign seed s (trained above) with its own
# score caches and analogy features, weights searched on valid halves (held-out report),
# refit on full valid, ONE test shot
for s in 0 1 2 3 4 5 6 7 8 9; do
  M=model_final_seed$s; A=analogy_s$s        # seed 0's features were named analogy / analogy_s0_t3 (shot #3)
  uv run python cache_scores.py --split valid --device cuda --models campaign_final/$M.pt   # + --split test
  uv run python analogy_member.py --device cuda --models campaign_final/$M.pt --split valid \
    --out ens_cache/$A.valid.npz --out-top3 ens_cache/${A}_t3.valid.npz                    # + --split test
  uv run python ensemble_weights.py --exclude <everything except $M $A ${A}_t3 jaccard jaccard_t3>
  uv run python freeze_test.py --members $M $A ${A}_t3 jaccard jaccard_t3 --min-rows 4000 \
    --out frozen_single_s$s.npz --result committed_single_s$s.json
done

# leaderboard row 2 — 27M distilled + retrieval, ten runs: fresh student seed s, the campaign
# recipe unchanged, with the ten campaign checkpoints as frozen teachers (KL on each batch's
# own candidates); then exactly the feature/weight/shot procedure of row 1
for s in 0 1 2 3 4 5 6 7 8 9; do
  uv run python train_ogb.py --device cuda --steps 50000 --block-size 4 --seed $s \
    --distill campaign_final/model_final_seed{0..9}.pt --distill-w 1.0 --distill-T 1.0 \
    --save dist27_s$s.pt --eval valid
  # cache_scores / analogy_member / ensemble_weights as above, members
  # dist27_s$s analogy_dist27_s$s analogy_dist27_s${s}_t3 jaccard jaccard_t3, then
  uv run python freeze_test.py --members dist27_s$s analogy_dist27_s$s analogy_dist27_s${s}_t3 jaccard jaccard_t3 \
    --min-rows 4000 --out frozen_dist_s$s.npz --result committed_dist_s$s.json
done

# ---- exploratory single shots from the recipe search (not submitted) ----
# shot #2: a stronger 48M single (k=16, 16,384 negatives, per-entity bias) + its own features
uv run python train_ogb.py --device cuda --steps 50000 --block-size 4 --neg 16384 --k 16 \
  --ent-bias --save pool3_k16_entbias.pt --eval valid
uv run python cache_scores.py --split valid --device cuda --models pool3_k16_entbias.pt   # + --split test
uv run python analogy_member.py --device cuda --models pool3_k16_entbias.pt --split valid \
  --out ens_cache/analogy_eb.valid.npz --out-top3 ens_cache/analogy_eb_t3.valid.npz   # + --split test
uv run python ensemble_weights.py --exclude <everything except the five members>
uv run python freeze_test.py --members pool3_k16_entbias analogy_eb analogy_eb_t3 jaccard jaccard_t3 \
  --out frozen_single_eb.npz --result committed_single_eb.json

# shot #4: the shot-#2 recipe distilled from 12 frozen checkpoints (10 campaign seeds + 2 pool models)
uv run python train_ogb.py --device cuda --steps 50000 --block-size 4 --neg 16384 --k 16 --ent-bias \
  --distill campaign_final/model_final_seed{0..9}.pt pool2_neg16k_k16.pt pool3_k16_entbias.pt \
  --distill-w 1.0 --distill-T 1.0 --save pool5_distill.pt --eval valid
uv run python freeze_test.py --members pool5_distill analogy_dist analogy_dist_t3 jaccard jaccard_t3 \
  --out frozen_single_dist.npz --result committed_single_dist.json

Single shots: pure_eb.log (held-out 0.8474), committed_single_eb.log (test 0.8472, hits@1 0.7886 / @3 0.8883 / @10 0.9520); pure_s0.log (held-out 0.8434), committed_single_s0.log (test 0.8433, hits@1 0.7838 / @3 0.8849 / @10 0.9510); pure_distill.log (held-out 0.8540), committed_single_dist.log (test 0.8535, hits@1 0.7979 / @3 0.8927 / @10 0.9537); pure_dist27.log (held-out 0.8516), committed_dist_s0.log (test 0.8509); shot #1: committed_single3.json (0.8368). Ten-run rows: pure_s{0..9}.log / committed_single_s{0..9}.json (test 0.8433 0.8432 0.8425 0.8420 0.8422 0.8419 0.8411 0.8427 0.8431 0.8429) and pure_dist_s{1..9}.log / committed_dist_s{0..9}.json (test 0.8509 0.8508 0.8506 0.8503 0.8508 0.8498 0.8502 0.8506 0.8505 0.8509); student logs pool7_k12_distill.log (seed 0) and remote_logs/dist27_s{1..9}.log (valid alone 0.8297 0.8291 0.8288 0.8288 0.8284 0.8273 0.8284 0.8291 0.8287 0.8287). Students trained on rented RTX 5090s (~9 min each when run alone, three concurrently in practice), features ~20 min per split per seed on one CPU core; PLAN.md carries the pre-registered expectation of each protocol (0.843 ± 0.002 and 0.850 ± 0.001) and every deviation.

How the recipe was foundHow we tuned the orchestra

The starting point was a hand-picked baseline: k=12 spectra, 2×2 relation blocks, 4,096 negatives per positive, lr 5·10⁻³ with cosine decay, 25,000 training steps — settings that worked on smaller graphs but had never been questioned on this one. Each default was then treated as a hypothesis and tested with a pre-registered, tiered protocol — every experiment logged with its pass mark before running: a cheap 12,500-step screen on the validation split, promotion only if it beat the current recipe by ≥ +0.004 MRR, then a full 50,000-step confirmation. No lever decision ever looked at the test split; it was scored only for campaign candidates (the 25k baseline, 0.8024; the 50k confirmation, 0.8084; then the final recipe), each run once. Sixteen alternatives went through this pipeline. Two survived.Every setting was chosen by audition: we wrote down the pass mark before each tryout, gave every idea a cheap short rehearsal first, and only ideas that beat the mark earned a full-length trial. The final exam stayed locked away until the very end. Sixteen ideas auditioned. Two got in.

Survivor 1 — train twice as longSurvivor 1 — more rehearsal time

First question: is the model even done learning? The baseline’s 25,000-step budget was inherited from experiments on smaller graphs, where validation MRR had plateaued by then — on ogbl-biokg nobody had checked. Sweeping the budget (6,250 → 50,000 steps, each run with its own cosine schedule) showed it hadn’t plateaued at all: 50k steps buys +0.005 validation MRR over 25k. The gains per doubling shrink fast enough that 100k projects ≤ +0.002 — not worth doubling the campaign cost — so the budget was doubled once and frozen there.The orchestra was simply under-rehearsed: 25,000 practice run-throughs was a number picked arbitrarily at the start. Doubling it to 50,000 made the concert measurably better; doubling again would cost twice the money for almost nothing. So we measured where more rehearsal stops helping instead of guessing.

Validation MRR vs training steps (seed 0, k=12). Grey: 2×2 blocks; the green endpoint is the final 4×4-block recipe at 50k (0.8140).Score vs rehearsal time. Grey: the original setup keeps improving up to 50,000 run-throughs, then flattens. Green: the final recipe with the upgraded mixers at full rehearsal.
Reproduce the budget curve
for S in 6250 12500 25000 50000; do
  uv run python train_ogb.py --device cuda --steps $S --eval valid \
    --save model_ogb_s$S.pt > ogb_s$S.log 2>&1
done
# final-recipe endpoint (4x4 blocks):
uv run python train_ogb.py --device cuda --steps 50000 --block-size 4 \
  --seed 0 --eval both --save model_ogb_bs4_seed0.pt > ogb_bs4_seed0.log 2>&1

Logs: ogb_s*.log · each step count gets its own cosine anneal — a truncated long run is a different schedule

Survivor 2 — fancier relation blocks, not longer spectraSurvivor 2 — upgrade the conductors, not the musicians

Second question: if the model deserves more capacity, where should it go? There are two places to put it: raise k (every one of 93,773 entities gets a longer spectrum — millions of parameters) or enlarge the relation blocks 2×2 → 4×4 (only 102 relation operators — ~59k parameters). In cheap 2,000-step probes, MRR grows log-linearly with k with no plateau in sight — but those gains compress with budget: k=16’s +0.025 probe advantage shrinks to +0.003 at 50k, and even k=32 misses the pre-registered bar. Blocks are the opposite: their +0.0064 screen gain barely compresses to +0.0055 at 50k — expressivity, not just capacity — at roughly 1/300th the parameter price per MRR point. Final config: k=12, 4×4 blocks, 0.8140 valid / 0.8134 test (seed 0). Stacking both (k=16 + 4×4) adds almost nothing — the two knobs buy overlapping capacity.Two ways to make the model smarter. Way 1: give everyone longer melodies (raise k). More notes, more detail — but all 93,773 entities need the longer melody, which nearly doubles the model. Way 2: give the machines fancier moves — let each mixer swirl four notes at a time instead of two. There are only 102 machines against 93,773 melodies, so upgrading them costs almost nothing: it’s like improving the whole orchestra by upgrading 102 conductors instead of retraining 93,773 musicians. The auditions were decisive: longer melodies look great in short rehearsals but the advantage nearly vanishes by the full concert, while the fancier machines keep almost all of theirs — at roughly 1/300th the price per point. Doing both adds little: they buy overlapping skills.

Capacity anatomy. Blue: valid MRR vs k at 2k-step probes (log-linear, no plateau). Dark: the same ks at 50k — the curve nearly flattens. Green: k=12 with 4×4 blocks beats k=32 with 165M fewer parameters.Blue: longer melodies help a lot in short rehearsals. Dark: by the full concert the advantage has nearly vanished. Green: upgrading the mixers instead beats even the longest melody — with 165 million fewer parameters.
Reproduce the capacity sweep
# 2k-step probes across k (each ~2-3 min on GPU)
for K in 4 8 12 16 20 24 32; do
  uv run python train_ogb.py --device cuda --quick --k $K --eval valid \
    --save model_ogb_k${K}_quick.pt > ogb_k${K}_quick.log 2>&1
done
# 50k confirmations for the promoted ks
uv run python train_ogb.py --device cuda --steps 50000 --k 16 --eval valid > ogb_k16_s50000.log 2>&1
uv run python train_ogb.py --device cuda --steps 50000 --k 32 --eval valid > ogb_k32_s50000.log 2>&1
# the block lever (screen, then 50k confirmation)
uv run python train_ogb.py --device cuda --steps 12500 --block-size 4 --eval valid > ogb_bs4_s12500.log 2>&1
uv run python train_ogb.py --device cuda --steps 50000 --block-size 4 --eval valid > ogb_bs4_s50000.log 2>&1
# blocks were first kill-tested on Hetionet, on CPU:
uv run python h4_update_threshold.py --block-size 4 --steps 6250 > h8a_bs4_s6250.log 2>&1

Logs: ogb_k*_quick.log, ogb_k16_s50000.log, ogb_k32_s50000.log, ogb_bs4_*.log, h8a_*.log

What didn’t work — and why that’s the findingFourteen ideas that didn’t survive the audition

The remaining fourteen levers — mostly the classic tricks that other leaderboard models rely on — went through the same screen-then-confirm pipeline on top of the adopted recipe (the 0.7893 reference in the table below is that recipe’s own 12,500-step validation score) and were all dropped, each with a mechanism we can name. First pattern: the architecture pre-empts the tricks. A relation-prediction auxiliary loss is redundant because training batches are already grouped by entity type, which forces the model to tell relations apart; N3 regularization is redundant because renormalizing after every hop already disciplines vector magnitudes; Guu-style path training has nothing to fix because hops compose exactly by construction. Second pattern: several levers win the 12,500-step screen and vanish at 50k — they speed up convergence without raising the plateau. Hence the standing rule: never adopt a screen-tier win without the long-budget check.Fourteen ideas were rejected — each with a reason we can name, and the reasons are the real finding. The classic tricks other leaderboard models rely on flopped here because our architecture already does those jobs internally: bolting them on is like hiring a metronome for a band that already has a drummer — they just get in each other’s way. Several other ideas won the short audition but faded by the full concert: they made the band learn faster, yet the final performance was no better — the student who crams brilliantly in week one and ends the semester with the same grade. So the standing rule: never trust an audition win until it’s re-checked at full length. Strangest result of all: forcing the machines to only shift pitch, never volume — mathematically elegant — made things dramatically worse. The volume knobs were load-bearing.

LeverIdeaScreen @12.5k
(ref 0.7893)
Short audition
(beat 0.7893)
50k check
(ref 0.8140)
Full-length check
(beat 0.8140)
Verdict
Relation-prediction aux loss (λ=0.25 / 1 / 4)Extra homework: also predict the relation--aux-rp 0.25|1|4 0.7886 / 0.7849 / 0.7741 dropped monotone-negative; type-matched batches already disambiguate relationsthe model already knows which machine it’s using — extra homework only distracted it
N3 regularization (W=0.001 / 0.01 / 0.1)A penalty on loud notes (N3)--n3 0.001|0.01|0.1 0.7902 / 0.7912 / 0.7894 dropped real interior peak, half the promotion bara real but tiny gain — under the pass mark
Compositional (path) training (P=0.25 / 0.5)Practicing two-step chains during training--compose 0.25|0.5 0.7711 / 0.7475 dropped hops compose exactly already; chains only tax the update budgetour machines already chain perfectly by construction — the extra drills just ate practice time
More negatives (16,384; knee of the curve)More decoys per practice question (16,384)--neg 16384 0.7963 ✓0.8148 (+0.0008) not adopted convergence-speed effect — gone at the plateaulearns faster, but the final concert is identical
Tied reverse relations (adjoint; ½ relation params)Reverse machines = forward machines, mirrored--tied-reverse 0.7936 ✓0.8128 (−0.0012) not adopted same story; free reverse blocks aren’t slack at 50ksame story — a faster learner with the same finale
One-cycle schedule (max-lr 3e-2)A trendier practice schedule (one-cycle)--sched onecycle --max-lr 3e-2 0.7474 dropped screened on the 2×2 baseline vs its cosine 0.7829; update count is the bottleneck, not the schedulethe plain schedule won — total practice matters, not its shape
Uniform 8×8 blocksEven fancier mixers (8 notes at a time)--block-size 8 0.7904 dropped flat; 4×4 is the sweet spot on both datasetsno better than 4 — that’s the sweet spot
Unitary (phase-only) relationsMachines may shift pitch but never volumedepth_ceiling.py --unitary never reaches 90% at any depth dropped off-unit-circle moduli are load-bearing — relations need volume, not just phasedramatically worse — the volume knobs were load-bearing
50k training budgetDouble the rehearsal time0.8085 (50k) vs 0.8032 (25k) adopted
4×4 relation blocksFancier mixers (4 notes at a time)0.7893 ✓0.8140 ✓ adopted the only lever whose gain survives the budgetthe only upgrade whose gain survives the full concert
Reproduce any row
# screen template: 12,500 steps on the bs=4 baseline, validation only
uv run python train_ogb.py --device cuda --steps 12500 --block-size 4 \
  --eval valid  <FLAG FROM THE ROW>  > screen.log 2>&1
# e.g. the N3 interior peak:
uv run python train_ogb.py --device cuda --steps 12500 --block-size 4 \
  --n3 0.01 --eval valid > ogb_n3_0.01_s12500.log 2>&1
# 50k confirmation of a passing screen, e.g. negatives:
uv run python train_ogb.py --device cuda --steps 50000 --block-size 4 \
  --neg 16384 --eval valid > h9b_neg16384_s50000.log 2>&1
# the unitarity kill-test (CPU, synthetic depth testbed, 3 seeds both arms):
uv run python depth_ceiling.py            > dc5k_free.log    2>&1
uv run python depth_ceiling.py --unitary  > dc5k_unitary.log 2>&1

Logs: ogb_rp*_s12500.log, ogb_n3*_s12500.log, h9a_c*.log, h9b_neg*.log, ogb_tied_s12500.log, ogb_oc12500.log, ogb_bs8_s12500.log, dc5k_*.log

Depth and speedHow far can it think, and how fast?

Two properties the leaderboard doesn’t show — ogbl-biokg is a single-edge task, so both are measured with the same architecture trained on graphs that have multi-hop questions, not with the 27M leaderboard table. Depth, measured on a synthetic chain-reasoning testbed where the true answer at every depth is known exactly: trained only on chains of depth ≤4, the model holds 0.987 accuracy at depth 8, crosses 90% around depth 11 and 50% around 17–19 — and the ceiling is a function of training precision: at 5k steps it collapses right past the trained depth, at 25k it extrapolates ~4.4× beyond it. Mechanism: accumulated angular error in the composed transfer functions. Speed, measured with the 28M-parameter DRKG model on that real 5.87M-edge graph: batched multi-hop queries run 0.08–0.11 ms flat at every depth 1–6 while exact graph traversal grows with fan-out — up to 45× slower at depth 5–6; restricting the readout to entities of the answer’s type, even single queries beat traversal from depth 1.Chaining machines is like re-recording a tape through a row of effects pedals: each is very slightly out of tune, and the errors add up. Trained only on chains of 4 steps, the model still gets 98.7% right at 8 steps and stays above 90% out to about 11 — and that reach isn’t fixed: rehearse 5× longer and the chain holds roughly 4× deeper. On speed: a 6-step question costs the same as a 1-step one (about a tenth of a millisecond) — you just play the melody through six machines. Walking the actual graph instead slows down at every step as the possibilities fan out, ending up to 45× slower.

Reproduce depth & latency
# depth ceiling: synthetic testbed, exact ground truth, 3 seeds, trained depth ≤4
uv run python depth_ceiling.py --steps 25000 > depth_ceiling_25k.log 2>&1
uv run python depth_ceiling.py --steps 5000  > depth_ceiling_5k.log  2>&1
# latency crossover vs exact traversal (DRKG, depths 1-6)
uv run python h3_latency.py      > h3.log      2>&1
uv run python h3_fast_readout.py > h3_fast.log 2>&1

Curve: depth_ceiling_curve.svg · latency measured on battery — shape robust, absolute ms may flatter traversal

Oracle validation and what it’s forThe fact-checker, and what this is actually for

The leaderboard run is the credential; the tool itself is the same architecture trained on two other biomedical graphs — Hetionet (47,031 nodes, 2.25M edges; 13.6M parameters) and DRKG (97,238 nodes, 5.87M edges; 28.1M parameters) — where it answers metapaths: typed question templates like compound → treats → disease. There a standing project rule applies: no model metric is ever reported without oracle validation. The repo carries an exact oracle (hetiograph.py) that walks the real graph, so every top answer the model gives is checked against ground truth and labeled: [ok] — the mechanism path exists in the graph, cited hop by hop — or [NOVEL] — no supporting path exists, so it is an honest hypothesis, not a fact. On the trained metapath libraries the model’s top answer is oracle-verified 94.6–100% of the time across all 10 DRKG paths, and 99–100% on all 8 Hetionet paths with the final 4×4-block config — with zero unverifiable claims presented as facts. Every [NOVEL] proposal additionally ships analogy evidence: the drugs most similar to the query (by spectral cosine) that already carry that exact edge in the real graph. That lookup is measured, not decorative — on the benchmark’s rarest side effects it alone outperforms the full 27M-parameter embedding (held-out MRR 0.0175 vs 0.0147, blend 0.0196, paired-bootstrap significant): the embedding compresses the neighborhood at training time, the graph remembers it exactly at query time.The model is creative; the oracle is a librarian who checks every claim against the actual books. Every answer comes stamped: [ok] — “here’s the shelf and the page, this connection really exists, follow the citations” — or [NOVEL] — “the band improvised this; it sounds right, but nobody has written it down yet.” We never pass an improvisation off as a published song. The librarian confirms the model’s top answer 94.6–100% of the time across every tested question type on both big graphs. And when the band improvises, it now shows its influences: every [NOVEL] answer names the most similar drugs that already do this — “three melodies close to yours are known to play this song.” We tested that trick on the hardest, rarest cases, and the simple “who resembles me” lookup actually beat the whole trained model there — so it’s evidence, not decoration.

Use case 1 — drug repurposing queriesUse case 1 — “what else might this drug do?”

The CLI answers any library metapath in a few milliseconds on a laptop CPU (~0.1 ms/query batched), oracle flags inline. Ask Metformin — treats? and it returns the 3 known indications marked [ok] plus obesity, hypertension and type-1 diabetes as [NOVEL] repurposing candidates. On DRKG’s 496 held-out treats edges, the model ranks the true disease 4.0× better than a popularity baseline (bar was 2×).Ask “Metformin — what does it treat?” and in a few milliseconds you get its 3 known uses stamped [ok], plus obesity, hypertension and type-1 diabetes flagged [NOVEL] — candidate new uses for an old, cheap, well-understood drug. On a hidden test of 496 real drug–disease facts, the model ranks the right disease 4× better than just betting on popular diseases.

$ uv run python query.py Metformin CtD -k 8
[hetio] 47,031 nodes, model loaded in 2.2s
resolved 'Metformin' -> Metformin (Compound)

Metformin  --CtD-->  top-8          # CtD = Compound-treats-Disease
  #    score  oracle   entity
  1    13.26  [NOVEL]  obesity (Disease)
               ↳ analogy: Phendimetrazine (sim 0.48), Cimetidine (0.47), Benzphetamine (0.46) already have this edge
  2    12.94  [ok]     type 2 diabetes mellitus (Disease)
  3    12.87  [ok]     metabolic syndrome X (Disease)
  4    12.80  [ok]     polycystic ovary syndrome (Disease)
  5    11.86  [NOVEL]  hypertension (Disease)
               ↳ analogy: Reserpine (sim 0.47), Fosinopril (0.45), Carvedilol (0.45) already have this edge
  6    10.39  [NOVEL]  type 1 diabetes mellitus (Disease)
               ↳ analogy: Ramipril (sim 0.41) already has this edge
  7    10.22  [NOVEL]  coronary artery disease (Disease)
               ↳ analogy: Atorvastatin (sim 0.43), Losartan (0.42), Pitavastatin (0.41) already have this edge
  8    10.07  [NOVEL]  hematologic cancer (Disease)
               ↳ analogy: Doxorubicin (sim 0.43), Methotrexate (0.40), Epirubicin (0.39) already have this edge

3/8 oracle-verified · exact-answer set |valid| = 3 · model 3.42ms · oracle 0.44ms
$ uv run python query.py Ibuprofen CbG -k 8   # which genes does it bind?

Ibuprofen  --CbG-->  top-8
  1    10.96  [ok]     UGT2B4 (Gene)
  2    10.33  [NOVEL]  AKR1C3 (Gene)
  3    10.29  [ok]     SLC22A8 (Gene)
  4    10.22  [ok]     SLC22A6 (Gene)
  5    10.10  [ok]     CYP2C8 (Gene)
  6    10.02  [NOVEL]  IKBKB (Gene)
  7    10.00  [ok]     SLC22A11 (Gene)
  8     9.99  [ok]     PTGS1 (Gene)

6/8 oracle-verified · exact-answer set |valid| = 26 · model 3.52ms · oracle 0.43ms
$ uv run python query.py Ibuprofen CbG.~DaG -k 6   # binds gene → gene linked to disease

Ibuprofen  --CbG.~DaG-->  top-6
  1    12.24  [ok]     hypertension (Disease)
  2    11.68  [ok]     prostate cancer (Disease)
  3    11.56  [ok]     breast cancer (Disease)
  4    11.51  [ok]     obesity (Disease)
  5    11.46  [ok]     epilepsy syndrome (Disease)
  6    11.43  [ok]     hematologic cancer (Disease)

6/6 oracle-verified · model 3.50ms · oracle 0.71ms
$ uv run python query.py Metformin CbG.GiG.~DaG -k 5   # 3 hops through gene interactions

Metformin  --CbG.GiG.~DaG-->  top-5
  1    11.61  [ok]     hematologic cancer (Disease)
  2    11.56  [ok]     lung cancer (Disease)
  3    11.41  [ok]     breast cancer (Disease)
  4    11.35  [ok]     epilepsy syndrome (Disease)
  5    11.19  [ok]     hypertension (Disease)

5/5 oracle-verified · model 3.56ms · oracle 0.79ms
$ uv run python query.py Metformin CrC.CtD -k 4   # what do similar compounds treat?

Metformin  --CrC.CtD-->  top-4
  1    10.61  [NOVEL]  type 2 diabetes mellitus (Disease)
  2    10.36  [NOVEL]  obesity (Disease)
  3    10.06  [NOVEL]  coronary artery disease (Disease)
  4     9.94  [NOVEL]  metabolic syndrome X (Disease)

0/4 oracle-verified · exact-answer set |valid| = 0 · model 3.51ms · oracle 0.43ms
Real CLI output (Hetionet, 2026-09-01, unedited). score is the model’s inner-product confidence; [ok] means the oracle found the exact path in the graph (the answer is citable), [NOVEL] means no such path exists — a candidate discovery, never presented as a fact. The ↳ analogy line under each [NOVEL] answer is its case-based evidence: the most similar drugs that already carry the proposed edge — note how obesity’s holders are anti-obesity agents and hypertension’s are antihypertensives. The proposals hold up outside the graph: Metformin’s top [NOVEL] indications (obesity, hypertension, type-1 diabetes) have all been studied in real clinical trials, and both [NOVEL] gene targets for Ibuprofen have literature support — AKR1C3 is a documented NSAID-inhibited enzyme and IKBKB sits in the NF-κB pathway NSAIDs are known to dampen. The last query shows the honesty layer under stress: the graph contains no compound-resembles-compound-treats path from Metformin at all (|valid| = 0), so every answer is flagged — yet the model’s generalization still puts the drug’s actual #1 indication on top.Real, unedited screen output. score is how confident the model is; [ok] means the librarian found the claim in the books — you can follow the citations; [NOVEL] means the band improvised it — a lead worth checking, never sold as a fact. The ↳ analogy line under each improvisation names the similar drugs that already play that song — see how obesity’s are diet pills and hypertension’s are blood-pressure drugs. And the improvisations are good: Metformin’s flagged diseases have all been through real clinical trials, and Ibuprofen’s two flagged genes are targets scientists have written about — just missing from this library’s shelves. The last question is the stress test: the library holds zero answers for it, the librarian flags every single one — and the model’s best guess is still the drug’s actual number-one use.

Use case 2 — absorbing new knowledge in minutesUse case 2 — learning new facts without forgetting old ones

Knowledge graphs go stale; retraining is the usual price. Fine-tuning the 496 held-out edges into a trained model took 3.7 GPU-minutes and absorbed all of them to rank 1 (MRR 0.147 → 1.000) — while the worst metapath regression was −0.008, effectively zero. That closes the loop the planned tool server (an MCP server, so LLM assistants can call it) is built around: the model proposes a [NOVEL] edge, the calling assistant corroborates it against literature, and a minutes-long fine-tune absorbs it.Medical knowledge moves, and most models must be retrained from scratch to catch up. Ours learned 496 brand-new facts in under 4 minutes of fine-tuning — perfectly, every one to rank 1 — while forgetting essentially nothing (its worst skill dipped 0.8%). That enables the loop the whole tool is built around: the model proposes a new connection, an AI assistant checks the literature for it, and a minutes-long update absorbs it.

Reproduce the system results
# DRKG training + oracle-verified metapath table + held-out link-pred (73 min on GPU)
uv run python train_drkg.py --device cuda > train_drkg_full.log 2>&1
# Hetionet final config (CPU, 15 min): any-valid@1 0.99-1.00 on all 8 paths
uv run python h4_update_threshold.py --block-size 4 --steps 6250 \
  --save model_h8a_bs4_s6250.pt > h8a_bs4_s6250.log 2>&1
# the repurposing demo (fuzzy entity lookup, [ok]/[NOVEL] flags)
uv run python query.py Metformin CtD
# H5 incremental update: 496 new edges in, 3.7 GPU-min, worst regression -0.008
uv run python h5_incremental.py > h5.log 2>&1

Logs: train_drkg_full.log, h8a_bs4_s6250.log, h5.log · models: model_drkg.pt, model_drkg_incr.pt

Expansion — the path to 100M entitiesWhat it would take to go a thousand times bigger

Everything above runs on graphs of ≤100k entities. For a graph 1,000× larger (100M entities, ~500M edges — Wikidata scale) the binding constraint is not compute but where the entity table lives: a training step touches ~10k rows (12 MB) whatever N is, yet dense Adam makes the whole table resident with two moment tensors (at k=12: 115 GB of table, 230 GB of state) and materialises a dense gradient every step. Experiment H19 (2026-09-03) removed that dependency and measured the cost against criteria fixed in advance. The entity table now trains under row-wise Adagrad — the optimiser of PyTorch-BigGraph and TorchRec: one accumulator per row (0.4 GB at 100M), no decay, so a row nobody touches is left bit-identical and a dense run at 97k entities is the same arithmetic as a sharded run at 100M. The serving index stores the table in fp16 (848 B/row including HNSW links, half of fp32). Cost, paired against Adam on the same seeds and held-out edges: Hetionet 3-seed link-pred hits@10 0.782 vs 0.782, oracle any-valid@1 0.993 vs 0.991; DRKG link-pred MRR 0.157 vs 0.163 (within one standard error of 496 held-out edges — the Adam control itself moved 0.016 between machines), metapath any-valid@1 0.990 vs 0.987. The fp16 index returns the identical top-10 on 100% of 3,334 queries over both graphs, max score deviation 1.3e-5. What remains is plumbing, not science: storing the table through its real view so PyTorch emits row-sparse gradients (complex sparse embeddings are unsupported in torch 2.6), a type-sharded table across GPUs with an all-to-all of the ~12 MB of rows each batch needs (negatives are already drawn within the answer’s type, so the training distribution is unchanged), and per-type ANN shards so a query loads only its answer type. The table below is a projection from measured per-row costs — nothing on it has been run; a 20–50M entity rehearsal (deps.dev or a Wikidata subset) is the next step.Everything here was done on graphs of about a hundred thousand things. What about a hundred million — the size of Wikipedia’s knowledge graph? The hard part isn’t the thinking: each rehearsal step only ever looks at about ten thousand entries, however big the library is. The problem was the bookkeeping. The standard training method keeps two extra notes for every number in the library — tripling its size — and rewrites the whole thing at every step. We swapped in a leaner bookkeeper that keeps one note per entry and touches only the entries it uses, and checked — with pass/fail bars set before running — that the band plays just as well on both real graphs. We also halved the size of the finished lookup table with no measurable change in any answer. What’s left is engineering — spreading the library across several graphics cards — not new science. The table below is an estimate of what that costs; none of it has been run yet.

At 100M entities, k=12 (projected)At 100 million things (estimate)Size / timeSize / timeBasisWhere the number comes from
training table, fp32the library, full precision115 GB1,152 B/rowmeasured per entry
optimiser state (row-wise Adagrad)the bookkeeper’s notes0.4 GBone float per row; 230 GB under Adamone note per entry — was 230 GB
per-step working setwhat one rehearsal step actually reads~12 MB~10k rows, independent of Nthe same at any library size
serving index, fp16 HNSWthe finished lookup table, in RAM85 GB848 B/row measured on DRKG; ~0.3 ms/querymeasured per entry on the DRKG model
serving index, DiskANN-stylethe same, mostly on SSD~5 GB RAM + NVMe~3 ms/querya few milliseconds per question
training, 500M edges × 20 epochsrehearsing the whole library 20 times~7 GPU-h on one 5090-class card; ~1–2 h on 8×80 GBper-step cost independent of N; 20 epochs is what ogbl-biokg neededextrapolated from the leaderboard runs
Reproduce H19 (row-wise Adagrad vs Adam; fp16 index vs exact readout)
# Hetionet, paired seeds (~7 min each on the 1080 Ti); hits@10 Adam vs RowAdagrad
for s in 0 1 2; do
  uv run python h4_update_threshold.py --device cuda --seed $s \
    --save h19/adam_s$s.pt > h19/adam_s$s.log 2>&1
  uv run python h4_update_threshold.py --device cuda --seed $s --opt rowadagrad \
    --table-lr 0.3 --save h19/rowada0.3c_s$s.pt > h19/rowada0.3c_s$s.log 2>&1
done
# DRKG (~33 min each): link-pred MRR 0.163 (Adam) vs 0.157 (RowAdagrad, table-lr 0.6)
uv run python train_drkg.py --device cuda --save h19/drkg_adam_s0.pt > h19/drkg_adam_s0.log 2>&1
uv run python train_drkg.py --device cuda --opt rowadagrad --table-lr 0.6 \
    --save h19/drkg_rowada0.6c_s0.pt > h19/drkg_rowada0.6c_s0.log 2>&1
# fp16 serving table vs the exact fp32 readout, both graphs
uv run python compile_index.py check --dataset hetio --table fp16
uv run python compile_index.py check --dataset drkg  --table fp16
uv run python rowadagrad.py   # self-test: dense == sparse, untouched rows bit-identical

Logs and models: h19/ · pre-registration and both RESULT blocks: PLAN.md, H19 · optimiser: rowadagrad.py

Reproduce everythingCheck our work yourself

The repo ships a self-audit. verify.py re-derives the two claims that matter: the train/valid/test splits we actually loaded are pairwise disjoint (zero overlapping triples), and all ten released campaign checkpoints — the single-model row — reproduce their logged test MRRs on a different GPU and a different torch version, max drift 4·10⁻⁵. The two retrieval rows are reproduced by the loops in the retrieval section; each of their twenty test shots ships its frozen weights (frozen_*.npz) and Evaluator result (committed_*.json).We publish the sheet music and the recordings. verify.py is a script anyone can run that checks two things: the exam questions never appeared in the study materials (no cheating), and the ten released models really produce the scores we claim — even on completely different hardware, matching to the 5th decimal. Every number on this page has the exact command that produced it, one click away.

The audit
cd resonate
uv run python verify.py --device cuda --models-dir campaign_final
# == 1. split disjointness ==      train ∩ valid = 0, train ∩ test = 0, valid ∩ test = 0
# == 2. checkpoint reproduction == ALL 10 CHECKPOINTS REPRODUCE. test MRR 0.8118 +/- 0.0013

Full output: verify_run.log · environment: uv sync; torch 2.6.0+cu124 on Pascal (pinned), ≥2.7+cu128 on Blackwell

Prefer a notebook? reproduce.ipynb in the repo walks the same audit cell by cell — split disjointness, all ten checkpoint re-evaluations, and the ensemble — with the expected output stated at every step.Prefer a notebook? reproduce.ipynb in the repo walks the same checks cell by cell, with the expected output shown at every step.

Protocol notes, disclosed: all tuning was on the validation split (standard leaderboard practice); reciprocal relations are trained and head-direction queries score through the reverse operator (a protocol-legal modeling choice, standard since ComplEx-N3, Lacroix et al. 2018). The retrieval features read training edges only; their per-relation blend weights are fitted on the validation split — held-out on half of it for the estimate, then refitted in-sample on all of it for the shot (in-sample validation reported next to every row). The distilled row’s teachers are the ten campaign checkpoints, themselves trained on training triples only, and the pool is shared by all ten students, so that row’s ± is student-seed variance only. Every test number on this page is one shot per run, taken after its expected range was written in PLAN.md, and nothing was re-run after a test number was seen; the ensemble rule, likewise, was chosen on validation and shot once.Full disclosure, in plain terms: every tuning decision used the practice exam only; the one modeling shortcut we take — learning separate forward and reverse machines — is explicitly allowed by the benchmark rules; the memory only ever reads the training graph, and how much to trust it was settled on the practice exam; the ten taught bands all learned from the same ten teachers, so their spread only measures the students’ luck; and every real exam was sat exactly once, with the expected score written down first.