What does torch.compile do? Find out in Python · torch.compile ทำอะไร? มาดูด้วย Python · torch.compile 做了什么?用 Python 看个明白

This page teaches what happens when you add one line, model = torch.compile(model), to a PyTorch program, and why it makes some models much faster and others no faster at all. You learn it by running small Python files that show each part at work: · หน้านี้สอนว่าเกิดอะไรขึ้นเมื่อคุณเพิ่มบรรทัดเดียว model = torch.compile(model) ลงในโปรแกรม PyTorch และทำไมบางโมเดลจึงเร็วขึ้นมาก ส่วนบางโมเดลไม่เร็วขึ้นเลย คุณเรียนด้วยการรันไฟล์ Python เล็ก ๆ ที่แสดงการทำงานของแต่ละส่วน: · 这一页教你:在 PyTorch 程序里加上一行 model = torch.compile(model) 时发生了什么,以及为什么有的模型快了很多,有的却一点也没快。你通过运行几个小小的 Python 文件来学,它们会展示每个部分怎样工作:

It follows a short video by IBM Technology. You need some Python, about a week of it, and no knowledge of how a neural network learns. Every Python file on this page was run, and what it printed is shown under it. Two of them you can run right here with ▶ Run. A quiz after most steps checks the idea. · หน้านี้เดินตามวิดีโอสั้นของ IBM Technology คุณต้องรู้ Python บ้าง ประมาณหนึ่งสัปดาห์ และไม่ต้องรู้ว่าโครงข่ายประสาทเรียนรู้อย่างไร ไฟล์ Python ทุกไฟล์ในหน้านี้ถูกรันจริง และสิ่งที่มันพิมพ์แสดงอยู่ข้างใต้ มีสองไฟล์ที่คุณรันตรงนี้ได้เลยด้วย ▶ Run และหลังขั้นตอนส่วนใหญ่มีแบบทดสอบเช็กความเข้าใจ · 它跟着 IBM Technology 的一个短视频走。你需要会一点 Python,大约学过一个星期就行,不需要知道神经网络怎样学习。这一页的每个 Python 文件都运行过,它打印的内容就显示在下面。其中两个你可以就在这里用 ▶ Run 运行。大多数步骤之后有一个小测验,检查你是否懂了。

Before you start · ก่อนเริ่ม · 开始之前

🇬🇧 English

The video is What Is torch.compile? Dynamo, PyTorch & Faster AI Models Explained, in which Avery Blanchard of IBM Technology explains the idea in 7 minutes. Watch it first, or come back to it. This page takes the same path, and at each stop you run the thing yourself.

PyTorch normally runs your model one operation at a time: Python reaches a line, and PyTorch does that one job. torch.compile gives it a chance to look at many operations together and write better code for all of them. The important word is chance. It is not a promise, and step 9 shows both results.

Seven words you will meet:

  • tensor: a table of numbers, the thing PyTorch works on.
  • model: a Python class whose forward method does the calculation.
  • kernel: a piece of compiled code that does one piece of the work.
  • graph: a list of the operations and of what flows between them.
  • guard: a condition that must still be true to reuse compiled code.
  • fusion: doing several operations in one kernel.
  • graph break: a place where capture stops, because of code that cannot go in the graph.

🇹🇭 ไทย

วิดีโอคือ What Is torch.compile? Dynamo, PyTorch & Faster AI Models Explained ซึ่ง Avery Blanchard จาก IBM Technology อธิบายแนวคิดนี้ใน 7 นาที จะดูก่อนหรือกลับมาดูทีหลังก็ได้ หน้านี้เดินเส้นทางเดียวกัน และในแต่ละจุดคุณจะได้รันเอง

ปกติ PyTorch รันโมเดลทีละการดำเนินการ: Python ถึงบรรทัดหนึ่ง แล้ว PyTorch ก็ทำงานนั้นงานเดียว torch.compile ให้โอกาสมันมองการดำเนินการหลาย ๆ อย่างพร้อมกัน แล้วเขียนโค้ดที่ดีกว่าให้ทั้งหมด คำสำคัญคือโอกาส มันไม่ใช่คำสัญญา และขั้นตอนที่ 9 แสดงผลทั้งสองแบบ

เจ็ดคำที่คุณจะได้เจอ:

  • tensor (เทนเซอร์): ตารางตัวเลข สิ่งที่ PyTorch ทำงานด้วย
  • model (โมเดล): คลาส Python ที่เมธอด forward ทำการคำนวณ
  • kernel (เคอร์เนล): โค้ดที่คอมไพล์แล้วซึ่งทำงานหนึ่งส่วน
  • graph (กราฟ): รายการการดำเนินการ และสิ่งที่ไหลระหว่างการดำเนินการเหล่านั้น
  • guard (การ์ด): เงื่อนไขที่ต้องยังจริงอยู่ จึงจะใช้โค้ดที่คอมไพล์แล้วซ้ำได้
  • fusion (การรวมงาน): การทำหลายการดำเนินการในเคอร์เนลเดียว
  • graph break (กราฟขาด): จุดที่การจับกราฟหยุด เพราะมีโค้ดที่ใส่ในกราฟไม่ได้

🇨🇳 中文

视频是 What Is torch.compile? Dynamo, PyTorch & Faster AI Models Explained,IBM Technology 的 Avery Blanchard 用 7 分钟讲清这个思路。可以先看,也可以回头再看。这一页走同样的路,每到一站你都亲手运行一次。

PyTorch 平常一次运行模型的一个操作:Python 走到一行,PyTorch 就做那一件事。torch.compile 给它一个机会,把许多操作放在一起看,为它们写出更好的代码。关键词就是“机会”。它不是保证,第 9 步会展示两种结果。

你会遇到的七个词:

  • tensor(张量):一张数字表,PyTorch 处理的就是它。
  • model(模型):一个 Python 类,它的 forward 方法负责计算。
  • kernel(内核):一段编译好的代码,完成一部分工作。
  • graph(图):操作的清单,以及它们之间流动的东西。
  • guard(守卫):要重用编译好的代码,必须仍然成立的条件。
  • fusion(融合):在一个内核里完成几个操作。
  • graph break(图中断):捕获停下来的地方,因为那里的代码放不进图。
Dynamo
Watches your Python run, captures the tensor operations in a graph, and records guards.เฝ้าดู Python ของคุณขณะรัน จับการดำเนินการกับเทนเซอร์ลงในกราฟ และบันทึก guard看着你的 Python 运行,把张量操作捕获成图,并记下 guard。
see_graph.py, recompile.py
→
AOT Autograd
Prepares the graph for the compiler, and for training also captures the backward graph.เตรียมกราฟให้คอมไพเลอร์ และเมื่อฝึกโมเดลก็จับกราฟขากลับด้วย为编译器准备好图;训练时还会捕获反向图。
train_graphs.py
→
Inductor
Writes the code for your hardware, and fuses operations where that helps.เขียนโค้ดสำหรับฮาร์ดแวร์ของคุณ และรวมการดำเนินการในที่ที่ช่วยได้为你的硬件写代码,在有帮助的地方把操作融合起来。
see_kernels.py
→
Later callsครั้งต่อ ๆ ไป之后的调用
Check the guards. If they still hold, reuse the code. If not, compile again.ตรวจ guard ถ้ายังจริงก็ใช้โค้ดเดิมซ้ำ ถ้าไม่จริงก็คอมไพล์ใหม่检查 guard。还成立就重用代码,不成立就重新编译。
benchmark.py

1 check_setup.py: what it needs · check_setup.py: สิ่งที่ต้องมี · check_setup.py:它需要什么

🇬🇧 English

On a CPU, Inductor writes C++, so torch.compile needs a C++ compiler and Python's own C header, Python.h. PyTorch does not bring them. We ran this page in Ubuntu, under WSL on a Windows laptop. Linux and WSL take the commands below. On Windows, first type wsl --install in PowerShell, restart, and open Ubuntu.

Each command prepares one thing: g++ is the compiler, python3-dev holds Python.h, python3-venv makes a separate place for this project's libraries. The address after --index-url asks for PyTorch without GPU support, about 200 MB and not several GB, which is all this page needs.

🇹🇭 ไทย

