你写了这一行代码,从张量中提取一个数值。也许是为了与阈值进行比较,或者仅仅是为了打印出来。这感觉似乎毫无代价,对吧?其实并非如此。在编译后的模型中,这一行代码会对你造成两种截然不同的影响。而几乎所有人都会将这两者混为一谈:它既打断了编译器,使其无法将整个模型视为一个整体进行处理;又会让你的加速器处于闲置状态。

如果这是你第一次观看此类内容,这里简要说明背景:实际上有两件事同时发生。Torch.compile 会在模型运行前审视你的代码,尽可能将其转化为一个优化的整体块;与此同时,加速器的工作进度领先于 Python。Python 将任务扔进队列,芯片则逐步执行处理这两者都害怕被打断,而我将要写的这一行代码恰恰同时打断了二者。

这里就是那行代码。我使用了 if 语句,因为这能直观地揭示问题所在,但 print 或 log 操作也会产生完全相同的效果。关键在于那个 .item() 调用。它实际上是在请求将那个数值作为普通的 Python 标量返回,而此刻该数值仍位于尚未完成计算的加速器上。

假设 A 到 E 是模型编译过程中的五个工作块。你希望编译器将这五部分全部接纳,并将其视为一个连续的区域进行处理。这样做的好处在于:编译器能一次性看到更多内容,从而进行更多优化操作,例如将多个步骤融合、重新排序规划整个区域的内存分配。给它一个大块的数据,你就能获得高效的处理结果。

然而,如果在中间插入标量读取操作,编译器执行到该行时就会停止。因为无法为请求 Python 值的指令进行编译,这被称为“图断裂”(graph break)。请注意接下来的部分,因为误解往往源于此。并没有发生任何错误,只是捕获过程在边界处停止。尴尬的部分作为普通 Python 代码运行,随后捕获在另一侧重新开始。你原本的一个大区域现在变成了两个较小的区域,中间夹着 Python 代码。

还有第二件事正在发生,且与上述情况完全独立。为了将那个数值交给 Python,主机必须等待芯片完成计算。这就是同步机制,它与图断裂是不同的机制。一个 .item() 调用可能同时触发这两种情况。观察这两行:主机等待并决定下一步操作,而在这段时间内,芯片没有任何排队任务可执行。这段空隙正是你所付出的性能代价。