Builder's Journal

I Ran a Fruit Fly's Entire Brain on My DGX Spark, Then Gave It a Body

I got Eon Systems' whole-brain fruit fly emulation running on an NVIDIA DGX Spark: 138,639 neurons wired from a real fly's connectome. It reproduced the paper's sugar-to-feeding result, needed a CUDA 13 fix to build the faithful GPU path, and ended up driving a simulated body that walks and turns on its own descending-neuron signals.

I wanted one thing: SSH into my DGX Spark, run one command, and watch spikes propagate through an entire fruit fly brain on my own hardware.

That works now. One script loads 138,639 neurons, wired the way they're wired in a real fly, and stimulates the sugar-sensing neurons. It prints activity spreading through the brain 10 milliseconds at a time, until the motor neuron that extends the fly's proboscis to feed fires, about 27 ms in. Then I went further and connected the brain to a simulated body. Stimulate one side of its walking circuit and the fly turns toward that side.

The 90-Day AI Transformation: A Week-by-Week Playbook for CIOs Who Need Results Now

The 90-Day AI Transformation: A Week-by-Week Playbook for CIOs Who Need Results Now

Stop planning your 18-month AI roadmap. This week-by-week playbook takes tech leaders from zero AI deployments to measurable production results in 90 days.

Learn More

Every number below came off my machine.

A simulated fruit fly walking in a left circle, driven by its connectome The connectome steering a NeuroMechFly body: side view, top-down view, and the brain's descending-neuron readout for that frame.

What this actually is

In 2024, Shiu et al. published a whole-brain model of the adult fruit fly in Nature, built on the FlyWire connectome. FlyWire is a neuron-by-neuron wiring diagram reconstructed from electron microscope images of one fly's brain. Eon Systems packaged the model as fly-brain, with half a dozen simulation backends: Brian2 on CPU, Brian2CUDA, PyTorch, NEST GPU, GeNN and Brian2GeNN.

The data ships in the repo: 138,639 neurons and 15.1 million connections. Each connection is a presynaptic neuron, a postsynaptic neuron and a signed synapse count. There's no download step and no git-lfs.

The repo was built and tested on an RTX 4070 under WSL2 with CUDA 12. My Spark has none of those: it's ARM64, CUDA 13, and Blackwell compute capability 12.1. That gap is where every interesting problem was.

How I ran it

I didn't type most of the commands. I wrote a phased brief and handed it to Claude Code running on the Spark itself:

  • First, get the PyTorch backend working and prove it's on the GPU.
  • Then, attempt Brian2CUDA.
  • Then, scope the embodied version and ask me before starting.

The rule was no claims of success without evidence, and my job was reading that evidence. All three phases took about an hour of wall-clock time.

The Spark also runs Ollama as my always-on LLM server. During this, its two pinned models held about 57 GB of the 121 GB of unified memory, and I didn't want them disturbed. That was fine, because the brain is smaller than you'd think. But on this machine you check free -g before doing anything, because the GPU's memory is the system's memory.

Finding 1: PyTorch on ARM64 just works now

I expected a fight and didn't get one. PyTorch publishes CUDA 13 wheels for aarch64 on its own index:

uv venv --python 3.12 --managed-python .venv
uv pip install --python .venv/bin/python "torch==2.14.0" \
    --index-url https://download.pytorch.org/whl/cu130