บน CPU นั้น Inductor เขียน C++ ดังนั้น torch.compile จึงต้องมีคอมไพเลอร์ C++ และไฟล์เฮดเดอร์ภาษา C ของ Python เอง คือ Python.h ซึ่ง PyTorch ไม่ได้ให้มาด้วย เรารันหน้านี้ใน Ubuntu ภายใต้ WSL บนแล็ปท็อป Windows ส่วน Linux และ WSL ใช้คำสั่งข้างล่าง ถ้าใช้ Windows ให้พิมพ์ wsl --install ใน PowerShell ก่อน รีสตาร์ต แล้วเปิด Ubuntu

แต่ละคำสั่งเตรียมสิ่งหนึ่ง: g++ คือคอมไพเลอร์ python3-dev มี Python.h และ python3-venv สร้างที่แยกไว้สำหรับไลบรารีของโปรเจกต์นี้ ที่อยู่หลัง --index-url ขอ PyTorch แบบไม่รองรับ GPU ขนาดประมาณ 200 MB ไม่ใช่หลาย GB ซึ่งหน้านี้ใช้แค่นั้นก็พอ

🇨🇳 中文

在 CPU 上,Inductor 写的是 C++,所以 torch.compile 需要一个 C++ 编译器,以及 Python 自己的 C 头文件 Python.h。PyTorch 不会带上它们。我们是在 Windows 笔记本的 WSL 里、在 Ubuntu 上运行这一页的。Linux 和 WSL 用下面的命令。如果用 Windows,先在 PowerShell 里输入 wsl --install,重启,再打开 Ubuntu。

每条命令准备一样东西:g++ 是编译器,python3-dev 里有 Python.h,python3-venv 为这个项目的库单独开一个地方。--index-url 后面的地址要的是不带 GPU 支持的 PyTorch,大约 200 MB,而不是好几 GB,这一页有它就够了。

In the Ubuntu terminalในเทอร์มินัลของ Ubuntu在 Ubuntu 终端里

sudo apt update
sudo apt install g++ python3-dev python3-venv
mkdir ~/compile_lab
cd ~/compile_lab
python3 -m venv venv
source venv/bin/activate
pip install --index-url https://download.pytorch.org/whl/cpu torch
pip install numpy

not run by us: on a Mac, instead of the lines aboveเราไม่ได้รันเอง: บนแมค ใช้แทนบรรทัดข้างบน我们没有运行过:在 Mac 上,代替上面几行

xcode-select --install
mkdir ~/compile_lab
cd ~/compile_lab
python3 -m venv venv
source venv/bin/activate
pip install torch numpy

🇬🇧 English

Save each file of this page in ~/compile_lab, with vim check_setup.py (see Python start for vim) or its Download button. Run each one with python and its name, with the venv active. If you come back later, type source venv/bin/activate in that folder first.

🇹🇭 ไทย

บันทึกแต่ละไฟล์ของหน้านี้ไว้ใน ~/compile_lab ด้วย vim check_setup.py (ดูเรื่อง vim ที่ Python start) หรือด้วยปุ่ม Download ของมัน แล้วรันแต่ละไฟล์ด้วย python ตามด้วยชื่อไฟล์ โดยเปิด venv ไว้ ถ้ากลับมาทำทีหลัง ให้พิมพ์ source venv/bin/activate ในโฟลเดอร์นั้นก่อน

🇨🇳 中文

把这一页的每个文件都保存在 ~/compile_lab 里,用 vim check_setup.py(vim 的用法见 Python start)或它的 Download 按钮。在 venv 激活的状态下,用 python 加文件名运行每个文件。以后回来时,先在那个文件夹里输入 source venv/bin/activate。

check_setup.py

# Is everything here that torch.compile needs?
import shutil
import sys
import sysconfig
from pathlib import Path

import torch

print("Python ", sys.version.split()[0])
print("PyTorch", torch.__version__)
print("GPU    ", torch.cuda.is_available())

# Inductor writes C++ for a CPU, so a C++ compiler has to do the last step.
for compiler in ["g++", "clang++", "cl"]:
    print(f"{compiler:8}", "found" if shutil.which(compiler) else "not found")

# The C++ that Inductor writes has #include <Python.h> in it.
header = Path(sysconfig.get_paths()["include"]) / "Python.h"
print("Python.h", "found" if header.exists() else "not found")

What it printed for usสิ่งที่มันพิมพ์ให้เรา它给我们打印的内容

Python  3.14.4
PyTorch 2.15.0+cpu
GPU     False
g++      found
clang++  not found
cl       not found
Python.h found
If there is no compilerถ้าไม่มีคอมไพเลอร์如果没有编译器 We also tried plain Windows, with no compiler installed. import torch worked and the wrapping worked, and the first call stopped with the line below. Steps 2, 4, 6 and 8 stop before Inductor, so they work even so, except the fullgraph=True part of step 8. We did not try installing Microsoft's compiler; WSL was the shorter road. On Ubuntu, the matching message without python3-dev is fatal error: Python.h: No such file or directory. เราลองบน Windows ธรรมดาที่ไม่มีคอมไพเลอร์ด้วย import torch ใช้ได้ การห่อก็ใช้ได้ และการเรียกครั้งแรกหยุดพร้อมบรรทัดข้างล่าง ขั้นตอนที่ 2, 4, 6 และ 8 หยุดก่อนถึง Inductor จึงยังทำงานได้ ยกเว้นส่วน fullgraph=True ของขั้นตอนที่ 8 เราไม่ได้ลองติดตั้งคอมไพเลอร์ของ Microsoft เพราะ WSL เป็นทางที่สั้นกว่า บน Ubuntu ถ้าไม่มี python3-dev ข้อความที่ตรงกันคือ fatal error: Python.h: No such file or directory 我们也试过没有装编译器的普通 Windows。import torch 可以,包装也可以,第一次调用则停在下面这一行。第 2、4、6、8 步在到达 Inductor 之前就停了,所以照样能运行,只有第 8 步的 fullgraph=True 部分除外。我们没有试过安装微软的编译器,因为 WSL 是更短的路。在 Ubuntu 上,缺 python3-dev 时对应的消息是 fatal error: Python.h: No such file or directory。
torch._inductor.exc.InductorError: RuntimeError: Compiler: cl is not found.

2 A Python class that wraps a function · คลาส Python ที่ห่อฟังก์ชัน · 一个包住函数的 Python 类

🇬🇧 English

Before the real thing, here is its shape in plain Python, with no PyTorch at all. LittleCompile holds a function and a dictionary. Three pieces of Python do the work:

  • __call__ lets you write compiled([1, 2, 3]), using an object as if it were a function. That is how the wrapper can stand in for what it wraps.
  • The guard is a tuple: how many numbers, and of what type. A tuple can be a dictionary key.
  • The cache is the dictionary. One finished version for each guard.

Press ▶ Run. Then change a list in main(), and guess before you run it again which lines will say new.

🇹🇭 ไทย

ก่อนถึงของจริง นี่คือรูปร่างของมันใน Python ล้วน ๆ ไม่มี PyTorch เลย LittleCompile เก็บฟังก์ชันหนึ่งตัวและดิกชันนารีหนึ่งตัว Python สามส่วนทำงานนี้:

  • __call__ ทำให้คุณเขียน compiled([1, 2, 3]) ได้ คือใช้ออบเจกต์เหมือนเป็นฟังก์ชัน ตัวห่อจึงยืนแทนสิ่งที่มันห่อได้
  • guard คือทูเพิล: มีตัวเลขกี่ตัว และเป็นชนิดอะไร ทูเพิลใช้เป็นคีย์ของดิกชันนารีได้
  • cache คือดิกชันนารี หนึ่ง guard มีเวอร์ชันที่เสร็จแล้วหนึ่งเวอร์ชัน

กด ▶ Run แล้วแก้ลิสต์ใน main() และเดาก่อนรันอีกครั้งว่าบรรทัดไหนจะพูดว่า new

🇨🇳 中文

在看真东西之前,先看它在纯 Python 里的样子,完全不用 PyTorch。LittleCompile 存着一个函数和一个字典。三样 Python 的东西完成这件事:

  • __call__ 让你可以写 compiled([1, 2, 3]),把对象当函数用。包装器就是这样代替被包装的东西的。
  • guard 是一个元组:有多少个数字,是什么类型。元组可以当字典的键。
  • cache 就是那个字典。每个 guard 对应一个做好的版本。

按 ▶ Run。然后改一改 main() 里的列表,再次运行之前先猜猜哪些行会说 new。

toy_compile.py

# A pretend torch.compile in plain Python: a wrapper, a guard and a cache.
# Nothing here is PyTorch. It only shows the shape of the idea.
import time


