🏠 总目录📚 本教程 06 · 读源码路线图 ← →
📑 本页目录(点开跳转)

06 · 读源码路线图

⏱ 66 分钟 | ⭐ 从一条 Python 报错走到 C++ 实现,全程不需要下载源码,更不需要能编译 PyTorch


🎯 一句话

读 PyTorch 源码不是「把仓库 clone 下来从头看」,而是【从一条报错反查】:报错给你算子名 → 算子名给你 schema → dispatcher 给你注册位置 → native_functions.yaml 给你真正算数的那个函数名。 这条链上的前三步,用你机器上已经装好的 torch 就能查完;只有最后一步要去 GitHub 上看一眼文本。


🧩 一、⭐ 起点还是那条报错

本章 Python 侧的输出全是实跑的,环境 torch 2.13.0+cpu + CPython 3.13,CUDA 不可用。

沿用 05 章第一节那条:

# c06_start.py
import torch

x = torch.ones(2, 2).to_sparse()
torch.nn.functional.gelu(x)
NotImplementedError: Could not run 'aten::gelu' with arguments from the 'SparseCPU' backend.

⭐ 05 章到「格子空着」就停了。本章往下走一步:aten::gelu 那个非空的格子里,代码到底在哪个文件里。

⭐ 先把整条路线摆出来,后面每一节就是其中一格:

步 手里有什么 用什么工具 得到什么 在哪一节
① 一条报错 读 算子名 aten::gelu 本节
② 算子名 torch.ops.aten.gelu.default._schema 完整签名 第五节
③ 算子名 torch._C._dispatch_dump 「注册在哪个文件第几行」 05 章第四节
④ ⚠️ 那个文件名 —— ⚠️ 它在 GitHub 上根本不存在 第三节
⑤ 算子名 去仓库搜 native_functions.yaml ⭐ 真正算数的那个函数名 第三、六节
⑥ 函数名 在 aten/src/ATen/native/ 里搜它 实现 第六节

⚠️ 第 ④ 步那个转弯是本章的关键,绝大多数人卡在这里:dump 明明告诉你 RegisterCPU_3.cpp:4482,你去仓库里搜,一个结果都没有。原因在第三节。


🧩 二、三个目录名,一句话一个

翻源码时最先撞上的是三个陌生目录。先记住它们各自的边界,比记住里面有什么重要得多。

目录 全称 / 由来 一句话 里面典型有什么
c10/ Caffe2 + ATen 合并时的公共底座(读作 "see-ten") ⭐ 最底层的公共设施,不认识任何具体算子 DispatchKey、TensorImpl、Device、Scalar、intrusive_ptr
aten/ A Tensor library ⭐ 算子的家 —— 张量库本体 native/ 下的手写 kernel、native_functions.yaml
torch/csrc/ PyTorch 自己的 C++ 源 ⭐ 认识 Python 的那一层,以及框架级功能 pybind 绑定、autograd/、jit/、distributed/

⭐ 判断一个东西该在哪个目录,只问两个问题:

它认识 Python 吗 它认识具体算子吗 那它在
❌ ❌ c10/
❌ ✅ aten/
✅ —— torch/csrc/

⚠️ c10 不认识算子这条是硬的:DispatchKey 这个枚举定义在 c10 里,但「aten::gelu 在 CPU 这一格填了什么」不在。表的骨架归 c10,表的内容归 aten。 这也解释了 05 章那个现象 —— 表是运行时一批批填起来的,因为定义骨架的那层压根不知道有哪些行。

⭐ torch/csrc 是唯一会 #include <Python.h> 的一层。 01 章讲的那条边界,物理上就落在这个目录里。反过来说:你在 aten/ 和 c10/ 里看到 Python 相关的东西,基本可以确定自己看错文件了。


🧩 三、⭐ native_functions.yaml:算子声明的总账本

这是全 PyTorch 最该知道的一个文件名。路径是 aten/src/ATen/native/native_functions.yaml,一个几万行的 YAML,站在整条生成链的最上游。

⚠️⚠️ 这个文件不在 pip 装的 wheel 里 —— 它是构建期的输入,编译完就没它什么事了。 所以下面不贴仓库原文(贴了你也没法在本机核对,本教程不写无法验证的东西), 只讲它的形状,然后用你本机就能跑出来的东西去印证。

一条 entry 由三段构成:

