# FlyScale
### π **[Read the paper β aniviza.com/paper.pdf](https://aniviza.com/paper.pdf)** Β· **[Landing page](https://aniviza.com/paper.html)**
*26 pages, 5.7 MB. GitHub's in-page preview can't display this PDF, so the links above serve the
file directly. You can also browse [`paper.pdf`](paper.pdf) in the repo, or open it after
`git clone`. A counted mirror, which logs how the paper is read, lives at
[`vybfly-paper-relay.aniviza.workers.dev/paper.pdf`](https://vybfly-paper-relay.aniviza.workers.dev/paper.pdf).*
A 25-page study of the adult *Drosophila* whole-brain connectome: **discrete event simulation** of
the FlyWire map on a Vyb-native runtime, scaled across a 10Γ ladder β and an honest test of whether
"bigger" still behaves like a brain.
---
## What is this
A fruit fly has about **139,000 neurons** and roughly **500 million synapses** β the tiny
connection points where one neuron talks to another. In 2024 a large collaboration called
**FlyWire** mapped *every one* of those neurons and connections in an adult fly brain. It is the
first complete wiring diagram of a brain that anyone has ever had.
This project takes that map and asks a simple question:
> **Can you make the map bigger and have it still work?**
We grow the brain's wiring from 1Γ to **2Γ, 5Γ and 10Γ** β up to **1.4 million neurons and 27.8
million connections** β then check, carefully, whether the enlarged versions still look and behave
like the original: the same kind of wiring, the same way a signal spreads through it, the same
abilities, and what it costs in energy.
The interesting part is not that we could. It is *where it worked, where it didn't, and what that
tells you* β including one result that goes against the obvious guess.
## Why "geometric"?
Instead of rewiring things at random, we first learn a **hidden geometry** for the brain: a
coordinate system in which neurons that are "close" are likely to be connected. Adding neurons
then means placing new points near old ones in that geometry and wiring them by the learned rule.
This is what keeps connections growing in step with neurons, instead of letting the brain become an
all-to-all tangle.
## The headline results
**It works, structurally.** The 10Γ brain keeps the same sparsity as the real one β connections grow
as N1.02, essentially in step with neuron count rather than NΒ² β mean degree stays flat at
~19 partners per neuron, and connection strength stays ~12.6 synapses. Take the 10Γ brain and shrink
it back the way it was grown, and you recover **exactly** the original number of connections, with a
degree-distribution difference of 0 and community structure agreeing perfectly (ARI 1.0).
| scale | neurons | connections | synapses | mean partners |
|---|---|---|---|---|
| 1Γ (real brain) | 139,255 | 2,700,513 | 34,153,566 | 19.4 |
| 2Γ | 278,510 | 5,186,025 | 68,428,042 | 18.6 |
| 5Γ | 696,275 | 13,500,829 | 171,970,190 | 19.4 |
| 10Γ | 1,392,550 | 27,842,102 | 346,934,550 | 20.0 |
**Smaller brains still behave like the big one.** Compress the brain to half, a quarter, a tenth of
its neurons, send the same signal through, and the pattern of activity overlaps the original by
85β95% β the replicas reproduce the *behavior*, not just the shape.
**Bigger brains get more able β up to a point.** Stimulus information rises in proportion to size,
and memory capacity goes from 8 associations to at least 96. But nothing we built does *time*: tests
about delayed or ordered stimuli never beat chance, at any scale. Some curves had to be marked
"censored" β our test grid ran out before the system did, so we cannot honestly say how far capacity
grows.
**A result that goes the other way.** We trained a model of the fly's learning center (the mushroom
body) on odorβreward association. It learns well β from chance to perfect in 58 trials. But replace
the brain's actual wiring with a randomly shuffled version that has the same statistics, and **it
learns exactly as well** (+0.000 Β± 0.000). What mattered was the *anatomy of the dopamine gating*,
not the specific wiring. A negative result, and a useful one.
**The energy has two answers, and we never mix them.** Measured on the GPU: 438 β 2,022 joules across
the ladder, with cost per spike *falling* from 8.9 to 4.2 Β΅J (the device stops being idle-bound). By
biology's own accounting β calorimetry on real fly brains β the equivalent figure is linear in neuron
count *by construction*. Those two numbers answer different questions, so they are never added
together, and the paper says so on every page where they appear.
**The verdict**, against the success criteria written down before the work started:
| criterion | verdict |
|---|---|
| minimum: a 2Γ graph that preserves the statistics and renormalizes back | **supported** |
| strong: a 10Γ graph retaining the invariants and renormalizing back | **supported** |
| exceptional: capability growing *faster* than neuron count | **not supported** |
## Two implementations, on purpose
The project runs the same thing twice: a **Python reference** (the oracle every number is checked
against) and a **Vyb-native production path** β a loader, a discrete-event simulation engine, and GPU
kernels written in [Vyb](https://github.com/rickenator/Vyb).
Building the second one turned out to be the most informative part of the work: it surfaced **twelve
compiler defects**, including a blocking one where a kernel-mode store wrote eight bytes into a
four-byte slot and silently zeroed a neighbor. That one is
[filed, fixed upstream, and re-verified](https://github.com/rickenator/Vyb/issues/270) by this
project.
The GPU kernels now agree with the CPU reference tick for tick. Honest scope: that equivalence is
proven on a 512-neuron fixture, not yet on the whole 139k-neuron connectome.
### The Vyb CPU ladder: whole connectome, 1x to 10x, bit-exact
`src/vyb/` also contains three multithreaded CPU engines that run the full `sparse_prop` LIF
workload on the 1xβ10x ladder graphs. The workload is dt 0.5 ms, with 8 epochs Γ 1000 steps
= 4 s of biological time.
- `ladder_prop` uses per-(thread, block) edge bins. It is the fastest engine at 1x and 2x.
- `ladder_seg` pulls along static per-row target segments.
- `ladder_desc` streams 4-byte segment descriptors that are built once per step. It is the
fastest engine at 5x and 10x.
Each engine reproduces the spike-set digest of the deterministic C++ reference
(`scripts/cpu/sparse_prop_cpu.cpp`) **bit for bit**, at every scale and at every thread count
tried (1, 4, 8, 10, 12, 16, 20). All three were re-verified on stock Vyb 0.7.7.
The table below was measured on an i9-10850K (10C/20T) at 16 threads, using simulation time and
the best engine per scale. The baseline is the first Vyb engine (`1928e58`). The full numbers
are in `results/energy/vyb_ladder.json`.
| scale | synaptic events | baseline | now | events/s | wall s per bio-s | RSS | C++ ref, 1 thread | GPU kernel (RTX 3090) |
|---|---|---|---|---|---|---|---|---|
| 1x | 1.05 G | 1.55 s | **1.21 s** (prop) | 870 M | 0.30 | 146 MB | 8.32 s | 3.75 s |
| 2x | 2.00 G | 4.16 s | **2.60 s** (prop) | 767 M | 0.65 | 193 MB | 14.97 s | 3.15 s |
| 5x | 5.22 G | 13.11 s | **7.50 s** (desc) | 696 M | 1.88 | 363 MB | 43.45 s | 5.74 s |
| 10x | 10.69 G | 31.03 s | **16.59 s** (desc) | 644 M | 4.15 | 667 MB | 88.06 s | 10.68 s |
The 1x and 2x "now" values were measured with a Vyb build that includes the JIT changes in the
open Vyb PR (inlining at `-O2`, a host-CPU target machine). The 5x and 10x values were measured
on stock Vyb 0.7.7, after `ladder_desc` moved to 4-byte descriptors. The JIT changes do not
matter here.
A clean back-to-back rerun on an idle machine (2 repetitions per engine and scale, both builds
interleaved) gave identical digests on stock Vyb 0.7.7, and the best engine at each scale ran
within about 2% of the PR build: 1.22 s at 1x, 2.60 s at 2x, 7.72 s at 5x and 17.97 s at 10x
(those last two are with the old 8-byte descriptors).
Only `ladder_seg` gained, by 2β3%. The compiler friction this work turned up is in
`docs/DOGFOODING.md`.
### The Vyb GPU engine: bit-exact, 2.4x to 4.8x the CUDA kernel on the same GPU
`src/vyb/ladder_gpu.vyb` runs the same ladder workload on an NVIDIA GPU through the CUDA driver
API, with device kernels written in Vyb (`src/vyb_kernels/ladder_gpu_kernel.vyb`). It matches
the C++ reference **bit for bit** at every scale: the spike count, synaptic events, epoch-0
spikes and both digests. The existing CUDA kernel (`sparse_prop.cu`) does not, because it
accumulates with float `atomicAdd`.
The engine is exact without float atomics:
- The push kernel only sets bits. A warp per active source sets one bit per out-edge in a
bitmap over CSC positions, which are each target's in-edges sorted by source.
- The update kernel uses one thread per neuron. It walks its CSC range in ascending order and
sums the weights in exactly the reference order, keeping 8 weight loads in flight per round.
Most weights come from a 1-byte code instead of a 4-byte float (see below). It then
integrates and fires, and appends spikes warp-aggregated.
| scale | Vyb GPU engine (RTX 3090) | GPU kernel (RTX 3090) | Vyb GPU engine (GB10) | `sparse_prop.cu` (same GB10) | Vyb CPU, 20 Arm cores (GB10 host) | C++ ref, 1 thread |
|---|---|---|---|---|---|---|
| 1x | **0.98 s** | 3.75 s | **0.77 s** | 3.67 s (inexact) | 0.96 s (prop, 16 threads) | 8.32 s |
| 2x | **0.90 s** | 3.15 s | **0.93 s** | 3.04 s (inexact) | 1.88 s (prop, 16 threads) | 14.97 s |
| 5x | **1.71 s** | 5.74 s | **2.22 s** | 5.29 s (inexact) | 4.25 s (prop) | 43.45 s |
| 10x | **2.46 s** | 10.68 s | **4.17 s** | 10.29 s (inexact) | 7.52 s (prop) | 88.06 s |
| 20x | **4.04 s** | | **7.45 s** | | | 174.4 s |
| 40x | **8.72 s** | | **14.71 s** | | | 320.6 s |
Every Vyb GPU number above matches the C++ reference digests exactly. The 3090 times are the
best of 3 runs on an idle GPU (Godzilla). The GB10 is a DGX Spark with 273 GB/s of unified
memory. Its 1x to 10x times are the best of 3 with the GPU otherwise idle, and 20x and 40x are
the best of 2. Earlier runs beside a resident LLM server were no slower (10x best 3.94 s), so
the GB10 engine is bandwidth-bound, not contention-bound. At 10x, nsys puts the step at about
410 us of update plus 110 us of push, close to the memory-bandwidth floor of about 75 MB per
step. On the 3090 (936 GB/s), 1x is slower than 2x because the 8,000 steps of two launches
each are launch-latency-bound at that size. The 20x and 40x GB10 times here predate the
u8 weight codes and the stimulus-flag bitmap: with them the same 8-epoch 40x run is
**12.77 s** on one GB10 and **6.59 s** on the two-Spark RDMA split (see the u8-codes
section below), still bit-exact. These GB10 times are from before the u8 weight codes
described below; with them the GB10 takes 0.85, 0.90, 1.84, 3.34, 6.45 and 12.77 s at 1x to 40x.
The 20x and 40x graphs come from `scripts/` the same way as the ladder (n = 2,785,100 and
5,570,200; 56.0M and 112.3M edges). Their C++ reference runs `sparse_prop_cpu --max-active N`
with N = n, because the default 300,000-entry active list overflows at 20x and the reference
then keeps the lowest ids, which no parallel engine reproduces. Set `VYBFLY_MAX_ACTIVE` to n
for the same uncapped run in Vyb.
#### Faster GB10 kernels: u8 weight codes
nsys on a GB10 at 40x put a step at 1.31 ms of update and 0.59 ms of push. Update is the hot
kernel. For every touched in-edge it reads a 4-byte f32 weight from a scattered place in the
CSC. The weight is f32(gain * syn / t), where syn is the edge's synapse count and t is the
target's total input. Now each edge also has a 1-byte code c, which is its syn (1 to 255),
and each target has an f32 pair (hi, lo) with hi + lo close to gain / t. The kernel computes
`fma(c, hi, c * lo)` in f32. The host keeps a code only when it has proved, edge by edge,
that this gives exactly the reference f32 weight:
- in double, c * hi is exact, and so is c * lo before it is rounded to f32;
- a TwoSum check shows the double sum is exact;
- the result equals the stored weight.
Otherwise the code is 0, and the kernel reads the f32 weight as before. 1.4% of the edges
escape. The weights, the summation order and every digest are unchanged.
Push now also sets one bit per stimulated target in a small flag bitmap. Update then reads
one 32-bit word per warp instead of each neuron's stimulus count. Times are 8 epochs, best of
2, old and new kernels interleaved in one session, and every run matches the C++ reference
digests:
| 8 epochs, GB10 | before | u8 codes | change |
|---|---|---|---|
| 1x | 0.81 s | 0.85 s | +5% |
| 2x | 0.93 s | **0.90 s** | -3% |
| 5x | 2.21 s | **1.84 s** | -17% |
| 10x | 4.26 s | **3.34 s** | -22% |
| 20x | 7.49 s | **6.45 s** | -14% |
| 40x | 14.59 s | **12.77 s** (12.51 s in an earlier run) | -12% |
| 40x, two GB10s, RDMA + zero-copy | 7.87 s | **6.59 s** | -16% |
At 40x, nsys shows update falling from 1.31 to 1.01 ms per step, while push stays at
0.58 ms. At 1x the working set fits in L2, so the extra arithmetic costs more than the bytes
it saves. A few other changes did not help:
- Push is bound by about 10.8M random bit-ORs per step. Giving each source 16 or 8 lanes
instead of a warp was slower.
- Sweeping the block size (128 to 512) and push blocks (1024 to 4096) gained 1.5% at best,
so the defaults stay.
- Fusing push into update would need a grid-wide sync between them.
- Computing the weights in f64, which is exact by construction, made update 2.5x slower,
because the GB10's FP64 rate is low. So everything stays f32.
Proving the codes adds host load time (40x: 1.8 s to 2.9 s). `VYBFLY_GPU_W8=0` turns the
codes off: every edge escapes, which is exact but slower than the old kernel.
#### Two Sparks, one network: a bit-exact split
`src/vyb/ladder_gpu_split.vyb` runs one connectome across both Sparks. Neurons are cut by
target at half the in-edges. Each rank keeps every source row, but only the edges into its own
neurons, so a step needs nothing from the peer except the list of neurons that fired. After
each step the ranks swap fired-id lists on the direct 200 GbE link, over TCP or with RDMA
writes (`VYBFLY_SPLIT_NET=rdma`), then push the peer's spikes into their own targets. It is bit-exact at 10x, 20x and 40x (digests equal to the C++
reference), and with `VYBFLY_SPLIT_PARTS=1` it is the single-GPU engine.
| 20x, 8 epochs | sim | GPU time per rank | network |
|---|---|---|---|
| one GB10 (`ladder_gpu`) | 7.45 s | 7.45 s | none |
| two GB10s, serialized exchange | 8.85 s | ~3.7 to 4.1 s | ~3.4 s |
| two GB10s, full-duplex exchange | 7.51 s (profiled), 6.79 s | ~3.7 to 4.1 s | ~3.1 to 3.5 s |
| two GB10s, full-duplex, own push overlapped with the exchange | 6.64 s, 7.30 s, 7.59 s | ~3.7 to 4.1 s | ~2.7 to 3.2 s (sim minus GPU) |
| two GB10s, RDMA exchange (`VYBFLY_SPLIT_NET=rdma`), overlap on | **4.54 s**, 4.57 s | ~3.3 to 3.7 s (GPU plus copy-back) | ~0.24 to 1.08 s |
At 40x the split wins clearly. Two GB10s (duplex and overlap on) take **11.35 s** (best of 2;
the other run took 12.29 s) against 14.71 s on one GB10. That is 1.30x faster, and again
bit-exact. The GPU work per step doubles from 20x, but the exchange cost grows less.
The split halves the GPU time, but the exchange costs about 0.35 ms per step. A ping-pong test
over the same link shows why: 36 us for 8 bytes with busy-polling, but about 475 us for 240 KB
each way, and about 800 us with blocking sockets. Socket buffers are capped at 212 KB
(`net.core.wmem_max`) without root. So over TCP two Sparks only just beat one at 20x, beat it by 1.30x at 40x, and lose at
10x (6.09 s against about 4.2 s, measured with the serialized exchange). The overlap's gain is within
run-to-run noise, since the GPU push is short next to the exchange. The overlap and the duplex
exchange are on by default (`VYBFLY_SPLIT_OVERLAP`, `VYBFLY_SPLIT_DUPLEX`). Both only reorder
bit-ORs and integer sums, so they cannot change a result.
RDMA removes most of the exchange cost, and it needs no root. The ConnectX ports expose RoCE v2
devices that an ordinary user can open (`/dev/infiniband/uverbs*` is world-writable and the
memlock limit is unlimited). With `VYBFLY_SPLIT_NET=rdma` each rank registers its send buffer and
a two-slot receive buffer, swaps queue-pair details over the TCP connection, then posts one RDMA
write-with-immediate per step into the peer's slot, with the id count in the immediate.
libibverbs is opened with `dlopen` at run time, so a TCP run needs nothing new. At 20x this takes
**4.54 s** (best of 2; the other run took 4.57 s), against 7.59 s over TCP in the same session and
7.45 s on one GB10. That is 1.64x faster than one GB10, and bit-exact. A ping-pong written in Vyb
over the same verbs takes 2.3 us for 16 bytes and 87.5 us for 1 MB each way (12 GB/s), against
100 to 230 us and 355 to 454 us over TCP. Most of the remaining exchange time is the copy of the
fired list back to the host.
At 10x, 40x and 80x RDMA pays off as well. These are 8 epochs, two runs each, all bit-exact (10x
42322697548985024/188222298830040, 40x 239142599791732164/1733953483748172, 80x
508755035752662158/103693891554914; 80x is 11.1M neurons and 225M edges, every neuron allowed active):
| 8 epochs | one GB10 | two GB10s, TCP | two GB10s, RDMA | RDMA vs one GB10 |
|---|---|---|---|---|
| 10x | 4.19 s | 6.09 s (serialized) | **2.61 s**, 2.63 s | 1.60x |
| 20x | 7.45 s | 7.59 s | **4.54 s**, 4.57 s | 1.64x |
| 40x | 14.71 s | 11.35 s | **8.05 s**, 8.18 s | 1.83x |
| 80x | 34.05 s, 34.10 s | not run | **15.60 s**, 15.62 s | 2.18x |
So with RDMA two Sparks now beat one at every scale tried, including 10x, where TCP lost, and the
gain grows with scale. At 80x the wire itself is 0.9 to 1.7 s of the 15.6 s per rank. An earlier
version of this note put most of the rest down to a 12.3 to 12.6 s fired-list copy back to the host.
That was wrong: the copy ran on the legacy stream, so `dtoh_ms` also counted the wait for that
step's GPU work. The copy itself is about 2 to 3% of the step.
#### 100x: 13.9M neurons, and the graph built in Vyb
The largest scale run so far is **100x**: 13,925,500 neurons and 270,051,300 edges. Both halves of
it are now Vyb. The graph comes from `src/vyb/upscale_graph.vyb`, which reads the canonical 1x
out-CSR, applies the 5-synapse rule, and writes the c-times CSR the ladder engines read β no Python
in the graph path. It reproduces the published `s1` files byte for byte at factor 1 (md5-identical
indptr, indices and syn), builds the 100x graph in about two and a half minutes, and its subdivision
keeps each child's inherited partner set, so `E = 100 x E_1x` exactly (degree 19.39, the parent's).
Its header states the one difference from the Python generator: the target child inside a partner
family comes from a hash of (edge, child), not a softmax over the fitted geometry, and sibling edges
are omitted β statistics preserved, microstructure different.
The run, 8 epochs x 1000 steps (`results/ladder/split_runs.json`):
| 100x, 8 epochs | sim | load | digests |
|---|---|---|---|
| two GB10s, RDMA | **17.14 s** | 21.1 s per rank | bit-exact |
| one GB10 | 37.91 s | 4.5 s | bit-exact |
| RTX 3090 | 24.62 s | 39.0 s | bit-exact |
| 16 CPU cores (Vyb `ladder_prop`) | 248.1 s | 3.7 s | oracle |
All four agree digit for digit: `digest_sum` 637865169974127257, `digest_xor` 1325515887917234,
4,786,725,072 spikes, 102,365,909,896 synaptic events, `overflow` 0. Two Sparks are 2.21x one GB10
and 14.5x the 16-core CPU engine. The stimulus is the same mt19937_64 schedule the other scales use
(`scripts/make_stim.cpp` reproduces the published `stim_s1.bin`/`stim_s80.bin` byte for byte), and
the CPU engine needed `VYBFLY_MAX_ACTIVE` β its 300,000-entry active list, inherited from the C++
reference, silently dropped 1.6e9 spikes at this scale before.
The zero-copy exchange (`VYBFLY_SPLIT_ZC`, default 1, used with two ranks and the overlapped push)
removes even that. The GB10's GPU shares the host's memory, so the send and receive buffers are pinned in place
with `cuMemHostRegister` and the kernels use them directly. The update kernel writes the fired list
straight into the RDMA send buffer, and the push kernel reads the peer's list straight from the
receive buffer. Only an 8-byte count comes back per step. GPUDirect RDMA from device memory is not
available here: the driver reports no dma-buf and no GPUDirect RDMA support, and registering a device
pointer with ibverbs fails without root. On GB10, though, pinned host memory is the same physical
memory, so registering it has the same effect. If pinning fails the engine warns and falls back to
copies (`VYBFLY_SPLIT_ZC=0` forces that). Both modes are bit-exact (8 epochs):
| 8 epochs | ZC off | ZC on |
|---|---|---|
| 10x, RDMA | 2.61 s | **2.51 s** |
| 10x, TCP | 4.75 s | **4.10 s** |
| 40x, RDMA | 8.06 s | **7.81 s** |
| 80x, RDMA | 15.66 s | **15.28 s**, 15.38 s |
At 40x with ZC on, rank 1 spends 7.27 s waiting on the GPU, 0.04 s on copies and 0.48 s on the
network. Against an ideal of 7.35 s, what is left is about 0.46 s of per-step exchange and barrier
latency. Sending a bitmap instead of an id list would cut the bytes only about 1.4x at this firing
rate. At 80x ZC on removes the copies (`dtoh_ms` falls from 12.3-12.5 s to 0.04 s per rank) but
gains only about 2%: each rank then waits 13.3-14.4 s on the GPU, so the 80x step is bound by the
kernels themselves, not by the transfer. The three 80x runs above were back to back and all
bit-exact.
Run it, rank 0 first, on two machines that share the graph files:
```sh
# on the rank-0 host (it listens on VYBFLY_SPLIT_PORT)
VYBFLY_SPLIT_PARTS=2 VYBFLY_SPLIT_RANK=0 scripts/gpu/run_split_rank.sh /s20 /stim_s20.bin
# on the rank-1 host
VYBFLY_SPLIT_PARTS=2 VYBFLY_SPLIT_RANK=1 VYBFLY_SPLIT_PEER= \
scripts/gpu/run_split_rank.sh /s20 /stim_s20.bin
# for the RDMA exchange, set VYBFLY_SPLIT_NET=rdma on both hosts (VYBFLY_RDMA_DEV and
# VYBFLY_RDMA_GID pick the RoCE device and GID index; defaults rocep1s0f0 and 3)
```
On the Arm host, `ladder_prop` also beats `ladder_desc` at 5x and 10x, the opposite of the i9. `scripts/cpu/run_ladder.sh` therefore picks `ladder_prop` on aarch64.
Run it:
```sh
VYBFLY_ENGINE=gpu VYBFLY_LADDER_DIR=/s10 VYBFLY_LADDER_STIM=/stim_s10.bin \
VYBFLY_EPOCHS=8 VYB=vyb scripts/cpu/run_ladder.sh --module-path "$VYB_STDLIB"
```
This works on stock Vyb 0.7.7, because the kernel is committed as PTX (sm_86, which the driver
JIT-compiles for newer GPUs). Regenerating the PTX needs the kernel intrinsics in Vyb PRs
`perf/kernel-atomics-bits` and `perf/kernel-friction` (which adds `shfl_xor_i64`):
```sh
vyb --kernel src/vyb_kernels/ladder_gpu_kernel.vyb --gpu sm_86 --ptx src/vyb_kernels/ladder_gpu_kernel.ptx
```
Further knobs are `VYBFLY_GPU_BLOCK` (256), `VYBFLY_GPU_PUSH_BLOCKS` (2048),
`VYBFLY_GPU_W8` (1, the u8 weight codes), `VYBFLY_GPU_DEVICE` (0) and `VYBFLY_MAX_ACTIVE` (300000, the active-list capacity).
## Jargon decoder
| term | what it means here |
|---|---|
| connectome | the full wiring map: which neuron connects to which, and how strongly |
| synapse / connection | a synapse is one contact point; a "connection" means 5+ synapses on the same neuron pair, the convention the published papers use |
| graph, node, edge | a network: neurons are nodes, connections are edges |
| geometric renormalization | rescaling a network by moving it in a hidden geometry, then checking you get the original back |
| coarse-graining (downscaling) | merging nearby neurons into one to get a smaller replica |
| subdivision (upscaling) | replacing one neuron with *c* nearby children to get a bigger graph |
| LIF model | "leaky integrate-and-fire", the simplest standard neuron model: voltage builds up, fires past a threshold, then resets |
| discrete-event simulation | simulate only the moments when something actually happens, instead of stepping through every millisecond |
| AUC | a 0.5β1.0 score for "can this tell connected pairs from unconnected ones?"; 0.5 is a coin flip |
| hyperbolic embedding | coordinates on a curved (PoincarΓ©) disk, in which distance predicts connection probability better than raw 3-D position does |
## Run it yourself
```sh
cd ~/Projects/VybFly # paths in this repo are written relative to your home directory
uv venv .venv --python 3.12
.venv/bin/python scripts/fetch_data.py # downloads the FlyWire release, verifies md5
.venv/bin/python scripts/phase0_build.py # builds the canonical graph
.venv/bin/python scripts/phase0_gate.py # checks it against published values (11/11 required)
```
Then any phase β for example the scaling ladder and its closure test:
```sh
.venv/bin/python scripts/phase4_geometry.py # fit the geometries
.venv/bin/python scripts/phase6_upscale.py --geometry auto --factors 2 5 10 --save
.venv/bin/python scripts/phase7_closure.py --from-saved --factors 2 5 10
.venv/bin/python scripts/phase9_capability.py --stage all # capability battery
.venv/bin/python scripts/assess_success_criteria.py # the verdicts
```
A scale's harness inputs are built in Vyb too β the graph and the stimulus schedule:
```sh
g++ -O2 -o make_stim scripts/make_stim.cpp # same schedule as sparse_prop
VYBFLY_FACTOR=100 VYBFLY_OUT=results/ladder/s100 \
"$VYB/build/vyb" src/vyb/upscale_graph.vyb # 1x CSR -> 100x CSR
./make_stim 13925500 results/ladder/s100/stim_s100.bin # n = 139255 * factor
```
The Vyb side needs the Vyb toolchain (`VYB` and `VYB_STDLIB` env vars; defaults assume
`~/Projects/Vyb`):
```sh
.venv/bin/python scripts/vyb_des_check.py # Vyb discrete-event engine vs the Python reference
.venv/bin/python scripts/phase3_gpu_check.py # Vyb GPU kernels vs the CPU reference
```
The CPU ladder engines read a ladder graph's CSR (`results/energy/curves/s/`, which the
energy phase produces) and a stimulus file that the C++ reference writes:
```sh
g++ -O3 -march=native -ffp-contract=off -o sparse_prop_cpu scripts/cpu/sparse_prop_cpu.cpp
./sparse_prop_cpu --dir results/energy/curves/s10 --epochs 8 --steps 1000 --syn-gain 6 \
--refr-steps 20 --dump-stim stim_s10.bin # prints the reference digest
VYBFLY_LADDER_DIR=results/energy/curves/s10 VYBFLY_LADDER_STIM=stim_s10.bin \
VYBFLY_THREADS=16 VYBFLY_EPOCHS=8 \
$VYB --module-path $VYB_STDLIB src/vyb/ladder_desc.vyb # or ladder_prop / ladder_seg
```
`scripts/cpu/run_ladder.sh` picks the engine for you with the same environment variables. It
chooses `ladder_prop` below 10 M edges (1x and 2x) and `ladder_desc` at or above that (5x and
10x), which matches the measured crossover. Set `VYBFLY_ENGINE` to force a particular engine:
```sh
VYBFLY_LADDER_DIR=results/energy/curves/s10 VYBFLY_LADDER_STIM=stim_s10.bin \
VYBFLY_THREADS=16 VYBFLY_EPOCHS=8 VYB=$VYB \
scripts/cpu/run_ladder.sh --module-path $VYB_STDLIB
```
Compare the `digest_sum` and `digest_xor` values on the engine's `RESULT` line against the
reference's output; they must be equal.
The paper is built from the artifacts rather than typed by hand:
```sh
.venv/bin/python scripts/paper_collect_data.py # every number -> results/paper/data.json
.venv/bin/python scripts/paper_make_figures.py
.venv/bin/python scripts/paper_make_tables.py
pdflatex paper.tex && pdflatex paper.tex && pdflatex paper.tex
```
## What's in here, and what isn't
```
PROJECT-VYBFLY.md the scope document, written before the work started, including the non-goals
paper.tex paper.pdf the paper, plus the scripts that generate its figures and tables
src/flyscale/ the Python reference: dataset, metrics, renormalization, dynamics, capability
src/vyb/ Vyb-native loader and discrete-event engine
src/vyb_kernels/ Vyb GPU kernels (NVPTX) and the probes used to isolate compiler defects
scripts/ one runnable script per phase, plus data fetch and paper generation
results/ the artifacts every claim traces to (JSON, PTX/cubin, summaries, notes)
tests/ the checks that gate the work (canonical dataset, LIF engines, renormalization)
docs/ DATASET.md (sources, conventions, gate), VYB-PORT.md, ROADMAP.md
```
**Not in the repo, on purpose:** the FlyWire source data and every derived array (replica graphs,
feature matrices, large CSVs). They are hundreds of MB and fully regenerable β the fetch script
verifies the download by md5 and the phase scripts rebuild everything from it. Keeping them out
makes the repo small and the pipeline the source of truth. In the run commands recorded inside
`results/`, paths are written as `~/Projects/...`.
Dataset sources, the 5-synapse threshold convention and the full Phase 0 gate table:
**[docs/DATASET.md](docs/DATASET.md)**.
## What we do not claim
- **No consciousness, no "it's thinking."** This is a graph and a simulator, and the paper says so.
- **No efficiency claims from GPU numbers.** Comparing a GPU's joules to a brain's is exactly the
category error we avoid; hardware and biological energy are separate tracks, never combined.
- **The closure test proves invertibility, not self-similarity.** Children inherit their parent's
partners, so shrinking the 10Γ graph back is close to an identity *for that reason*. The stronger
test β letting an inferred geometry, rather than the generator, decide what collapses β has not
been run.
- **The geometry question is unresolved at the top.** A learned embedding beats raw anatomy, but it
does not beat a degree-only baseline by more than its own run-to-run spread; and the geometry that
*predicts* connections best is not the one that *renormalizes* best.
- **Capability curves are censored** at the test grid's ceiling from 2Γ upward, which is why the
exceptional criterion is undecided rather than refuted.
- **The whole-connectome GPU claim is still fixture-scale** (512 neurons), as noted above.
More of these β including corrections we made to our own earlier claims β are in the paper's
limitations section and its honesty-log appendix.
## Status
| milestone | state |
|---|---|
| M0 canonical FlyWire graph + published-value gate | **done** β 11/11 required checks |
| V1 Vyb-native loader | **done** β 26/26 invariants match the Python reference |
| M1 whole-connectome LIF baseline | **done** β dense and sparse engines agree exactly; 1 s of brain in 120 s of CPU |
| M2 Vyb discrete-event engine | **done** β 4/4 replications match the reference exactly |
| M2b Vyb multithreaded CPU ladder (1xβ10x) | **done** β bit-exact digests vs the C++ reference; 10x runs at 4.15 s wall per bio-s |
| M3 Vyb GPU engine (ladder workload) | **done** β bit-exact digests, spike counts and synaptic events against the C++ reference at 1xβ40x on an RTX 3090 and 1xβ10x on a GB10; 2.4xβ4.8x the CUDA `sparse_prop.cu` kernel on the same GPU, without float atomics. (The older 512-neuron fixture agreement is the M3 harness, not the ceiling.) |
| M3b two-Spark split | **done** β one connectome across both Sparks, bit-exact at 10x, 20x, 40x and 80x; 2.18x over one GB10 at 80x with RDMA (15.60 s vs 34.05 s), 15.28 s with the zero-copy exchange, which is then bound by the kernels rather than the wire |
| M4 energy instrumentation | **done** β GPU device power measured; CPU power unavailable on this machine |
| M5βM8 geometry, downscaling, upscaling, closure | **done** β see section 5 of the paper |
| M9 dynamic validation of the replicas | **done** β 85β95% active-set overlap |
| M11βM14 learning, capability, energy scaling, capability per watt | **done** β including the negative control result |
| M15 100Γ ~14M neurons | **done** β the 100Γ graph is built by Vyb (`src/vyb/upscale_graph.vyb`: 13,925,500 neurons, 270,051,300 edges; at factor 1 its CSR is byte-identical to the published 1Γ) and 8 epochs run on two Sparks in **17.14 s**, bit-exact against the Vyb CPU engine (`results/ladder/split_runs.json`). One GB10: 37.91 s; RTX 3090: 24.62 s; 16 CPU cores: 248.1 s |
## Credits and data
Built on the **FlyWire** whole-brain connectome of adult *Drosophila*:
- Dorkenwald et al., *Neuronal wiring diagram of an adult brain*, Nature 634 (2024) β
[10.1038/s41586-024-07558-y](https://doi.org/10.1038/s41586-024-07558-y)
- Lin et al., *Network statistics of the whole-brain connectome of Drosophila*, Nature 634 (2024) β
[10.1038/s41586-024-07968-y](https://doi.org/10.1038/s41586-024-07968-y) β source of the published
values the gate checks against
- Data release v783 β [10.5281/zenodo.10676866](https://doi.org/10.5281/zenodo.10676866)
- Conceptual basis for the scaling method: GarcΓa-PΓ©rez, BoguΓ±Γ‘ & Serrano, *Multiscale unfolding of
real networks by geometric renormalization*, Nature Physics 14 (2018) β
[10.1038/s41567-018-0072-5](https://doi.org/10.1038/s41567-018-0072-5)
- Biological energy anchor: Panda et al., calorimetry of individual live fly brains, Cell Reports
Methods (2026) β [10.1016/j.crmeth.2026.101501](https://doi.org/10.1016/j.crmeth.2026.101501)
The full reference list, including the per-spike and per-vesicle biology sources, is in the paper.
The Vyb language and compiler used for the production path:
[rickenator/Vyb](https://github.com/rickenator/Vyb).
---
Built and executed with an autonomous agent harness ([hermes-agent](https://hermes-agent.nousresearch.com)).
Every number in the paper is generated from an artifact in `results/` β there are no illustrative
figures, and nothing in this README that isn't measured somewhere on disk.
## The Vyb family
Vyb is a language with more than one consumer; each project below is written in
it and is a real, running thing rather than a demo.
| project | what it is |
|---|---|
| **[Vyb](https://github.com/rickenator/Vyb)** | the language, the LLVM JIT/AOT compiler and the standard library β [vyb-lang.org](https://vyb-lang.org/) |
| **[VybOS](https://github.com/rickenator/VybOS)** | a Linux-based OS whose declarative, content-addressed configuration and control framework are Vyb; PID 1 is Vyb |
| **VybWin** *(private for now)* | the window environment for VybOS: apps are cards summoned into an arena, with the ledger one flip away |
| **[VybForge](https://github.com/rickenator/VybForge)** | the shop floor: a desired-state interviewer for VybOS, a Vyb-native on-GPU inference substrate, and the self-contained voice loop |