Skip to content

Commit 6270e13

Browse files
committed
mod
1 parent 8cd4747 commit 6270e13

147 files changed

Lines changed: 35268 additions & 4671 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/01-python314-features.md

Lines changed: 46 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,63 @@
1-
# Python 3.14Free-threaded 与实验性 JIT
1+
# Python 3.14: Free-threaded 与实验性 JIT
22

3-
本文档汇总与演讲「Breaking the Speed Limit」相关的 **Python 3.14** 运行时特性。不仅提供结论,更深入探讨其底层设计机制,为现场回答高级 Python 工程师的技术提问提供充足弹药。
3+
本文档保留 Python 3.14 在演讲中的定位,但不再引用已经清理掉的
4+
历史本地 benchmark 结果。当前仓库的实验证据以 MacBook Air `latest` 结果与
5+
Linux server/A100 `long_safe_20260503_190133` 结果为准。
46

5-
## 1. Free-threaded 构建(PEP 703 / PEP 779)
7+
## Free-threaded 的讲法
68

7-
### 1.1 核心机制:移除 GIL 带来的底层重构
9+
Free-threaded Python 让同一进程内的 Python 线程可以真正并行执行
10+
Python bytecode。它对统计计算最有价值的场景不是“自动让所有 NumPy
11+
代码变快”,而是:
812

9-
- **内存分配器(mimalloc)**:为了在多线程环境下保证无锁的内存分配效率,Free-threaded 构建默认集成了 `mimalloc`。由于去除了 GIL,多个线程会并发分配/释放内存,如果继续使用旧版的 `pymalloc` 将会导致严重的锁争用。
10-
- **偏置引用计数(Biased Reference Counting, BRC)与延迟引用计数(Deferred Reference Counting)**
11-
- 传统 CPython 每个对象的赋值/销毁都会原子的增减 refcount,这在无 GIL 时会造成可怕的 CPU 缓存行伪共享(False Sharing)。
12-
- BRC 允许将引用计数偏向"创建对象的那个线程",通过本地线程进行操作,只有跨线程访问时才使用原子的原子操作(atomic instructions)。
13-
- 对于常驻对象(如单例、内置类型或顶层函数),采用延迟引用计数或不追踪(Immortalization, PEP 683),进一步降低锁争用。
13+
- 多个线程共享同一份大型只读数组,避免 `multiprocessing` 的 pickle
14+
与进程内存复制。
15+
- 每个 worker 写独立输出槽,最后合并结果,避免共享 list/dict 的锁争用。
16+
- Python 层调度成本足够高,而且 C/NumPy 内核已经不是唯一瓶颈。
1417

15-
### 1.2 行为与性能代价
18+
当前主线中,置换检验仍然使用 `experiments/permutation/`
19+
reference/NumPy/JAX matrix 实现作为可验证统计任务。server CPU 结果里
20+
`permutation_worker_sweep.csv` 说明 workers 不能盲目拉满:固定
21+
`n=5,000, p=10,000, R=10,000` 时,本次运行的 8 workers 最快,更多
22+
workers 增加开销但没有线性收益。
1623

17-
- 在 free-threaded 模式下,**单线程**代码相对传统 GIL 构建通常有 **约 5–10%** 的性能损失。原因在于即便有 BRC,依然会有不可避免的轻量级锁检查和内存屏障(Memory Barriers)开销。
18-
- 本地自适应解释器(PEP 659 的 Specializing Adaptive Interpreter)在无 GIL 环境下面临并发覆写字节码的问题。在 Python 3.13/3.14 中,这需要精细的锁或 RCU (Read-Copy-Update) 机制来保证类型专化的线程安全。
24+
## 实验性 JIT 的讲法
1925

20-
### 1.3 实验中的表现(在 GIL 构建上的天花板)
26+
Python 3.14 的实验性 JIT 更适合作为“纯 Python 循环为什么会慢”的背景:
27+
JIT 能降低解释器 dispatch/boxing 开销,但不会替代算法重写、NumPy 的 C
28+
内核、Numba 的 LLVM 编译,或 JAX/XLA 的 whole-function lowering。
2129

