Skip to content

Latest commit

 

History

History
417 lines (265 loc) · 9.21 KB

File metadata and controls

417 lines (265 loc) · 9.21 KB

第 10 章:Brian2 怎样把方程变成程序,以及性能数字怎样才可信

对应实验: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 的区别。
  • 构建开销与仿真开销的区别。
  • 什么时候应该优化。

1. 为什么不直接手写 NumPy 循环

当然可以手写:

for step in range(num_steps):
    v += ...
    spiking = v > 1
    ...

但你需要自己负责:

  • 单位一致性。
  • 阈值与 reset 顺序。
  • 不应期。
  • 突触延迟队列。
  • 多时钟调度。
  • 记录器。
  • 随机数。
  • 性能后端切换。

Brian2 把这些共性机制封装起来,同时保留方程可读性。

代码生成的意义不是“让你不懂底层”,而是把模型语义与执行实现分离。

2. NumPy 目标怎样工作

prefs.codegen.target = "numpy"

Brian2 生成使用 NumPy 数组的运行时代码。

优点:

  • 无需 C/C++ 编译器。
  • 第一次运行启动快。
  • 报错和调试较直接。
  • 适合小模型和开发阶段。

局限:

  • 某些逐事件或复杂循环无法充分向量化。
  • 大模型或长仿真可能比编译代码慢。

NumPy 目标不是“低级模式”,而是很有价值的原型模式。

3. Cython 目标大致怎样工作

Cython 目标会生成并编译扩展代码,在当前 Python 进程中调用。

优点:

  • 许多循环和事件计算更快。
  • 运行后仍能方便地回到 Python 分析。

代价:

  • 需要兼容编译器和构建环境。
  • 首次运行有编译时间。
  • Windows 工具链配置可能较复杂。

若仿真本身只需 0.1 秒,花数秒编译不一定划算。若同一代码反复运行很久,编译开销可以摊薄。

4. C++ standalone 为什么是另一种 device

set_device("cpp_standalone")

它不仅改变一个局部计算目标,而是改变整个实验执行方式:

  1. Python 构建 Brian2 网络。
  2. Brian2 生成独立 C++ 工程。
  3. 编译工程。
  4. 运行可执行程序。
  5. 读取结果。

适合:

  • 网络结构和实验流程在运行前已确定。
  • 仿真规模大或时长长。
  • 希望减少 Python 运行时参与。
  • 需要 OpenMP 等进一步加速。

不适合频繁在每个小步骤后回到 Python 动态决定下一步的实验。

5. 为什么示例使用 1000 个相同神经元

代码:

neurons = NeuronGroup(
    1000,
    "dv/dt = (1.1-v)/(10*ms) : 1",
    ...
)

目的不是建立生物网络,而是制造一份可测量的工作量。

所有神经元:

  • 方程相同。
  • 初始状态相同。
  • 没有随机输入。
  • 没有突触。

因此它们会完全同步。

100 ms 内单神经元约放 4 次,1000 个神经元约产生:

4 * 1000 = 4000 个脉冲

输出 Spikes=4000 是很好的正确性检查。

6. 墙钟时间测量了什么

代码:

started = time.perf_counter()
network.run(100 * ms, profile=True)
elapsed = time.perf_counter() - started

perf_counter() 测量从调用前到返回后的真实经过时间。

它可能包括:

  • before_run 检查。
  • 方程准备。
  • 代码生成。
  • 后端初始化。
  • 实际时间步推进。
  • Monitor 写入。
  • Python 调用开销。

因此它代表用户等待时间,但不等于纯状态更新时间。

7. profiling_summary 测量了什么

profiling_summary(network, show=5)

Brian2 内部统计具体 CodeObject 的运行时间,例如:

  • neurongroup_stateupdater
  • neurongroup_spike_thresholder
  • neurongroup_spike_resetter
  • spikemonitor

这些时间之和可能小于墙钟时间,因为墙钟还包含:

  • 准备工作。
  • Python 层开销。
  • 未单独归类的工作。

两种测量回答不同问题:

  • 墙钟时间:用户总共等了多久?
  • profiling:仿真内部哪里最耗时?

8. 为什么不能只运行一次就宣布某后端更快

可靠基准测试需要考虑:

预热与编译

第一次运行可能需要生成或编译代码。

缓存

后续运行可能复用已生成代码。

系统噪声

后台进程、CPU 频率和磁盘活动会造成波动。

模型规模

小模型的固定开销占比高,大模型的计算开销占比高。

记录负担