There were two small things:

  • No Python headers. The system Python has none (python3-dev isn't installed), so the environment uses a uv-managed Python, which ships its own.
  • sm121 vs sm120. The GB10 is sm_121, and the wheel ships kernels only up to sm_120. That's fine, because Blackwell 12.x binaries are compatible across the family. But it's the first place I'd look if a kernel ever fails with "no kernel image is available."

Finding 2: it reproduces the paper

The headline result of Shiu et al. is that activating the sugar-sensing gustatory neurons drives MN9, the motor neuron that extends the proboscis to feed. So that was the test: drive 21 sugar neurons at 200 Hz and see what the rest of the brain does.

0.1 s of simulated time
Stimulated sugar neurons that fired 21 / 21
Unstimulated neurons that fired 308
Recruited by 10 / 25 / 50 / 100 ms 10 / 81 / 270 / 308
MN9 first spike 27.8 ms (left), 37.8 ms (right)

A wave moves through the brain, and the feeding motor neuron fires on cue. Drive the P9 walking neurons instead and a different circuit responds: descending steering neurons fire, and MN9 stays silent.

The demo script prints this live, one line per 10 ms bin, and calls out named neurons the moment they first fire:

  t (ms) | spikes | active | recruited | GPU% | active neurons (# = 5)
    30.0 |    174 |    143 |       141 | ...
         >>> MN9 L  (proboscis motor, feeding) first spike at 26.4 ms
         >>> MN9 R  (proboscis motor, feeding) first spike at 27.7 ms

Proving it's on the GPU. A sim that quietly falls back to the CPU is the classic way these projects mislead you, so I asked for evidence instead of assertions:

Evidence Result
GPU utilization, idle 5%, 12.5 W
GPU utilization, during a 10 s sim 84% mean, 91% max, 40.9 W
nvidia-smi process list the sim's PID, next to Ollama's
Profiler, top kernel cuSPARSE csrmv: 84% of GPU time, 0.86 ms per step

That kernel is the whole story of this model: each 0.1 ms step is one sparse matrix–vector multiply over the connectome. The demo also refuses to run if torch can't see CUDA, instead of quietly crawling along on the CPU.

Finding 3: Brian2CUDA broke on CUDA 13, not on ARM

Brian2CUDA is the faithful spiking path. It generates CUDA source for your model at runtime and compiles it with nvcc, so I expected it to be what broke on ARM64. It did break, but ARM had nothing to do with it.

The first failure wasn't even CUDA. brian2==2.8.0 doesn't cap its numpy version, so a fresh install gets numpy 2.5, where ndarray.ptp no longer exists, and import brian2 dies. Pinning numpy==1.26.4 fixes it.

The real failure came at compile time:

cccl/thrust/detail/config/cpp_dialect.h:78: error: #error Thrust requires at least C++17.

CUDA 13's Thrust and libcu++ require C++17. Brian2CUDA hardcodes -std=c++11 in its makefile template, and Brian2 passes another -std=c++11 to the host compiler. The repo was tested on CUDA 12, which still accepted that. This would break on x86 with CUDA 13 just the same.

We tested the flag combinations directly with nvcc before touching Brian2:

Flags Result
-std=c++11 -std=c++17 works: the last one wins
nvcc C++17, host compiler C++11 fails
nvcc C++17, host compiler C++17 works
C++11 plus CCCL_IGNORE_DEPRECATED_CPP_DIALECT fails: CCCL really uses C++17 features

The first fix attempt was wrong, and it's instructive. Brian2 reads a brian_preferences file from the working directory, so we put both flags there. That broke every Brian2 run, CPU included, because Brian2 validates the file when it's imported, before Brian2CUDA has registered its preferences.

The fix that worked is a tiny wrapper. It imports Brian2CUDA first, rewrites the flags, and then runs the unmodified entry point:

import brian2cuda  # registers the cuda_standalone prefs
from brian2 import prefs

def with_cxx17(flags):
    return [f for f in flags if not f.startswith('-std=')] + ['-std=c++17']

prefs.codegen.cpp.extra_compile_args_gcc = with_cxx17(
    prefs.codegen.cpp.extra_compile_args_gcc)
prefs.devices.cuda_standalone.cuda_backend.extra_compile_args_nvcc = with_cxx17(
    prefs.devices.cuda_standalone.cuda_backend.extra_compile_args_nvcc)

After that, Brian2CUDA built for sm_121 in 16 seconds and ran.

Finding 4: the CPU beat both GPU backends

Here are all three backends on the same 1-second sugar experiment:

Backend Build Simulation Speed vs real time Spikes
Brian2, C++ on the Grace CPU 1.6 s 1.8 s 0.55× 17,403
Brian2CUDA 16.3 s 7.9 s 0.13× 16,817
PyTorch none 12.0 s 0.08× 17,295

With a single trial, the 20-core Grace CPU is four to seven times faster than the GPU. Each 0.1 ms step does only a little sparse work, so the GPU spends its time launching kernels and waiting rather than computing. The GPU paths should win when you batch many trials at once. I didn't measure that, so I won't claim it.

Are the GPU versions even correct? Brian2 on the CPU is the paper's reference, so I compared both against it. I also compared the reference against a second run of itself, because the stimulus is random and no two runs are identical:

Comparison Firing-rate correlation Same neurons active (Jaccard)
Brian2CUDA vs Brian2 CPU 0.9957 0.877
PyTorch vs Brian2 CPU 0.9950 0.888
Brian2 CPU vs Brian2 CPU 0.9945 0.902

The repo's comparison script labels both GPU backends "CLOSE" rather than "MATCH." But the reference itself only reaches 0.90 against a second run. The neurons that flip in and out between runs fire just one to three spikes each. That's noise, not error, and you'd never know it without running the reference twice.

Finding 5: giving it a body

For the body I used NeuroMechFly v2 (the flygym package) on MuJoCo. The alternative, flybody, has a good body model, but its trained walking controllers need TensorFlow 2.8 and dm-reverb, and neither publishes ARM64 wheels.

flygym had its own surprise. Version 2 moved the walking controllers out of the PyPI package into a separate flygym_demo package that only exists in the source repo, so install from a clone of the matching tag. Rendering over SSH works fine through EGL (MUJOCO_GL=egl), with no display required.

The real question was how the brain talks to the body. The brain doesn't control the legs directly. The ventral nerve cord coordinates them, and that isn't in this connectome. Instead, the brain sends commands down through a small population of descending neurons. So I checked which descending neurons the walking circuit actually recruits:

Descending neurons No stimulus Both P9s Left P9 Right P9
oDN1 (forward), left / right silent 15 / 19 Hz 15 Hz / silent silent / 8 Hz
DNa02 (steering), left / right silent 15 / 12 Hz 25 Hz / silent silent / 6 Hz

Those aren't the neurons I stimulated. That's signal travelling through the wiring, and it's strongly one-sided: stimulate the left P9 and only the left-side descending neurons respond.

The glue is a small decoder. It turns those firing rates into the two numbers flygym's walking controller takes, a left drive and a right drive:

forward = (oDN1 left + oDN1 right) / 20 Hz        clipped to 0..1
steer   = (DNa left - DNa right) / 20 Hz          clipped to -1..1
drive   = forward x [1 - 0.7*steer, 1 + 0.7*steer]

A silent brain means a still fly, and more left-side steering activity turns it left. Brain and body run in lockstep at the same 0.1 ms step, and every 10 ms of simulated time the brain's output updates the body's drive.

Stimulus What the brain did What the fly did
none silent stood still
both P9s forward and steering neurons on both sides walked 21 mm, veering left
left P9 left steering neurons at 24 Hz, right at 1 Hz walked a full circle to the left
right P9 weaker, right side only turned right

Thorax path, descending-neuron rates, drive and heading over the five-second run Five seconds of fly: the path by stimulus phase, then the descending-neuron rates, the decoded drive and the heading.

My favorite detail is the veer. Under a symmetric stimulus the fly still drifts left, because its left steering neurons switch on before the right ones. Nobody programmed that; it comes from the wiring.

It isn't fast: 5 seconds of fly time takes about 64 seconds to compute, two thirds of it in the brain. But it runs, it's deterministic with a fixed seed, and it renders a video with both camera angles and the brain's readout on every frame.

One last gotcha, because it cost me a few minutes: the videos wouldn't open in the Spark's own video player. Ubuntu's default player decodes through GStreamer, and a stock install has no H.264 decoder. The scripts now write a VP9 .webm alongside the .mp4, and that plays out of the box.

Is it really a fly's brain?

It's a model, but one built on a real fly's wiring.

The wiring is real: who connects to whom, and how strongly, reconstructed synapse by synapse from one individual fly. The neurons are not. Every one of the 138,639 is the same simple integrate-and-fire unit with the same handful of parameters. There are no neuromodulators, no electrical synapses, no learning and no internal state. With no input, not a single neuron fires, and a living brain is never silent.

It's still remarkably useful for one question: if I activate these neurons, which others light up? The paper reports that its predictions matched real experiments about 91% of the time. The body is a further step removed. The walking rhythm comes from flygym's controller, and the brain contributes two numbers through a decoder I chose.

Real wiring, simplified neurons, hand-written glue. It's closer to a very detailed circuit simulation than to a fly.

What I'd tell someone who wants to try this

It fits comfortably on a Spark, even with a big LLM server resident. The PyTorch brain needs about 200 MB on the GPU. The heaviest moments are about 5 GB of RAM: building the weight cache the first time, and running brain and body together.

If you're on CUDA 13 or ARM64, these are the traps:

  • Pin numpy==1.26.4 for Brian2 2.8.
  • Force C++17 for Brian2CUDA, with a wrapper rather than a preferences file.
  • Use PyTorch's cu130 index for aarch64 CUDA wheels, and a uv-managed Python if your system lacks headers.
  • Install flygym from source if you want its walking controllers.
  • Write WebM if you want to watch the output on a stock Ubuntu desktop.

And run the reference twice before you judge a backend against it. The most useful number I got all afternoon was the ground truth disagreeing with itself.


All figures came from commands run on my Spark. The full runbook, with every command that worked, every one that didn't, and the evidence for each claim, lives next to the code.

Found this article helpful?

Explore more tutorials and guides on API development, AI, and software architecture.

Browse All ArticlesGet Expert Help