From 6f6c21d4225bd52ded78beaec5930f9a4ca3834d Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 10 Apr 2026 20:33:03 +0100 Subject: [PATCH 1/5] Update translation: lectures/need_for_speed.md --- lectures/need_for_speed.md | 105 +++++++++++-------------------------- 1 file changed, 32 insertions(+), 73 deletions(-) diff --git a/lectures/need_for_speed.md b/lectures/need_for_speed.md index 36ed987..877ade3 100644 --- a/lectures/need_for_speed.md +++ b/lectures/need_for_speed.md @@ -58,7 +58,7 @@ translation: ## 概述 -可以说,Python 是科学计算领域最流行的编程语言。 +Python 是科学计算许多方面最流行的编程语言。 这得益于以下几点: @@ -101,35 +101,36 @@ mpl.rcParams['font.family'] = ['Source Han Serif SC'] # i18n import random ``` - ## 主要科学库 让我们简要回顾一下 Python 的科学库。 - ### 为什么需要它们? -我们使用科学库的一个原因是它们实现了我们想要使用的程序。 +我们需要 Python 科学库有两个原因: -* 数值积分、插值、线性代数、求根等。 +1. Python 体量小 +2. Python 速度慢 -例如,使用现有的求根程序通常比从头编写一个新程序更好。 +**Python 体量小** -(对于标准算法,如果社区能够统一使用一套由专家编写、并经用户调优以尽可能快速和健壮的共同实现,效率将达到最高!) +核心 Python 在设计上体量较小——这有助于优化、易用性和维护。 -但这并不是我们使用 Python 科学库的唯一原因。 +科学库提供了我们不想——也可能不应该——自己编写的程序: -另一个原因是纯 Python 速度不够快。 +* 数值积分、插值、线性代数、求根等。 -因此,我们需要那些专门用于加速 Python 代码执行的库。 +**Python 速度慢** -它们通过两种策略来实现这一目标: +我们需要科学库的另一个原因是纯 Python 的速度相对较慢。 -1. 使用编译器将类 Python 语句转换为针对单一逻辑线程的快速机器码,以及 -2. 在多个"工作单元"(例如 CPU、GPU 内部的各个线程)之间并行化任务。 +科学库通过三种主要策略来加速执行: -我们将在本讲座及本系列剩余讲座中广泛讨论这些思想。 +1. 向量化:提供已编译的机器码以及使该代码可访问的接口 +1. JIT 编译:在运行时将类 Python 语句转换为快速机器码的编译器 +2. 并行化:将任务分配到多个线程 / CPU / GPU / TPU +我们将在下文深入讨论这些思想。 ### Python 的科学生态系统 @@ -151,8 +152,7 @@ import random * Pandas 提供用于操作数据的类型和函数。 * Numba 提供一个即时编译器,与 NumPy 配合良好,有助于加速 Python 代码。 -我们将在本系列讲座中广泛讨论所有这些库。 - +我们将在本系列讲座中详细讨论所有这些库。 ## 纯 Python 速度慢 @@ -162,7 +162,6 @@ import random 对于这个主题,如果我们理解是什么导致了执行速度慢,将会很有帮助。 - ### 高级语言与低级语言 像 Python 这样的高级语言是为人类优化的。 @@ -179,12 +178,10 @@ import random 另一方面,Python 的标准实现(称为 CPython)无法与 C 或 Fortran 等编译语言的速度相媲美。 - ### 瓶颈在哪里? 为什么会这样呢? - #### 动态类型 ```{index} single: Dynamic Typing @@ -215,14 +212,11 @@ a, b = ['foo'], ['bar'] a + b ``` -(我们说运算符 `+` 是*重载的*——它的动作取决于它所作用的对象的类型。) - 因此,在执行 `a + b` 时,Python 必须首先检查对象的类型,然后调用正确的操作。 -这涉及到不可忽视的开销。 - -如果我们在一个紧密的循环中反复执行此表达式,这种不可忽视的开销就会变成巨大的开销。 +这涉及到额外的开销。 +如果我们在一个紧密的循环中反复执行此表达式,这种开销就会变得很大。 #### 静态类型 @@ -257,7 +251,6 @@ int main(void) { 无需类型检查,因此没有额外开销。 - ### 数据访问 高级语言速度慢的另一个原因是数据访问。 @@ -266,22 +259,15 @@ int main(void) { #### 使用编译代码求和 -在 C 或 Fortran 中,这些整数通常存储在数组中,数组是一种用于存储同类数据的简单数据结构。 - -这样的数组存储在单个连续的内存块中: +在 C 或 Fortran 中,整数数组存储在单个连续的内存块中: -* 在现代计算机中,内存地址分配给每个字节(1字节 = 8位)。 * 例如,一个 64 位整数存储在 8 字节的内存中。 * $n$ 个这样的整数组成的数组占据 $8n$ 个*连续*的内存槽。 -此外,编译器通过程序员的声明得知数据类型。 - -* 在本例中为 64 位整数。 +此外,数据类型在编译时是已知的。 因此,每个连续的数据点都可以通过在内存空间中向前移动一个已知且固定的量来访问。 -* 在本例中为 8 字节。 - #### 在纯 Python 中求和 Python 在一定程度上试图复制这些思想。 @@ -292,11 +278,7 @@ Python 在一定程度上试图复制这些思想。 因此,访问数据值本身仍然存在开销。 -这对速度是一个相当大的拖累。 - -事实上,内存流量通常是导致执行缓慢的主要因素。 - - +这种开销是导致执行缓慢的主要因素。 ### 总结 @@ -314,11 +296,6 @@ Python 在一定程度上试图复制这些思想。 这个任务最好留给专门的编译器! -某些 Python 库在并行化科学代码方面具有出色的能力——我们将在后续内容中进一步讨论。 - - - - ## 加速 Python 在本节中,我们将介绍三种加速 Python 代码的相关技术。 @@ -327,8 +304,6 @@ Python 在一定程度上试图复制这些思想。 稍后我们将研究具体的库以及它们如何实现这些思想。 - - ### {index}`向量化 ` ```{index} single: Python; Vectorization @@ -359,7 +334,7 @@ Python 在一定程度上试图复制这些思想。 ```{figure} /_static/lecture_specific/need_for_speed/matlab.png ``` - +NumPy 使用类似的模型,灵感来自 MATLAB。 ### 向量化 vs 纯 Python 循环 @@ -417,50 +392,39 @@ with qe.Timer(): 在本系列后续讲座中,我们将学习现代 Python 库如何利用即时编译器生成快速、高效、并行化的机器码。 - - - ## 并行化 近年来,CPU 时钟速度(即单条逻辑链的运行速度)的增长已大幅放缓。 芯片设计师和计算机程序员通过寻求一条不同的路径来应对这一放缓:并行化。 -硬件制造商增加了每台机器中嵌入的核心数量(物理 CPU)。 - -对于程序员来说,挑战在于通过同时运行多个进程(即并行)来充分利用这些多个 CPU。 +这涉及到: -这在科学编程中尤为重要,因为科学编程需要处理: +1. 增加每台机器中嵌入的 CPU 数量 +1. 连接 GPU 和 TPU 等硬件加速器 -* 大量数据,以及 -* CPU 密集型的模拟和其他计算。 +对于程序员来说,挑战在于利用这些硬件并行运行多个进程。 下面我们讨论科学计算中的并行化,重点关注: -1. Python 中最佳的并行化工具,以及 +1. Python 中的并行化工具,以及 1. 这些工具如何应用于定量经济学问题。 - ### CPU 上的并行化 让我们回顾一下科学计算中常用的两种主要 CPU 并行化方式,并讨论它们的优缺点。 - #### 多进程 -多进程是指使用多个处理器并发执行多个进程。 - -在这个语境中,**进程**是一系列指令(即一个程序)。 +多进程是指使用多个处理器并发执行多条逻辑线程。 -多进程可以在一台拥有多个 CPU 的机器上进行,也可以在通过网络连接的多台机器上进行。 +多进程可以在一台拥有多个 CPU 的机器上进行,也可以在通过网络连接的机器集群上进行。 -在后一种情况下,这些机器的集合通常称为**集群**。 - -在多进程中,每个进程都有自己的内存空间,尽管物理内存芯片可能是共享的。 +在多进程中,*每个进程都有自己的内存空间*,尽管物理内存芯片可能是共享的。 #### 多线程 -多线程与多进程类似,不同之处在于,在执行期间,所有线程共享同一内存空间。 +多线程与多进程类似,不同之处在于,在执行期间,所有线程 *共享同一内存空间*。 由于一些[遗留的设计特性](https://wiki.python.org/moin/GlobalInterpreterLock),原生 Python 难以实现多线程。 @@ -478,7 +442,6 @@ with qe.Timer(): 对于我们在这些讲座中所做的绝大多数工作,多线程就足够了。 - ### 硬件加速器 虽然拥有多核的 CPU 已成为并行计算的标准,但随着专用硬件加速器的兴起,发生了更为深刻的变化。 @@ -504,7 +467,6 @@ TPU 是较新的发展,由 Google 专门为机器学习工作负载设计。 与 GPU 一样,TPU 擅长并行执行大量矩阵运算。 - #### 为何 TPU/GPU 至关重要 使用硬件加速器带来的性能提升可能是惊人的。 @@ -515,7 +477,6 @@ TPU 是较新的发展,由 Google 专门为机器学习工作负载设计。 这对科学计算尤为重要,因为许多算法天然地映射到 GPU 的并行架构上。 - ### 单 GPU 与 GPU 服务器 访问 GPU 资源有两种常见方式: @@ -530,7 +491,6 @@ TPU 是较新的发展,由 Google 专门为机器学习工作负载设计。 现代 Python 库(如本系列讲座中广泛讨论的 JAX)可以以最少的代码改动自动检测并使用可用的 GPU。 - #### 多 GPU 服务器 对于规模更大的问题,包含多个 GPU(通常每台服务器 4-8 个 GPU)的服务器越来越普遍。 @@ -544,11 +504,10 @@ TPU 是较新的发展,由 Google 专门为机器学习工作负载设计。 这使研究人员能够处理在单个 GPU 或 CPU 上不可行的问题。 - ### 总结 GPU 计算正变得越来越容易获取,尤其是在 Python 中。 一些 Python 科学库(如 JAX)现在支持 GPU 加速,对现有代码的改动极少。 -我们将在后续讲座中更详细地探讨 GPU 计算,并将其应用于一系列经济学应用。 \ No newline at end of file +我们将在后续讲座中更详细地探讨 GPU 计算,并将其应用于一系列经济学应用。 From 512f7ac0c834ac50363f61499a65b5e696f0a829 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 10 Apr 2026 20:33:04 +0100 Subject: [PATCH 2/5] Update translation: .translate/state/need_for_speed.md.yml --- .translate/state/need_for_speed.md.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.translate/state/need_for_speed.md.yml b/.translate/state/need_for_speed.md.yml index 2605926..c9970c5 100644 --- a/.translate/state/need_for_speed.md.yml +++ b/.translate/state/need_for_speed.md.yml @@ -1,6 +1,6 @@ -source-sha: cc9c3256dc35bd277cb25d0089f0a0452c0fa94e -synced-at: "2026-03-20" +source-sha: 21f1ea0669031ccc0ee0194878439a87de5d248d +synced-at: "2026-04-10" model: claude-sonnet-4-6 -mode: NEW +mode: UPDATE section-count: 5 -tool-version: 0.13.1 +tool-version: 0.14.1 From a4a6f6027a96ad10fc95dec47542dc7a811654ce Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 10 Apr 2026 20:33:05 +0100 Subject: [PATCH 3/5] Update translation: lectures/numba.md --- lectures/numba.md | 295 +++++++++++++--------------------------------- 1 file changed, 81 insertions(+), 214 deletions(-) diff --git a/lectures/numba.md b/lectures/numba.md index 73be1d8..fa47b0e 100644 --- a/lectures/numba.md +++ b/lectures/numba.md @@ -14,11 +14,10 @@ translation: headings: Overview: 概述 Compiling Functions: 编译函数 - Compiling Functions::An Example: 一个示例 - Compiling Functions::How and When it Works: 工作原理及适用场景 + Compiling Functions::An Example: 示例 + Compiling Functions::How and When it Works: 工作原理与适用场景 Decorator Notation: 装饰器语法 Type Inference: 类型推断 - Compiling Classes: 编译类 Dangers and Limitations: 危险与局限 Dangers and Limitations::Limitations: 局限性 'Dangers and Limitations::A Gotcha: Global Variables': 一个陷阱:全局变量 @@ -61,29 +60,30 @@ mpl.rcParams['font.family'] = ['Source Han Serif SC'] # i18n ## 概述 -在{doc}`之前的讲座 `中,我们学习了向量化,这是提高数值计算速度和效率的一种方法。 +在 {doc}`之前的讲座 ` 中,我们讨论了向量化,这是一种通过将数组处理操作批量发送到高效底层代码来提高执行速度的方法。 -向量化将数组处理操作批量发送到高效的底层代码。 +然而,正如 {ref}`在那次讲座中所讨论的 `,传统的向量化方案(例如在 MATLAB、Julia 和 NumPy 中)存在一些弱点。 -然而,正如{ref}`之前所讨论的 `,向量化有几个弱点。 +* 对于复合数组操作,内存消耗极大 +* 对某些算法无效甚至无法应用。 -其一是在处理大量数据时内存消耗极大。 +绕过这些问题的一种方法是使用 [Numba](https://numba.pydata.org/),这是一个面向数值计算的 Python **即时(JIT)编译器**。 -其二是能够完全向量化的算法集并非通用的。 +Numba 在运行时将函数编译为本地机器码指令。 -事实上,对于某些算法,向量化是无效的。 +编译成功后,Numba 的性能将与低级语言生成的机器码相当。 -幸运的是,一个名为 [Numba](https://numba.pydata.org/) 的新 Python 库解决了许多这些问题。 +此外,Numba 还可以完成其他有用的技巧,例如 {ref}`多线程 ` 或通过 `numba.cuda` 与 GPU 进行交互。 -它通过一种称为**即时(JIT)编译**的技术来实现这一点。 +Numba 的 JIT 编译器在许多方面与 JULIA 中的 JIT 编译器类似。 -核心思想是在运行时将函数编译为本地机器码指令。 +主要区别在于它的目标更为保守,只尝试编译语言的一个较小子集。 -编译成功后,编译后的代码速度极快。 +虽然这听起来像是一个缺陷,但在某些方面却是一个优势。 -除了编译带来的速度提升之外,Numba 还专门为数值计算而设计,并且还可以完成其他技巧,例如{ref}`多线程 `。 +Numba 精简、易用,且非常擅长它所做的事情。 -本讲座将介绍主要思路。 +本讲座将介绍核心思路。 (numba_link)= ## {index}`编译函数 ` @@ -91,20 +91,19 @@ mpl.rcParams['font.family'] = ['Source Han Serif SC'] # i18n ```{index} single: Python; Numba ``` -如上所述,Numba 的主要用途是在运行时将函数编译为快速的本地机器码。 (quad_map_eg)= -### 一个示例 +### 示例 -让我们考虑一个难以向量化的问题:给定初始条件,生成差分方程的轨迹。 +让我们考虑一个难以向量化的问题(即难以交给数组处理操作来完成)。 -我们将采用的差分方程是二次映射 +该问题涉及通过二次映射生成轨迹 $$ -x_{t+1} = \alpha x_t (1 - x_t) + x_{t+1} = \alpha x_t (1 - x_t) $$ -在下文中,我们设定 +在下文中,我们设置 ```{code-cell} ipython3 α = 4.0 @@ -128,7 +127,7 @@ ax.set_ylabel('$x_{t}$', fontsize = 12) plt.show() ``` -要使用 Numba 加速函数 `qm`,我们的第一步是 +要使用 Numba 加速函数 `qm`,第一步是 ```{code-cell} ipython3 from numba import jit @@ -136,11 +135,11 @@ from numba import jit qm_numba = jit(qm) ``` -函数 `qm_numba` 是 `qm` 的一个版本,它被"定向"为 JIT 编译。 +函数 `qm_numba` 是 `qm` 的一个版本,它被"定向"用于即时编译(JIT-compilation)。 -我们稍后将解释这意味着什么。 +我们将在稍后解释这意味着什么。 -让我们对这两个版本进行相同的函数调用计时和比较,首先从原始函数 `qm` 开始: +让我们对这两个版本进行相同的函数调用并计时比较,首先从原始函数 `qm` 开始: ```{code-cell} ipython3 n = 10_000_000 @@ -150,7 +149,7 @@ with qe.Timer() as timer1: time1 = timer1.elapsed ``` -现在让我们试试 qm_numba +现在让我们尝试 qm_numba ```{code-cell} ipython3 with qe.Timer() as timer2: @@ -158,9 +157,9 @@ with qe.Timer() as timer2: time2 = timer2.elapsed ``` -这已经是一个非常大的速度提升。 +这已经是非常大的速度提升了。 -事实上,下次及之后每次运行时速度都会更快,因为函数已经被编译并存储在内存中: +事实上,下一次及所有后续运行会更快,因为函数已经被编译并存储在内存中: (qm_numba_result)= @@ -171,35 +170,33 @@ time3 = timer3.elapsed ``` ```{code-cell} ipython3 -time1 / time3 # 计算速度提升倍数 +time1 / time3 # Calculate speed gain ``` -相对于修改的简单和清晰程度,这种速度提升令人印象深刻。 +### 工作原理与适用场景 -### 工作原理及适用场景 +Numba 尝试使用 [LLVM Project](https://llvm.org/) 提供的基础设施生成快速的机器代码。 -Numba 尝试使用 [LLVM 项目](https://llvm.org/) 提供的基础设施生成快速的机器码。 +它通过动态推断类型信息来实现这一目标。 -它通过即时推断类型信息来实现这一点。 - -(有关类型的讨论,请参阅我们{doc}`之前关于科学计算的讲座 `。) +(有关类型的讨论,请参阅我们 {doc}`早期关于科学计算的讲座 `。) 基本思路如下: -* Python 非常灵活,因此我们可以用多种类型调用函数 `qm`。 +* Python 非常灵活,因此我们可以用多种类型调用函数 qm。 * 例如,`x0` 可以是 NumPy 数组或列表,`n` 可以是整数或浮点数,等等。 -* 这使得*预*编译函数(即在运行时之前编译)变得困难。 -* 然而,当我们实际调用函数时,比如运行 `qm(0.5, 10)`,`x0` 和 `n` 的类型就变得清晰了。 -* 此外,一旦知道输入类型,`qm` 中其他变量的类型也可以被推断出来。 -* 因此,Numba 和其他 JIT 编译器的策略是等到这一时刻,*然后*再编译函数。 +* 这使得*提前*(即在运行时之前)生成高效的机器代码非常困难。 +* 然而,当我们实际*调用*函数时,例如运行 `qm(0.5, 10)`,`x0` 和 `n` 的类型就变得明确了。 +* 此外,一旦输入类型已知,`qm` 中*其他变量*的类型*可以被推断出来*。 +* 因此,Numba 和其他即时编译器的策略是*等到函数被调用时再进行编译*。 -这就是为什么它被称为"即时"编译。 +这就是所谓的"即时"编译(just-in-time compilation)。 -请注意,如果您调用 `qm(0.5, 10)`,然后紧跟着调用 `qm(0.9, 20)`,编译只发生在第一次调用时。 +请注意,如果你调用 `qm(0.5, 10)` 后紧接着调用 `qm(0.9, 20)`,编译只在第一次调用时发生。 -编译后的代码会被缓存并按需复用。 +这是因为编译后的代码会被缓存并在需要时重复使用。 -这就是为什么在上面的代码中,`time3` 比 `time2` 小。 +这就是为什么在上述代码中,`time3` 小于 `time2`。 ## 装饰器语法 @@ -255,9 +252,7 @@ Numba 也与 NumPy 数组配合良好,因为它们具有明确定义的类型 在理想情况下,Numba 可以推断出所有必要的类型信息。 -这使它能够生成本地机器码,而无需调用 Python 运行时环境。 - -在这种情况下,Numba 将与低级语言的机器码相媲美。 +这使它能够生成高效的本地机器码,而无需调用 Python 运行时环境。 当 Numba 无法推断所有类型信息时,它将引发错误。 @@ -265,8 +260,8 @@ Numba 也与 NumPy 数组配合良好,因为它们具有明确定义的类型 ```{code-cell} ipython3 @jit -def bootstrap(data, statistics, n): - bootstrap_stat = np.empty(n) +def bootstrap(data, statistics, n_resamples): + bootstrap_stat = np.empty(n_resamples) n = len(data) for i in range(n_resamples): resample = np.random.choice(data, size=n, replace=True) @@ -298,114 +293,6 @@ with qe.Timer(): bootstrap(data, mean, n_resamples) ``` -## 编译类 - -如上所述,目前 Numba 只能编译 Python 的一个子集。 - -然而,这个子集一直在扩展。 - -值得注意的是,Numba 现在在编译类方面相当有效。 - -如果一个类被成功编译,那么它的方法就像 JIT 编译的函数一样运行。 - -举一个例子,让我们考虑在{doc}`本讲座 `中创建的用于分析索洛-斯旺增长模型的类。 - -要编译这个类,我们使用 `@jitclass` 装饰器: - -```{code-cell} ipython3 -from numba import float64 -from numba.experimental import jitclass -``` - -注意,我们还导入了一个叫做 `float64` 的东西。 - -这是一种表示标准浮点数的数据类型。 - -我们在这里导入它是因为当 Numba 尝试处理类时,它需要一些关于类型的额外帮助。 - -以下是我们的代码: - -```{code-cell} ipython3 -solow_data = [ - ('n', float64), - ('s', float64), - ('δ', float64), - ('α', float64), - ('z', float64), - ('k', float64) -] - -@jitclass(solow_data) -class Solow: - r""" - Implements the Solow growth model with the update rule - - k_{t+1} = [(s z k^α_t) + (1 - δ)k_t] /(1 + n) - - """ - def __init__(self, n=0.05, # population growth rate - s=0.25, # savings rate - δ=0.1, # depreciation rate - α=0.3, # share of labor - z=2.0, # productivity - k=1.0): # current capital stock - - self.n, self.s, self.δ, self.α, self.z = n, s, δ, α, z - self.k = k - - def h(self): - "Evaluate the h function" - # 解包参数(去掉 self 以简化符号) - n, s, δ, α, z = self.n, self.s, self.δ, self.α, self.z - # 应用更新规则 - return (s * z * self.k**α + (1 - δ) * self.k) / (1 + n) - - def update(self): - "Update the current state (i.e., the capital stock)." - self.k = self.h() - - def steady_state(self): - "Compute the steady state value of capital." - # 解包参数(去掉 self 以简化符号) - n, s, δ, α, z = self.n, self.s, self.δ, self.α, self.z - # 计算并返回稳态 - return ((s * z) / (n + δ))**(1 / (1 - α)) - - def generate_sequence(self, t): - "Generate and return a time series of length t" - path = [] - for i in range(t): - path.append(self.k) - self.update() - return path -``` - -首先,我们在 `solow_data` 中指定了类的实例数据类型。 - -之后,将类定向为 JIT 编译只需在类定义前添加 `@jitclass(solow_data)` 即可。 - -当我们调用类中的方法时,这些方法就像函数一样被即时编译。 - -```{code-cell} ipython3 -s1 = Solow() -s2 = Solow(k=8.0) - -T = 60 -fig, ax = plt.subplots() - -# 绘制共同的稳态资本值 -ax.plot([s1.steady_state()]*T, 'k-', label='稳态') - -# 为每个经济体绘制时间序列 -for s in s1, s2: - lb = f'从初始状态 {s.k} 出发的资本序列' - ax.plot(s.generate_sequence(T), 'o-', lw=2, alpha=0.6, label=lb) -ax.set_ylabel('$k_{t}$', fontsize=12) -ax.set_xlabel('$t$', fontsize=12) -ax.legend() -plt.show() -``` - ## 危险与局限 让我们回顾上述内容并补充一些注意事项。 @@ -414,13 +301,11 @@ plt.show() 正如我们所见,Numba 需要推断所有变量的类型信息以生成快速的机器级指令。 -对于简单的例程,Numba 推断类型非常出色。 - -对于较大的例程,或使用外部库的例程,它很容易失败。 +对于较大的例程或使用外部库的例程,此过程很容易失败。 因此,在使用 Numba 时,明智的做法是专注于加速代码中小而关键的片段。 -这将比在 Python 程序中大量使用 `@njit` 语句带来更好的性能。 +这将比在 Python 程序中大量使用 `@jit` 语句带来更好的性能。 ### 一个陷阱:全局变量 @@ -451,13 +336,9 @@ print(add_a(10)) (multithreading)= ## Numba 中的多线程循环 -除了 JIT 编译之外,Numba 还为 CPU 上的并行计算提供了强大支持。 - -通过在多个 CPU 核心上分配计算任务,我们可以为许多数值算法实现显著的速度提升。 - -Numba 中并行化的关键工具是 `prange` 函数,它告诉 Numba 在可用的 CPU 核心上并行执行循环迭代。 +除了 JIT 编译之外,Numba 还为 CPU 和 GPU 上的并行计算提供了支持。 -这种多线程方法适用于科学计算和定量经济学中的广泛问题。 +Numba 中在 CPU 上进行并行化的关键工具是 `prange` 函数,它告诉 Numba 在可用的核心上并行执行循环迭代。 为了说明,让我们首先看一个简单的单线程(即非并行化)代码片段。 @@ -478,20 +359,19 @@ $$ 以下是代码: ```{code-cell} ipython3 -from numpy.random import randn -from numba import njit +from numba import jit -@njit +@jit def h(w, r=0.1, s=0.3, v1=0.1, v2=1.0): """ Updates household wealth. """ - # 抽取冲击 - R = np.exp(v1 * randn()) * (1 + r) - y = np.exp(v2 * randn()) + # Draw shocks + R = np.exp(v1 * np.random.randn()) * (1 + r) + y = np.exp(v2 * np.random.randn()) - # 更新财富 + # Update wealth w = R * s * w + y return w ``` @@ -515,29 +395,15 @@ plt.show() 现在,假设我们有一个庞大的家庭群体,并且想知道中位财富将是多少。 -这个问题很难用纸笔求解,因此我们将使用模拟。 - -具体来说,我们将模拟大量家庭,然后计算该群体的中位财富。 +这个问题很难用纸笔求解,因此我们将使用模拟: -假设我们对这一中位数随时间变化的长期平均值感兴趣。 - -事实证明,对于我们上面选择的参数规格,我们可以通过截取长时间模拟结束时群体中位财富的一个单期快照来计算这个值。 - -此外,只要模拟期足够长,初始条件就不重要。 - -* 这是由于一种称为遍历性的性质,我们将在[后面](https://python.quantecon.org/finite_markov.html#id15)讨论。 - -因此,总结来说,我们将通过以下方式模拟 50,000 个家庭: - -1. 任意设定初始财富为 1,以及 -1. 向前模拟 1,000 个时期。 - -然后我们将计算最终时期的中位财富。 +1. 向前模拟大量家庭 +2. 计算中位财富 以下是代码: ```{code-cell} ipython3 -@njit +@jit def compute_long_run_median(w0=1, T=1000, num_reps=50_000): obs = np.empty(num_reps) @@ -564,7 +430,7 @@ with qe.Timer(): ```{code-cell} ipython3 from numba import prange -@njit(parallel=True) +@jit(parallel=True) def compute_long_run_median_parallel(w0=1, T=1000, num_reps=50_000): obs = np.empty(num_reps) @@ -586,12 +452,14 @@ with qe.Timer(): 速度提升非常显著。 +注意,我们是跨家庭进行并行化,而非跨时间——单个家庭跨时期的更新本质上是顺序的。 + ## 练习 ```{exercise} :label: speed_ex1 -{ref}`之前 `我们考虑了如何用蒙特卡洛方法近似 $\pi$。 +{ref}`之前 ` 我们考虑了如何用蒙特卡洛方法近似 $\pi$。 在这里使用相同的思路,但使用 Numba 使代码高效。 @@ -605,13 +473,11 @@ with qe.Timer(): 以下是一种解法: ```{code-cell} ipython3 -from random import uniform - @jit def calculate_pi(n=1_000_000): count = 0 for i in range(n): - u, v = uniform(0, 1), uniform(0, 1) + u, v = np.random.uniform(0, 1), np.random.uniform(0, 1) d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2) if d < 0.5: count += 1 @@ -632,9 +498,9 @@ with qe.Timer(): calculate_pi() ``` -如果我们通过删除 `@njit` 来关闭 JIT 编译,代码在我们的机器上大约需要慢 150 倍。 +如果我们通过删除 `@jit` 来关闭 JIT 编译,代码在我们的机器上大约需要慢 150 倍。 -因此,通过添加四个字符,我们获得了 2 个数量级的速度提升——这是巨大的。 +因此,通过添加四个字符,我们获得了 2 个数量级的速度提升。 ```{solution-end} ``` @@ -676,7 +542,7 @@ with qe.Timer(): :class: dropdown * 将低状态表示为 0,高状态表示为 1。 -* 如果您想在 NumPy 数组中存储整数,然后应用 JIT 编译,请使用 `x = np.empty(n, dtype=np.int_)`。 +* 如果您想在 NumPy 数组中存储整数,然后应用 JIT 编译,请使用 `x = np.empty(n, dtype=np.int64)`。 ``` @@ -700,7 +566,7 @@ p, q = 0.1, 0.2 # 分别为离开低状态和高状态的概率 ```{code-cell} ipython3 def compute_series(n): - x = np.empty(n, dtype=np.int_) + x = np.empty(n, dtype=np.int64) x[0] = 1 # 从状态 1 开始 U = np.random.uniform(0, 1, size=n) for t in range(1, n): @@ -757,7 +623,7 @@ with qe.Timer(): ```{exercise} :label: numba_ex3 -在{ref}`之前的练习 `中,我们使用 Numba 加速了通过蒙特卡洛方法计算常数 $\pi$ 的工作。 +在 {ref}`之前的练习 ` 中,我们使用 Numba 加速了通过蒙特卡洛方法计算常数 $\pi$ 的工作。 现在尝试添加并行化,看看是否能获得进一步的速度提升。 @@ -779,13 +645,11 @@ with qe.Timer(): 以下是一种解法: ```{code-cell} ipython3 -from random import uniform - -@njit(parallel=True) +@jit(parallel=True) def calculate_pi(n=1_000_000): count = 0 for i in prange(n): - u, v = uniform(0, 1), uniform(0, 1) + u, v = np.random.uniform(0, 1), np.random.uniform(0, 1) d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2) if d < 0.5: count += 1 @@ -806,7 +670,7 @@ with qe.Timer(): calculate_pi() ``` -通过打开和关闭并行化(在 `@njit` 注解中选择 `True` 或 `False`),我们可以测试多线程在 JIT 编译之上提供的速度增益。 +通过打开和关闭并行化(在 `@jit` 注解中选择 `True` 或 `False`),我们可以测试多线程在 JIT 编译之上提供的速度增益。 在我们的工作站上,我们发现并行化将执行速度提高了 2 到 3 倍。 @@ -815,10 +679,11 @@ with qe.Timer(): ```{solution-end} ``` + ```{exercise} :label: numba_ex4 -在{doc}`我们关于 SciPy 的讲座 `中,我们讨论了在标的股票价格具有简单且众所周知的分布的情况下,如何为看涨期权定价。 +在 {doc}`我们关于 SciPy 的讲座 ` 中,我们讨论了在标的股票价格具有简单且众所周知的分布的情况下,如何为看涨期权定价。 这里我们讨论一个更现实的情境。 @@ -872,10 +737,12 @@ $$ ``` + ```{solution-start} numba_ex4 :class: dropdown ``` + 令 $s_t := \ln S_t$,价格动态变为 $$ @@ -884,14 +751,14 @@ $$ 利用这一事实,解可以写成如下形式。 + ```{code-cell} ipython3 -from numpy.random import randn M = 10_000_000 n, β, K = 20, 0.99, 100 μ, ρ, ν, S0, h0 = 0.0001, 0.1, 0.001, 10, 0 -@njit(parallel=True) +@jit(parallel=True) def compute_call_price_parallel(β=β, μ=μ, S0=S0, @@ -908,10 +775,10 @@ def compute_call_price_parallel(β=β, h = h0 # 向前模拟 for t in range(n): - s = s + μ + np.exp(h) * randn() - h = ρ * h + ν * randn() + s = s + μ + np.exp(h) * np.random.randn() + h = ρ * h + ν * np.random.randn() # 将 max{S_n - K, 0} 的值累加到 current_sum - current_sum += np.maximum(np.exp(s) - K, 0) + current_sum += max(np.exp(s) - K, 0) return β**n * current_sum / M ``` @@ -921,4 +788,4 @@ def compute_call_price_parallel(β=β, 如果您使用的是具有多个 CPU 的机器,差异应该很显著。 ```{solution-end} -``` \ No newline at end of file +``` From 543f47ecc07b7a80810c3a10f2d4a2e7baaa3701 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 10 Apr 2026 20:33:05 +0100 Subject: [PATCH 4/5] Update translation: .translate/state/numba.md.yml --- .translate/state/numba.md.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.translate/state/numba.md.yml b/.translate/state/numba.md.yml index c5f1f07..d5a9787 100644 --- a/.translate/state/numba.md.yml +++ b/.translate/state/numba.md.yml @@ -1,6 +1,6 @@ -source-sha: cc9c3256dc35bd277cb25d0089f0a0452c0fa94e -synced-at: "2026-03-20" +source-sha: 21f1ea0669031ccc0ee0194878439a87de5d248d +synced-at: "2026-04-10" model: claude-sonnet-4-6 -mode: NEW -section-count: 8 -tool-version: 0.13.1 +mode: UPDATE +section-count: 7 +tool-version: 0.14.1 From 69b1c5905e526a1740b828ff23ab99ef861d6b79 Mon Sep 17 00:00:00 2001 From: Humphrey Yang <39026988+HumphreyYang@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:05:58 +1000 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lectures/numba.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lectures/numba.md b/lectures/numba.md index fa47b0e..b698e11 100644 --- a/lectures/numba.md +++ b/lectures/numba.md @@ -75,7 +75,7 @@ Numba 在运行时将函数编译为本地机器码指令。 此外,Numba 还可以完成其他有用的技巧,例如 {ref}`多线程 ` 或通过 `numba.cuda` 与 GPU 进行交互。 -Numba 的 JIT 编译器在许多方面与 JULIA 中的 JIT 编译器类似。 +Numba 的 JIT 编译器在许多方面与 Julia 中的 JIT 编译器类似。 主要区别在于它的目标更为保守,只尝试编译语言的一个较小子集。 @@ -170,7 +170,7 @@ time3 = timer3.elapsed ``` ```{code-cell} ipython3 -time1 / time3 # Calculate speed gain +time1 / time3 # 计算速度提升倍数 ``` ### 工作原理与适用场景 @@ -367,11 +367,11 @@ def h(w, r=0.1, s=0.3, v1=0.1, v2=1.0): Updates household wealth. """ - # Draw shocks + # 抽取冲击 R = np.exp(v1 * np.random.randn()) * (1 + r) y = np.exp(v2 * np.random.randn()) - # Update wealth + # 更新财富 w = R * s * w + y return w ```