Monitor 可能成为主要瓶颈。

重复次数

应多次运行,报告中位数或分布,而不是只取一次最快结果。

9. 更合理的性能实验流程

  1. 先验证两个后端产生一致的关键结果。
  2. 固定模型、dt、仿真时长和记录内容。
  3. 分开记录构建时间和运行时间。
  4. 预热后重复多次。
  5. 报告硬件、软件版本和编译器。
  6. 同时记录内存,而不只看速度。
  7. 在真实目标规模上比较。

如果一个优化让结果变化,它首先是模型变更或数值变更,不应直接称为“加速”。

10. 编译器检测在检查什么

compiler = shutil.which("cl") or shutil.which("g++")

这只是快速检查当前终端 PATH 中是否能找到常见编译器。

它不能完整证明:

  • 编译器版本与 Python 兼容。
  • Cython 构建链完整。
  • Windows SDK 已安装。
  • 编译器能处理当前路径。

因此“找到编译器”是必要线索,不是完整健康检查。

11. 为什么路径也可能影响构建

编译工具链会处理:

  • 源文件路径。
  • 临时目录。
  • 命令行参数。
  • 生成的构建文件。

某些旧工具或配置对:

  • 非 ASCII 字符。
  • 空格。
  • 云同步目录。
  • 路径过长。

处理不佳。

这不是 Brian2 模型问题。排查 standalone 编译时,可以先在短、纯 ASCII 的本地路径建立最小项目,区分模型错误与工具链错误。

12. 怎样读本章报告

示例:

NumPy target wall time: 0.428 s
Spikes: 4000
C++ compiler: not found

正确解读:

  • 当前机器上这一次 NumPy 运行总耗时约 0.428 秒。
  • 脉冲数与理论预期一致。
  • 当前终端未找到常见 C++ 编译器。

错误解读:

  • NumPy 永远需要 0.428 秒。
  • 任何机器都应产生完全相同的耗时。
  • 没找到编译器就无法使用 Brian2。

13. 优化应该从哪里开始

推荐顺序:

1. 确认模型正确

用小规模、短时间、NumPy 目标。

2. 用 profiling 找热点

判断是:

  • 状态更新。
  • 突触事件。
  • 阈值。
  • Monitor。

3. 减少不必要工作

  • 只记录必要变量。
  • 降低 Monitor 采样率。
  • 使用 event-driven 方程。
  • 避免不必要的全连接。

4. 再切换代码生成目标

比较 Cython 或 standalone。

5. 最后考虑并行和硬件

没有 profiling 的“优化”很容易只是在改变不重要的部分。

14. 性能与数值精度的关系

最常见的错误加速方法是直接增大 dt。

这确实减少时间步:

步骤数 = 仿真时长 / dt

但可能改变:

  • 积分误差。
  • 阈值时刻。
  • 随机过程统计。
  • 网络同步。

增大 dt 是数值模型变更,必须通过收敛测试,而不是单纯性能参数。

15. 常见误区

误区一:编译后端一定比 NumPy 快

取决于模型规模、结构、运行时长和编译开销。

误区二:profiling 百分比等于总墙钟百分比

内部报告与完整调用计时范围不同。

误区三:脉冲数相同就证明所有结果相同

还应比较脉冲时刻、状态轨迹、权重和统计量。

误区四:优化只看运行时间

还要考虑构建时间、内存、可复现性和开发成本。

误区五:后端切换不会影响任何数值细节

不同编译器、浮点优化和算法实现可能产生微小差异,应验证关键结论。

16. 动手实验

实验 A:规模扫描

测试 100、1000、10000 个神经元,记录墙钟时间。观察是否线性增长。

实验 B:仿真时长扫描

固定构建模型,比较 10、100、1000 ms。判断固定开销占比。

实验 C:移除 SpikeMonitor

比较性能报告和墙钟时间,量化记录成本。

实验 D:重复运行

同一配置运行 10 次,报告中位数、最小值和最大值。

实验 E:安装编译器后比较

先验证结果,再比较 NumPy 与 Cython。把首次编译时间与后续运行时间分开报告。

本章小结

Brian2 执行链:

方程与事件
-> 解析和单位检查
-> 代码生成
-> NumPy/Cython/C++ 执行
-> Monitor 记录

性能分析链:

正确性
-> profiling
-> 减少无效工作
-> 后端比较
-> 规模化优化

最后一章综合项目会把神经元、随机输入、兴奋性突触、抑制性突触和群体记录组合成一个小型 E/I 网络。

官方参考