即时 (JIT) 编译提供了获取运行时信息的很大优势,但在执行过程中对每段代码都执行激进且耗时的优化通常不切实际。编译开销可能抵消性能提升,特别是对于只执行少数几次的代码,或其特性(如输入张量形状)频繁变化的代码。自适应和多层编译策略通过根据观测到的运行时行为动态调整编译工作量来解决此问题。主要原则是将代码执行视为一个连续体。频繁执行(“热点”)或展现稳定属性(如一致的输入形状)的代码,比很少运行或不可预测地运行的代码,值得投入更多优化。多层编译通过提供多个执行和编译级别(即层)来实现此目标。多层执行模型一种常见模型包含至少两个主要阶段:基线层 (Tier 0/1): 这个初始阶段侧重于快速启动和执行。它可能涉及:解释执行: 直接执行高级图或字节码表示,类似于即时执行。基线JIT: 对原生代码进行非常快速的编译,伴随最少或没有优化。这减少了解释执行开销,但避免了耗时的分析过程。性能分析: 重要的是,此层会检测代码或使用采样技术来收集运行时统计数据,例如特定函数或子图的执行频率、观测到的参数类型和形状,以及分支概率。优化层 (Tier 2+): 一旦性能分析数据表明某个特定函数或计算图段是“热点”(超过预定义的执行计数阈值)或展现出适合优化的稳定特性(例如,多次调用中张量形状一致),它就成为晋升到更高层的候选。这会触发:优化编译: JIT 调用一个更强大但速度较慢的优化编译器后端。这个后端应用前面讨论过的先进技术,例如激进的算子融合、内存布局转换、多面体循环优化、自动向量化,以及目标特定代码生成(例如,针对GPU或专用加速器)。专业化: 如果检测到稳定的形状或值,优化编译器可以生成针对那些特定运行时条件优化的代码,可能带来显著的加速。多个优化层: 一些复杂的系统甚至可能采用多个优化层(例如,Tier 2、Tier 3),在每个后续级别应用日益激进和耗时的优化,专用于最热点和最稳定的代码区域。层之间的转换由启发式方法和阈值控制,这些方法和阈值经过精心调整,以平衡编译开销与预期的执行时间节省。digraph G { rankdir=LR; graph [fontname="Arial"]; node [shape=box, style=filled, fontname="Arial", fontsize=10]; edge [fontname="Arial", fontsize=9]; subgraph cluster_tier0 { label = "第0/1层:基线执行 / 轻量JIT"; style=filled; bgcolor="#e9ecef"; // gray node [fillcolor="#dee2e6"]; // lighter gray exec0 [label="执行 / 解释执行\n(初始调用)"]; profile0 [label="收集性能分析数据\n(调用次数,形状稳定性)"]; exec0 -> profile0 [label="执行期间"]; } subgraph cluster_tier_opt { label = "第2+层:优化编译"; style=filled; bgcolor="#a5d8ff"; // blue node [fillcolor="#bac8ff"]; // lighter blue compile_opt [label="调用优化编译器\n(融合,布局,专业化)"]; exec_opt [label="执行优化代码"]; compile_opt -> exec_opt; } check [label="阈值检查\n(热点?稳定?)", shape=diamond, fillcolor="#ffec99", style=filled]; // yellow profile0 -> check [label="性能分析数据"]; check -> exec0 [label=" 否 (继续第0/1层) "]; check -> compile_opt [label=" 是 (提升层级) "]; exec_opt -> profile0 [label="继续执行\n(可能重新分析)"] // 优化代码仍需监测 }流程图说明了一个多层JIT编译过程。执行从带性能分析的基线层开始。如果达到阈值,代码将被提升到优化编译器层以提升性能。自适应优化技术自适应编译涉及根据运行时观测动态修改优化策略或重新编译代码:运行时形状专业化: 如前所述,如果JIT观测到函数始终以相同形状(或在可预测范围内的形状)的张量调用,它可以触发重新编译以生成专门针对这些维度优化的代码。这通常需要“守卫”——在专业化代码之前插入的运行时检查,以验证输入形状是否仍与专业化假设匹配。如果守卫失败,系统必须去优化,可能回退到更通用的代码版本或基线层。配置文件引导优化 (PGO): 基线层收集的性能分析数据不仅仅用于层级提升;它可以直接指导优化编译器。例如,知道条件语句的哪些分支被更频繁地采用,可以实现更好的代码布局,从而提升指令缓存性能。关于频繁值范围的信息可能实现更有效的循环展开或向量化。动态重新编译/去优化: 运行时条件可能发生剧烈变化。最初处理小批量数据的模型可能突然接收到非常大的批量,使得先前优化编译期间做出的假设失效(例如缓存平铺策略)。自适应系统可能检测到此类变化,并触发使用更新启发式方法的重新编译,甚至在执行模式变得高度不稳定时去优化回较低层,从而防止浪费编译工作。栈上替换 (OSR): 为了避免等待热点函数的下一次调用才能从优化中获益,一些先进的JIT实现了OSR。这使得系统能够在函数执行过程中,通常在循环回边处,从执行基线版本切换到优化版本。这正确实现很复杂,但可以显著减少达到峰值性能的延迟。挑战与考量实现有效的自适应和多层JIT编译带来了一些工程挑战:开销管理: 性能分析消耗CPU周期和内存。编译本身,尤其是在优化层,可能资源密集。系统必须确保这些开销不会超过性能优势。高效的性能分析技术(采样、低开销计数器)和分层编译有助于管理这一点。启发式方法调整: 确定层级提升的最佳阈值(例如,多少次调用使函数变为“热点”?)和专业化的标准需要仔细调整,这通常基于代表性工作负载的经验数据。糟糕的启发式方法可能导致过早优化(浪费时间)或延迟优化(错过性能机会)。复杂性: 管理多个版本的编译代码、层之间的转换、守卫机制和去优化逻辑,会给JIT编译器和运行时系统增加很大复杂性。内存占用: 存储基线代码、多个优化层的代码以及相关的性能分析数据,会增加应用程序的内存消耗。高效的代码缓存和淘汰策略是必要的。虽然像Java HotSpot VM这样的传统例子明确定义了层级,但像XLA和TorchScript中的组件这样的ML JIT编译器采用了自适应原则。XLA的编译由使用触发,其强大的优化流水线充当高性能层。TorchScript使用跟踪或脚本进行初始图捕获(类似于基线),然后应用优化。其自适应性在于何时触发编译以及如何运行时信息(如跟踪到的形状)影响优化过程。未来的ML编译器可能会继续改进这些自适应和多层策略,以提供快速交互性能和要求高的AI工作负载的高峰值吞吐量。