对应实验:lessons/10_codegen_performance.py
输出报告:outputs/10_codegen_performance.txt
Brian2 的代码看起来很高级:
dv/dt = (1.1-v)/(10*ms)
计算机最终必须执行具体循环和数组运算。
Brian2 的重要能力是:
你负责描述模型,Brian2 根据目标后端生成执行代码。
本章要区分:
- NumPy、Cython、C++ standalone 的执行方式。
- 墙钟时间与 Brian2 profiling 的区别。
- 构建开销与仿真开销的区别。
- 什么时候应该优化。
当然可以手写:
for step in range(num_steps):
v += ...
spiking = v > 1
...但你需要自己负责:
- 单位一致性。
- 阈值与 reset 顺序。
- 不应期。
- 突触延迟队列。
- 多时钟调度。
- 记录器。
- 随机数。
- 性能后端切换。
Brian2 把这些共性机制封装起来,同时保留方程可读性。
代码生成的意义不是“让你不懂底层”,而是把模型语义与执行实现分离。
prefs.codegen.target = "numpy"Brian2 生成使用 NumPy 数组的运行时代码。
优点:
- 无需 C/C++ 编译器。
- 第一次运行启动快。
- 报错和调试较直接。
- 适合小模型和开发阶段。
局限:
- 某些逐事件或复杂循环无法充分向量化。
- 大模型或长仿真可能比编译代码慢。
NumPy 目标不是“低级模式”,而是很有价值的原型模式。
Cython 目标会生成并编译扩展代码,在当前 Python 进程中调用。
优点:
- 许多循环和事件计算更快。
- 运行后仍能方便地回到 Python 分析。
代价:
- 需要兼容编译器和构建环境。
- 首次运行有编译时间。
- Windows 工具链配置可能较复杂。
若仿真本身只需 0.1 秒,花数秒编译不一定划算。若同一代码反复运行很久,编译开销可以摊薄。
set_device("cpp_standalone")它不仅改变一个局部计算目标,而是改变整个实验执行方式:
- Python 构建 Brian2 网络。
- Brian2 生成独立 C++ 工程。
- 编译工程。
- 运行可执行程序。
- 读取结果。
适合:
- 网络结构和实验流程在运行前已确定。
- 仿真规模大或时长长。
- 希望减少 Python 运行时参与。
- 需要 OpenMP 等进一步加速。
不适合频繁在每个小步骤后回到 Python 动态决定下一步的实验。
代码:
neurons = NeuronGroup(
1000,
"dv/dt = (1.1-v)/(10*ms) : 1",
...
)目的不是建立生物网络,而是制造一份可测量的工作量。
所有神经元:
- 方程相同。
- 初始状态相同。
- 没有随机输入。
- 没有突触。
因此它们会完全同步。
100 ms 内单神经元约放 4 次,1000 个神经元约产生:
4 * 1000 = 4000 个脉冲
输出 Spikes=4000 是很好的正确性检查。
代码:
started = time.perf_counter()
network.run(100 * ms, profile=True)
elapsed = time.perf_counter() - startedperf_counter() 测量从调用前到返回后的真实经过时间。
它可能包括:
before_run检查。- 方程准备。
- 代码生成。
- 后端初始化。
- 实际时间步推进。
- Monitor 写入。
- Python 调用开销。
因此它代表用户等待时间,但不等于纯状态更新时间。
profiling_summary(network, show=5)Brian2 内部统计具体 CodeObject 的运行时间,例如:
neurongroup_stateupdaterneurongroup_spike_thresholderneurongroup_spike_resetterspikemonitor
这些时间之和可能小于墙钟时间,因为墙钟还包含:
- 准备工作。
- Python 层开销。
- 未单独归类的工作。
两种测量回答不同问题:
- 墙钟时间:用户总共等了多久?
- profiling:仿真内部哪里最耗时?
可靠基准测试需要考虑:
第一次运行可能需要生成或编译代码。
后续运行可能复用已生成代码。
后台进程、CPU 频率和磁盘活动会造成波动。
小模型的固定开销占比高,大模型的计算开销占比高。
Monitor 可能成为主要瓶颈。
应多次运行,报告中位数或分布,而不是只取一次最快结果。
- 先验证两个后端产生一致的关键结果。
- 固定模型、dt、仿真时长和记录内容。
- 分开记录构建时间和运行时间。
- 预热后重复多次。
- 报告硬件、软件版本和编译器。
- 同时记录内存,而不只看速度。
- 在真实目标规模上比较。
如果一个优化让结果变化,它首先是模型变更或数值变更,不应直接称为“加速”。
compiler = shutil.which("cl") or shutil.which("g++")这只是快速检查当前终端 PATH 中是否能找到常见编译器。
它不能完整证明:
- 编译器版本与 Python 兼容。
- Cython 构建链完整。
- Windows SDK 已安装。
- 编译器能处理当前路径。
因此“找到编译器”是必要线索,不是完整健康检查。
编译工具链会处理:
- 源文件路径。
- 临时目录。
- 命令行参数。
- 生成的构建文件。
某些旧工具或配置对:
- 非 ASCII 字符。
- 空格。
- 云同步目录。
- 路径过长。
处理不佳。
这不是 Brian2 模型问题。排查 standalone 编译时,可以先在短、纯 ASCII 的本地路径建立最小项目,区分模型错误与工具链错误。
示例:
NumPy target wall time: 0.428 s
Spikes: 4000
C++ compiler: not found
正确解读:
- 当前机器上这一次 NumPy 运行总耗时约 0.428 秒。
- 脉冲数与理论预期一致。
- 当前终端未找到常见 C++ 编译器。
错误解读:
- NumPy 永远需要 0.428 秒。
- 任何机器都应产生完全相同的耗时。
- 没找到编译器就无法使用 Brian2。
推荐顺序:
用小规模、短时间、NumPy 目标。
判断是:
- 状态更新。
- 突触事件。
- 阈值。
- Monitor。
- 只记录必要变量。
- 降低 Monitor 采样率。
- 使用 event-driven 方程。
- 避免不必要的全连接。
比较 Cython 或 standalone。
没有 profiling 的“优化”很容易只是在改变不重要的部分。
最常见的错误加速方法是直接增大 dt。
这确实减少时间步:
步骤数 = 仿真时长 / dt
但可能改变:
- 积分误差。
- 阈值时刻。
- 随机过程统计。
- 网络同步。
增大 dt 是数值模型变更,必须通过收敛测试,而不是单纯性能参数。
取决于模型规模、结构、运行时长和编译开销。
内部报告与完整调用计时范围不同。
还应比较脉冲时刻、状态轨迹、权重和统计量。
还要考虑构建时间、内存、可复现性和开发成本。
不同编译器、浮点优化和算法实现可能产生微小差异,应验证关键结论。
测试 100、1000、10000 个神经元,记录墙钟时间。观察是否线性增长。
固定构建模型,比较 10、100、1000 ms。判断固定开销占比。
比较性能报告和墙钟时间,量化记录成本。
同一配置运行 10 次,报告中位数、最小值和最大值。
先验证结果,再比较 NumPy 与 Cython。把首次编译时间与后续运行时间分开报告。
Brian2 执行链:
方程与事件
-> 解析和单位检查
-> 代码生成
-> NumPy/Cython/C++ 执行
-> Monitor 记录
性能分析链:
正确性
-> profiling
-> 减少无效工作
-> 后端比较
-> 规模化优化
最后一章综合项目会把神经元、随机输入、兴奋性突触、抑制性突触和群体记录组合成一个小型 E/I 网络。