class LittleCompile:
    def __init__(self, program):
        self.program = program
        self.cache = {}  # one finished version for each guard

    # __call__ lets us use the object as if it were a function.
    def __call__(self, numbers):
        # The guard: what this version was made for.
        guard = (len(numbers), type(numbers[0]).__name__)
        if guard in self.cache:
            print(guard, "guard matches: reuse")
        else:
            print(guard, "new: compile (slow)")
            time.sleep(0.5)  # stands in for the real work of compiling
            self.cache[guard] = self.program
        return self.cache[guard](numbers)


def double_all(numbers):
    return [n * 2 for n in numbers]


def main():
    compiled = LittleCompile(double_all)
    print("wrapped, and nothing is compiled yet:", compiled.cache)
    print(compiled([1, 2, 3]))
    print(compiled([4, 5, 6]))
    print(compiled([1, 2, 3, 4]))
    print(compiled([1.5, 2.5, 3.5]))
    print(compiled([7, 8, 9]))
    print("versions kept:", len(compiled.cache))


if __name__ == "__main__":
    main()

What it printed for usสิ่งที่มันพิมพ์ให้เรา它给我们打印的内容

wrapped, and nothing is compiled yet: {}
(3, 'int') new: compile (slow)
[2, 4, 6]
(3, 'int') guard matches: reuse
[8, 10, 12]
(4, 'int') new: compile (slow)
[2, 4, 6, 8]
(3, 'float') new: compile (slow)
[3.0, 5.0, 7.0]
(3, 'int') guard matches: reuse
[14, 16, 18]
versions kept: 3

Quizแบบทดสอบ小测验

A wrapper keeps one version of a program for each (length, type) pair. It has run on three whole numbers. Which next input makes it compile again?ตัวห่อเก็บโปรแกรมหนึ่งเวอร์ชันต่อหนึ่งคู่ (ความยาว, ชนิด) มันเคยรันกับจำนวนเต็มสามตัวแล้ว ข้อมูลเข้าถัดไปแบบไหนทำให้มันคอมไพล์ใหม่?一个包装器为每一对(长度,类型)保留程序的一个版本。它已经用三个整数运行过。下一个什么样的输入会让它重新编译?

Yes.ถูกต้อง对。Not this time.ยังไม่ถูก这次不对。The values do not matter to this guard, only how many there are and their type. Decimals change the type.สำหรับ guard นี้ ค่าไม่สำคัญ สำคัญแค่จำนวนและชนิด ทศนิยมเปลี่ยนชนิด对这个 guard 来说,数值不重要,重要的只是个数和类型。小数改变了类型。

3 first_call.py: lazy compiling · first_call.py: คอมไพล์เมื่อถึงเวลา · first_call.py:懒惰的编译

🇬🇧 English

Now the real one. TinyNet is a very small model: torch.nn.Linear(64, 64) is a layer that multiplies by a table of weights and adds a bias, and torch.relu turns negative numbers into zero. x is a tensor of 8 rows with 64 numbers each. torch.no_grad() says we are not training, so nothing needs gradients.

Wrapping does almost nothing, because compiling is lazy: PyTorch waits for the first call, since only then can it see the shape, type and device of your input.

🇹🇭 ไทย

คราวนี้ของจริง TinyNet เป็นโมเดลเล็กมาก: torch.nn.Linear(64, 64) คือเลเยอร์ที่คูณด้วยตารางน้ำหนักแล้วบวกค่าเอนเอียง (bias) และ torch.relu เปลี่ยนจำนวนลบเป็นศูนย์ x คือเทนเซอร์ 8 แถว แถวละ 64 ตัวเลข torch.no_grad() บอกว่าเราไม่ได้ฝึกโมเดล จึงไม่ต้องใช้เกรเดียนต์

การห่อแทบไม่ได้ทำอะไร เพราะการคอมไพล์นั้นขี้เกียจ (lazy): PyTorch รอการเรียกครั้งแรก เพราะตอนนั้นเท่านั้นมันจึงเห็นรูปร่าง ชนิด และอุปกรณ์ของข้อมูลเข้า

🇨🇳 中文

现在看真的。TinyNet 是个很小的模型:torch.nn.Linear(64, 64) 这一层乘上一张权重表,再加上偏置(bias),torch.relu 把负数变成零。x 是一个张量,8 行,每行 64 个数字。torch.no_grad() 表示我们不在训练,所以不需要梯度。

包装几乎什么也没做,因为编译是懒惰(lazy)的:PyTorch 要等到第一次调用,因为只有那时它才看得到输入的形状、类型和设备。

first_call.py

# torch.compile only wraps. The first call pays for compiling. Later calls do not.
import time

import torch


