Inductor CPU 后端调试与性能分析
作者: Xuan Liao, Haozhe Zhu, Jiong Gong, Weihan Wang
概述
PyTorch 2.0 引入了名为 torch.compile
的编译 API。这一新特性通过默认的 Inductor 后端进行图级优化,相比即时执行模式(eager mode)提供了显著的加速效果。
本教程旨在深入介绍如何调试和分析 Inductor CPU 后端的性能,通过详细探讨 torch.compile
的复杂机制来实现这一目标。
与此同时,您还可以找到与 torch.compile
相关的其他教程,例如基本用法、全面的故障排除以及针对 GPU 的性能分析。
我们将从一个触发编译问题和精度问题的示例开始调试,展示如何通过调试过程来准确定位问题。
通过启用日志记录并探索生成的底层代码,您可以学习如何逐步缩小问题的范围,并最终找出根本原因。
接下来,我们将讨论如何对编译后的代码进行性能分析,并通过与即时执行模式的性能对比,详细阐述为什么 torch.compile
能够提供比即时模式更优的性能提升。
调试
以下是一个简单的示例,使用 Inductor 运行 torch.compile
并将其结果与 eager 模式进行比较:
importtorch
deffoo1(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)
cpp
代码生成中 neg
的正确实现如下:
defneg1(x):
return f"decltype({x})(-{x})"
为了演示调试过程,我们稍后将把函数修改为一个错误的版本。
获取更多日志信息
默认情况下运行这个简单示例不会提供任何调试信息。为了获取更有用的调试和日志信息,我们通常会像下面这样添加一个 TORCH_COMPILE_DEBUG
环境变量:
TORCH_COMPILE_DEBUG=1pythonxx.py
这将在输出日志中打印更多调试信息,并转储在代码生成过程中生成的中间 IR。您可以在日志中找到转储文件的路径,如下所示:
torch._inductor.debug:[WARNING]model___20debugtrace:/tmp/torchinductor_root/rx/crxfi2ybd7yp5sbj2pnhw33wfhtdw7wumvrobyp5sjvdui5ktjc2.debug
在此目录中,以下文件被保存用于调试目的:
文件 | 描述 |
---|---|
fx_graph_runnable.py |
可执行的FX图,分解后,模式匹配前 |
fx_graph_transformed.py |
转换后的 FX 图,经过模式匹配后 |
ir_pre_fusion.txt |
融合前的 Inductor IR |
ir_post_fusion.txt |
融合后的 Inductor IR |
output_code.py |
为图形生成的 Python 代码,包含 C++/Triton 内核 |
请注意,fx_graph_runnable.py
和 output_code.py
都是可运行且可编辑的,以便于调试。以下是从文件中提取的主要代码部分,我们将生成的 C++ 代码行与 FX 代码行进行关联。
fx_graph_runnable
:
defforward1(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++ 内核
importtorch
fromtorch._inductor.async_compileimport 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
的后端可以轻松实现这一点。
Code | 描述 |
---|---|
torch.compile(fn, backend="eager") |
启用 Dynamo |
torch.compile(fn, backend="aot_eager") |
启用 Dynamo + AOT Autograd |
torch.compile(fn, backend="inductor") |
启用 Dynamo + AOT Autograd + Inductor |
如果模型在将后端设置为 eager
或 aot_eager
时能够成功运行,而在 inductor
下失败,我们可以将问题缩小到 Inductor。
编译错误
众所周知,图级优化的演进链条如下:
torch.neg(Python)->torch.ops.aten.neg.default(withinFXgraph)->ops.neg(withinIRnode)->tmp2=-tmp1(withinC++kernel)
如果您遇到编译错误,说明在编译输出代码中的 C++ 内核时出现了问题。此类错误表明在将 IR 节点降级为输出代码时引入了错误。编译错误的根本原因通常会在回溯日志中显示。
例如,neg
函数被修改为如下形式:
defneg2(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"voidkernel(constunsignedchar*in_ptr0,
constunsignedchar*in_ptr1,
unsignedchar*out_ptr0)
{
{
#pragma GCC ivdep
for(longi0=static_cast<long>(0L);i0<static_cast<long>(8390L);i0+=static_cast<long>(1L))
{
#pragma GCC ivdep
for(longi1=static_cast<long>(0L);i1<static_cast<long>(8L);i1+=static_cast<long>(1L))
{
autotmp0=in_ptr0[static_cast<long>(i1+(8L*i0))];
autotmp1=in_ptr1[static_cast<long>(i1)];
autotmp2=-tmp1;
autotmp3=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],[])
classbuf0_loop_body:
var_ranges={z0:8390,z1:8}
index0=8*z0+z1
index1=z1
defbody(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)
returnstore
根据回溯日志记录,编译错误是由于 max_propagate_nan
输入的数据类型不一致引起的。通过检查 C++ 内核,我们发现 tmp2
在执行 -
操作后不再是 long
类型,因为 tmp0
是 long
类型。我们可以轻松地将 C++ 内核中的 -
和 max_propagate_nan
分别与 IR 节点中的 ops.neg
和 ops.maximum
对应起来。
现在我们成功找到了根本原因,即 cpp
代码生成中 ops.neg
的实现,它在执行 neg
操作时隐式地更改了数据类型。
精度调试
否则,如果模型运行出现其他错误或准确性问题,您可以使用 PyTorch 的调试工具 Minifier。
Minifier
的核心思想是不断移除图中的节点和输入,直到找到包含问题的最小图。它通过四种策略帮助自动生成一个最小化的有问题的图:截断后缀、增量调试、消除死代码和移除未使用的输入。
现在,我们将借助 Minifer
展示针对准确性问题的调试过程。准确性问题指的是后端 eager 和 inductor 的输出不一致的情况。
例如,我们像这样修改示例:
fromtorch._dynamo.utilsimport same
deffoo2(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
函数:
defneg3(x):
return f"decltype({x})(2 * {x})"
将产生如下准确性问题:
torch._dynamo.utils:[ERROR]Accuracyfailed:allclosenotwithintol=0.0001
Traceback(mostrecentcalllast):
File"test_script.py",line18,in<module>
assertsame(expected_result,actual_result)==True
AssertionError
要调试 Minifier 的精度问题,需要设置两个环境变量:
TORCHDYNAMO_REPRO_AFTER="aot"TORCHDYNAMO_REPRO_LEVEL=4pythonxx.py
这为我们提供了展示代码压缩步骤的日志信息:
Startedoffwith6nodes
Tryinggranularity2
Strategy:Truncatesuffix(G:2)(6nodes,2inputs)
SUCCESS:Wentfrom6to4nodes
Tryinggranularity4
Strategy:Removeunusedinputs(G:4)(4nodes,2inputs)
SUCCESS:Wentfrom4to3nodes
运行后,我们得到了包含目标节点 neg
的最终简化图:
defforward2(self, arg0_1):
neg = torch.ops.aten.neg.default(arg0_1); arg0_1 = None
return (neg,)
有关 Minifier 的更多使用详情,请参阅故障排除。
性能分析
在本节中,我们将展示如何对使用 Inductor CPU 后端编译的模型进行性能分析。在下面的示例中,我们对 Hugging Face Transformer 模型 MobileBertForQuestionAnswering
进行了基准测试,分别使用了 eager 模式和 Inductor 图模式。基准测试完成后,会打印出 Inductor 的执行时间和加速比。我们使用 Intel(R) Xeon(R) Platinum 8358 CPU @ 2.60GHz,并在第一个插槽上运行基准测试,以展示本节中的优化效果。作为在 Intel(R) CPU 上进行基准测试的最佳实践,我们设置了以下环境变量。
exportKMP_BLOCKTIME=1
exportKMP_SETTINGS=1
exportKMP_AFFINITY=granularity=fine,compact,1,0
exportLD_PRELOAD=${CONDA_PREFIX:-"$(dirname$(whichconda))/../"}/lib/libiomp5.so:${CONDA_PREFIX:-"$(dirname$(whichconda))/../"}/lib/libjemalloc.so
exportMALLOC_CONF="oversize_threshold:1,background_thread:true,metadata_thp:auto,dirty_decay_ms:-1,muzzy_decay_ms:-1"
numactl-C0-31-m0pythonbench.py
# bench.py
fromtransformersimport 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
importtimeit
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}")
输出:
eageruse:802.1023553796113ms/iter
inductoruse:339.95180135127157ms/iter
speedupratio:2.359459053287382
在我们的测试中,我们发现 Inductor CPU 后端将模型速度提升了约 2.355 倍。
接下来,让我们深入操作层面的性能分析,以了解速度提升的来源。Pytorch Profiler 是一个很好的工具,可以帮助我们实现这一目标。Inductor CPU 后端支持通过 enable_kernel_profile
配置选项向性能分析器报告融合内核的时间:
fromtorch._inductorimport config
config.cpp.enable_kernel_profile = True
按照 Pytorch Profiler 中的步骤,我们能够获取性能分析表和跟踪文件。
# bench.py
fromtorch.profilerimport profile, schedule, ProfilerActivity
RESULT_DIR = "./prof_trace"
my_schedule = schedule(
skip_first=10,
wait=5,
warmup=5,
active=1,
repeat=5)
deftrace_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 模型的性能分析表(省略了一些列):
*------------------------------------------------------------
NameCPUtotal%CPUtotal# of Calls
*------------------------------------------------------------
aten::addmm45.73%370.814ms362
aten::add19.89%161.276ms363
aten::copy_14.97%121.416ms488
aten::mul9.02%73.154ms194
aten::clamp_min8.81%71.444ms96
aten::bmm5.46%44.258ms48
ProfilerStep*100.00%810.920ms1
aten::div2.89%23.447ms24
aten::_softmax1.00%8.087ms24
aten::linear46.48%376.888ms362
aten::clone2.77%22.430ms98
aten::t0.31%2.502ms362
aten::view0.14%1.161ms850
aten::transpose0.17%1.377ms386
aten::index_select0.12%952.000us3
aten::expand0.12%986.000us458
aten::matmul8.31%67.420ms48
aten::cat0.09%703.000us1
aten::as_strided0.08%656.000us963
aten::relu8.86%71.864ms96
*------------------------------------------------------------
SelfCPUtimetotal:810.920ms
同样地,我们也得到了使用Inductor编译后的模型表格(省略了一些列):
*----------------------------------------------------------------------------------
NameCPUtotal%CPUtotal# of Calls
*----------------------------------------------------------------------------------
mkl::_mkl_linear68.79%231.573ms362
aten::bmm8.02%26.992ms48
ProfilerStep*100.00%336.642ms1
graph_0_cpp_fused_constant_pad_nd_embedding_00.27%915.000us1
aten::empty0.27%911.000us362
graph_0_cpp_fused__mkl_linear_add_mul_relu_1510.27%901.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_2260.27%899.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_3610.27%898.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_1210.27%895.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_310.27%893.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_760.26%892.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_2560.26%892.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_3460.26%892.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_2410.26%891.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_3160.26%891.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_910.26%890.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_1060.26%890.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_2110.26%890.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_610.26%889.000us1
graph_0_cpp_fused__mkl_linear_add_mul_relu_2860.26%889.000us1
*----------------------------------------------------------------------------------
SelfCPUtimetotal:336.642ms
从 eager 模型的性能分析表中,我们可以看到最耗时的操作是 [aten::addmm
, aten::add
, aten::copy_
, aten::mul
, aten::clamp_min
, aten::bmm
]。与 inductor 模型的性能分析表对比,我们注意到一个 mkl::_mkl_linear
条目和多个以 graph_0_cpp_fused_*
形式出现的融合内核。这些是 inductor 模型进行的主要优化。让我们分别讨论它们。
(1) 关于 mkl::_mkl_linear
:你可能会注意到该内核的调用次数为 362,与 eager 模型性能分析表中的 aten::linear
完全相同。aten::linear
的 CPU 总时间为 376.888ms,而 mkl::_mkl_linear
为 231.573ms。这表明在“线性”部分有约 1.63 倍的加速。这种加速主要来自于将权重张量打包为块内存格式,并在 Inductor CPU 后端调用 cblas_sgemm_compute,从而在 GEMM 计算期间获得更好的缓存行为。
(2) 关于其他内存密集型操作:在我们的测试中,eager/inductor 模型的端到端延迟分别为 802/339 毫秒。因此,我们可以大致推断出其他内存密集型操作的加速比约为 3.94 倍。让我们通过阅读生成的代码来理解 inductor 如何实现这一显著的优化。您可以通过在 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
deffunc(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)
importtimeit
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}")
输出:
eageruse:5.780875144992024ms/iter
inductoruse:0.9588955780491233ms/iter
speedupratio:6.0286805751604735
这只是一个示例。分析表显示,在此模型中,所有逐元素操作都在 Inductor 中自动融合。您可以在 output_code.py 中查看更多内核。
总结
该文档提供了关于 Inductor CPU 后端的深入教程。
通过激励性的示例,我们逐步介绍了调试和分析的过程。核心思路是逐步缩小问题范围。
我们展示了如何借助调试日志和工具 Minifier,逐步深入问题并找到故障的根本原因。首先确定故障发生的组件,然后尝试生成能够重现故障的最小代码片段。
当 Inductor 的性能优于 eager 模式时,我们提供了一套严谨的性能分析方法。我们演示了如何使用 PyTorch Profiler 找到耗时的热点,并找出操作符级别或内核级别的原因来解释这一现象。