22-
本仓库的置换检验线程实验在同一台 Apple Silicon 8 核机器上比较了 `py312` GIL build 与 `py314t` free-threaded build(n=10k, R=10000,详见 [`perm_threads_py312_py314t.png`](../experiments/results/v2/perm_threads_py312_py314t.png))。同一份 `ThreadPoolExecutor` 代码在 8 workers 下
30+
在当前仓库里,k-means 的可讲证据来自
2331

24-
- `py312` GIL build:warm median **0.32 s**
25-
- `py314t` free-threaded build:warm median **0.17 s**
32+
- `experiments/kmeans/kmeans_reference.py`:接近统计定义的 reference。
33+
- `experiments/kmeans/kmeans_numpy_broadcast.py`:展示临时数组风险。
34+
- `experiments/kmeans/kmeans_numpy_matmul.py`:展示矩阵恒等式何时有用。
35+
- `experiments/kmeans/kmeans_numba.py`:展示 fused loop 与较低内存压力。
2636

27-
标准 GIL 下线程仍然有加速,是因为 NumPy 的 `.sum()``.permutation()` 在 C 层释放 GIL,使线程可以重叠。Free-threaded build 继续降低 Python 层同步成本,把同一段代码的上限往 8 核心推进。
37+
主结论应该是:Python JIT 是运行时进步,但科学计算代码仍然要先识别
38+
计算图、内存形状和统计等价性。
2839

29-
---
40+
## 当前可引用证据
3041

31-
## 2. 实验性 JIT(PEP 744 / PEP 774)
42+
- MacBook Air `latest`:3,840 个 k-means pass rows、480 个预期
43+
`skipped_memory_risk` rows、450 个 permutation equivalence pass rows。
44+
- Server CPU:540 个 k-means CPU pass rows、105 个 permutation CPU pass
45+
rows、3 个最大 permutation 形状 timeout rows。
46+
- Server A100:135 个 k-means A100 pass rows、15 个 permutation matrix
47+
A100 pass rows。
3248

33-
### 2.1 Copy-and-Patch 技术解析
49+
这些数字对应的 README:
3450

35-
Python 3.14 搭载的实验性 JIT 并非传统的追踪式 JIT(如 PyPy 的 Tracing JIT)或基于完整 LLVM 编译的 JIT,而是基于 **Copy-and-Patch** 技术的模板 JIT(Template JIT)。
51+
- `experiments/results/macbook_air_long/latest/README.md`
52+
- `experiments/results/linux_server_cpu/long_safe_20260503_190133/README.md`
53+
- `experiments/results/linux_server_a100/long_safe_20260503_190133/README.md`
3654

37-
- **Stencil(模板生成)**:在 CPython 的构建阶段(而非运行阶段),使用 LLVM 对 C 语言写好的解释器指令(Opcode)进行编译,提取出机器码模板(Stencils)。这正是 PEP 774 的核心——不再要求用户的运行环境有 LLVM,而是直接在 CPython 源码树中预编译机器码片段。
38-
- **运行时 Patch**:当一段 Python 代码变"热"(Hot)时,JIT 引擎只需把预设的机器码模板像"拼图"一样拷贝到内存可执行页中,把函数指针和偏移量作为参数(Patch)填入即可。
39-
- **优势**:编译开销极低(因为只是内存拷贝和重定位),预热极快,完全不需要运行时进行复杂的寄存器分配和指令选择。
40-
- **劣势**:因为 stencil 在构建时生成,**无法进行跨 opcode 的优化(例如 loop hoisting、公共子表达式消除)**,因此对于数值代码,与 Numba / JAX 的完整 LLVM 编译差距依然显著。
55+
## 口头边界
4156

42-
### 2.2 演讲实验的启示:什么能被加速?
57+
不要把 Python 3.14 讲成“免费提速按钮”。更稳的表达是:
4358