class TinyNet(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = torch.nn.Linear(64, 64)

    def forward(self, x):
        return torch.relu(self.layer(x) + 1.0)


def main():
    net = TinyNet()
    x = torch.randn(8, 64)

    # The very first torch.compile also loads the compiler's own Python files.
    # So we wrap once to get that out of the way, and time the second wrap.
    torch.compile(net)
    start = time.perf_counter()
    model = torch.compile(net)
    print(f"wrapping took {time.perf_counter() - start:.4f} s")
    print("model is a", type(model).__name__, "around a", type(net).__name__)

    with torch.no_grad():
        for call in [1, 2, 3]:
            start = time.perf_counter()
            model(x)
            print(f"call {call}: {time.perf_counter() - start:.4f} s")


if __name__ == "__main__":
    main()

The first time we ever ran it, with an empty compile cacheครั้งแรกสุดที่เรารันมัน โดยแคชการคอมไพล์ว่างเปล่า我们第一次运行它,编译缓存是空的

wrapping took 0.0007 s
model is a OptimizedModule around a TinyNet
call 1: 8.5745 s
call 2: 0.0002 s
call 3: 0.0000 s

The same file, run again straight afterไฟล์เดิม รันอีกครั้งทันทีหลังจากนั้น同一个文件,紧接着再运行一次

wrapping took 0.0007 s
model is a OptimizedModule around a TinyNet
call 1: 0.5145 s
call 2: 0.0001 s
call 3: 0.0001 s

🇬🇧 English

Three speeds to notice. The very first call took 8.5 s, and call 2 took 0.0002 s. The second program's first call took 0.5 s: a new program has to trace and check again, but much of the compiler's one-time work is kept in a cache folder on disk. So never judge torch.compile by the first call.

🇹🇭 ไทย

มีความเร็วสามแบบให้สังเกต การเรียกครั้งแรกสุดใช้เวลา 8.5 วินาที และการเรียกครั้งที่ 2 ใช้ 0.0002 วินาที การเรียกครั้งแรกของโปรแกรมรอบที่สองใช้ 0.5 วินาที: โปรแกรมใหม่ต้องติดตามและตรวจสอบอีกครั้ง แต่งานที่คอมไพเลอร์ทำครั้งเดียวส่วนใหญ่ถูกเก็บไว้ในโฟลเดอร์แคชบนดิสก์ ดังนั้นอย่าตัดสิน torch.compile จากการเรียกครั้งแรก

🇨🇳 中文

有三种速度值得注意。最初那次调用花了 8.5 秒,第 2 次调用花了 0.0002 秒。第二个程序的第一次调用花了 0.5 秒:新程序要重新跟踪和检查,但编译器那些只需做一次的工作,大部分保存在磁盘上的缓存文件夹里。所以千万不要凭第一次调用来评判 torch.compile。

Quizแบบทดสอบ小测验

Why does PyTorch wait for the first call before it compiles a model?ทำไม PyTorch จึงรอการเรียกครั้งแรกก่อนจะคอมไพล์โมเดล?PyTorch 为什么要等到第一次调用才编译模型?

Yes.ถูกต้อง对。Not this time.ยังไม่ถูก这次不对。The code is written for those particular inputs. It downloads nothing: the compiler has to be there already.โค้ดถูกเขียนสำหรับข้อมูลเข้าแบบนั้นโดยเฉพาะ มันไม่ดาวน์โหลดอะไร: คอมไพเลอร์ต้องมีอยู่แล้ว代码是专门为那些输入写的。它什么也不下载:编译器必须已经在那里。

4 see_graph.py: Dynamo · see_graph.py: Dynamo · see_graph.py:Dynamo

🇬🇧 English

Dynamo is the first stage. torch._dynamo.explain runs it and hands back a report, without calling any compiler. A name that starts with an underscore is Python's way of saying internal, and it may change, so these files were checked on PyTorch 2.15 only.

io.StringIO is a string that behaves like a file, and contextlib.redirect_stderr sends what would go to the error stream into it for the length of the with block. The guard log is long, so we keep it there and pick out what we want.

🇹🇭 ไทย

Dynamo คือขั้นแรก torch._dynamo.explain รันมันแล้วส่งรายงานกลับมาโดยไม่เรียกคอมไพเลอร์ใด ๆ ชื่อที่ขึ้นต้นด้วยขีดล่างเป็นวิธีที่ Python ใช้บอกว่า ใช้ภายใน และอาจเปลี่ยนได้ ไฟล์เหล่านี้จึงตรวจกับ PyTorch 2.15 เท่านั้น

io.StringIO คือสตริงที่ทำตัวเหมือนไฟล์ และ contextlib.redirect_stderr ส่งสิ่งที่จะออกไปทางสตรีมข้อผิดพลาดเข้าไปในนั้นตลอดบล็อก with บันทึกของ guard ยาวมาก เราจึงเก็บมันไว้ที่นั่นแล้วเลือกเฉพาะที่ต้องการ

🇨🇳 中文

Dynamo 是第一阶段。torch._dynamo.explain 运行它并交回一份报告,不调用任何编译器。以下划线开头的名字是 Python 在说 内部使用,可能会变,所以这些文件只在 PyTorch 2.15 上检查过。

io.StringIO 是一个行为像文件的字符串,contextlib.redirect_stderr 在 with 块期间把本该进入错误流的内容送进去。guard 的日志很长,所以我们把它存在那里,再挑出想要的部分。

see_graph.py

# Stage 1, Dynamo: what did it capture from our Python, and on what conditions?
import contextlib
import io

import torch


class TinyNet(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = torch.nn.Linear(64, 64)

    def forward(self, x):
        return torch.relu(self.layer(x) + 1.0)


def main():
    net = TinyNet()
    x = torch.randn(8, 64)

    # explain runs Dynamo only. It stops before Inductor, so it needs no compiler.
    report = torch._dynamo.explain(net)(x)
    print("graphs:      ", report.graph_count)
    print("graph breaks:", report.graph_break_count)
    print("operations:  ", report.op_count)

    print("\nThe graph, one line for each step:")
    for node in report.graphs[0].graph.nodes:
        print(f"  {node.op:14} {node.name}")

    # backend="eager" also stops after Dynamo. The guard log goes to stderr;
    # here it goes into a string, so that we choose what to print.
    log = io.StringIO()
    with contextlib.redirect_stderr(log):
        torch._logging.set_logs(guards=True)
        torch.compile(net, backend="eager")(x)
    torch._logging.set_logs()

    lines = log.getvalue().splitlines()
    print("\nThe guard on our input x:")
    for line in lines:
        if "check_tensor(L['x']" in line:
            print(" ", line.split("TENSOR_MATCH: ")[1].split("  #")[0])
    print("Guards in all:", sum("_MATCH:" in line for line in lines))


if __name__ == "__main__":
    main()

What it printed for usสิ่งที่มันพิมพ์ให้เรา它给我们打印的内容

graphs:       1
graph breaks: 0
operations:   3

The graph, one line for each step:
  placeholder    l_self_modules_layer_parameters_weight_
  placeholder    l_self_modules_layer_parameters_bias_
  placeholder    l_x_
  call_function  linear
  call_function  add
  call_function  relu
  output         output

The guard on our input x:
  check_tensor(L['x'], Tensor, DispatchKeySet(CPU, BackendSelect, ADInplaceOrView, AutogradCPU), torch.float32, device=None, requires_grad=False, size=[8, 64], stride=[64, 1])
Guards in all: 18

🇬🇧 English

Read the graph against forward. The three placeholder lines are what comes in: the weights, the bias and x. The 3 operations are your line of Python, in order: linear, add, relu. The Python itself is gone.

The guard is the part to remember. It says this code is valid for a tensor of torch.float32 with size=[8, 64]. There are 18 guards in all, about the model's class and more, and each is checked before the compiled code is reused.

🇹🇭 ไทย

อ่านกราฟเทียบกับ forward สามบรรทัด placeholder คือสิ่งที่เข้ามา: น้ำหนัก ค่าเอนเอียง และ x ส่วนการดำเนินการ 3 อย่างคือบรรทัด Python ของคุณตามลำดับ: linear, add, relu ตัว Python เองหายไปแล้ว

guard คือส่วนที่ต้องจำ มันบอกว่าโค้ดนี้ใช้ได้กับเทนเซอร์ชนิด torch.float32 ที่มี size=[8, 64] มี guard ทั้งหมด 18 ตัว เกี่ยวกับคลาสของโมเดลและอื่น ๆ และแต่ละตัวถูกตรวจก่อนจะใช้โค้ดที่คอมไพล์แล้วซ้ำ

🇨🇳 中文

对照 forward 读这张图。三行 placeholder 是进来的东西:权重、偏置和 x。那 3 个操作就是你的那行 Python,按顺序是 linear、add、relu。Python 本身已经不见了。

要记住的是 guard。它说这段代码只对 torch.float32 类型、size=[8, 64] 的张量有效。guard 一共有 18 个,涉及模型的类等等,重用编译好的代码之前每个都要检查。

5 recompile.py: when a guard fails · recompile.py: เมื่อ guard ไม่ผ่าน · recompile.py:当 guard 不成立

🇬🇧 English

What if the next input has 16 rows and not 8? The guard fails, so PyTorch compiles again. set_logs(recompiles=True) asks it to say so, and to say why. Save the file and run it.

🇹🇭 ไทย

ถ้าข้อมูลเข้าถัดไปมี 16 แถว ไม่ใช่ 8 ล่ะ? guard ไม่ผ่าน PyTorch จึงคอมไพล์ใหม่ set_logs(recompiles=True) ขอให้มันบอกออกมา และบอกเหตุผลด้วย บันทึกไฟล์แล้วรันเลย

🇨🇳 中文

如果下一个输入有 16 行而不是 8 行呢?guard 不成立,PyTorch 就重新编译。set_logs(recompiles=True) 让它说出来,并说明原因。保存文件,运行它。

recompile.py

# When a guard fails, PyTorch compiles a new version.
import time

import torch


class TinyNet(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = torch.nn.Linear(64, 64)

    def forward(self, x):
        return torch.relu(self.layer(x) + 1.0)


def timed_call(model, x, label):
    start = time.perf_counter()
    model(x)
    print(f"{label:28} {time.perf_counter() - start:8.4f} s")


def main():
    model = torch.compile(TinyNet())

    # These logs say out loud why PyTorch compiled again.
    torch._logging.set_logs(recompiles=True)

    with torch.no_grad():
        timed_call(model, torch.randn(8, 64), "8 rows, first time")
        timed_call(model, torch.randn(8, 64), "8 rows again")
        timed_call(model, torch.randn(16, 64), "16 rows, first time")
        timed_call(model, torch.randn(8, 64), "8 rows, back again")
        timed_call(model, torch.randn(16, 64), "16 rows again")
        timed_call(model, torch.randn(32, 64), "32 rows, first time")


if __name__ == "__main__":
    main()

What it printed for us. The log lines came out last, because they go to a different stream from the times.สิ่งที่มันพิมพ์ให้เรา บรรทัดบันทึกออกมาท้ายสุด เพราะอยู่คนละสตรีมกับเวลา它给我们打印的内容。日志行排在最后,因为它们和时间走的是不同的流。

8 rows, first time             0.5358 s
8 rows again                   0.0001 s
16 rows, first time            1.1396 s
8 rows, back again             0.0001 s
16 rows again                  0.0000 s
32 rows, first time            0.0226 s
V1101 20:21:41.637000 564 torch/_dynamo/guards.py:6374] [0/1] [__recompiles] Recompiling function forward in ~/compile_lab/recompile.py:12
V1101 20:21:41.637000 564 torch/_dynamo/guards.py:6374] [0/1] [__recompiles]     triggered by the following guard failure(s):
V1101 20:21:41.637000 564 torch/_dynamo/guards.py:6374] [0/1] [__recompiles]     - 0/0: tensor 'x' size mismatch at index 0. expected 8, actual 16

🇬🇧 English

The log gives the reason in plain words: size mismatch at index 0. expected 8, actual 16. The first 16 rows took 1.1 s. After that both versions are kept, so going back to 8 rows costs nothing.

