Show HN:Lucen —— 一个通过注释指令并行化 for 循环的 Python 编译器

Hacker News Top 工具

摘要

Lucen 是一个源到源的 Python 编译器,通过注释指令并行化 for 循环,保证产生位一致的结果,并在无法证明并行安全时安全地回退到顺序执行。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/25 02:05

fcmv/lucen

来源:https://github.com/fcmv/lucen

Lucen

PyPI(https://pypi.org/project/lucen/) Python 许可证 测试

Lucen 是一个面向普通 Python 的源到源编译器及自动循环并行化工具,由注释标记驱动。 与现有的 Python 并行框架不同,它要求你描述 哪里 允许并行,而不是 如何 实现并行,并且它只会并行化那些它能证明既安全又值得的循环。它的唯一保证没有层级,也不可退出:Lucen 永远不会产出错误的结果。

Lucen 并行化一个标记循环,输出位级相同

之前,普通 Python:

for i in range(len(records)):
    scores[i] = score(records[i])

之后,依然普通 Python:

# LUCEN START
for i in range(len(records)):
    scores[i] = score(records[i])
# LUCEN END

在 12 核上快 3.8 倍(CPython 3.14,CPU 密集型映射,经测量)。 输出位级相同,包括浮点数。 没有多进程代码:没有池、没有锁、没有调试 pickle 错误的风险。 采用无风险:如果 Lucen 无法证明某个循环安全,它将完全按照你所写的顺序 Python 执行,并附带结构化报告说明原因。

启动时调用一次即可激活:

import lucen
lucen.activate()

这些标记是普通的注释。一个移除了 Lucen、未安装、未激活或从未包含 Lucen 的文件,其运行方式与不存在 Lucen 时完全相同。我们称之为“注释不变性”,它是承重性的:采用 Lucen 的最坏情况就是你原有的程序。

它涵盖了另一种情况:代码库中已有的循环,没人愿意重构。无需工作函数、无需池生命周期、无需序列化管道、无需重写为框架的形状。两条注释可以无缝插入现有代码,删除它们则可撤销采用。


目录: 三大保证 | 安装 | 教程 | 进阶用法 | 专家指南 | 工作原理 | 性能 | 局限性 | 诚实契约 | 文档 | 贡献 | 许可证


三大保证

  1. 绝不产生错误结果。 每个块写入私有切片,在合并时检查不重叠性,并按块顺序提交。字典插入顺序、浮点归约比特以及错误中途的容器状态与顺序执行完全一致,逐比特相同。写冲突会放弃并行尝试,并透明地重新按顺序运行你的循环。
  2. 绝不造成破坏。 任何 Lucen 无法证明安全的循环,都会按照你编写的顺序 Python 运行,原因会写入结构化降级报告,而非 stderr。异常保持其类型、消息以及容器的精确顺序前缀状态。
  3. 绝不无声无息地做无用功。 收益性门控(静态预检查加上运行时探测,该探测在测量过程中执行实际工作)拒绝并行化那些会败给调度开销的循环,并报告这一点。你无法观察到的并行性在此是 bug,而非 shrug(无所谓)。

这些并非愿景。它们是通过构造强制执行的,并通过跨版本测试矩阵验证:7 个解释器 × 8 个工作负载 × 4 条执行路径,每个单元格都与普通 Python 位级相同。参见 BENCHMARK.md

安装

pip install lucen

需要 Python 3.9 或更高版本。3.11+ 无需依赖项;在 3.9 和 3.10 上,TOML 解析器依赖项(tomli)会自动安装。

从源码安装(可选 Rust 加速核心,需要 Rust 工具链):

git clone https://github.com/fcmv/lucen
cd lucen
pip install -e ".[dev]"
pytest

在 3.9 到 3.14 的 GIL 构建上,pip 会安装原生核心(Rust,abi3,每个平台一个二进制文件),用于运行编排热循环(写集审计和按引用归约折叠)。在自由线程构建上(abi3 核心无法加载),pip 会改为安装纯 Python wheel,因此安装始终成功;然后 Lucen 会运行其纯 Python 后备方案,该方案完全受支持并通过相同的测试套件。

支持的解释器

解释器状态原生核心
CPython 3.9 到 3.14(GIL)受支持,每次发布均测试
CPython 3.13t / 3.14t(自由线程)受支持,已测试纯 Python 后备
PyPy 3.11受支持,在后备方案上测试纯 Python 后备
GraalPy尽力而为,在后备方案上测试纯 Python 后备

教程:从零到首次加速

本演练假设你只有基础 Python 知识。每一步都显示真实的命令和输出结构。

步骤 1:安装

pip install lucen

步骤 2:从一个太慢的程序开始

保存为 work.py。它用一个 CPU 密集型函数对 20,000 条记录进行评分,纯 Python,目前没有任何 Lucen:

import math

def score(x):
    acc = 0.0
    for k in range(400):
        acc += math.sin(x * 0.001 + k) * math.cos(k * 0.5)
    return acc

def main():
    records = list(range(20_000))
    scores = [0.0] * len(records)
    for i in range(len(records)):
        scores[i] = score(records[i])
    print(f"checksum: {sum(scores):.6f}")

if __name__ == "__main__":
    main()
python work.py     # 大约花费一秒钟纯计算

步骤 3:标记循环

在循环周围添加两行注释。其他不做任何改动:

    # LUCEN START
    for i in range(len(records)):
        scores[i] = score(records[i])
    # LUCEN END

再次运行。什么都不会发生。这正是要点:标记是注释,而且你尚未激活任何功能。你的程序与之前一样安全。

步骤 4:运行它

使用 lucen run 运行该文件。它会在你指向的脚本中重写标记的循环,然后执行它,因此你刚刚标记的循环会并行运行:

lucen run work.py

对于一个直接启动的脚本,这就是全部内容。如果 Lucen 嵌入在你自行启动的更大应用程序中,只需在启动时激活一次导入钩子。激活会安装钩子,因此它必须在你包含标记循环的模块被导入之前运行,这就是为什么该循环放在一个导入的模块中:

# app.py
import lucen
lucen.activate()

import work
work.main()
python app.py

在多核机器上,该循环现在运行速度大约快 3 到 4 倍,并且校验和与数字完全相同,而不是近似相同。一模一样。

无论哪种方式,有两点需要了解:

  • 在 Windows 和 macOS 上,work.py 中的 if __name__ == "__main__": 保护很重要。Lucen 在这些平台上使用进程工作器,并且 Python 会在每个工作器内部重新导入入口模块。Lucen 会检测到缺少保护并回退到顺序执行,同时提示你添加该保护,因此故障模式是变慢,而非崩溃。
  • activate() 是幂等的,并且在程序启动时调用一次是安全的。

步骤 5:不运行任何内容,看看 Lucen 的决定

lucen explain work.py
work.py: 1 marked block(s) [gil interpreter assumed]

Block 1 (line 12)
  + Parallelized
  Backend: PROCESS (THREAD needs a free-threaded interpreter) (GIL interpreter assumed)
  Runtime-dependent (never reported statically): argument picklability,
  custom-callable well-formedness, pool availability -- see `lucen profile`.

explain 是静态且诚实的:事实作为事实报告,任何仅在调用时可知的内容绝不会报告为“是”或“否”。

步骤 6:理解拒绝情况

将循环体改为依赖前一个元素:

    # LUCEN START
    for i in range(1, len(scores)):
        scores[i] = scores[i - 1] + score(records[i])
    # LUCEN END
lucen explain work.py
Block 1 (line 12)
  - Sequential
  Reason: cross-iteration dependency 'scores[i - 1]' (monotonic chain); ...

你的程序仍然运行,并且仍然产生正确答案。Lucen 只是拒绝并行化它无法证明安全的内容,并会告诉你原因。这是第二个保证按设计工作。

步骤 7:理解“不值得”的情况

标记一个微不足道的循环:

    # LUCEN START
    for i in range(len(xs)):
        ys[i] = xs[i] * 2 + 1
    # LUCEN END

在运行时,Lucen 探测第一个块,测量每次迭代大约 50 纳秒,计算进程调度开销大于节省,然后全速按顺序运行整个循环。降级报告显示:

lucen fallback: PARALLEL_UNPROFITABLE (work.py:12): measured ~50
ns/iteration loses to dispatch overhead; ran SEQUENTIAL (calibrate=false
overrides, spec 5.17)

如果你认为门控对你的情况判断有误,可以按块覆盖:

# LUCEN START calibrate=false

步骤 8:以编程方式读取降级报告

import lucen
lucen.activate()

import work
work.main()

for record in lucen.get_fallback_report():
    print(record.error, record.file, record.line, record.message)

Lucen 决定的任何内容都不会隐藏。每次降级都有原因字符串和位置,lucen profile script.py 显示每个块实际运行的内容及时间。

以上就是完整的朴素工作流程:标记、激活、阅读反馈。你无需了解 slab 或 wavefront 是什么,就能获得正确的并行性。

进阶用法

调整通过标记子句进行。每个子句只会将 Lucen 持有的证明替换为用户持有的断言,或将精确性替换为速度,绝不会给出不同的答案。格式错误的子句会在导入时产生明确的错误,并附带“你是不是想用”建议,而不会无声地忽略。

# LUCEN START calibrate=false, timeout=5.0, on_error=collect

完整的表面有 # LUCEN START 上的 15 种子句和 # LUCEN TRUST 上的 2 种子句。完整参考(包含所有接受形式)在 docs/pragmas.md;总结如下:

子句作用
backend=固定后端:threadprocesssequential,可带 pool_size/chunks
calibrate=控制收益性门控(false 强制并行)
grainsize=已识别 DAG 波前的层级宽度
affinity=CPU 亲和性:compactscatterexplicit(cores=[...])
nested=当块位于另一个并行块内部时的策略
depend=断言独立性(none)或无环顺序(专家)
skip_runtime_check=禁用运行时写集审计(专家,需配合 depend=none
trust=放弃纯度或 pickle 检查:callablespickleall
reduce=指定归约操作(summin……)或 custom(fn=, identity=)
reduction_order=sequential_equivalent(默认,位级相同)、stablecustom
timeout=限制挂墙时间;抛出 ParallelTimeoutError
on_error=收集每次迭代的异常,而非快速失败(collect
strict=将此块的降级转为硬错误(CI 模式)
on_fallback=设置此块降级如何呈现
progress=按块或按迭代报告进度

关于 # LUCEN TRUST 放在辅助 def 之上:args=(参数如何被信任)和 qualname=(信任适用的可调用对象)。

项目范围的默认值和硬上限位于项目根目录的 lucen.toml 中:池大小、超时上限、实验功能否决、错误详细程度。退化值会在加载时被拒绝,其严格程度与标记子句相同。

CI 集成:lucen explain --strict --baseline baseline.json 如果任何块的分类相对于已提交的基线退化,则构建失败,因此重构中热循环被默默取消并行化的情况会在审查中被发现,而非生产环境。

专家指南

专家的表面是信任系统。Lucen 证明它所能证明的,其余部分信任你,每次一个明确的断言。

断言一个辅助函数是并行安全的。 Lucen 能静态证明辅助函数的纯度(如果能读取其源码)。一个被 证明 有状态的辅助函数(修改模块全局变量、使用 random 状态、执行 I/O)会使该块顺序执行,并附报告指明该辅助函数。三种覆盖方式,最窄优先:

# LUCEN TRUST
def my_helper(x):
    ...                      # 你断言此 def 在并行下是安全的
# LUCEN START trust=callables      (此块信任其所有辅助函数)
[trust]                                # lucen.toml,项目范围
callables = ["mylib.fast_path"]

断言分析器无法看到的独立性。 depend=none 断言你的写入是不重叠的。运行时写集审计仍会运行,如果断言为假则会捕获你。再加上 skip_runtime_check=true 将禁用该审计。需要这两个断言(两个明确的谎言)才能让 Lucen 产生错误结果;红队测试确认一个谎言始终会被捕获。

断言 pickle 保真度。 进程后端验证第一个块的参数包在序列化时是否在字节稳定的固定点保持不变,这可以捕获值偏移的 __reduce__/__getstate__ 实现。trust=pickle 放弃该检查。

实验功能,默认关闭,按进程启用:

lucen.activate(experimental=["early_exit", "typed_buffers"])
标志效果
early_exit并行化包含 break 的循环,具有精确的首次匹配语义
on_error=collect已在生产环境中
typed_buffers密集数组输出映射在 PROCESS 上发送类型化的结果 slab(传输速度约快 3 倍)
branch_sensitive_deps在运行时审计下进行分支级依赖分类

lucen.toml 中的 [limits] allow_experimental = false 会否决所有实验功能,适合集群运营者。

工作原理

一条管道,五个阶段,每个阶段都有书面的决策记录:

scanner -> rewriter -> selector -> codegen -> dispatch
(扫描)    (重写)     (选择)    (代码生成)  (调度)
(pragmas)  (分类)     (路由)    (双胞胎)   (执行 + 审计 + 提交)

重写器对块中的每个名称进行分类(循环局部变量、只读变量、归约累加器、索引写入、跨迭代读取),并分析性地识别依赖形状,包括 results[i // 2] 风格的 DAG。选择器将每个块路由到相应的后端:符合并行条件的形状送往后端,其他所有内容送往顺序执行,并附带原因。代码生成为每个块生成两个函数:一个供工作器使用的块函数,以及一个顺序双胞胎(也是降级路径),因此顺序行为 就是 你的原始循环。调度在持久池上运行块,在合并时审计写不重叠性,按精确元素顺序折叠归约,并按块顺序提交。

编排热循环在 Rust 核心中运行(根据测量,它们应该在那里):写集审计是一个原生调用,覆盖所有块(Python 循环的 4.5 倍),归约折叠通过 CPython 自己的数字协议 按引用 原生运行,因此大数、浮点比特和用户定义的操作符行为与顺序循环完全相同(5 倍)。逐元素提交已经移植,但测量发现比 CPython 专门的列表存储慢,因此故意保留在 Python 中;循环体本身始终是你的 Python 代码。将符合条件的体编译为原生内核是路线图中的旗舰项目,而非当前功能。

后端选择是静态的且与解释器无关:映射和归约在 GIL 和自由线程构建上都运行在 PROCESS 上(测量:即便没有 GIL,共享对象引用计数使得线程在共享容器工作负载上失败),THREAD 服务于按引用块和自由线程的重计算任务,所有更轻量的任务则顺序执行。完整细节见技术规范

性能

跨七个解释器(CPython 3.9、3.10、3.11、3.12、3.13、3.14 以及 3.14 自由线程)的总结,热身后 5 次的中位数,12 核机器。“原生 Python” 是指将标记视为注释的相同文件,c

相似文章

Show HN: Runloom – 适用于Python自由线程的Go风格协程

Hacker News Top

Runloom是一个新的Python库,为自由线程Python(3.13t/3.14t)提供了Go风格的栈式协程和任务窃取调度器,使得阻塞代码能够在无GIL的情况下跨核心并发运行。它声称在生成吞吐量和调度性能上可以与Go匹敌或超越。