段 是什么 对应到 05 章那张表
func: ⭐ schema 本尊 新开一行(等价于 C++ 侧的 m.def)
dispatch: 分派键 → C++ 函数名的映射 ⭐ 一次填好几个格子(等价于一串 m.impl)
structured_delegate: 「我不自己写,转给 xxx.out」 让代码生成器替你填 CPU / CUDA / Meta 那几格

🚦 ⭐ 用本机能跑的两条命令,把这张账本反推出来

⭐ func: 那一段不用去仓库找,它就在你装好的包里:

import torch
print(torch.ops.aten.gelu.default._schema)
# aten::gelu(Tensor self, *, str approximate="none") -> Tensor

⭐ dispatch: 那一段的结果,也能直接 dump 出来(本机 torch 2.13.0+cpu 实测):

import torch
print(torch._C._dispatch_dump("aten::gelu"))

真实输出里出现的分派键有: MkldnnCPU · Tracer · FuncTorchBatched · CPU · Meta · QuantizedCPU · NestedTensorCPU · NestedTensorHPU · Autograd[alias] · CompositeExplicitAutograd…

⚠️ 注意这是「填完之后的表」,不是账本原文 —— 你看不到每一格背后的 C++ 函数叫什么名字, 那个只有仓库里的 yaml 才有。但要判断「这个后端到底有没有实现」,这份 dump 就够了, 而这恰恰是你 90% 的时候真正想知道的事。

⭐ 这份 dump 里的行,来路不止一种。 像 MkldnnCPU / QuantizedCPU / NestedTensorCPU 这类 后端专属实现,是账本里一行行手写登记的;而 CPU / Meta 这几个基础后端,很多算子 (gelu 就是其一)根本没有手写注册,是 structured_delegate: 让代码生成器按 schema 替你生成的。

⚠️ 所以「dump 里有这一行」不等于「仓库里有人为它写过一个函数」—— 你去 grep 函数名却什么也搜不到时,八成就是撞上了这种生成出来的实现。这是读源码最容易卡住的一处。

⚠️ 这正好解释了第一节第 ④ 步那个转弯:

⭐ dump 里带 build\ 的路径全是【生成物】。 RegisterCPU_3.cpp、VariableType_1.cpp、RegisterSchema.cpp —— 这些文件在 PyTorch 仓库里根本不存在,是构建时按 native_functions.yaml 吐出来的,构建完才躺在 build/ 目录里。 所以拿这个文件名去 GitHub 搜,永远是零结果。