Now look at the last time. 32 rows is a size it had never seen, yet it took 0.0226 s, and the log has no second recompile. PyTorch's manual says that it detects when sizes change and compiles a more dynamic kernel when it recompiles, and here you can see that kernel at work: the version made for 16 rows also fits 32. As the video says, that does not remove every recompile. A program whose input keeps changing shape can spend its time compiling, and this log is how you catch it.

🇹🇭 ไทย

บันทึกให้เหตุผลตรง ๆ: size mismatch at index 0. expected 8, actual 16 16 แถวครั้งแรกใช้เวลา 1.1 วินาที หลังจากนั้นทั้งสองเวอร์ชันถูกเก็บไว้ การกลับไปที่ 8 แถวจึงแทบไม่เสียเวลา

ลองดูเวลาสุดท้าย 32 แถวเป็นขนาดที่มันไม่เคยเห็น แต่ใช้เวลาแค่ 0.0226 วินาที และบันทึกไม่มีการคอมไพล์ใหม่ครั้งที่สอง คู่มือของ PyTorch บอกว่ามันตรวจพบเมื่อขนาดเปลี่ยน และคอมไพล์เคอร์เนลที่ยืดหยุ่นกว่าเมื่อคอมไพล์ใหม่ และตรงนี้คุณเห็นเคอร์เนลนั้นทำงาน: เวอร์ชันที่สร้างสำหรับ 16 แถวใช้กับ 32 แถวได้ด้วย แต่อย่างที่วิดีโอบอก นั่นไม่ได้ขจัดการคอมไพล์ใหม่ทุกครั้ง โปรแกรมที่ข้อมูลเข้าเปลี่ยนรูปร่างไปเรื่อย ๆ อาจใช้เวลาหมดไปกับการคอมไพล์ และบันทึกนี้คือวิธีจับมันได้

🇨🇳 中文

日志直接说明了原因:size mismatch at index 0. expected 8, actual 16。第一次 16 行花了 1.1 秒。之后两个版本都保留着,所以回到 8 行几乎不花时间。

再看最后一个时间。32 行是它从没见过的尺寸,却只花了 0.0226 秒,日志里也没有第二次重新编译。PyTorch 的手册说,它会发现尺寸在变,并在重新编译时编译一个更灵活的内核,这里你就看到了那个内核在工作:为 16 行做的版本也适用于 32 行。但正如视频所说,这并不能消除所有的重新编译。输入形状不断变化的程序,可能把时间都花在编译上,而这份日志就是抓住它的办法。

Quizแบบทดสอบ小测验

A compiled model has already run on batches of 8 rows and of 16 rows. A batch of 8 rows comes in again. What happens?โมเดลที่คอมไพล์แล้วเคยรันกับชุดข้อมูล 8 แถวและ 16 แถวแล้ว ชุด 8 แถวเข้ามาอีกครั้ง จะเกิดอะไรขึ้น?一个编译好的模型已经在 8 行和 16 行的批次上运行过。又来了一个 8 行的批次,会发生什么?

Yes.ถูกต้อง对。Not this time.ยังไม่ถูก这次不对。Compiled versions are kept, each with its own guards, just as the dictionary kept them in the plain Python wrapper.เวอร์ชันที่คอมไพล์แล้วถูกเก็บไว้ แต่ละเวอร์ชันมี guard ของตัวเอง เหมือนที่ดิกชันนารีเก็บไว้ในตัวห่อแบบ Python ล้วน编译好的版本都保留着,各有各的 guard,就像纯 Python 包装器里的字典保留它们一样。

6 train_graphs.py: AOT Autograd · train_graphs.py: AOT Autograd · train_graphs.py:AOT Autograd

🇬🇧 English

The second stage prepares the graph for the compiler. To see what it hands over, this file puts our own small backend where Inductor would stand. aot_autograd takes two functions, one for the forward graph and one for the backward graph, and each receives a graph and returns something that can run. Ours print the steps and then run them unchanged, which is the job of make_boxed_func.

Training needs both directions: forward calculates the answer, and backward works out how to change each weight, which is what .backward() asks for.

🇹🇭 ไทย

ขั้นที่สองเตรียมกราฟให้คอมไพเลอร์ เพื่อดูว่ามันส่งอะไรต่อไป ไฟล์นี้วางแบ็กเอนด์เล็ก ๆ ของเราเองไว้ตรงที่ Inductor เคยอยู่ aot_autograd รับฟังก์ชันสองตัว ตัวหนึ่งสำหรับกราฟขาไป อีกตัวสำหรับกราฟขากลับ แต่ละตัวรับกราฟและคืนสิ่งที่รันได้ ของเราพิมพ์ขั้นตอนแล้วรันมันแบบไม่เปลี่ยนแปลง ซึ่งเป็นหน้าที่ของ make_boxed_func

การฝึกต้องใช้ทั้งสองทิศทาง: forward (ขาไป) คำนวณคำตอบ และ backward (ขากลับ) หาว่าควรเปลี่ยนน้ำหนักแต่ละตัวอย่างไร ซึ่งคือสิ่งที่ .backward() ขอ

🇨🇳 中文

第二阶段为编译器准备图。为了看它交出去的是什么,这个文件把我们自己的小后端放在 Inductor 本来的位置。aot_autograd 接受两个函数,一个处理前向图,一个处理反向图,每个都接收一张图,返回一个能运行的东西。我们的函数打印步骤,然后原样运行它们,这就是 make_boxed_func 的工作。

训练两个方向都需要:forward(前向)算出答案,backward(反向)算出每个权重该怎么改,这正是 .backward() 要求的。

train_graphs.py

# Stage 2, AOT Autograd: look at the graphs it prepares for the compiler.
# Our own small backend stands where Inductor would stand, and only prints.
import torch
from functorch.compile import make_boxed_func
from torch._dynamo.backends.common import aot_autograd