44-
- JIT 对于 **纯 Python 密集循环**、条件分支(控制流)有着显著加速。
45-
- **NumPy/C 扩展盲区**:如果在 CPython JIT 下运行高度 NumPy 向量化的代码,JIT **毫无作用**。因为运行时大部分时间在 `numpy` 的 C 库中。
46-
- 在我们的 k-means 实验中,`kmeans_loops.py`(N=2000, d=10, k=5)在标准 CPython 3.12 下 warm median **0.414 s**;对照 Numba 版本在 **N=100k** 时 warm median **0.0064 s**。这一巨大差距正是 Python 循环解释开销的量化,也是 CPython JIT 值得去优化的地方——但本地 `py314` 只暴露 `sys._jit``is_available()``False`,所以本轮只把 JIT 作为诚实限制呈现。**这是向观众传达的真实界限**:JIT 加速的是 Python 解释器层面的开销,而不是改变算法的渐进复杂度或 C 核心的执行速度。
47-
48-
---
49-
50-
## 3. 陷阱:JIT 与 Free-threaded 的正交与互斥
51-
52-
**Python 3.14** 的实验性阶段,你需要向观众诚实指出:
53-
54-
- **现阶段,JIT 与 Free-threading 是互相独立的,甚至部分版本构建存在排斥。**
55-
- 并发修改字节码(JIT 编译时替换指令)在 Free-threaded 下需要非常复杂的同步原语,因此很多时候我们是在 **单线程 JIT****多线程无 JIT** 之间做选择。
56-
- 演讲建议:将两者解耦讲解。K-means 用于讲 JIT 带来的"原生 Python"收益;Permutation Test 用于讲 Free-threaded 解决的多进程共享内存痛点。
57-
- 验证方式:`python -VV``sys._is_gil_enabled()``sys.flags.jit`。3.15+ 可能会放宽此约束。
58-
59-
---
60-
61-
## 4. Free-threaded 与科学计算栈的真实交互
62-
63-
在 Permutation Test 的实验中,我们会发现 Free-threaded 版本的线程池性能非常优异,但有两个容易踩坑的点:
64-
65-
1. **Thread-Safety 并不免费**:即便释放了 GIL,如果在 Python 循环里依然对全局状态(如往 `list.append` 结果)频繁访问,会导致 CPython 内部的微观锁争用,从而拖慢速度。正确的做法是:**为每个线程分配独立的写入 Buffer,最后统一 merge**(本仓库 [`permtest_freethreaded.py`](../experiments/permutation_test/permtest_freethreaded.py) 即按此设计)。
66-
2. **NumPy 的内部锁**:NumPy 在执行向量化运算时,内部的内存分配也可能遭遇 C 层面的锁。好在现代 NumPy 和底层 BLAS(如 OpenBLAS / MKL)对多线程有着不同程度的支持和隔离。
67-
68-
---
69-
70-
## 5. 专家视角结论
71-
72-
在准备这篇演讲时,应该让观众明确:Python 3.14 的到来并不意味着我们可以盲目写纯 Python 循环。它降低了"偶尔写出慢代码"的惩罚,并为构建高性能的多线程 Python 库(如在单进程内共享巨大矩阵的统计工具)提供了基础设施。
73-
74-
**实测数据要点(Apple Silicon 8 核, Python 3.12.2, NumPy 1.26.4, Numba 0.59.1, JAX 0.4.25;另测 Python 3.14t free-threaded)**
75-
76-
- 同一份 ThreadPool 置换检验代码在 8 workers 下,`py312` GIL build 为 **0.32 s**`py314t` free-threaded build 为 **0.17 s**
77-
- `multiprocessing` 在 R=10000 时的子进程 RSS 加总 **833 MiB**,对照同规模线程池的 Python-level 峰值约 **1-2 MiB**[`perm_memory.png`](../experiments/results/v2/perm_memory.png) 把这一差距做成了一张直观的对数刻度条形图。
78-
- 在 CPU 上运行基于 `jax.vmap` 的置换检验(R=10000)耗时 **~37 s**——比纯 NumPy 慢约 44×。JAX 在加速器上才能发挥,它在 CPU 上并非默认选项。
59+
1. 先用 reference 与 simulation 证明统计量没有变。
60+
2. 再用 profile/shape evidence 判断瓶颈是 Python loop、temporary array、
61+
process copy、还是 accelerator batching。
62+
3. Python 3.14、Numba、JAX 都是工具箱的一部分;谁赢取决于 workload
63+
形状,而不是 logo。

docs/02-numba-guide.md

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,61 @@
1-
# Numba 数值计算速查(含实测数据)
1+
# Numba 数值计算速查
22

3-
本文档面向演讲中的 **k-means(循环密集)****置换检验(embarrassingly parallel)** 两类 workload,总结 Numba 常用模式、实测数字与常见陷阱。
3+
本文档对应演讲中的 k-means 与 permutation workloads。旧版脚本和图片已经
4+
移除;请使用当前 `experiments/kmeans/`,
5+
`experiments/permutation/`, `experiments/server/`
6+
`experiments/results/*/README.md`
47

