cd /news/artificial-intelligence/making-a-166000-neuron-brain-bit-exa… · home › topics › artificial-intelligence › article
[ARTICLE · art-152510] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=↑ positive

Making a 166,000-neuron brain bit-exact on any GPU

The fly.ai project built a bit-exact integer simulation of the adult fruit fly central nervous system connectome — 166,700 neurons and 25.6 million connections of leaky integrate-and-fire neurons — that produces identical spike trains on NVIDIA, AMD and CPU hardware. To make volunteer browser-based GPU runs verifiable, the team replaced floating point with Q16/Q20 fixed-point arithmetic and 32-bit wraparound, so integer addition's associativity removes summation-order differences, and per-neuron noise is derived from a stateless hash rather than a shared RNG. Each run's spikes are folded into keyed 32-bit hash sums so a server can re-run samples and check a stranger's answer exactly.

by read7 min views16 publishedOct 18, 2026

How we got a spiking fruit-fly connectome to produce identical spikes on NVIDIA, AMD and a CPU, so strangers' browsers can run it and every answer can be checked.

Disclosure: fly.ai is funded by a memecoin ($FLYAI); nothing described here needs the token, and the code is MIT-licensed.

fly.ai simulates the complete central nervous system of an adult male fruit fly: the MaleCNS v1.0 connectome, 166,700 neurons and 25.6 million connections, as a network of leaky integrate-and-fire neurons. Each step, every neuron leaks a little, adds up input from the neurons that fired into it, and spikes and resets if it crosses threshold.

We wanted volunteers to run that brain for us from a browser tab: open a page, lend a GPU, and the network works through a grid of experiments. That raises a problem that sounds boring and turns out to be the whole design: how do you check a stranger's answer?

The obvious way to check a volunteer is to re-run a sample of their jobs on the server and compare. For that to work, an honest machine must produce exactly the server's result. Not close. Exactly.

Floating point can't promise that across GPU drivers:

a*b + c into a single FMA with one rounding instead of two. Whether it does depends on the driver.

For most numerical work, a different last bit is noise. For a spiking network it's fatal. Somewhere among 166,700 neurons, one voltage sits a hair under threshold on one GPU and a hair over on another. That neuron spikes a step early, its targets get input a step early, and within a few steps the two spike trains have nothing to do with each other. Two honest GPUs would disagree, and the checker would zero both. So we stopped using floats.

Compute jobs run the connectome entirely in integers, in a module called fixed.ts. The representation is fixed-point:

| voltage | Q16 (1.0 = 65536); spike at ≥ 65536, reset to 0 | | synapse | Q20 weight; inputs summed with 32-bit wraparound | | update | v ← mulshift16(v, decay) + mulshift16(input, gain·4096) + tonic + drive + noise | | noise | lowbias32(neuron, step key) under a threshold | | record | each spike adds a keyed hash of (neuron, step) into two 32-bit sums |

A few of these choices deserve a closer look.

Voltage is a 32-bit integer where 65536 means 1.0, the firing threshold. Synaptic weights get more fractional bits (Q20), because each weight is a synapse count normalized so a neuron's inputs add up to 1. Spread across many inputs, individual weights are small, so they get finer resolution than the voltage.

The two scales meet in the update. Summed input is in Q20; multiplying it by the gain expressed in Q12 (gain·4096) and shifting right by 16 lands the result back in Q16, the voltage's scale. Decay works the same way: a Q16 multiplier on a Q16 voltage, shifted by 16.

This is the trick that kills the summation-order problem. Integer addition modulo 2³² is associative and commutative. If every input to a neuron is added with 32-bit wraparound, the final sum is the same whatever order the threads add in. A CPU walking a neuron's inputs in one order and a GPU walking them in another produce the same bits.

Every operation in the integer engine wraps at 32 bits exactly the way WGSL does, so the CPU (WebAssembly) and the GPU (WebGPU) implement literally the same arithmetic.

The model needs noise: a neuron fires spontaneously now and then. A shared random number generator is a disaster for parallel determinism, since the sequence of draws depends on scheduling. Instead, each neuron's noise is a hash: lowbias32 of the neuron index and a per-step key, compared against a threshold. Every neuron draws independently, there's no state to share, and any machine can compute neuron 91,204's noise at step 600 without knowing anything else.