class TinyNet(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = torch.nn.Linear(4, 4)

    def forward(self, x):
        return torch.relu(self.layer(x) + 1.0)


def print_steps(title, graph_module):
    print(title)
    for node in graph_module.graph.nodes:
        if node.op == "call_function":
            print("  ", node.target)
    return make_boxed_func(graph_module.forward)


def forward_compiler(graph_module, example_inputs):
    return print_steps("FORWARD graph:", graph_module)


def backward_compiler(graph_module, example_inputs):
    return print_steps("BACKWARD graph:", graph_module)


def main():
    backend = aot_autograd(fw_compiler=forward_compiler, bw_compiler=backward_compiler)
    model = torch.compile(TinyNet(), backend=backend)
    x = torch.randn(2, 4)

    print("--- training: the gradients are needed ---")
    model(x).sum().backward()

    print("\n--- inference: a new batch size, and no gradients ---")
    with torch.no_grad():
        model(torch.randn(3, 4))


if __name__ == "__main__":
    main()

What it printed for usสิ่งที่มันพิมพ์ให้เรา它给我们打印的内容

--- training: the gradients are needed ---
FORWARD graph:
   aten.t.default
   aten.addmm.default
   aten.add.Tensor
   aten.relu.default
   aten.detach.default
BACKWARD graph:
   aten.detach.default
   aten.threshold_backward.default
   aten.t.default
   aten.mm.default
   aten.t.default
   aten.sum.dim_IntList
   aten.view.default
   aten.t.default

--- inference: a new batch size, and no gradients ---
FORWARD graph:
   aten.t.default
   aten.addmm.default
   aten.add.Tensor
   aten.relu.default

🇬🇧 English

Compare this with step 4. There, the graph had one linear. Here it has become two lower-level steps: aten.t.default, which turns the weights on their side, and aten.addmm.default, which multiplies and adds the bias. That is a decomposition, and it made the graph longer. So this stage is preparing the graph, not simply shrinking it.

Training produced a backward graph, where aten.threshold_backward.default is the reverse of relu. Inference produced no backward graph at all, because nothing asked for one.

🇹🇭 ไทย

เทียบกับขั้นตอนที่ 4 ตอนนั้นกราฟมี linear หนึ่งตัว ตอนนี้มันกลายเป็นสองขั้นตอนระดับล่าง: aten.t.default ซึ่งพลิกตารางน้ำหนัก และ aten.addmm.default ซึ่งคูณแล้วบวกค่าเอนเอียง นี่คือการแตกย่อย (decomposition) และมันทำให้กราฟยาวขึ้น ขั้นนี้จึงเป็นการเตรียมกราฟ ไม่ใช่แค่ทำให้เล็กลง

การฝึกสร้างกราฟขากลับ ซึ่ง aten.threshold_backward.default คือด้านกลับของ relu ส่วนการอนุมาน (inference) ไม่สร้างกราฟขากลับเลย เพราะไม่มีใครขอ

🇨🇳 中文

和第 4 步比较一下。那里的图有一个 linear。这里它变成了两个更低层的步骤:aten.t.default 把权重翻转过来,aten.addmm.default 做乘法并加上偏置。这叫分解(decomposition),它让图变长了。所以这个阶段是在准备图,而不只是把它变小。

训练产生了反向图,其中 aten.threshold_backward.default 是 relu 的反向。推理(inference)完全没有产生反向图,因为没有谁要它。

7 see_kernels.py: Inductor and fusion · see_kernels.py: Inductor และการรวมงาน · see_kernels.py:Inductor 与融合

🇬🇧 English

The third stage writes code. Its best trick is fusion: doing several operations in one kernel. First see the idea in plain Python. Both functions add a bias and then apply an activation. two_trips goes through the data twice and keeps a whole list in between. one_trip goes through once and keeps no list. Press ▶ Run.

🇹🇭 ไทย

ขั้นที่สามเขียนโค้ด กลเม็ดที่ดีที่สุดของมันคือการรวมงาน (fusion): ทำหลายการดำเนินการในเคอร์เนลเดียว ดูแนวคิดใน Python ล้วน ๆ ก่อน ทั้งสองฟังก์ชันบวกค่าเอนเอียงแล้วใช้ฟังก์ชันกระตุ้น two_trips เดินผ่านข้อมูลสองรอบและเก็บลิสต์ทั้งลิสต์ไว้ตรงกลาง one_trip เดินผ่านรอบเดียวและไม่เก็บลิสต์ กด ▶ Run

🇨🇳 中文

第三阶段写代码。它最拿手的是融合(fusion):在一个内核里完成几个操作。先在纯 Python 里看这个想法。两个函数都是加上偏置,再做激活。two_trips 把数据走两遍,中间还留着一整个列表。one_trip 只走一遍,不留列表。按 ▶ Run。

toy_fusion.py

# What fusion means, in plain Python: one trip through the data, not two.
import time

SIZE = 1_000_000


def two_trips(numbers):
    # Add a bias, and keep the whole in-between list in memory.
    added = [n + 1.0 for n in numbers]
    # Then make a second trip for the activation.
    return [max(n, 0.0) for n in added]


def one_trip(numbers):
    # The same two jobs in one trip, and no in-between list.
    return [max(n + 1.0, 0.0) for n in numbers]


def seconds_for(job, numbers):
    start = time.perf_counter()
    answer = job(numbers)
    return time.perf_counter() - start, answer


def main():
    numbers = [float(n % 7) - 3.0 for n in range(SIZE)]
    slow, answer_a = seconds_for(two_trips, numbers)
    fast, answer_b = seconds_for(one_trip, numbers)
    print("same answer:", answer_a == answer_b)
    print(f"two trips: {slow:.3f} s")
    print(f"one trip:  {fast:.3f} s")


if __name__ == "__main__":
    main()

What it printed for usสิ่งที่มันพิมพ์ให้เรา它给我们打印的内容

same answer: True
two trips: 0.058 s
one trip:  0.041 s

🇬🇧 English

The same answer, 0.058 s against 0.041 s, and less memory. Now read what Inductor really wrote. The log it writes with output_code=True is the Python and C++ it made. This file prints only the lines that name the work.

🇹🇭 ไทย

คำตอบเหมือนกัน 0.058 วินาที เทียบกับ 0.041 วินาที และใช้หน่วยความจำน้อยกว่า คราวนี้อ่านสิ่งที่ Inductor เขียนจริง ๆ บันทึกที่มันเขียนเมื่อใช้ output_code=True คือ Python และ C++ ที่มันสร้าง ไฟล์นี้พิมพ์เฉพาะบรรทัดที่บอกชื่องาน

🇨🇳 中文

答案相同,0.058 秒对 0.041 秒,内存也用得更少。现在来读 Inductor 真正写的东西。用 output_code=True 时它写的日志,就是它生成的 Python 和 C++。这个文件只打印那些点明工作的行。

see_kernels.py

# Stage 3, Inductor: read the code it wrote, for two small models.
import contextlib
import io

import torch


class BiasThenRelu(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.bias = torch.nn.Parameter(torch.randn(64))

    def forward(self, x):
        return torch.relu(x + self.bias)


class MatmulThenRelu(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.weights = torch.nn.Parameter(torch.randn(64, 64))

    def forward(self, x):
        return torch.relu(x @ self.weights)


def show_code_for(net):
    # The log goes to stderr. Here it goes into a string, so we choose what to print.
    log = io.StringIO()
    with contextlib.redirect_stderr(log), torch.no_grad():
        torch._logging.set_logs(output_code=True)
        torch.compile(net)(torch.randn(8, 64))
    torch._logging.set_logs()

    lines = log.getvalue().splitlines()
    print(type(net).__name__, "-", len(lines), "lines of log")
    for line in lines:
        if "= async_compile" in line:
            print("   a kernel it wrote:", line.split(" = async_compile")[0].split()[-1])
        elif "extern_kernels." in line:
            print("   a library call:   ", line.split("extern_kernels.")[1].split("(")[0])
        elif "Original ATen" in line:
            print("     made from:", line.split("Original ATen: ")[1])


def main():
    show_code_for(BiasThenRelu())
    show_code_for(MatmulThenRelu())


if __name__ == "__main__":
    main()

What it printed for usสิ่งที่มันพิมพ์ให้เรา它给我们打印的内容

BiasThenRelu - 115 lines of log
   a kernel it wrote: cpp_fused_add_relu_0
MatmulThenRelu - 110 lines of log
   a kernel it wrote: cpp_fused_relu_0
     made from: [aten.mm]
   a library call:    mm

🇬🇧 English

The names tell the story. BiasThenRelu has one kernel, cpp_fused_add_relu_0: the add and the relu are one piece of C++, the example from the video. MatmulThenRelu is different: the matrix multiplication stays a library call, mm, because a tuned library already does that job better, and only the relu is a kernel, cpp_fused_relu_0. The aim is good code, not the fewest kernels.

On a GPU, Inductor writes Triton kernels and not C++. We ran no GPU here, so that sentence comes from the video and not from our files.

🇹🇭 ไทย

ชื่อเล่าเรื่องทั้งหมด BiasThenRelu มีเคอร์เนลเดียวคือ cpp_fused_add_relu_0: การบวกและ relu เป็น C++ ชิ้นเดียว ตามตัวอย่างในวิดีโอ MatmulThenRelu ต่างออกไป: การคูณเมทริกซ์ยังเป็นการเรียกไลบรารี mm เพราะไลบรารีที่ปรับแต่งมาแล้วทำงานนั้นได้ดีกว่าอยู่แล้ว และมีแค่ relu ที่เป็นเคอร์เนล cpp_fused_relu_0 เป้าหมายคือโค้ดที่ดี ไม่ใช่เคอร์เนลที่น้อยที่สุด

บน GPU นั้น Inductor เขียนเคอร์เนล Triton ไม่ใช่ C++ เราไม่ได้รันบน GPU ที่นี่ ประโยคนี้จึงมาจากวิดีโอ ไม่ได้มาจากไฟล์ของเรา

🇨🇳 中文

名字说明了一切。BiasThenRelu 只有一个内核 cpp_fused_add_relu_0:加法和 relu 成了同一段 C++,这正是视频里的例子。MatmulThenRelu 不一样:矩阵乘法仍然是库调用 mm,因为调校过的库本来就做得更好,只有 relu 是内核 cpp_fused_relu_0。目标是好代码,而不是最少的内核。

在 GPU 上,Inductor 写的是 Triton 内核,而不是 C++。我们这里没有运行 GPU,所以这句话来自视频,不是来自我们的文件。

Quizแบบทดสอบ小测验

What does a compiler gain by fusing an add and an activation into one kernel?คอมไพเลอร์ได้อะไรจากการรวมการบวกกับฟังก์ชันกระตุ้นไว้ในเคอร์เนลเดียว?编译器把加法和激活融合进一个内核,有什么好处?

Yes.ถูกต้อง对。Not this time.ยังไม่ถูก这次不对。The aim is the same answer with less work. Compiling should never change what the model calculates.เป้าหมายคือคำตอบเดิมด้วยงานที่น้อยลง การคอมไพล์ไม่ควรเปลี่ยนสิ่งที่โมเดลคำนวณ目标是用更少的工作得到同样的答案。编译不应该改变模型算出的东西。

8 graph_break.py: graph breaks · graph_break.py: กราฟขาด · graph_break.py:图中断

🇬🇧 English

A Python if needs a real True or False right now, but a graph has no values yet, only steps. So when .item() asks for a value inside a tensor, Dynamo finishes the graph it has, runs that part in ordinary Python, and starts a new graph after it. The program still gives the right answer, with smaller pieces for the compiler to work on.

torch.where makes the same choice as a tensor operation, so it can stay in the graph. It calculates both sides and then picks, so it is not the right swap for every if. fullgraph=True is the strict setting: one whole graph, or an error.

🇹🇭 ไทย

if ของ Python ต้องการ True หรือ False จริง ๆ เดี๋ยวนั้น แต่กราฟยังไม่มีค่า มีแต่ขั้นตอน ดังนั้นเมื่อ .item() ขอค่าในเทนเซอร์ Dynamo จึงปิดกราฟที่มีอยู่ รันส่วนนั้นด้วย Python ธรรมดา แล้วเริ่มกราฟใหม่ต่อจากนั้น โปรแกรมยังให้คำตอบที่ถูกต้อง แต่คอมไพเลอร์ได้ชิ้นงานที่เล็กลง

torch.where เลือกแบบเดียวกันในรูปการดำเนินการกับเทนเซอร์ จึงอยู่ในกราฟได้ มันคำนวณทั้งสองทางแล้วค่อยเลือก จึงไม่ใช่ตัวแทนที่เหมาะกับ if ทุกตัว fullgraph=True คือการตั้งค่าแบบเข้มงวด: กราฟเดียวทั้งหมด หรือไม่ก็ข้อผิดพลาด

🇨🇳 中文

Python 的 if 此刻就要一个真正的 True 或 False,可是图里还没有值,只有步骤。所以当 .item() 要张量里的值时,Dynamo 就结束手上的图,用普通 Python 运行那一部分,然后在它之后开始一张新图。程序仍然给出正确答案,只是留给编译器的片段变小了。

torch.where 用张量操作来做同样的选择,所以能留在图里。它会把两边都算出来再挑,所以并不是每个 if 都适合这样换。fullgraph=True 是严格的设置:要么一整张图,要么报错。

graph_break.py

# A graph break: Dynamo meets code it cannot capture, and cuts the graph there.
import torch


class WithIf(torch.nn.Module):
    def forward(self, x):
        x = x + 1.0
        # Python needs a real True or False, so this asks for a value in a tensor.
        if x.sum().item() > 0:
            x = x * 2.0
        return torch.relu(x)


class WithWhere(torch.nn.Module):
    def forward(self, x):
        x = x + 1.0
        # torch.where keeps the choice inside the graph.
        x = torch.where(x.sum() > 0, x * 2.0, x)
        return torch.relu(x)


def show(name, net, x):
    report = torch._dynamo.explain(net)(x)
    print(name)
    print("  graphs:", report.graph_count, " breaks:", report.graph_break_count)
    for reason in report.break_reasons:
        print("  why:", reason.reason.splitlines()[0])


def main():
    x = torch.ones(4, 4)
    show("WithIf", WithIf(), x)
    show("WithWhere", WithWhere(), x)

    print("\nfullgraph=True on each of them:")
    for net in [WithWhere(), WithIf()]:
        name = type(net).__name__
        try:
            torch.compile(net, fullgraph=True)(x)
            print(" ", name, "compiled as one graph")
        except Exception as error:
            print(" ", name, "stopped with", type(error).__name__)


if __name__ == "__main__":
    main()

What it printed for us. The warning is PyTorch's own, and it names the line.สิ่งที่มันพิมพ์ให้เรา คำเตือนเป็นของ PyTorch เอง และบอกบรรทัดด้วย它给我们打印的内容。警告是 PyTorch 自己发的,还指出了是哪一行。

WithIf
  graphs: 2  breaks: 1
  why: Unsupported Tensor.item() call with capture_scalar_outputs=False
WithWhere
  graphs: 1  breaks: 0

fullgraph=True on each of them:
  WithWhere compiled as one graph
  WithIf stopped with UserError
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0] Graph break from `Tensor.item()`, consider setting:
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0]     torch._dynamo.config.capture_scalar_outputs = True
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0] or:
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0]     env TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS=1
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0] to include these operations in the captured graph.
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0] 
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0] Graph break: from user code at:
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0]   File "~/compile_lab/graph_break.py", line 9, in forward
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0]     if x.sum().item() > 0:
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0]        ~~~~~~~~~~~~^^
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0] 
W1101 20:21:53.837000 673 torch/_dynamo/variables/tensor.py:2043] [0/0] 

