Skip to main content

概述

Tensor dump 会记录模型推理时每个叶子节点的 输出激活值,每次 Engine.step() 写一个文件。当你需要回答”某一层在运行时究竟产出了什么”时,它就是首选工具。 排查数值回归、对比两套后端(如 flashinfer vs eager,或 bf16 vs FP8 构建)、 或把新移植的模型对照参考实现做校验。 它基于 PyTorch 的 forward hook:给每个被选中且没有子模块的 nn.Module 挂一个 hook,捕获它的返回值,搬到 CPU,并以模块的点分名字 (model.expert_stack.layers.0.o_proj)累积起来。不会 dump 权重,因为 权重是静态的,本就存在 checkpoint 里;这里捕获的是随输入变化的中间Tensor。
Tensor dump 只能在 eager 模式下工作。 捕获后的 CUDA graph 在 replay 时不会 重新进入 Python,所以 forward hook 在 graph replay 期间永远不触发。一旦你设置了 dump 目录,引擎会 强制 use_cuda_graph=False(并打印 warning),让 hook 真正 执行。dump 时请按 eager 模式的速度预期 —— 这是调试路径,不是生产路径。

启用方式

Tensor dump 默认关闭。可以通过 runtime 配置打开,也可以纯靠环境变量打开, 按你的工作流选择。
在不改调用方代码的前提下,为单次运行临时打开 dump 的最轻量方式。PHYAI_* 变量会叠加在程序传入的任意 config 之上,即便脚本自己构造了(一旦有 ENV 变量,其优先级 > EngineConfig) EngineConfig 也照样生效:
用一个 JSON 数组的正则(对每个算子的完整点分名字匹配)来限定捕获范围:

选择 dump 什么

VLA 模型并不是单一同构的 decoder stack。仅 pi0.5 就有三个 layers.<int> stack(视觉 encoder、PaliGemma 语言模型、动作 expert),外加一批根本没有层号的 组件(headsrope、各种 embedding / projector)。filter 就是为这个情况准备的 filter 接受三种形式:
捕获每一个叶子算子。pi0.5 每步约 1500 个Tensor,所以一旦你明确了要看什么, 就尽量用更窄的 filter 来自己捕获 Tensor。
每条 pattern 用 re.search 对算子名匹配,多条之间取并集(OR)。示例:
对于正则表达不了的逻辑,传一个 (name: str, module: nn.Module) -> bool 谓词。它还能拿到 module,所以可以按类型分派:
在 config 或环境变量里以 "my_pkg.filters:keep"(import 路径)或 "/tmp/myfilter.py:keep"(文件路径,临时调试时不用装包,方便使用)指向它。

输出布局

每个 rank 写到各自的子目录,避免并发进程之间互相覆盖;每次 Engine.step() 产生一个 带编号的 pass 文件:
/tmp/dump
rank0_pid3069569
pass00000.pt
pass00001.pt
pass00002.pt
每个 .pt 文件是一个 {算子名: cpu_tensor} 的字典。当一个算子在单步内触发多次。 视觉塔每个相机跑一次、动作 expert 每个 Euler 去噪步跑一次,每次调用都会被保留: 第一次以裸名字为 key,之后的加 ::callN 后缀。

加载 dump

load_pass 读回一个 pass 文件: