Function

Sign in

Python at the Speed of Rust

Python at the Speed of Rust 蛇正在追赶螃蟹!

Python 是世界上最流行的编程语言。它是一种极其简单且易于访问的语言,使其成为众多领域开发人员的首选。从计算机科学入门课程到驱动我们正在经历的 AI 革命,它被应用于各种场景。

然而,Python 的便利性带来了两个显着的缺点:首先,与 C 或 Rust 等原生语言相比,运行解释型语言会导致执行速度慢得多。其次,将 Python 驱动的函数(例如 Numpy、PyTorch)嵌入到跨平台的消费者应用程序(例如 Web 应用程序、移动应用程序)中非常困难。

但是,如果我们能将 Python 编译成原始的本机代码呢?

编译一个玩具函数

人工智能,特别是大型语言模型 (LLM),严重依赖矩阵乘法。这些矩阵运算的核心是被称为 fused multiply-add (FMA) 的基本运算:

def fma (x, y, z):
  """
  执行一个 fused multiply-add.
  """
  return x * y + z

Nvidia 等硬件供应商提供了专门的指令,可以在一个步骤中执行 FMA,从而减少计算开销并提高数值精度。鉴于 LLM 执行数十亿次这些操作,即使是很小的性能差异也会显着影响整体效率。

result = fma(x=3, y=-1, z=2)
print(result)
# -1

让我们探索如何编译 fma 函数,使其能够以本机速度跨平台运行。

追踪函数

我们首先捕获函数中执行的所有操作作为计算图。我们称之为中间表示 (IR)。此 IR 图显式表示每个操作——算术运算、方法调用和数据访问——使其成为强大的编译抽象。

为了构建此图,我们利用 CPython 的 frame evaluation API 来执行符号追踪 (Symbolic Tracing)。 这允许我们检查 Python 字节码执行,在函数执行时动态捕获每个指令的输入、操作和输出。 通过实时跟踪每个 Python 操作,我们构建了函数逻辑的准确 IR。 例如:

from torch._dynamo.eval_frame import set_eval_frame
# 定义一个 tracer
class Tracer:
  def __call__ (self, frame, _):
    print(frame.f_code, frame.f_func, frame.f_locals)
# 设置 frame evaluation handler
tracer = Tracer()
set_eval_frame(tracer)
# 调用函数
result = fma(x=3, y=-1, z=2)
print(result)
# <code object fma at 0x106c51ca0, file "fma.py", line 9> <function fma at 0x106ba4860> {'x': 3, 'y': -1, 'z': 2}
# -1

跳过几个步骤,我们最终得到一个如下所示的图:

type      name    target    args
-------------- ---------- ------------- --------
input     x     x       ()
input     y     y       ()
input     z     z       ()
call_function mul_result _operator.mul (x, y)
call_function add_result _operator.add (mul_result, z)
output     output   output    (add_result,)

一位敏锐的读者可能会注意到,为了构建上面的 IR 图,我们需要实际调用 fma 函数。 要做到这一点,我们需要将具有正确类型的输入传递给该函数。 我们可以简单地将类型注释添加到我们的 fma 函数,并生成虚假输入来调用该函数:

def fma (x: float, y: float, z: float) -> float:
  """
  执行一个 fused multiply-add.
  """
  return x * y + z

降低到原生代码

现在真正的乐趣开始了! 通过我们的 IR 图和带注释的输入类型,我们开始将 IR 图降低到原生代码的过程。 让我们看一下图中的第一个操作,x * y

我们可以编写(*ahem * 生成)本机代码中 _operator.mul 操作的相应实现。 例如,这是一个 C 实现:

float _operator_mul (float x, float y) {
  return x * y;
}

请注意,由于上面本机实现的返回类型,mul_result 的类型现在被限制为 float。 放大来看,这意味着给定具有已知类型的输入(即来自 Python 中的类型注释)以及 Python 操作的本机实现,我们可以完全确定操作输出的本机类型。 通过对 IR 图中的后续操作重复此过程,我们可以通过整个 Python 函数传播本机类型:

我们现在可以为我们想要的任何平台(WebAssembly、Linux、Android 等等)交叉编译这个本机实现。 这就是我们让 Python 像 Rust 一样快地运行——并且随处运行的方式!

编译函数

让我们使用 Function 基于上述过程编译 fma 函数。 首先,为 Python 安装 Function:

# 在终端中运行
$ pip install --upgrade fxn

接下来,使用 @compile 装饰器装饰 fma 函数:

from fxn import compile
@compile(
  tag="@yusuf/fma",
  description="Fused multiply-add."
)
def fma (x: float, y: float, z: float) -> float:
  """
  执行一个 fused multiply-add.
  """
  return x * y + z

要使用 Function 编译函数,请使用 @compile 装饰器。

最后,使用 Function CLI 编译函数:

# 在终端中运行
$ fxn compile fma.py

使用 Function CLI 编译函数。

让我们进行基准测试!

首先,让我们修改我们的 fma 函数以重复执行 fused multiply-add:

def fma (x: float, y: float, z: float, n_iter: int) -> float:
  for _ in range(n_iter):
    result = x * y + z
  return result

接下来,我们将创建一个等效的 Rust 实现:

use std::os::raw::c_int;
#[no_mangle]
pub extern "C" fn fma (x: f32, y: f32, z: f32, n_iter: c_int) -> f32 {
  let mut result = 0.0;
  for _ in 0..n_iter {
    result = x * y + z;
  }
  result
}

编译两者后,这是我的 MacBook Pro 上的性能图:

编译后的 Python 基准测试比 Rust 慢一个常数因子,因为 Function 具有额外的 scaffolding 来调用预测函数,而 Rust 实现使用直接调用。 您可以检查生成的本机代码,并使用此存储库重现该基准测试:

GitHub - olokobayusuf/python-vs-rust: Python at the speed of Rust using the Function compiler.Python at the speed of Rust using the Function compiler. - olokobayusuf/python-vs-rustGitHubolokobayusuf

总结

能够编译 Python 的前景对我们来说非常令人兴奋。 这意味着我们可以加速科学计算、实时数据处理和 AI 工作负载,以便在更多设备上运行——所有这些都来自 Python 的便利性。

我们的编译器仍然是一个概念验证,但通过它,我们的设计合作伙伴一直在将应用程序投入生产,为从单目深度估计到实时姿势检测的所有内容提供动力。 下一步是什么? 设备上的 LLM 推理。 加入对话:

Join the Function Discord Server!Check out the Function community on Discord - hang out with 1317 other members and enjoy free voice and text chat.Discord

Yusuf Olokoba

Apr 8, 2025 ← Previous

Function © 2025. Powered by Ghost