备注
点击此处下载完整示例代码
电感 CPU 后端调试和性能分析¶
创建于:2025 年 4 月 1 日 | 最后更新:2025 年 4 月 1 日 | 最后验证:2024 年 11 月 5 日
作者:廖璇,朱浩哲,龚炯,王伟瀚
概述 ¶
PyTorch 2.0 引入了名为 torch.compile
的编译 API。这一新特性通过默认的 Inductor 后端驱动的图级别优化,在急切模式执行上提供了显著的加速。
本教程旨在深入介绍在 Inductor CPU 后端上进行调试和性能分析,通过深入了解 torch.compile
的细节。
同时,您还可以找到关于 torch.compile
的相关教程,包括基本用法、全面故障排除以及 GPU 特定知识,如 GPU 性能分析。
我们将从一个激发编译问题和精度问题的示例开始调试,通过展示调试过程来定位问题。
通过启用日志并探索底层生成的代码,您可以逐步缩小故障步骤,最终找出根本原因。
随后,我们将讨论如何分析编译后的代码,并通过与急切模式的性能比较,阐述为什么 torch.compile
与其急切对应版本相比可以提供额外的性能提升。
调试
下面是一个简单的示例,展示如何使用 Inductor 运行 torch.compile
,并将其结果与急切模式进行比较:
import torch
def foo1(x1, x2):
a = torch.neg(x1)
b = torch.maximum(x2, a)
y = torch.cat([b], dim=0)
return y
x1 = torch.randint(256, (1, 8), dtype=torch.uint8)
x2 = torch.randint(256, (8390, 8), dtype=torch.uint8)
compiled_foo1 = torch.compile(foo1)
result = compiled_foo1(x1, x2)
neg
在 cpp
代码生成中的正确实现如下:
def neg1(x):
return f"decltype({x})(-{x})"
为了演示调试,我们稍后会修改函数为错误版本。
获取更多日志信息
如果您默认运行此简单示例,则不会提供调试信息。为了获取更多有用的调试和日志信息,我们通常添加如下 TORCH_COMPILE_DEBUG
环境变量:
TORCH_COMPILE_DEBUG=1 python xx.py
这将在输出日志中打印更多的调试信息,并且还会在代码生成过程中生成的中间 IRs 进行转储。您可以在日志中找到转储的文件路径,如下所示:
torch._inductor.debug: [WARNING] model___20 debug trace: /tmp/torchinductor_root/rx/crxfi2ybd7yp5sbj2pnhw33wfhtdw7wumvrobyp5sjvdui5ktjc2.debug
在此目录中,以下文件保存用于调试目的:
文件 |
描述 |
---|---|
|
可执行 FX 图,分解后,在模式匹配前 |
|
转换后的 FX 图,在模式匹配后 |
|
电流感抗 IR 在融合前 |
|
电流感抗 IR 在融合后 |
|
使用 C++/Triton 内核生成的图形 Python 代码 |
注意, fx_graph_runnable.py
和 output_code.py
均可运行和编辑,以便更容易进行调试。以下是代码文件中提取的主要部分,我们将生成的 C++代码行与 FX 代码行进行关联。
fx_graph_runnable
:
def forward1(self, arg0_1, arg1_1):
neg = torch.ops.aten.neg.default(arg0_1); arg0_1 = None
maximum = torch.ops.aten.maximum.default(arg1_1, neg); arg1_1 = neg = None
clone = torch.ops.aten.clone.default(maximum); maximum = None
return (clone,)
output_code
中的 C++内核:
import torch
from torch._inductor.async_compile import AsyncCompile
async_compile = AsyncCompile()
cpp_fused_cat_maximum_neg_0 = async_compile.cpp('''
#include "/tmp/torchinductor_root/gv/cgv6n5aotqjo5w4vknjibhengeycuattfto532hkxpozszcgxr3x.h"
extern "C" void kernel(const unsigned char* in_ptr0,
const unsigned char* in_ptr1,
unsigned char* out_ptr0)
{
{
#pragma GCC ivdep
for(long i0=static_cast<long>(0L); i0<static_cast<long>(8390L); i0+=static_cast<long>(1L))
{
#pragma GCC ivdep
for(long i1=static_cast<long>(0L); i1<static_cast<long>(8L); i1+=static_cast<long>(1L))
{
auto tmp0 = in_ptr0[static_cast<long>(i1 + (8L*i0))];
auto tmp1 = in_ptr1[static_cast<long>(i1)];
// Corresponding FX code line: neg = torch.ops.aten.neg.default(arg0_1); arg0_1 = None
auto tmp2 = decltype(tmp1)(-tmp1);
// Corresponding FX code line: maximum = torch.ops.aten.maximum.default(arg1_1, neg); arg1_1 = neg = None
auto tmp3 = max_propagate_nan(tmp0, tmp2);
// Corresponding FX code line: clone = torch.ops.aten.clone.default(maximum); maximum = None
out_ptr0[static_cast<long>(i1 + (8L*i0))] = tmp3;
}
}
}
}''')
确定误差分量 ¶
遇到错误或准确性问题时,找到问题的直接解决方案是缩小问题范围。首先要确定错误发生的组件。幸运的是,可以通过简单地更改 torch.compile
的后端来实现。
代码 |
描述 |
---|---|
|
启用 Dynamo |
|
启用 Dynamo + AOT 自动微分 |
|
启用 Dynamo + AOT 自动微分 + 电感器 |
如果模型在将后端设置为 eager
或 aot_eager
时可以成功运行,而使用 inductor
时失败,我们可以将故障缩小到电感器。
编译错误 ¶
如我们所知,图级别优化的演变链路如下:
torch.neg (Python) -> torch.ops.aten.neg.default (within FX graph) -> ops.neg (within IR node) -> tmp2 = -tmp1 (within C++ kernel)
如果你在输出代码中遇到编译错误,说明在编译 C++内核时出现了问题。这种错误表明在将 IR 节点降低到输出代码时引入了错误。编译错误的根本原因通常会在跟踪日志中显示。
例如, neg
函数被修改如下:
def neg2(x):
return f"-{x}"
日志给出了以下编译错误,原因相当明确。
torch._dynamo.exc.BackendCompilerFailed: backend='inductor' raised:
CppCompileError: C++ compile error
/tmp/torchinductor_root/xg/cxga5tk3b4lkwoxyigrtocjp5s7vc5cg2ikuscf6bk6pjqip2bhx.cpp: In function ‘void kernel(const unsigned char*, const unsigned char*, unsigned char*)’:
/tmp/torchinductor_root/xg/cxga5tk3b4lkwoxyigrtocjp5s7vc5cg2ikuscf6bk6pjqip2bhx.cpp:17:57: error: no matching function for call to ‘max_propagate_nan(unsigned char&, int&)’
17 | auto tmp3 = max_propagate_nan(tmp0, tmp2);
| ^
In file included from /tmp/torchinductor_root/xg/cxga5tk3b4lkwoxyigrtocjp5s7vc5cg2ikuscf6bk6pjqip2bhx.cpp:2:
/tmp/torchinductor_root/gv/cgv6n5aotqjo5w4vknjibhengeycuattfto532hkxpozszcgxr3x.h:27:17: note: candidate: ‘template<class scalar_t> scalar_t max_propagate_nan(scalar_t, scalar_t)’
27 | inline scalar_t max_propagate_nan(scalar_t a, scalar_t b) {
| ^~~~~~~~~~~~~~~~~
/tmp/torchinductor_root/gv/cgv6n5aotqjo5w4vknjibhengeycuattfto532hkxpozszcgxr3x.h:27:17: note: template argument deduction/substitution failed:
/tmp/torchinductor_root/xg/cxga5tk3b4lkwoxyigrtocjp5s7vc5cg2ikuscf6bk6pjqip2bhx.cpp:17:57: note: deduced conflicting types for parameter ‘scalar_t’ (‘unsigned char’ and ‘int’)
17 | auto tmp3 = max_propagate_nan(tmp0, tmp2);
| ^
让我们再看看输出代码和 IR 节点中对应的 C++内核。
C++内核:
include "/tmp/torchinductor_root/gv/cgv6n5aotqjo5w4vknjibhengeycuattfto532hkxpozszcgxr3x.h"
extern "C" void kernel(const unsigned char* in_ptr0,
const unsigned char* in_ptr1,
unsigned char* out_ptr0)
{
{
#pragma GCC ivdep
for(long i0=static_cast<long>(0L); i0<static_cast<long>(8390L); i0+=static_cast<long>(1L))
{
#pragma GCC ivdep
for(long i1=static_cast<long>(0L); i1<static_cast<long>(8L); i1+=static_cast<long>(1L))
{
auto tmp0 = in_ptr0[static_cast<long>(i1 + (8L*i0))];
auto tmp1 = in_ptr1[static_cast<long>(i1)];
auto tmp2 = -tmp1;
auto tmp3 = max_propagate_nan(tmp0, tmp2);
out_ptr0[static_cast<long>(i1 + (8L*i0))] = tmp3;
}
}
}
}
IR 节点:
buf0: SchedulerNode(ComputedBuffer)
buf0.writes = [MemoryDep('buf0', c0, {c0: 67120})]
buf0.unmet_dependencies = []
buf0.met_dependencies =
[ MemoryDep('arg0_1', c1, {c0: 8390, c1: 8}),
MemoryDep('arg1_1', c0, {c0: 67120})]
buf0.users = [NodeUser(node=OUTPUT, can_inplace=False)]
buf0.group.device = cpu
buf0.group.iteration = ((8390, 8), ())
buf0.sizes = ([8390, 8], [])
class buf0_loop_body:
var_ranges = {z0: 8390, z1: 8}
index0 = 8*z0 + z1
index1 = z1
def body(self, ops):
get_index = self.get_index('index0')
load = ops.load('arg1_1', get_index)
get_index_1 = self.get_index('index1')
load_1 = ops.load('arg0_1', get_index_1)
neg = ops.neg(load_1)
maximum = ops.maximum(load, neg)
get_index_2 = self.get_index('index0')
store = ops.store('buf0', get_index_2, maximum, None)
return store
根据回溯日志,编译错误是由 max_propagate_nan
的输入数据类型不一致引起的。通过检查 C++内核,我们知道在执行 -
后, tmp2
不再是 long
,因为 tmp0
是 long
。我们可以在 C++内核中轻松地将 -
和 max_propagate_nan
与 IR 节点中的 ops.neg
和 ops.maximum
分别匹配。
现在我们成功找到根本原因是 ops.neg
在 cpp
代码生成中的实现,它在执行 neg
时静默地改变了数据类型。
准确性调试 ¶
否则,如果模型运行时出现其他错误或准确性问题,您可以使用名为 Minifier 的 PyTorch 调试工具。
Minifier
的核心思想是不断移除图中的节点和输入,直到找到具有问题的最小图。它通过以下 4 种策略自动生成最小化的问题图:截断后缀、delta 调试、消除死代码和移除未使用输入。
我们现在将借助 Minifer
展示调试准确度问题的过程。准确度问题指的是后端急切和感应器输出不同的情形。
例如,我们这样修改示例:
from torch._dynamo.utils import same
def foo2(x1, x2):
a = torch.neg(x1)
b = torch.maximum(x2, a)
y = torch.cat([b], dim=0)
return y
x1 = torch.randn((1, 8), dtype=torch.float32)
x2 = torch.randn((8390, 8), dtype=torch.float32)
expected_result = foo2(x1, x2)
compiled_foo2 = torch.compile(foo2)
actual_result = compiled_foo2(x1, x2)
assert same(expected_result, actual_result) == True
同时修改 neg
函数:
def neg3(x):
return f"decltype({x})(2 * {x})"
如下将引发准确度问题:
torch._dynamo.utils: [ERROR] Accuracy failed: allclose not within tol=0.0001
Traceback (most recent call last):
File "test_script.py", line 18, in <module>
assert same(expected_result, actual_result) == True
AssertionError
调试 Minifier 的准确性问题时,需要两个环境变量:
TORCHDYNAMO_REPRO_AFTER="aot" TORCHDYNAMO_REPRO_LEVEL=4 python xx.py
这为我们提供了日志信息,展示了压缩的步骤:
Started off with 6 nodes
Trying granularity 2
Strategy: Truncate suffix (G: 2) (6 nodes, 2 inputs)
SUCCESS: Went from 6 to 4 nodes
Trying granularity 4
Strategy: Remove unused inputs (G: 4) (4 nodes, 2 inputs)
SUCCESS: Went from 4 to 3 nodes
运行后,我们得到最终压缩的包含目标节点 neg
的图:
def forward2(self, arg0_1):
neg = torch.ops.aten.neg.default(arg0_1); arg0_1 = None
return (neg,)
关于 Minifier 的更多使用详情,请参阅故障排除。
性能分析
在本节中,我们将演示使用 Inductor CPU 后端编译的模型进行性能分析的过程。以下示例中,我们使用 Hugging Face Transformer 模型 MobileBertForQuestionAnswering
,在急切模式和 Inductor 图模式中进行基准测试。基准测试后,打印出 Inductor 的执行时间和加速比。我们使用 Intel(R) Xeon(R) Platinum 8358 CPU @ 2.60GHz,并在第一个插槽上运行基准测试,以展示本节中的优化。我们按照最佳实践设置以下环境变量以在 Intel(R) CPU 上进行基准测试。
export KMP_BLOCKTIME=1
export KMP_SETTINGS=1
export KMP_AFFINITY=granularity=fine,compact,1,0
export LD_PRELOAD=${CONDA_PREFIX:-"$(dirname $(which conda))/../"}/lib/libiomp5.so:${CONDA_PREFIX:-"$(dirname $(which conda))/../"}/lib/libjemalloc.so
export MALLOC_CONF="oversize_threshold:1,background_thread:true,metadata_thp:auto,dirty_decay_ms:-1,muzzy_decay_ms:-1"
numactl -C 0-31 -m 0 python bench.py
# bench.py
from transformers import MobileBertForQuestionAnswering
# Initialize an eager model
model = MobileBertForQuestionAnswering.from_pretrained("csarron/mobilebert-uncased-squad-v2")
seq_length = 128
bs = 128
vocab_size = model.config.vocab_size
input = torch.randint(0, vocab_size, (bs, seq_length), dtype=torch.int64)
input_dict = {"input_ids": input}
# Initialize the inductor model
compiled_model = torch.compile(model)
with torch.no_grad():
compiled_model(**input_dict)
NUM_ITERS=50
import timeit
with torch.no_grad():
# warmup
for _ in range(10):
model(**input_dict)
eager_t = timeit.timeit("model(**input_dict)", number=NUM_ITERS, globals=globals())
with torch.no_grad():
# warmup
for _ in range(10):
compiled_model(**input_dict)
inductor_t = timeit.timeit("compiled_model(**input_dict)", number=NUM_ITERS, globals=globals())
# print(f"eager use: {eager_t * 1000 / NUM_ITERS} ms/iter")
# print(f"inductor use: {inductor_t * 1000 / NUM_ITERS} ms/iter")
# print(f"speed up ratio: {eager_t / inductor_t}")
输出:
eager use: 802.1023553796113 ms/iter
inductor use: 339.95180135127157 ms/iter
speed up ratio: 2.359459053287382
在我们的测试中,我们发现 Inductor CPU 后端将模型加速了约 2.355 倍。
接下来,让我们深入到操作级别来了解速度提升的原因。PyTorch Profiler 是一个很好的工具来帮助我们。Inductor CPU 后端支持使用 enable_kernel_profile
配置选项将融合内核的时间报告给分析器:
from torch._inductor import config
config.cpp.enable_kernel_profile = True
按照 PyTorch Profiler 的步骤,我们能够获取分析表和跟踪文件。
# bench.py
from torch.profiler import profile, schedule, ProfilerActivity
RESULT_DIR = "./prof_trace"
my_schedule = schedule(
skip_first=10,
wait=5,
warmup=5,
active=1,
repeat=5)
def trace_handler(p):
output = p.key_averages().table(sort_by="self_cpu_time_total", row_limit=20)
# print(output)
p.export_chrome_trace(f"{RESULT_DIR}/{p.step_num}.json")
for _ in range(10):
model(**input_dict) # compiled_model(**input_dict) to get inductor model profiling
total = 0
with profile(
activities=[ProfilerActivity.CPU],
schedule=my_schedule,
on_trace_ready=trace_handler
) as p:
for _ in range(50):
model(**input_dict) # compiled_model(**input_dict) to get inductor model profiling
p.step()
我们得到了以下针对 eager-mode 模型的性能分析表(省略了一些列):
------------------------- ------------ ------------ ------------
Name CPU total % CPU total # of Calls
------------------------- ------------ ------------ ------------
aten::addmm 45.73% 370.814ms 362
aten::add 19.89% 161.276ms 363
aten::copy_ 14.97% 121.416ms 488
aten::mul 9.02% 73.154ms 194
aten::clamp_min 8.81% 71.444ms 96
aten::bmm 5.46% 44.258ms 48
ProfilerStep* 100.00% 810.920ms 1
aten::div 2.89% 23.447ms 24
aten::_softmax 1.00% 8.087ms 24
aten::linear 46.48% 376.888ms 362
aten::clone 2.77% 22.430ms 98
aten::t 0.31% 2.502ms 362
aten::view 0.14% 1.161ms 850
aten::transpose 0.17% 1.377ms 386
aten::index_select 0.12% 952.000us 3
aten::expand 0.12% 986.000us 458
aten::matmul 8.31% 67.420ms 48
aten::cat 0.09% 703.000us 1
aten::as_strided 0.08% 656.000us 963
aten::relu 8.86% 71.864ms 96
------------------------- ------------ ------------ ------------
Self CPU time total: 810.920ms
同样,我们也得到了使用 Inductor 编译的模型的分析表(省略了一些列):
----------------------------------------------- ------------ ------------ ------------
Name CPU total % CPU total # of Calls
----------------------------------------------- ------------ ------------ ------------
mkl::_mkl_linear 68.79% 231.573ms 362
aten::bmm 8.02% 26.992ms 48
ProfilerStep* 100.00% 336.642ms 1
graph_0_cpp_fused_constant_pad_nd_embedding_0 0.27% 915.000us 1
aten::empty 0.27% 911.000us 362
graph_0_cpp_fused__mkl_linear_add_mul_relu_151 0.27% 901.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_226 0.27% 899.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_361 0.27% 898.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_121 0.27% 895.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_31 0.27% 893.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_76 0.26% 892.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_256 0.26% 892.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_346 0.26% 892.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_241 0.26% 891.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_316 0.26% 891.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_91 0.26% 890.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_106 0.26% 890.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_211 0.26% 890.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_61 0.26% 889.000us 1
graph_0_cpp_fused__mkl_linear_add_mul_relu_286 0.26% 889.000us 1
----------------------------------------------- ------------ ------------ ------------
Self CPU time total: 336.642ms
从急切模型的配置文件表中,我们可以看到耗时最多的操作是[ aten::addmm
, aten::add
, aten::copy_
, aten::mul
, aten::clamp_min
, aten::bmm
]。与感应器模型配置文件表相比,我们注意到有一个 mkl::_mkl_linear
条目和多个以 graph_0_cpp_fused_*
形式融合的内核。它们是感应器模型进行的主要优化。让我们分别讨论它们。
(1) 关于 mkl::_mkl_linear
:你可能注意到这个内核的调用次数为 362,这与急切模型配置文件表中的 aten::linear
完全相同。 aten::linear
的 CPU 总耗时为 376.888ms,而 mkl::_mkl_linear
为 231.573ms。这表明“线性”部分的加速比约为 1.63 倍。加速主要来自于将权重张量打包成块内存格式,并在感应器 CPU 后端调用 cblas_sgemm_compute,以在 GEMM 计算期间获得更好的缓存行为。
(2) 关于其他内存密集型操作:在我们的测试中,急切/感应器模型的端到端延迟为 802/339ms。因此,我们可以大致推断其他内存密集型操作的加速比约为 3.94 倍。让我们阅读生成的代码,以了解感应器如何实现这种令人印象深刻的优化。你可以通过在 output_code.py
中搜索 cpp_fused__mkl_linear_add_mul_relu_151
来找到生成的代码。
cpp_fused__mkl_linear_add_mul_relu_151 = async_compile.cpp('''
#include <ATen/record_function.h>
#include "/tmp/torchinductor_root/lr/clrlgu27q4ggd472umdzwsu6qcpqxcuusjxqvx2hwitjbujiiz7z.h"
extern "C" void kernel(float* in_out_ptr0,
const float* in_ptr0,
const float* in_ptr1,
const float* in_ptr2,
const float* in_ptr3)
{
RECORD_FUNCTION("graph_0_cpp_fused__mkl_linear_add_mul_relu_151", c10::ArrayRef<c10::IValue>({}));
#pragma omp parallel num_threads(32)
{
{
#pragma omp for
for(long i0=static_cast<long>(0L); i0<static_cast<long>(16384L); i0+=static_cast<long>(1L))
{
for(long i1=static_cast<long>(0L); i1<static_cast<long>(512L); i1+=static_cast<long>(8L))
{
auto tmp0 = at::vec::Vectorized<float>::loadu(in_ptr0 + static_cast<long>(i1 + (512L*i0)));
auto tmp1 = at::vec::Vectorized<float>::loadu(in_ptr1 + static_cast<long>(i1));
auto tmp3 = at::vec::Vectorized<float>::loadu(in_out_ptr0 + static_cast<long>(i1 + (512L*i0)));
auto tmp5 = at::vec::Vectorized<float>::loadu(in_ptr2 + static_cast<long>(i1));
auto tmp7 = at::vec::Vectorized<float>::loadu(in_ptr3 + static_cast<long>(i1));
auto tmp2 = tmp0 + tmp1;
auto tmp4 = tmp2 + tmp3;
auto tmp6 = tmp4 * tmp5;
auto tmp8 = tmp6 + tmp7;
tmp8.store(in_out_ptr0 + static_cast<long>(i1 + (512L*i0)));
}
}
}
}
}''')
从上面的生成代码中,我们可以看到这个内核对 [add, add, mul, add]
进行了典型的循环融合。这是一个内存瓶颈,阻碍了良好的性能。为了更直观地了解这种优化,我们可以推断输入的大小和步长,并进一步基准测试这个 [add, add, mul, add]
模式。
# bench.py
def func(arg_0, arg_1, arg_2, arg_3, arg_4):
add_0 = arg_0 + arg_1
add_1 = add_0 + arg_2
mul_1 = add_1 * arg_3
add_2 = mul_1 + arg_4
arg_2 = add_2
return arg_2
arg_0 = torch.rand(16384, 512)
arg_1 = torch.rand(1, 512)
arg_2 = torch.zeros(16384, 512)
arg_3 = torch.rand(1, 512)
arg_4 = torch.rand(1, 512)
input = (arg_0, arg_1, arg_2, arg_3, arg_4)
inductor_func = torch.compile(func)
with torch.no_grad():
inductor_func(*input)
import timeit
NUM_ITERS=100
with torch.no_grad():
# warmup
for _ in range(10):
func(*input)
eager_t = timeit.timeit("func(*input)", number=NUM_ITERS, globals=globals())
with torch.no_grad():
# warmup
for _ in range(10):
inductor_func(*input)
inductor_t = timeit.timeit("inductor_func(*input)", number=NUM_ITERS, globals=globals())
# print(f"eager use: {eager_t * 1000 / NUM_ITERS} ms/iter")
# print(f"inductor use: {inductor_t * 1000 / NUM_ITERS} ms/iter")
# print(f"speed up ratio: {eager_t / inductor_t}")
输出:
eager use: 5.780875144992024 ms/iter
inductor use: 0.9588955780491233 ms/iter
speed up ratio: 6.0286805751604735
这只是一个例子。性能分析表显示,在这个模型中,所有逐元素操作都在电感器中自动融合。您可以在 output_code.py 中阅读更多内核。
结论 ¶
该文档深入讲解了 Inductor CPU 后端教程。
通过激励性的例子,我们一步步讲解调试和性能分析的过程。主要思想是缩小问题范围。
我们一步步演示如何深入问题,并利用调试日志和 Minifier 工具找到失败的根本原因。首先确定失败发生在哪个组件中,然后尝试生成最小的代码片段来重现失败。
当使用 Inductor 的性能优于急切模式时,我们提供了一种可靠的性能分析方法。我们展示了如何使用 PyTorch Profiler 找到耗时热点,并找出解释现象的操作级别或内核级别的理由。
脚本总运行时间:(0 分钟 0.000 秒)