The answer a volunteer sends back has to capture the whole run, not just a few counts. Every spike adds a keyed hash of (neuron, step) into two 32-bit sums. Because they're sums, they don't depend on the order spikes are recorded in, which matters when thousands of neurons fire in the same step on parallel threads. Two different spike trains colliding in both sums by accident is vanishingly unlikely, and a cheater can't aim for a known value, because the hashes are never published.

The one place floats sneak back in is setup: the weight table and the decay constant come from Math.exp, and Math.exp can differ in the last bit between JavaScript engines. So the clients don't compute them. The server ships those numbers in /api/model, and every engine starts from the same integers.

Quantizing a brain could quietly turn it into a different brain. So npm run validate compares the integer engine with the float engine (the one the 3-D fly world uses) over 6 conditions × 4 seeds:

They agree statistically, not spike for spike. That's the honest framing: the integer brain is a slightly different simulation of the same wiring, and on every measure we checked it behaves like the original. What it adds is that the integer brain agrees with itself, bit for bit, everywhere.

The WebGPU engine runs the integer brain for up to 32 jobs at once. Each simulated step is two compute passes, one invocation per neuron:

The batching trick is in the first pass. fired[j] isn't a boolean; it's a 32-bit mask with one bit per brain. Each neuron walks its incoming synapses, and for each input one 32-bit read says in which of the 32 brains that input fired. Most neurons are silent most of the time, so an input that's silent in all 32 brains costs a single read and nothing else.

A final pass totals each brain's record and spike counts, and only that small result is read back to the CPU. The heavy state never leaves the GPU.

Each job is 15 simulated seconds: 5 at rest, then 10 with a sensory channel driven (one of the eye channels LPLC2, LC4, LPLC1 or LC10a, or touch, on either side), recording spikes per motor group in both windows. The grid covers 4 drive strengths, 3 gains, 3 tonic levels and 3 seeds, plus undriven controls: 1,107 jobs per round, with a new round of seeds added whenever the queue runs low.

Benchmarked in headless Chrome on one laptop with two GPUs:

jobs/hour CPU, 1 thread GPU, 1 job GPU, batch 16 GPU, batch 32
RTX 4060 Laptop 579 1,144 9,278 11,129 (19× a CPU thread)
Radeon 890M (integrated) 514–583 216–355 2,121–2,929 3,263–3,881 (5.6–7.6×)

And the part this whole design exists for: four very different jobs gave identical hashes and spike counts on the CPU, on both GPUs, and at scattered positions in a batch of 32. NVIDIA and AMD agree bit for bit.

In real mining from the live page at batch 32, the RTX 4060 ran 640 jobs in 5 minutes, at 124–162 jobs a minute (the spread is likely thermal throttling). The server checked 13 of those answers; all matched. The Radeon 890M ran 65.7 jobs a minute; the server re-ran 9, and all matched.

Phones run it too, on CPU threads or WebGPU where the browser supports it. Each CPU thread holds about 350 MB, so phones get at most 2 threads, and GPU batches start at 8.

Once every honest machine produces the same bits, checking becomes cheap and statistical instead of expensive and exhaustive:

None of that works with floats. With exact integers, a single mismatch is proof of a wrong answer, not a disagreement between drivers.

The result is a volunteer compute network where anyone's browser can run a full fly connectome and nobody has to be trusted. Checked work is credited as points to the miner's wallet. And because the brain is just integer arithmetic in a page, the same network can run other work too: buyers' WebAssembly programs, GPU shaders and text embeddings.

The neuron model is simple: point neurons, one global parameter set, no dendrites, no graded neurons, a rough sign rule for transmitters. None of it is validated against recordings from real flies. Bit-exactness makes the simulation reproducible; it doesn't make it right. And the float engine elsewhere in the project still agrees with the integer one only statistically.

But for the narrow goal, many untrusted machines running the same 166,700-neuron brain and getting the same answer, integers turned out to be the whole solution.

pip install flybrain fixed.ts and the WebGPU engine: mine/) Connectome: MaleCNS v1.0 by FlyEM (HHMI Janelia), the University of Cambridge, the MRC Laboratory of Molecular Biology and Google Research, used under CC BY 4.0. The neuron model is adapted from Fly64 by Jessica Paquette.

Originally published on Medium.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @fly.ai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/making-a-166000-neur…] indexed:0 read:7min 2026-10-18 · —