dotFly——用C#模拟的果蝇大脑 | Konrad “Dev Nerd” Kokosa
Google 和 Janelia 发布 MaleCNS 果蝇连接组,166,700 个神经元在笔记本上用 C# 实时模拟,但演示并非真正的大脑。
中文
复制

你上面看到的是:一只果蝇在客厅里飞来飞去,试图跟随一团气味,落在糖罐上,伸出喙进食。往哪边转才能避开障碍物、气味是在变浓还是变淡、风从哪边吹来、落脚的这东西尝起来是不是糖——这些都由一只真实果蝇的完整中枢神经系统控制:166,700 个神经元、2490 万个突触,连接方式完全按照电子显微镜观测到的结果,在一台笔记本上用纯 C# 实时模拟。嗯……差不多是这样。而这个「差不多」,正是本文要讲的。
这一堆热闹到底是怎么回事?
2026 年 9 月 3 日,Google Research 和 HHMI Janelia 发布了 MaleCNS 连接组——一只成年雄性黑腹果蝇的脑以及「腹神经索」(负责在脑与身体之间传递信号)的完整接线图。几天之内,互联网就干了互联网该干的事:果蝇脑开始玩 Doom、玩 Super Mario 64、跳过仙人掌、在浏览器里走路、坐在 Mac 桌面上。我也上钩了。就像当初对 dotLLM 那样,我想搞清楚这里面到底发生了什么——而对我来说,面对 AI,真正理解一件事的最好办法就是亲手把它做出来。于是我做了 dotFly:一个原生 .NET 的果蝇连接组「推理引擎」,快到足以塞进 Godot 的游戏循环里。没有 Python,没有原生库封装,纯 CPU。本文会讲:连接组究竟是什么、它是怎么做出来的;大家跑的那个「果蝇脑模型」到底是什么(更重要的是,它不是什么);那些演示在做什么;dotFly 如何用 C# 实现,附一些基准测试;最后诚实地谈谈整件事的局限。
等等,这合乎伦理吗?
在进入细节之前,有个问题会立刻冒出来:如果这真的是在模拟一个真实的大脑,我们到底该不该做这件事?模拟果蝇在 Mario 里撞墙时,它会痛苦吗?r/AskScienceDiscussion 和 Hacker News 上都有人这么问,其中一条评论写道:「这个程序的用处,值不值得拿 10% 的概率去赌我们自信地造出了可能是最残忍的机器?」 我们是不是创造了一个「真实生命」,然后在虚拟环境里折磨它?!这些问题听起来再科幻,我觉得都问得合理,我不想一笔带过。但诚实的回答需要先理解被模拟的到底是什么,简短版本是:它在某种意义上的确在模拟真实的大脑——接线是从真实动物身上一个神经元、一个突触测出来的——同时它又离一个活的大脑非常远。它省略了大脑的大部分化学机制和神经元通信的细节,也忽略了每个神经元内部发生的事。没有激素,没有内部状态,没有学习。只有一种「全部 2500 万个突触共用一个权重」式的简化。那些演示之所以能「玩游戏」,靠的是手写的适配器——换任何一个非线性动力系统上去,看起来都会像在玩游戏。这个模型里没有任何东西能证明被扫描的那个个体、它的记忆,或任何类似体验的东西被保留了下来。所以,我不认为我们造出了「可能的最残忍的实验」。嗯,暂时不认为。但我也觉得这种不适感是个有用的提醒:要精确说清这些演示到底是什么——本文剩下的部分就想做到这一点。
从基础讲起:什么是连接组?
三个术语就够理解后面的一切:
- 连接组——重建出来的「接线」图:哪个神经元连到哪个,有多少个突触接触,用什么神经递质
- 神经模型——让这套接线动起来的方程(连接组本身不计算任何东西,它只是一张图)
- 具身——模型与身体、游戏或世界之间工程化的「接口」
互联网下载了那张接线图(connectome),又给它加上了动力学和接口。它下载的并不是一只活苍蝇的冻结可执行副本。
它是怎么做出来的
把整个故事压缩成“Google 在九月绘制了一张苍蝇大脑地图”很容易。实际情况是一连串跨越多年研究的成果:
- 2018——Zheng 等人发表了成年雌性苍蝇大脑的第一份完整电子显微镜体积(FAFB 数据集)。只是图像——一个约 100 TB 的切片堆栈
- 2024——FlyWire 联盟发表了该大脑的完整重建:约 140,000 个神经元,50 M 个突触(Dorkenwald 等,Nature)
- 2024——Shiu 等人(Nature)发表了该完整大脑的第一个计算模型,并用真实实验进行了验证。这就是现在所有人都在运行的神经模型
- 2025-2026——MaleCNS:一只五天大的成年雄性(标本
Z0720-07m)连接组,大脑 + 颈部 + 腹神经索一起解剖。用醛类固定,用重金属染色,包埋在树脂中,用热刀切成 66 块约 20 µm 厚的切片,并用增强型聚焦离子束 SEM 以 8×8×8 nm 分辨率成像:镓离子束铣削掉薄薄一层,SEM 对新切面成像,重复。七台定制仪器,约 160 万亿体素,约 13 个月的成像。然后 Google 的 flood-filling networks 对神经元进行了分割,自动检测器找到了突触,一个 ResNet50 根据 EM 邻域对神经递质进行了分类——29 名专家校对员花了约 44 人年修复并注释这一切。结果:166,700 个神经元,11,710 种细胞类型,25.6 M 条有向边,124 M 个突触接触。2026 年 9 月 3 日发表在 Cell 上(Berg、Beckett、Costa、Schlegel 等),v1.0 数据自六月起可用。
它不是什么
连接组是一张死亡、切片、金属染色大脑的“照片”。它不是活体大脑的 fMRI 或电极记录。它不测量:
- 膜电压或任何活动
- 突触的功能强度(只测量有多少接触)
- 激素影响
- 当时学到、记住或感受到的任何东西(好吧,苍蝇已经死了……) 它是一张接线图——一张非常非常好的接线图——每个神经元带有一个预测的神经递质标签。
“输入”和“输出”是什么?
如果图只是 166,700 个带 ID 的神经元,我们怎么知道眼睛在哪里?这就是一个世纪的果蝇遗传学和神经解剖学发挥作用的地方。模型中的每个神经元都带有一个注释——细胞类型(LC4、T4a、MN9、DNp04……)、类别、侧别——由校对员根据其形态、位置和连接性分配,并与文献匹配。而文献非常庞大,例如我们已经知道:
LC4神经元已知(来自钙成像、光遗传学、行为)会对逼近物体(越来越近/越来越大的物体)作出反应,T4a-d对四个方向的运动作出反应,ORN_DM1对特定气味/味道作出反应,- 所谓的糖味觉受体神经元(
GRNs)对糖作出反应, MN9是伸出喙的运动神经元,DNp04和 Giant Fiber 是下行到神经索的逃逸下行神经元(“输出”)。 所以,感觉神经元是输入,例如:光感受器、嗅觉和味觉感知神经元、苍蝇“触角”的运动和触觉感知神经元。而所谓的下行神经元和运动神经元是输出:约 1,300 个下行神经元将大脑的命令传递到神经索,运动神经元驱动肌肉。这不是模型的假设——这是解剖结构。让我们跟随一个信号走一遍,苍蝇最简单的反射——有东西朝我来了,快逃:
graph LR
A["a dark spot grows
in the left eye"] --> B["photoreceptors
R1–R6"]
B --> C["lamina, medulla
<small>the optic lobe</small>"]
C --> D["LC4 (left)
<small>71 neurons:
'something is looming'</small>"]
D --> E["DNp04, DNp02, DNp11,
Giant Fiber
<small>descending neurons</small>"]
E --> F["ventral nerve cord
→ wing & leg motor neurons"]
F --> G["take off,
turn away"]
现在把同样的事情做成实验。我取来 MaleCNS 图,以 120 Hz 随机脉冲驱动 71 个左侧 LC4 神经元一秒——这就是“一个物体在左眼中膨胀”在大脑看来是什么样子——并记录谁放电。一个 dotfly run --stimulate type:LC4@L --hz 120 之后:
| 神经元 | 类型 | 是什么 | 放电率 |
|---|---|---|---|
| 531898 | DNp04(左侧) | 逃逸下行神经元 | 308 Hz |
| 10010 | DNp01 | Giant Fiber——果蝇的“跳跃”指令 | 276 Hz |
| 10197 | DNp02 | 逃逸下行神经元 | 256 Hz |
| 10259 | DNp11 | 逃逸下行神经元 | 244 Hz |
| 10752 | DNp03 | 逃逸下行神经元 | 220 Hz |
共有 4,792 个神经元被激活(总数 166,700 个),而排在榜首的正是文献所描述的逃逸通路——并且位于受刺激眼睛的同侧。没有人告诉模型 LC4 与 DNp04 相连。这种“连接”本来就存在:激活输入,输出就会亮起。整个玩法就是这样:激活输入,观察输出。
大家都在用的模型:Shiu et al. 2024
这张图本身什么都不会做。Shiu 等人用最简陋但能跑通的办法让它动了起来:每个神经元都是一个漏积分放电(LIF)点神经元。忘掉神经元的形状、树突和离子通道。剩下的只是一个数字——它的“膜电位”。传入的脉冲把它推高或压低,它向静息值泄漏,一旦越过阈值,神经元就发出自己的脉冲并重置。就这么简单:
注意要让它放电需要多大的输入:来自一个拥有 90 个突触接触的神经元的三个脉冲,且必须在几毫秒之内——单个输入神经元的一个脉冲本身毫无作用(那大约需要 162 个接触)。放电是汇聚的结果。脉冲能推动多大力,是连接组进入方程的唯一入口。从神经元 到神经元 的每条边都有一个权重:
是两个神经元之间的突触接触数量——显微镜数出来的。 对全部 2500 万个突触都是同一个值:每个接触 0.275 mV。这是整个模型唯一经过校准的参数。Shiu 等人对它进行了拟合,使得刺激感糖神经元能产生强度符合实际的喙伸展反应,然后就到此为止。其余的一切——时间常数、阈值、不应期、突触延迟——都来自此前的电生理学研究。
这个符号对像我这样不是生物学家的人需要解释一下。神经元之间靠释放一种化学物质——神经递质——在突触处传递信号,而每个神经元(大多)只用一种。有些递质会兴奋接收神经元,把它的电位推向阈值;另一些则抑制它,把电位压下去。连接组同样没有测出每个神经元用的是哪种化学物质——而是由一个分类器根据突触在电镜图像中的样子预测出来的。模型随后把整套化学机制压缩成一个比特:在果蝇中,乙酰胆碱是主要的兴奋性递质,所以
+1。GABA 和谷氨酸是抑制性的,所以-1。另外三种——多巴胺、章鱼胺、血清素——实际上是调质:它们改变其他突触的行为方式,承载奖赏、唤醒这类信号,而 LIF 模型根本无法表达这一点。Shiu 等人干脆把它们当作兴奋性处理,+1。这是一种建模约定,不是生理事实:给一个多巴胺神经元赋上+1并不会实现奖赏或学习。
研究者用这个模型做预测——“如果我们激活这些神经元,那些应该会放电”——并用光遗传学和钙成像在真实果蝇身上检验了其中 164 条。论文说 91% 一致,意思就是字面意思:这 164 条预测中有 91% 在摄食和理毛回路中成立。不是整个果蝇行为的 91%。
连接组是固定的——我们不训练它
这是关键所在。权重是固定的,来自每对神经元之间的连接数。没有人训练这个网络玩 Mario,也不该有人这么做:一旦你开始拿 25M 个权重去拟合游戏得分,它就不再是“果蝇的大脑”,而只是一个恰好从果蝇形状的图起步的神经网络。原始模型里根本没有任何可塑性规则。所以整个“游戏”依然是:激活输入,观察输出。用 150 Hz 的泊松脉冲驱动糖神经元,看另外 127,000 个神经元里哪些开始放电。让某一个神经元沉默,看会发生什么变化。这也正是它的价值所在——一个可编程、可检验的神经回路模型,在这里,一个假设就是一次运行。
那这些演示到底在做什么?
每一个“果蝇大脑玩 X”的演示,包括我做的那个,都是三层东西叠在一起:
graph LR
W["World / game"] -->|"encoder
<small>engineered</small>"| I["Input neurons
<small>measured identity</small>"]
I --> B["Connectome + LIF dynamics
<small>measured wiring, one calibrated weight</small>"]
B --> O["Readout neurons
<small>measured identity</small>"]
O -->|"decoder
<small>engineered (fixed or trained)</small>"| W
线路和动力学是共用的。真正让各个 demo 拉开差距的是适配器,大部分表面智能也正来自这里:把像素或其他状态转成放电率的编码器(比如视觉神经元——具体是哪些神经元,多少 Hz 对应什么?),以及把放电率(来自下行神经元,即大脑的“运动指令”细胞)转成按键或身体动作的解码器。看几个例子:Xenova 的果蝇模拟——完整的 166,700 神经元 MaleCNS LIF 在浏览器里跑 WebGPU,驱动一个 3D 果蝇身体。你可以在神经元上涂抹刺激,或者用预设:“walk”刺激两侧的 LC9,“turn left”刺激左侧的 LC9 加左侧的 DNa02,“fly”刺激两侧的 LC4。LC4 和 LC9 是视觉投射神经元——每种 LC 类型都是一个检测器,向中央脑报告某一类视觉事件(LC4:“有东西正朝我扩张过来”);DNa02 是一个下行神经元,实验显示它驱动转向。输出侧,这个 demo 读取少数几个下行神经元:DNp09/DNg100/DNg97 对应行走速度,DNa02/DNa11/DNg13 对应左转还是右转,MDN(“moonwalker”倒退神经元)对应跳跃,DNp01(Giant Fiber)对应逃逸飞行——每个都配了一个手工选定的增益(8 mm/s、3.8 rad/s……),并在它的 model.json 里有记录,这点我很欣赏。有一点要注意:“turn”预设直接刺激的是输出神经元 DNa02,而不只是视觉输入。作者解释了原因——单靠视觉输入激活 DNa02 太弱——但这意味着即使你把大脑内部的所有连接都切断,果蝇照样会转向。尽管如此,这仍是那个试图在不套厚重适配器的情况下展示真实线路活动的工具。Fly Dino——完全不同的思路。不是整个大脑,而是一个 80 神经元的子回路:32 个视觉检测神经元(LC4、LC11、LPLC2……同一批“有东西来了”家族)和 16 个下行神经元,挑选依据是在连接组里它们构成了从“视觉事件”到“运动指令”连接最强的路径,再加上中间的细胞。以分级速率模型(tanh)运行,而不是 Shiu 的脉冲 LIF。八个游戏特征(障碍物距离、大小、速度……不是像素)被注入视觉神经元。然后是解码器适配器:16 个输出神经元的活动进入一个极小的神经网络——总共 243 个数——由它选出 Run、Jump 或 Duck。这个小网络是在 15,680 局游戏中靠试错训练出来的;果蝇回路本身从未改变。所以这里发生的事是,我们训练了一个输出适配器去解读部分线路活动,并把它们翻译成游戏动作。这就是储备池计算:大脑被当作一个固定的、复杂的变换来用,只有最后那层“选哪个动作”的映射是从训练中拟合出来的。好的一点是对照实验:关掉果蝇回路但保留训练好的适配器,得分从 2,886 掉到 41。所以回路确实在做事情——它充当输入的固定非线性变换,那个小网络学会了去读它。“Fly Mario”式 demo(Fly64、fly-games)——完整的 MaleCNS LIF 以粗粒度时间片(20 ms)步进,游戏帧被转成视觉检测器的特征(LC4:威胁,LPLC2:有东西在靠近,LC10a:有东西可追),输出侧则是神经元到按键的映射:十几个下行神经元组——DNa02 左/右用于转向,DNp09 用于向前走,等等——每组映射到一个按键,带一个阈值。在 Fly64 里这个映射是手工做的(“如果左侧 DNa02 放电多于右侧,就按 Left”);在 fly-games 里它是用一个小线性回归从录制的示例游玩中拟合出来的。又是储备池计算。在它从未见过的录制片段上,它能以 90% 的准确率预测正确的按键——而在实时游玩中果蝇仍然很快就死,因为实时情况会偏离录制数据。Mario 会撞到墙上然后停在那儿。这些都不是批评。这些都是诚实的实验。要点在于——适配器极其重要,一个会动的身体并不能证明线路贡献了什么,除非你也跑对照组——切断大脑内部的连接,和/或切断输入。
这就引出了 dotFly
dotFly 首先是一个库:一个面向 fly-connectome 脉冲模型的 .NET 推理引擎,Godot、控制台应用、测试和基准测试都以进程内方式链接它。它从 MaleCNS 和 FlyWire 的发布版本构建内存映射检查点,在其上运行 Shiu 等人的模型——既精确(float64 后端与 Brian2 逐脉冲一致),又快速(float32 SIMD 后端在笔记本上实时运行整个 MaleCNS)——并对外暴露:输入刺激,输出脉冲和发放率,记录与回放,以及因果开关。房间演示只是一个演示:我构建它是为了看看一个薄具身,在保留果蝇自身感官的情况下,是否还需要大量“适配器”。
房间演示
一个 Godot 4(.NET)3D 场景:阳光明媚的客厅、一只果蝇、桌上的糖,以及从少数几个地方顺风飘来的“气味云”。MaleCNS 连接组在 dotFly 上运行,是她的感官与行动之间的唯一环节。以下是每个面板的含义:

感官 → 输入(编码器,固定)——两个摄像头,每个复眼一个,渲染 32×32 亮度图像。对每只眼,暗部比例及其增长成为 LC4(逼近)速率。水平和垂直图像位移成为 T4a-d(光流)速率。两个触角处的气味浓度成为 ORN_DM1 速率,风向成为 JO-C/E(Johnston 氏器)速率,着陆时的糖接触驱动果蝇足部和口部的味觉感知神经元。总共 7,524 个输入神经元。连接组(实测)——全部 166,700 个神经元,绘制在其真实位置,并由其真实脉冲点亮。这里没有任何东西来自脚本。读出(实测回路)——每个读取的回路都是我用 CLI 在检查点中找到的(dotfly inspect --upstream-of、dotfly run --stimulate),而非假设——13 组共 38 个神经元:
LC4 → DNp04 L/R, Giant Fiber——逃逸T4a → HS、T4b → H2、T4c → VS——光流ORN_DM1 → DM1 projection neurons——气味量JO-C/E → AMMC012——风向GRN → MN9——进食
解码器(固定,手写)——打印在屏幕上,紧挨着它所消费的读出,其中的每个数字都是一种选择:
| 脑信号(读出) | 规则 | 行为 |
|---|---|---|
DNp04 左 vs 右 | yaw += 1.2 · (DNp04_L − DNp04_R) | 转向远离逼近的一侧 |
HS + H2 左 vs 右 | yaw += 0.2 · ((HS_R + H2_L) − (HS_L + H2_R)) | 反向转动以对抗旋转(视动反应) |
HS 左 vs 右 | yaw −= 0.3 · (HS_R − HS_L) | 在墙壁之间居中 |
DNp01(巨纤维) | speed = 120 + DNp01 体长/秒 | 巡航,逃逸时爆发(真实果蝇:100–170 BL/s) |
DM1_lPN(气味量) | > 110 Hz → 上升时冲刺;下降时翻滚;丢失时掉头,然后扫掠;> 270 Hz → 下降,着陆 | 气味搜索 |
AMMC012 左 vs 右(风) | 冲刺时:yaw −= 0.6 · (wind_L − wind_R) | 逆风飞行 |
MN9(平滑 0.3 秒) | > 15 Hz → 进食;停止 → 起飞 | 进食 |
有趣的发现是,气味方向根本无法从任何读数中推导出来(无论刺激哪根触角,两个 DM1 候选神经元的反应都一样),所以我不得不在适配器中加入一些“逻辑”,让果蝇在气味变强时向前飞行来搜索,气味减弱时掉头,而风信号来自大脑。
dotFly 内部:C# 性能
dotFly 在约定和精神上都是 dotLLM 的兄弟:内存映射模型文件、非托管状态、SIMD 内核、热路径上零分配。相同的分层布局,每个项目都是一个 NuGet 包:
src/DotFly.Core .dfb checkpoint reader (mmap), graph tables, NeuronModel, RNG, IBackend
src/DotFly.Data MaleCNS / FlyWire release readers
src/DotFly.Cpu SIMD LIF kernel, compute thread pool
src/DotFly.Engine Simulation, input/output ports, real-time driver, recorder, adapters
src/DotFly.Adapters.* ONNX Runtime / ML.NET readout adapters
src/DotFly.Cli dotfly build | inspect | run | bench | explore
以及 API 演示所针对的程序:
// Neurons are addressed by their exact 64-bit IDs from the connectome release (FlyWire root IDs
// or MaleCNS body IDs)
ulong[] sugarGrnIds = [720575940624963786, 720575940630233916, /* 21 sugar GRNs from Shiu et al. */];
const ulong Mn9 = 720575940660219265; // MN9, the proboscis motor neuron (FlyWire v630 root ID)
using Brain brain = Brain.Open(".data/shiu2024/flywire-v630.dfb");
using Simulation sim = brain.CreateSimulation(new SimulationOptions { Seed = 1 });
NeuronSet sugar = brain.ByBodyIds("sugar_GRN_R", sugarGrnIds);
sim.Input(sugar, InputKind.PoissonToV).Fill(150f); // Poisson drive, 150 Hz each
OutputPort mn9 = sim.Output(brain.ByBodyId(Mn9), OutputKind.Rate(50.Ms()));
sim.Run(1.Seconds()); // neural time
Console.WriteLine($"MN9 {mn9.Snapshot.Mean():F1} Hz at {sim.Clock.RealTimeFactor:F1}× real time");
这就是 Shiu 等人的糖实验,用十行代码实现:驱动 21 个糖感应味觉神经元,读取喙运动神经元。完整示例(samples/DotFly.Sample.SugarExperiment)运行论文的 30 次试验及其沉默实验,并与已发表的结果进行比较:
flywire-v630: 127,400 neurons, 14,687,178 edges. Sugar GRNs: 21, MN9 index 127193.
30 trials × 1 s in 3.0 s wall (10.1 simulated s per wall s).
Active neurons (mean over trials): 433. Top 10:
720575940621502051 152.3 Hz (sugar GRN)
720575940632425919 150.7 Hz (sugar GRN)
...
MN9 (720575940660219265): 83.3 Hz mean over 30 trials.
Silencing experiments (outgoing synapses zeroed; the neuron still spikes):
silence 720575940621502051: MN9 83.3 Hz (intact 83.3 Hz)
silence 720575940632425919: MN9 78.7 Hz (intact 83.3 Hz)
silence 720575940633143833: MN9 79.6 Hz (intact 83.3 Hz)
Comparison with published results/example/sugarR.parquet (Brian2 2.5.1, 30 trials):
active neurons: 448 vs 433 (Jaccard 0.949); rate correlation 0.9946; spike-count ratio 0.803
MN9: 93.3 Hz vs 83.3 Hz
一个 127,400 个神经元的大脑进行三十次一秒试验只需三秒,而论文得到的结果是:糖输入,喙运动神经元以约 80 Hz 输出。对于群体,你通常通过注释来查询——brain.Query(type: "LC4", side: Side.Left)、brain.Query(cls: "Kenyon_Cell")——而原始 ID 用于复现论文或读取单个命名细胞。对于游戏循环,new RealtimeDriver(sim).Start() 在自己的线程上运行网络,游戏线程无锁地写入 InputPorts 并读取 OutputPort.Snapshot 帧。
一些起到关键作用的设计决策:
- 检查点即内存映射文件(
.dfb,dotFly 的 GGUF)——以压缩稀疏行(CSR)形式存储的图——rowPtr[N+1]、col[E]、w[E](int16接触计数,带符号)——外加标注、位置信息,以及一个记录数据版本、过滤器和符号策略的文件头。从 1 GB 的 Arrow 发布版构建它约需 10 秒,打开只需几毫秒 - 非托管、64 字节对齐内存中的数组结构(SoA)状态——
v[N]、g[N]位于float32,一个不应期倒计时,一个由 19 个 spike 列表组成的环。整个 MaleCNS 约 2 MB——能装进 L2 - LIF 内核是
Vector256——spike 索引用TrailingZeroCount压缩。内核在TFloat : IBinaryFloatingPointIeee754上泛型化,因此double(与 Brian2 匹配到 1e-11 mV 的参考实现)和float(快速路径)共用同一份实现 - 每个逐步方法上都加
[MethodImpl(MethodImplOptions.AggressiveOptimization)]——这是最大的意外。没有它,分层 JIT 会让内核和批处理循环在一次短运行的大部分时间里停留在 tier-0/OSR:每步 21 µs,而不是 6。
基准测试
端到端,在我这台 8 核笔记本(Ryzen 7 5800HS,AVX2)上运行 dotfly bench:
| 场景 | 神经元数 | 实时倍率 | 步/秒 |
|---|---|---|---|
| FlyWire v630,21 个 sugar GRN @ 150 Hz(Shiu 实验) | 127,400 | 19.6× | 196,000 |
| MaleCNS v1.0,无输入 | 166,700 | 14.5× | 145,000 |
| MaleCNS v1.0,1,684 个 T4a 神经元 @ 150 Hz(0.5 M spikes/s,0.9 G 次投递/s) | 166,700 | 6.2× | 62,000 |
Python、JavaScript、C#
我忍不住想看看这和其他人跑的东西比起来如何。有三个参照物容易确定。Python 那个是 Brian2——已发表模型所运行的通用脉冲神经网络模拟器。JavaScript 和 WebGPU 那两个都是 Xenova 的浏览器演示:它的 CPU 核心,即 src/brain.js 中的 BrainCPU,是一百行干净、朴素的 JS,实现了同一个模型——精确的 LIF 积分,dt = 0.1 ms,而 BrainGPU 则把同样的东西作为 WGSL 计算着色器来运行。我在 Node 24 里跑了 JavaScript 核心,又在无头 Chrome 里跑了 Xenova 的浏览器演示,用的是同一个 MaleCNS 图(它自己的数据文件,25.6 M 条边),刺激与 dotfly bench 相同,在同一台笔记本上背靠背运行。各实现的 spike 计数在 RNG 范围内一致(T4a 场景在每个实现中都是 534-539 k),所以做的是同样的工作:
| 实现 | 场景 | 步/秒 | 实时倍率 |
|---|---|---|---|
| Shiu 等人的论文,Brian2(按论文报告,每 CPU 线程) | v630,sugar GRNs @ 150 Hz | ~33 | 0.003× |
| Brian2 2.9,numpy 后端,Python(这台笔记本) | v630,sugar GRNs @ 150 Hz | ~400 | 0.04× |
Xenova BrainCPU,Chrome worker 中的 JavaScript,1 线程 | MaleCNS,1,684 T4a @ 150 Hz | 628 | 0.06× |
Xenova BrainCPU,Node 24 中的 JavaScript,1 线程 | MaleCNS,1,684 T4a @ 150 Hz | 851 | 0.09× |
Xenova BrainGPU,集成显卡 Radeon(Vega 8)上的 WebGPU | MaleCNS,1,684 T4a @ 150 Hz | 2,409 | 0.24× |
Xenova BrainGPU,RTX 3050 Ti Laptop GPU 上的 WebGPU | MaleCNS,1,684 T4a @ 150 Hz | 5,383 | 0.54× |
| dotFly,C#,1 线程 | MaleCNS,1,684 T4a @ 150 Hz | 10,740 | 1.07× |
| dotFly,C#,8 线程 | MaleCNS,1,684 T4a @ 150 Hz | 43,656 | 4.37× |
| dotFly,C#,8 线程 | v630,sugar GRNs @ 150 Hz | 196,000 | 19.6× |
在相同线程数下,C# 比 JS 参考实现快约 11 倍——这并非因为 JavaScript 在这件事上慢(V8 处理 typed array 相当称职),而是因为 JIT 无法免费提供这些东西:Vector256 中每条指令处理八个神经元、状态布局能塞进 L2、scatter 不做边界检查,以及投递循环只访问 g 中自己那块常驻缓存的切片。
这张表没有包含的:以 dt = 20 ms 步进的 demo(Fly64、fly-games)。步数少了 200 倍,它们天然便宜,代价是不再是 Shiu 模型。
局限——以及接下来往哪走
现在说点实在的。模型本身有两个大局限,恰好体现了直接用 connectome 与补齐输入/输出解码器之间的取舍。
视觉边界划错了地方——今天 C# 计算的是“视叶”该算的东西:视网膜编码器把图像平移和扩张直接变成 T4 和 LC4 信号——跳过了前面那张 looming 图里的前四个方框。果蝇的眼睛由约 800 个微小视觉单元和一整个视叶(占大脑神经元相当大的比例!)组成,其职责正是从光感受器信号中计算运动和 looming。下一个里程碑是把光直接送进光感受器,看中间的连接是否自己就能算出运动和 looming——如果能,编码器就缩成“每只眼睛的亮度”,剩下的交给 connectome;如果不能,退路是混合方案,用一个 Flyvis 风格的训练过的视叶。
果蝇自身的导航回路是沉默的——这是最关键的一点。在演示中,往哪里走——逆风冲刺、翻滚、跨步、降落——由我的解码器决定。真正的果蝇是在自己的大脑里完成这个决策的,而那条回路就藏在连接组里:有些神经元追踪果蝇的航向,把它与风和气味结合起来,再驱动转向神经元 DNa02 向左或向右。把风和气味输入打开后,这些神经元一个都不放电。我们得到的果蝇能闻到气味、能感觉到风,但不会转向。原因在于这条回路是循环的——它反馈到自身。它的活动之所以能维持,只是因为活跃的神经元不断兴奋邻居、同时抑制其余神经元。目前的 Shiu 模型无法表示这一点,我们需要支持更复杂的“循环”神经模型。
结语
又有两件事同时成立。平台层面:.NET 能在一台笔记本上实时运行一个 166,700 个神经元、2500 万个突触的脉冲网络,在不同线程数下逐位一致,并与科学参考实现吻合——Vector256、NativeMemory、内存映射文件和手写的线程池就够了(🔥)。科学层面:连接组是一项了不起的测量,LIF 模型是一个了不起的起点,而今天让果蝇“玩游戏”的一切,都是适配器——它们应该显示在屏幕上,并在对照运行中关掉。
dotFly 采用 GPLv3——数据(MaleCNS、FlyWire)为 CC BY 4.0,需单独下载。如果你是 .NET 开发者,想用 C# 戳一戳真正的大脑,或者你是神经科学家,想要一个比参考实现快三个数量级的内循环,不妨试试。刺激点什么。让什么安静下来。然后告诉我哪些神经元放电。
- GitHub:github.com/kkokosa/dotFly
- 文档:kkokosa.github.io/dotFly——指南、房间演示、实测与工程化清单、发现、CLI 与 API 参考
- 参考文献:Berg et al. 2026 (MaleCNS)、Shiu et al. 2024、Dorkenwald et al. 2024 (FlyWire)、Lappalainen et al. 2024 (Flyvis)