🇬🇧 English

WithIf became 2 graphs with 1 break, and the reason is printed: Unsupported Tensor.item() call. WithWhere stayed whole. A break does not lose all the speed, but when the speed disappoints, breaks are one of the first things to check, and explain finds them without a compiler.

🇹🇭 ไทย

WithIf กลายเป็น 2 กราฟและขาด 1 จุด พร้อมพิมพ์เหตุผล: Unsupported Tensor.item() call ส่วน WithWhere ยังเต็มอยู่ การขาดไม่ได้ทำให้เสียความเร็วทั้งหมด แต่เมื่อความเร็วน่าผิดหวัง กราฟขาดคือสิ่งแรก ๆ ที่ควรตรวจ และ explain หามันเจอโดยไม่ต้องใช้คอมไพเลอร์

🇨🇳 中文

WithIf 变成了 2 张图、1 处中断,原因也打印了出来:Unsupported Tensor.item() call。WithWhere 保持完整。中断并不会让速度全部丢失,但当速度令人失望时,中断是最先要检查的东西之一,而且 explain 不用编译器就能找到它们。

Quizแบบทดสอบ小测验

A model has a graph break in its default settings. What is true?โมเดลหนึ่งมีกราฟขาดเมื่อใช้ค่าเริ่มต้น ข้อใดจริง?一个模型在默认设置下有一处图中断。下面哪个说法正确?

Yes.ถูกต้อง对。Not this time.ยังไม่ถูก这次不对。An error comes only with the strict setting, fullgraph=True.ข้อผิดพลาดเกิดเฉพาะกับการตั้งค่าแบบเข้มงวด fullgraph=True只有严格的设置 fullgraph=True 才会报错。

9 benchmark.py: is it faster? · benchmark.py: เร็วขึ้นจริงไหม · benchmark.py:真的更快吗

🇬🇧 English

Now the question everyone asks, measured fairly. This file does three things that a careless benchmark does not:

  • It times the first compiled call apart from the rest, and warms both up.
  • It takes the median of many calls, so one slow moment does not decide the result.
  • The plain and compiled models take turns, so neither gets a quieter moment of the machine.

On a GPU the work runs in the background, so finish_gpu_work waits for it before the clock is read. Without that you would time only the handing over of the work. On our machine that if was false, so its inside line was never run by us.

🇹🇭 ไทย

คราวนี้มาถึงคำถามที่ทุกคนถาม โดยวัดอย่างยุติธรรม ไฟล์นี้ทำสามอย่างที่การวัดแบบไม่ระวังไม่ได้ทำ:

  • มันจับเวลาการเรียกครั้งแรกที่คอมไพล์แยกจากครั้งอื่น และอุ่นเครื่องทั้งคู่
  • มันใช้ค่ามัธยฐาน (median) ของการเรียกหลายครั้ง จังหวะช้าครั้งเดียวจึงไม่ตัดสินผล
  • โมเดลธรรมดาและโมเดลที่คอมไพล์ผลัดกันรัน จึงไม่มีตัวไหนได้จังหวะที่เครื่องว่างกว่า

บน GPU งานรันอยู่เบื้องหลัง finish_gpu_work จึงรอมันก่อนอ่านนาฬิกา ถ้าไม่รอ คุณจะจับเวลาแค่การส่งงานออกไป ในเครื่องของเรา if นั้นเป็นเท็จ บรรทัดข้างในจึงไม่เคยถูกเรารันเลย

🇨🇳 中文