5-
## 1. 核心概念
8+
## 适用场景
69

7-
- **Numba**:通过 LLVM 将装饰的函数编译为机器码,适合 **NumPy 数组 + 数值循环**
8-
- **`@njit`**(即 `@jit(nopython=True)`):**nopython 模式**,避免回退到对象模式,性能最可预期。
9-
- **冷启动**:首次编译某函数通常有 **数百毫秒到数秒** 级延迟。例如本仓库 k-means Numba 核(d=10, K=5)在当前本机 `py312` 下首次调用约 0.36 s([`kmeans_sweep.csv`](../experiments/results/v2/kmeans_sweep.csv)`cold_s` 列)。必须靠 `warmup``cache=True` 抵消。
10+
Numba 最适合把“统计定义已经很清楚、但 Python 正在执行热循环”的代码
11+
编译成机器码:
1012

11-
## 2. `@njit`:单线程加速
13+
- k-means assignment/update 的内层距离循环。
14+
- 置换、bootstrap、simulation replicate 这类独立重复任务。
15+
- 小型固定结构的归约、计数和状态更新。
1216

13-
典型用途:
17+
不适合的场景是任意 Python 对象、字符串、字典、动态 shape 控制流,或已经
18+
完全由高质量 BLAS/GPU kernel 主导的代码。
1419

15-
- 手写 `for` 循环遍历样本/特征;
16-
- 内层距离计算、聚类分配、小数组上的归约。
20+
## 当前仓库里的 Numba 角色
1721

18-
注意:
22+
`experiments/kmeans/kmeans_numba.py` 保留了显式 loop 结构,因此可以和
23+
reference/NumPy 版本逐步对照。它的意义不是“Numba 魔法”,而是:
1924

20-
- nopython 模式只支持 **Python 与 NumPy 的子集**(无任意 Python 对象、字符串、字典等)
21-
- 部分 NumPy API 在 Numba 中 **未实现或行为不同**,需查 [Supported NumPy features](https://numba.readthedocs.io/en/stable/reference/numpysupported.html)
22-
- `np.random.default_rng` 在 Numba 里不支持;我们在置换检验里改用手写 **SplitMix64 + xorshift64**[`permtest_numba.py`](../experiments/permutation_test/permtest_numba.py)),并且记录了一次 RNG 陷阱:直接用 `(base_seed ^ i*const)` 作为种子会产生高度相关的流,导致 null 分布均值偏移 ~0.05σ。演讲时可以用这个例子提醒观众「并行 RNG 要从高质量种子产出独立流」
25+
- 不生成 `(N, K, d)` 广播临时张量
26+
- 将 labels、centroids 和 inertia 的循环留在一个 compiled path
27+
- 保持和 reference 相同的初始化与 stopping rule,便于检查 inertia
2328

24-
## 3. `@njit(parallel=True)` + `prange`
29+
Server CPU 的 `kmeans_cpu_scaling.csv` 说明 Numba 在低维 k-means 上很强;
30+
但当 `d` 变大、矩阵乘法更饱满时,NumPy matmul 会变得更有竞争力。
2531

26-
用于 **独立迭代** 可并行的外层循环(例如多次置换、多 bootstrap 次):
32+
## 并行与线程数
2733

28-
```python
29-
from numba import njit, prange
34+
`@njit(parallel=True)``prange` 适合独立外层循环,但线程数仍然要调。
35+
本次 server CPU `kmeans_numba_thread_sweep.csv` 的结论是:
3036

31-
@njit(parallel=True)
32-
def parallel_kernel(...):
33-
for i in prange(n_iter):
34-
...
35-
```
37+
- 32--64 threads 是本次 k-means Numba sweep 的高效区间。
38+
- 128 threads 不是更好;调度、缓存和内存带宽会反噬。
3639

37-
要点:
40+
置换检验也有同样模式:worker count 不是越多越好。当前 slide/poster 中把
41+
这一点放在“tune, do not maximize”的主线上。
3842

