如何讓 166,000 神經元的果蠅腦在不同 GPU 上逐位一致
Making a 166,000-neuron brain bit-exact on any GPU
fly.ai 的整數引擎模擬包含 166,700 個神經元的成年雄性果蠅中樞神經系統,並令 CPU、NVIDIA 和 AMD GPU 得出逐位一致結果。引擎採用 Q16 電壓、Q20 突觸權重、32 位回繞加總,以及按神經元和步數計算的雜湊噪聲。在 6 個條件和 4 個種子的驗證中,整數引擎與浮點引擎的羣體放電率相差約 0.004 Hz,但尖峯序列並非逐項相同;模型亦未以真實果蠅的記錄驗證。
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?
Why floats can't do it
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:
-
Fused multiply-add. A compiler may contract
a*b + cinto a single FMA with one rounding instead of two. Whether it does depends on the driver. - Summation order. Float addition isn't associative. A neuron with thousands of inputs summed in parallel gets a different last bit depending on the order the threads finish in.
- Vendor rounding. Transcendental functions and some edge cases differ between vendors.
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.
The integer brain
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.
Q16 voltages, Q20 weights
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.
Wraparound sums: order stops mattering
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.
Noise without shared state
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.
Hashing the spike train
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 last float
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.
Is it still the same brain?
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:
- Population rates agree to within about 0.004 Hz (2.260 vs 2.259 Hz at rest).
- Sensory effects agree to within a few percent. Driving the left LPLC2 looming detectors raises the left giant fiber, DNp01, by +11.10 Hz in float and +11.15 Hz in integer. Touch raises the right leg kick by +8.17 Hz vs +8.21 Hz.
- Motor-group rates correlate at 1.000 across 240 comparisons.
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.
On the GPU: 32 brains per pass
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:
- Synapses. Sum incoming weights from neurons that fired, for every brain at once.
- Neurons. Update voltage, spike, and record, for every brain.
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.
The numbers
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.
What bit-exactness buys you
Once every honest machine produces the same bits, checking becomes cheap and statistical instead of expensive and exhaustive:
- Canaries. A share of every miner's jobs already have a known server answer. They cost the server nothing. Faking 100 jobs when 15% are canaries meets one with probability 1 − 0.85¹⁰⁰, which is effectively certain.
- Re-runs. Other answers are re-run with a probability that falls as the day goes on, around 24 re-runs a day for a miner doing 10,000 jobs. A miner can't tell which answers get checked; every submit gets the same reply.
- One strike. A wrong answer zeroes the miner's day and stops their unchecked answers counting.
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.
Limits worth saying out loud
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.
Try it
- Simulator:
pip install flybrain - Code, including
fixed.tsand the WebGPU engine: https://github.com/alextitonis/fly.ai (seemine/) - Run the brain in your browser: flyaiworld.com/compute
- Watch the full connectome puppet a giant wizard in a 3-D world: flyaiworld.com/simulation
- Discord: https://discord.gg/2KqfZs4aK8 · X: @flydotai
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.
來源:DEV Community · dev.to