现在来回答大家都问的问题,并且公平地测量。这个文件做了粗心的测速不会做的三件事:

  • 它把第一次编译后的调用和其余的分开计时,并让两边都先热身。
  • 它取多次调用的中位数(median),所以一次慢的时刻决定不了结果。
  • 普通模型和编译后的模型轮流运行,所以谁也占不到机器更空闲的时刻。

在 GPU 上,工作在后台运行,所以 finish_gpu_work 要等它完成才读时钟。不等的话,你测到的只是把工作交出去的时间。在我们的机器上那个 if 为假,所以里面那一行我们从没运行过。

benchmark.py

# Is it faster? Keep compile time apart from the steady speed, and ask two models.
import statistics
import time

import torch

WARM_UP_CALLS = 3
TIMED_CALLS = 50


class ManySmallSteps(torch.nn.Module):
    # Lots of small steps, one after another, on every number.
    def forward(self, x):
        for _ in range(20):
            x = torch.relu(x + 0.1) * 0.99
        return x


class OneBigMatmul(torch.nn.Module):
    # One large matrix multiplication, already done by a tuned library.
    def forward(self, x):
        return x @ x


def finish_gpu_work():
    # A GPU works in the background, so wait for it before reading the clock.
    if torch.cuda.is_available():
        torch.cuda.synchronize()


def seconds_for_one_call(model, x):
    start = time.perf_counter()
    model(x)
    finish_gpu_work()
    return time.perf_counter() - start


def compare(net, x):
    model = torch.compile(net)
    first = seconds_for_one_call(model, x)

    for _ in range(WARM_UP_CALLS):
        seconds_for_one_call(net, x)
        seconds_for_one_call(model, x)

    # They take turns, so that neither one gets a quieter moment of the machine.
    plain_times = []
    compiled_times = []
    for _ in range(TIMED_CALLS):
        plain_times.append(seconds_for_one_call(net, x))
        compiled_times.append(seconds_for_one_call(model, x))

    plain_ms = statistics.median(plain_times) * 1000
    compiled_ms = statistics.median(compiled_times) * 1000
    print(type(net).__name__)
    print(f"  first compiled call {first:8.2f} s")
    print(f"  plain, each call    {plain_ms:8.2f} ms")
    print(f"  compiled, each call {compiled_ms:8.2f} ms")
    print(f"  plain / compiled    {plain_ms / compiled_ms:8.2f} times")


def main():
    print(torch.get_num_threads(), "CPU threads")
    with torch.no_grad():
        compare(ManySmallSteps(), torch.randn(256, 256))
        compare(OneBigMatmul(), torch.randn(1500, 1500))


if __name__ == "__main__":
    main()

What it printed for usสิ่งที่มันพิมพ์ให้เรา它给我们打印的内容

11 CPU threads
ManySmallSteps
  first compiled call     1.60 s
  plain, each call        0.46 ms
  compiled, each call     0.06 ms
  plain / compiled        7.15 times
OneBigMatmul
  first compiled call     0.11 s
  plain, each call       17.25 ms
  compiled, each call    17.89 ms
  plain / compiled        0.96 times
Modelโมเดล模型 First compiled callการเรียกครั้งแรกที่คอมไพล์编译后第一次调用 Plain, each callธรรมดา ต่อครั้ง普通,每次调用 Compiled, each callคอมไพล์แล้ว ต่อครั้ง编译后,每次调用 Plain / compiledธรรมดา / คอมไพล์แล้ว普通 / 编译后
ManySmallSteps 1.60 s 0.46 ms 0.06 ms 7.15 ×
OneBigMatmul 0.11 s 17.25 ms 17.89 ms 0.96 ×

🇬🇧 English

This is the answer to why does it help some models and not others? ManySmallSteps is the kind of work fusion was made for. OneBigMatmul spends its time in one library call, which was already fast, so compiling has nothing to fuse and the two times are the same. In five runs of this file the first ratio stayed between 6 and 10, and the second between 0.9 and 1.1, so read the size of the difference and not the last digit.

Our first version of this file timed the plain model first and the compiled one after, and it reported that the compiled matrix multiplication was slower, a ratio of 0.74. It was not. Timed in turns, the difference vanished. It is easy to fool yourself with a benchmark, and we did.

🇹🇭 ไทย

นี่คือคำตอบของคำถาม ทำไมมันช่วยบางโมเดลแต่ไม่ช่วยโมเดลอื่น? ManySmallSteps คืองานแบบที่การรวมงานถูกสร้างมาเพื่อมัน OneBigMatmul ใช้เวลาไปกับการเรียกไลบรารีครั้งเดียวซึ่งเร็วอยู่แล้ว การคอมไพล์จึงไม่มีอะไรให้รวม และเวลาทั้งสองก็เท่ากัน ในการรันไฟล์นี้ห้าครั้ง อัตราส่วนแรกอยู่ระหว่าง 6 ถึง 10 และอัตราส่วนที่สองอยู่ระหว่าง 0.9 ถึง 1.1 จึงควรอ่านขนาดของความต่าง ไม่ใช่ตัวเลขหลักสุดท้าย

ไฟล์นี้เวอร์ชันแรกของเราจับเวลาโมเดลธรรมดาก่อนแล้วค่อยจับเวลาโมเดลที่คอมไพล์ และรายงานว่าการคูณเมทริกซ์ที่คอมไพล์ช้ากว่า ด้วยอัตราส่วน 0.74 ซึ่งไม่จริง พอจับเวลาแบบผลัดกัน ความต่างก็หายไป การวัดความเร็วหลอกตัวเองได้ง่าย และเราก็โดนมาแล้ว

🇨🇳 中文

这就是 为什么它帮得了有的模型,帮不了别的? 的答案。ManySmallSteps 正是融合最擅长的那类工作。OneBigMatmul 的时间都花在一次库调用上,而它本来就快,所以编译没有东西可融合,两个时间也就一样。这个文件运行五次,第一个比值都在 6 到 10 之间,第二个在 0.9 到 1.1 之间,所以要看差距的大小,而不是最后一位数字。

这个文件的第一版先给普通模型计时,再给编译后的模型计时,结果报告说编译后的矩阵乘法更慢,比值是 0.74。其实不是。轮流计时之后,差距就消失了。测速很容易骗到自己,我们就被骗过。

When it does not help, ask four questionsเมื่อมันไม่ช่วย ให้ถามสี่คำถาม它没帮上忙时,问四个问题

  1. Did we capture enough of the model? Look at the graph with explain.เราจับโมเดลได้มากพอไหม? ดูกราฟด้วย explain我们捕获到足够多的模型了吗?用 explain 看图。
  2. Are graph breaks limiting it? Count them with explain, or try fullgraph=True.กราฟขาดจำกัดมันอยู่ไหม? นับด้วย explain หรือลอง fullgraph=True图中断在限制它吗?用 explain 数一数,或试试 fullgraph=True。
  3. Are changing inputs causing recompiles? Read the log from set_logs(recompiles=True).ข้อมูลเข้าที่เปลี่ยนไปทำให้คอมไพล์ใหม่ไหม? อ่านบันทึกจาก set_logs(recompiles=True)变化的输入在引发重新编译吗?读 set_logs(recompiles=True) 的日志。
  4. Are we measuring the compile cost or the steady speed? Time them apart, as benchmark.py does.เรากำลังวัดค่าคอมไพล์หรือความเร็วคงที่? จับเวลาแยกกัน แบบที่ benchmark.py ทำ我们测的是编译开销,还是稳定速度?像 benchmark.py 那样分开计时。

Quizแบบทดสอบ小测验

Someone times 10 calls of a compiled model, including the first, and says compiling made it slower. What is the first thing to fix?มีคนจับเวลาการเรียกโมเดลที่คอมไพล์ 10 ครั้ง รวมครั้งแรกด้วย แล้วบอกว่าการคอมไพล์ทำให้ช้าลง สิ่งแรกที่ควรแก้คืออะไร?有人给编译后的模型的 10 次调用计时,包括第一次,然后说编译让它变慢了。首先该改什么?

Yes.ถูกต้อง对。Not this time.ยังไม่ถูก这次不对。The first call holds the compiling, which can take far longer than all the other calls together.การเรียกครั้งแรกรวมการคอมไพล์ไว้ด้วย ซึ่งอาจนานกว่าการเรียกอื่นทั้งหมดรวมกันมาก第一次调用包含了编译,它可能比其他所有调用加起来还要长得多。