39-
- **`prange`**:类似 OpenMP 的并行 for;Numba 负责线程划分与部分归约。
40-
- **归约变量**(如 `s += x[i]`)由 Numba 处理为安全的并行归约(需符合其规则)。
41-
- **只读大数组**:通常所有线程共享只读视图,避免在并行区写同一元素造成数据竞争。
42-
- Numba 有自己的线程池,即便 CPython 是 **GIL 构建**,它也能让外层 `prange` 并行执行——这是置换检验里它全场最快的原因。
43+
## 开发建议
4344

44-
## 4. 本仓库实测(Apple Silicon 8 核, Python 3.12.2)
45+
1. 先写 reference,并用小规模 simulation 锁定统计等价性。
46+
2. 再把最热的循环抽成小函数,添加 `@njit(cache=True)`
47+
3. 单线程版本通过后,再考虑 `parallel=True`
48+
4. 对随机数使用可复现、彼此独立的 seed 或 key;不要让并行顺序影响结果。
49+
5. 记录 cold/warm 时间,讲清首次编译成本和稳态吞吐是两回事。
4550

46-
| 实验 | Numba 版 warm 中位数 | 对照项 | 加速比 |
47-
|------|---------------------|--------|--------|
48-
| k-means N=1M, max_iter=30 | **0.482 s** | NumPy 朴素广播 5.22 s | **10.8×** |
49-
| Permutation R=10000, n=10k | **0.064 s** | NumPy 朴素循环 0.856 s | **13.4×** |
50-
| Permutation R=10000, n=10k | **0.064 s** | `multiprocessing` (8 workers) 1.71 s | **26.8×** |
51+
## 当前可引用材料
5152

52-
[图表链接](../experiments/results/v2/perm_speedup.png) · [数据链接](../experiments/results/v2/perm_sweep.csv)
53-
54-
**演讲可引用的一句话***「改写一个 `@njit(parallel=True)` 的核心,往往比起 `multiprocessing` 省下 8 个进程的启动成本、几百 MB 的数据重复与一整套 pickle 噪声,还能多出一个数量级的速度」。*
55-
56-
## 5. 与演讲实验的对应关系
57-
58-
| 实验 | Numba 角色 |
59-
|------|------------|
60-
| k-means | `@njit` 编译 Lloyd 内层循环/距离计算,对比「纯 NumPy 向量化」 |
61-
| 置换检验 | `@njit(parallel=True)` + `prange` 并行多次置换,对比 **multiprocessing 拷贝****JAX vmap** |
62-
63-
## 6. 调试与开发体验
64-
65-
- 编译失败时,错误信息指向 **不支持的 Python/NumPy 构造**;常需改写为显式循环或预分配缓冲区。新手的痛点 #1 是错误信息里掺杂着 Numba 的中间表示(IR),看不懂就容易放弃。**建议:先只用 `@njit` 单线程跑通,再加 `parallel=True`**
66-
- 可在 nopython 外使用 **print**(有限支持)或暂时关闭 `parallel` 缩小问题范围。
67-
- 性能调优:注意 **内存布局**`float64` 连续数组)、避免在热循环中分配新数组。`fastmath=True` 可开启重结合/矢量化(对 1 ULP 要求严格的统计代码需谨慎)。
68-
- **Numba 的 `cache=True`**:把 AOT 结果写入 `__pycache__/*.nbi/*.nbc`,下次启动复用。我们在 benchmark 中会主动清除此缓存,才能得到诚实的「冷启动」时间。
69-
70-
## 7. 延伸阅读
71-
72-
- [Numba User Guide](https://numba.readthedocs.io/en/stable/user/index.html)
73-
- [Parallel range (`prange`)](https://numba.readthedocs.io/en/stable/user/parallel.html)
74-
- 社区博文示例:[Parallel Bootstrap Sampling with Numba](https://medium.com/@jose.a.poblete/parallel-bootstrap-sampling-in-python-using-numba-b0fe55928a58)
53+
- k-means local figures:
54+
`experiments/results/macbook_air_long/latest/figures/kmeans_reference_equivalence.png`
55+
and `kmeans_shape_stress_runtime.png`
56+
- server CPU figures:
57+
`experiments/results/linux_server_cpu/long_safe_20260503_190133/figures/kmeans_cpu_runtime.png`
58+
and `kmeans_numba_threads.png`
59+
- talk-ready figures:
60+
`experiments/results/presentation_figures/server_kmeans_cpu_a100_summary.png`
61+
and `server_parallelism_tradeoff.png`

0 commit comments

Comments
 (0)