⭐ 正确走法:不要搜文件名,搜算子名。 在 native_functions.yaml 里找到 - func: gelu(,读它的 dispatch: 段拿到函数名,再拿函数名去 aten/src/ATen/native/ 里搜 —— 那才是人手写的、真正算数的代码。

⭐ 顺带解释了另外三件一直没头绪的事:

现象 原因
那些 Register*.cpp 动辄几千行、名字还带 _0 _1 _3 编号 ⭐ 生成物,编号是为了切开编译单元,否则一个文件编译不动
改一个算子签名在 PyTorch 里是件大事 ⭐ C++ 签名 / torch.ops.* / .pyi 存根 / ATen/ops/*.h 全从这一份 yaml 生成,动一处牵一大片
有些算子你在 native/ 里怎么也搜不到实现 它是 CompositeImplicitAutograd 的组合实现,或者被 structured_delegate 转走了

🛑 读到这里可以停 —— 前半章讲完了(约 19 分钟)。 后半章还有:你机器上已经有半个源码树 · ⭐ 不下载源码、只用 Python 就能查的三件事 · ⭐ 一次完整的定位演练 · ⚠️ 诚实收尾:你不需要能编译 PyTorch 才能读它的源码 回来的时候不用重读,直接从下一节接着看就行。


🧩 四、你机器上已经有半个源码树

⭐ 很多人不知道:pip 装的 torch 里就带着一大堆 C++ 头文件。 现场看一眼:

# c06_layout.py
import os
import torch

root = os.path.dirname(torch.__file__)          # ⭐ 现场取,别写死绝对路径
for p in ["include/ATen", "include/c10", "include/torch/csrc", "lib"]:
    ok = os.path.exists(os.path.join(root, *p.split("/")))
    print(("✅ 有" if ok else "❌ 无"), p)

# ⚠️ _C 的文件名带 Python 版本和平台,别写死
print([f for f in os.listdir(root) if f.startswith("_C.") and f.endswith((".pyd", ".so"))])

实跑输出:

要点

✅ 有 include/ATen

✅ 有 include/c10

✅ 有 include/torch/csrc

✅ 有 lib

['_C.cp313-win_amd64.pyd']

⚠️ _C.cp313-win_amd64.pyd 这个名字里的 cp313 和 win_amd64 就是 04 章那三种报错的来源 —— Python 版本和平台焊在文件名里,换一个都不认。⭐ 所以你的机器上一定不叫这个名字,上面那段代码才要动态列出来。

三样东西各是什么:

东西 是什么 ⭐ 你能从里面读到 ⚠️ 读不到
include/ 头文件(.h),第二节那三个目录都在 类型定义、函数签名、宏(05 章那段 TORCH_LIBRARY 展开就是从 include/torch/library.h 抄的)、大量注释 ⚠️ kernel 怎么算的 —— .cpp 不在 wheel 里
lib/ 动态库(.dll / .so),编译好的二进制 导出符号 源码(除非你想读汇编)
_C.*.pyd ⭐ Python 扩展模块本体,import torch 时被加载的就是它 —— ——

⭐ 这三样正好是「编译一个 C++ 扩展」需要的全部输入:编译时找 include/,链接时找 lib/,运行时和 _C 待在同一个进程里。04 章讲的 ABI 匹配,匹的就是这三样。

🚦 ⭐ 头文件不是废料,它是最好读的那部分

「只有声明没有实现」听起来像残缺品,实际上恰恰是最适合读的形态:

⚠️ 但别指望在头文件里找到 kernel:gelu 到底怎么算的那几十行在 aten/src/ATen/native/ 的 .cpp 里,wheel 里没有,必须去 GitHub。


🧩 五、⭐ 不下载源码、只用 Python 就能查的三件事

这一节是全章最实用的部分。一个 REPL 就够,不 clone、不编译。

# c06_probe.py
import torch

# ① 这个算子的签名到底长什么样
print(torch.ops.aten.add.Tensor._schema)

# ② 这个 tensor 会走哪一列
print(torch._C._dispatch_key_set(torch.ones(2)))

# ③ 全站算子表有多大
print(len(torch._C._jit_get_all_schemas()), "条 schema")

实跑输出:

关键信息

aten::add.Tensor(Tensor self, Tensor other, *, Scalar alpha=1) -> Tensor
DispatchKeySet(CPU, ADInplaceOrView, AutogradCPU, AutocastCPU)
4362 条 schema

三件事分别回答什么问题:

工具 回答的问题 ⭐ 什么时候用
torch.ops.aten.<op>.<overload>._schema 参数顺序、默认值、哪个参数会被就地写 写 C++ kernel 前对签名;看不懂某个算子的 out= 变体
torch._C._dispatch_key_set(t) 这个具体的 tensor 属于哪一列 遇到 Could not run ... backend 时,⭐ 第一件该做的事
torch._C._jit_get_all_schemas() 全部 4362 条 schema,可以拿来搜 找「有没有这么个算子」「它有几个重载」

⚠️ 4362 随版本变(torch 2.13.0+cpu 上是这个数,你的一定不同),要看的是量级:四千多条。⭐ 这个数字本身就是一条信息 —— 「把 PyTorch 源码通读一遍」在数量级上就是不成立的目标,你只能反查。

第三件的真正用法是当离线索引来 grep:

# c06_grep.py
import torch

kw = "gelu"                                              # ⭐ 换成你要找的算子
for s in torch._C._jit_get_all_schemas():
    if kw in str(s):
        print(s)

🗓️ 这一段的输出没在本章实跑核对(上面 c06_probe.py 那三行是实跑的),⚠️ 而且命中数一定随版本变 —— 自己跑一眼就知道了。

⭐ 加上 05 章第四节那个 torch._C._dispatch_dump("aten::gelu"),四件工具凑齐:签名、列、全表、以及每一格注册在哪个文件第几行。整条路线的前三步到此全部完成,一行源码都没下载。


🧩 六、⭐ 一次完整的定位演练

把前面五节串起来,走一遍 aten::gelu:

步 动作 结果
① 读报错 拿到 aten::gelu 和 SparseCPU
② torch._C._dispatch_key_set(x) 确认这个 tensor 的第一个键确实是 SparseCPU
③ torch.ops.aten.gelu.default._schema aten::gelu(Tensor self, *, str approximate="none") -> Tensor
④ torch._C._dispatch_dump("aten::gelu") ⭐ 没有 SparseCPU 那一行 —— 结论到手:格子空着
⑤ ⚠️ 拿 RegisterCPU_3.cpp 去 GitHub 搜 ❌ 零结果,它是生成物(第三节)
⑥ ⭐ 改搜 native_functions.yaml 里的 - func: gelu( 拿到 dispatch: 段和 structured_delegate:
⑦ 拿 dispatch: 段里的函数名去 aten/src/ATen/native/ 搜 ✅ 人手写的实现

⭐ 第 ⑤ 步失败不是意外,是这条路线的固定环节。 知道它必然失败,就不会在那里耗半小时。

🚦 ⚠️ 三个必踩的坑

① dump 里的路径是官方 CI 机器的。 C:\actions-runner\_work\pytorch\... 这种前缀,你本地绝对没有 —— 它是构建那个 wheel 的机器上的路径,⭐ 但文件名和行号是真的,对着同一个版本的 tag 看仍然有效。

② 一定要按 tag 看,不要看默认分支。 你本机是 torch.__version__ 那个版本,主分支已经往前走了几千个 commit,行号对不上是常态。⭐ 先 print(torch.__version__),再去仓库切到对应的 tag。

③ 搜到的可能是「另一个同名东西」。 算子名在 native/ 里经常有 CPU、CUDA、量化、嵌套张量好几份同名实现。⭐ 判据是 dispatch: 段里写的那个函数名,不是算子名 —— dispatch: 段告诉你 QuantizedCPU 对应的是 gelu_quantized_cpu 这种带后缀的名字,拿它去搜就不会串。

⚠️ 这三条全是「认词不认结构」的老毛病:拿算子名当函数名搜、拿生成物文件名当源文件搜、拿主分支当自己那个版本 —— 每一条都能让你在错的地方读半天正确的代码。


🧩 七、⚠️ 诚实收尾:你不需要能编译 PyTorch 才能读它的源码

这是本板块最后要说的一件事,也是最容易劝退人的一个误解。

🗓️ 未实跑 —— 本机无编译环境(05 章第五节已说明:MinGW g++ 配 MSVC 编译的 CPython + CPU 版 torch,编不出能导入的扩展)。以下关于「从源码编译 PyTorch」的描述作者没有在本机做过,只说它的性质,不给具体数字:

⚠️ 从源码全量编译 PyTorch 要装齐匹配的编译器工具链、CMake、(若要 GPU)CUDA toolkit,占用大量磁盘和内存,耗时以小时计,且第一次基本不会一次成功。

⭐ 但是 —— 上面六节里,需要编译的步骤是零。

三档阅读深度,按需选:

档 你能做什么 代价 什么时候够用
① 只用本机 Python 第五节那四件工具:schema、keyset、全表、dump ⭐ 零 ⭐ 绝大多数「这个算子为什么报错 / 走了哪条路」的问题,到这里就答完了
② 加上 GitHub 按 tag 看文本 第六节整条路线走完,读到 kernel 源码 一个浏览器 想搞懂「它到底怎么算的」「为什么这么设计」
③ 真编译 改 PyTorch 本身并验证 小时级 + 反复踩工具链 ⭐ 只有一种情况:你要给 PyTorch 提 PR

⭐ 想验证自己写的算子机制对不对,也不用编译整个 PyTorch —— 05 章第七节给的那两条路(load_inline 运行时编译、或者干脆用 torch.library 的 Python 接口填格子)都比全量编译便宜好几个数量级。

⭐ 「读源码」和「改源码」是两件事,成本差三个数量级。 把它们混为一谈,是很多人一辈子没打开过 PyTorch 源码的唯一原因。

⚠️ 最后一句提醒:读源码是手段不是目的。01 章第三节那张反面表和 AI 基础设施 07的三个条件依然管用 —— 先确认你真的需要下到这一层,再动手。


🔗 这一章连到哪里

相关的地方 为什么
05 章 本章第一、三、六节全建在它的 _dispatch_dump 输出上;⭐ 它讲格子怎么填,本章讲填格子的代码在哪个文件
04 章 第四节 _C.cp313-win_amd64.pyd 名字里的 cp313 / win_amd64,⭐ 就是那三种报错的直接来源;include/ + lib/ + _C 三件套正是 ABI 要匹的东西
01 章 第二节说 torch/csrc 是唯一认识 Python 的那层 —— 那条边界物理上就落在这个目录里
02 章 想读 c10/core/TensorImpl.h 的话,intrusive_ptr 的引用计数语义在那一章讲过
00 章 ⭐ 本板块到这里读完了,回去看一眼「读完之后往哪走」
《PyTorch 这个框架本身》09 ⭐ 想动手而不是只读的话,torch.library 的 Python 接口在那边 —— 第七节说的「不用编译也能验证机制」就是指它
AI 基础设施 07 第七节最后一句:下到这一层之前先对那三个条件

✅ 检查点

  1. 从一条 Could not run 'aten::gelu' 报错走到 C++ 实现,本章给的路线有哪几步?其中哪一步注定失败,为什么?
  2. c10 / aten / torch/csrc 各是什么?判断一个东西该放在哪个目录,本章给的两个问题是什么?
  3. 为什么 DispatchKey 的定义在 c10 里,而「aten::gelu 的 CPU 格填了什么」不在?这和 05 章哪个现象对得上?
  4. native_functions.yaml 在哪个路径?它的 func: / dispatch: / structured_delegate: 三段分别对应 05 章那张表上的什么动作?
  5. 05 章 dump 里的 CPU 和 Meta 两行并不在 dispatch: 段里,为什么它们照样出现了?
  6. RegisterCPU_3.cpp 为什么在 GitHub 上搜不到?正确的搜法是什么?这条也顺带解释了「为什么这些文件动辄几千行还带编号」——为什么?
  7. pip 装的 torch 里有 include/ lib/ _C.*.pyd,各能读到什么、读不到什么?_C 的文件名里那两段信息为什么不能写死?
  8. 不下载源码、只用 Python 能查的三件(加 05 章那件共四件)工具分别是什么、各回答什么问题?本机实跑的 schema 总数是多少,这个数字本身说明了什么?
  9. 去 GitHub 对源码时,本章点名的三个坑是什么?
  10. 读 PyTorch 源码的三档深度分别要付出什么代价?什么情况下才真的需要从源码编译 PyTorch?
👀 答案
  1. 六步:报错 → 算子名 → keyset 确认列 → schema 拿签名 → _dispatch_dump 拿「注册在哪个文件第几行」→ 去 native_functions.yaml 找 dispatch: 段 → 拿函数名去 aten/src/ATen/native/ 搜实现。⚠️ 注定失败的是「拿 dump 给的文件名去 GitHub 搜」:RegisterCPU_3.cpp 这类带 build\ 路径的文件是构建时生成的,仓库里根本不存在。知道它必然失败,就不会在那里耗时间。
  2. c10 = 最底层公共设施(DispatchKey、TensorImpl、Device、intrusive_ptr),不认识任何具体算子;aten = A Tensor library,算子的家(native/ 下的 kernel 和 native_functions.yaml);torch/csrc = 认识 Python 的那一层外加 autograd/ jit/ distributed/。两个问题:「它认识 Python 吗」(认识 → torch/csrc)、「它认识具体算子吗」(不认识 → c10,认识 → aten)。
  3. 因为 表的骨架归 c10,表的内容归 aten —— 定义骨架的那一层压根不知道有哪些行。这正好对上 05 章那个现象:这张表不是编译期写死的常量表,是运行时一批批被加载进来的(本机 CUDA 那列全空,就是因为 CPU 版轮子里没有那批库)。
  4. aten/src/ATen/native/native_functions.yaml。func: = schema 本尊 = 新开一行(等价于 C++ 的 m.def);dispatch: = 键→函数名映射 = 一次填好几个格子(等价于一串 m.impl);structured_delegate: = 「转给 gelu.out」,让代码生成器替你填 CPU / CUDA / Meta 那几格。
  5. 因为它们不是手写注册的,是生成的 —— structured_delegate: gelu.out 让代码生成器按 schema 吐出了这几格。反过来,dump 里的 MkldnnCPU / QuantizedCPU / NestedTensorCPU / NestedTensorHPU 正是 dispatch: 段里手写列出的那几个,两边严丝合缝对得上。
  6. 因为它是生成物,按 native_functions.yaml 在构建时吐出来、构建完躺在 build/ 目录里,仓库里没有。正确搜法是 不搜文件名、搜算子名:在 native_functions.yaml 里找 - func: gelu(,读 dispatch: 段拿到函数名,再拿函数名去 aten/src/ATen/native/ 搜。几千行加 _0 _1 _3 编号也是同一个原因 —— 生成出来的,编号是为了切开编译单元,否则一个文件编译不动。
  7. include/ = 头文件,能读到类型定义、函数签名、宏(05 章那段 TORCH_LIBRARY 展开就抄自本机 include/torch/library.h 第 975 行起)和大量注释,⚠️ 读不到 kernel 怎么算的(.cpp 不在 wheel 里);lib/ = 编译好的动态库,只有导出符号没有源码;_C.*.pyd = Python 扩展模块本体,import torch 加载的就是它。文件名里的 cp313(Python 版本)和 win_amd64(平台)焊死在名字里,⚠️ 换一个都不认 —— 那正是 04 章三种报错的来源,所以代码里要 os.listdir 动态列,不能写死。
  8. ① torch.ops.aten.<op>.<overload>._schema → 参数顺序、默认值、哪个参数会被就地写;② torch._C._dispatch_key_set(t) → 这个 tensor 属于哪一列(遇到 Could not run ... backend 第一件该做的事);③ torch._C._jit_get_all_schemas() → 全表,可以拿来 grep;④(05 章)torch._C._dispatch_dump(op) → 每一格注册在哪个文件第几行。实跑 4362 条(⚠️ 随版本变)。这个量级本身说明:「把 PyTorch 源码通读一遍」在数量级上就不成立,只能反查。
  9. ① dump 里的 C:\actions-runner\... 是官方 CI 机器的路径,本地绝对没有(但文件名行号是真的);② 必须按 tag 看不能看默认分支 —— 先 print(torch.__version__) 再切 tag,否则行号对不上是常态;③ 同名实现有好几份(CPU / CUDA / 量化 / 嵌套张量),判据是 dispatch: 段里那个函数名(如 gelu_quantized_cpu)而不是算子名。⚠️ 三条都是「认词不认结构」的老毛病。
  10. ① 只用本机 Python:零代价,绝大多数「为什么报错 / 走了哪条路」的问题到这就答完了;② 加 GitHub 按 tag 读文本:一个浏览器,能读到 kernel 源码;③ 真编译:要装齐匹配工具链 + CMake +(GPU 的话)CUDA toolkit,耗时以小时计且第一次基本不会成功(🗓️ 本章未实跑)。只有一种情况真需要它:你要给 PyTorch 提 PR。 想验证自己写的算子机制,用 load_inline 或 torch.library 的 Python 接口就够,便宜好几个数量级。

🛑 可以停在这里

⚡ 走神救援

🗺️ 读 PyTorch 源码不是「clone 下来从头看」,是【从一条报错反查】。 实测总 schema 有四千多条——⭐ 这个量级本身就判了「通读」死刑。

整条路线是六步:报错 → 算子名 → 确认分派键 → 拿 schema → 拿「注册在哪个文件第几行」→ 去 native_functions.yaml 拿真正的函数名 → 找实现。

⚠️⚠️ 其中有一步注定失败:dump 里写的那个文件名去仓库里搜是零结果——带 build\ 的全是构建时生成物。⭐ 知道它必然失败,就不会在那儿耗半小时。

📁 三个目录,两个问题就能分清:「它认识 Python 吗」——认识的是 torch/csrc(⭐ 唯一会 include Python.h 的一层,那条跨语言边界物理上就在这里);「它认识具体算子吗」——不认识的是 c10,认识的是 aten。⭐ 一句话记法:表的骨架归 c10,表的内容归 aten——这正好解释了「表不是编译期常量」,因为定义骨架的那层压根不知道有哪些行。

📒 native_functions.yaml 是整条生成链的上游:func: 等于新开一行,dispatch: 等于一次填好几个格子,而 structured_delegate: 让代码生成器替你填。⭐ 拿 dump 一对就明白:有些键在 dispatch: 段里手写着,有些不在段里却照样出现——后者就是生成的。

⭐ 正确搜法:不搜文件名,搜算子名。 ⚠️ 另外那个动态库的文件名把 Python 版本和平台焊死在名字上,换一个都不认——所以路径要动态取,绝不写死。

下一节 👉 附录A · 报错反查与速查

打卡记录保存在你的浏览器里,首页能看到总进度