元鉴
返回中文阅读流

NVIDIA Developer Blog

通过 NVIDIA CompileIQ 自动调优提升内核性能

NVIDIA CompileIQ 解决了性能工程中最棘手的问题之一:找到能够为特定……释放最佳性能的编译器选项

中文内容

已翻译official company source英文原文2026-06-05

NVIDIA CompileIQ 解决了性能工程中最棘手的问题之一:找到能够为特定工作负载释放最佳性能的编译器选项。

设想一个团队花了数周时间在 GPU 上优化一个 LLM 推理流水线,调整批量大小,量化到 FP8,采用 flash attention,融合他们能融合的每一个内核。性能分析器显示已经没有什么可榨取的空间了。

但如果你可以把编译器本身变成一个可调参数呢?现在你可以了。NVIDIA CUDA 13.3 的发布包含 CompileIQ,这是一个由 AI 驱动的编译器自动调优框架,使用进化算法和遗传算法来针对单个工作负载优化 NVIDIA 通用 GPU 编译器。

NVIDIA GPU 编译器会将相同的默认启发式规则(寄存器分配策略、指令调度决策、循环展开阈值等)应用于它们编译的每一个内核。这些启发式规则经过工程设计,旨在在广泛的工作负载中产生良好结果。但“全面表现良好”和“对你的工作负载最优”是两回事。

AI 基础设施领域的竞争格局已使这一差距无法再被忽视。构建自定义 CUDA、Triton 和 Helion 内核的团队正在为每一个百分点的吞吐量提升而努力。直到现在,还没有一种方法能够针对特定工作负载对代码生成进行微调。

90% 问题与机遇

要理解为什么编译器级优化如此重要,可以看看现代 LLM 推理中的 GPU 计算实际用在了哪里。

在注意力推理内核中,FFN/MLP 块的线性层中的 GEMM,加上 Q、K、V 和输出投影,约占总 FLOPs 的 70%。缩放点积注意力、融合注意力和 flash attention 变体又占约 25%。这两类内核合计占端到端推理计算量的 90% 以上。这并非 AI 推理所独有。在许多应用和算法中,大部分计算时间都花在相对较小的代码部分,这意味着这些小代码段对应用性能具有不成比例的影响。因此,这些代码部分的性能改进,即使只是零点几个百分点,也会对整体应用性能带来不成比例的提升。

介绍 CompileIQ

CompileIQ 是一个由 AI 驱动的编译器自动调优框架,使用进化算法和遗传算法来针对单个工作负载优化 NVIDIA GPU 编译器。CompileIQ 不再为所有工作负载接受一种通用的编译器配置,而是反其道而行之,生成专门的编译器配置,针对你最关键的各个内核量身定制。

在底层,CompileIQ 会探索丰富的内部编译器参数空间,这些参数不会通过任何公开的编译器标志暴露出来:寄存器分配策略、指令调度策略、循环变换等。其输出是一个高级控制文件(ACF),编译器通过 –apply-controls 标志读取该文件,从而生成专门针对你的工作负载优化的内核二进制文件。

可以这样理解:你的编译器本身已经具备为你的内核生成更好代码的能力。它只是不知道哪种内部设置组合能够实现这一点。CompileIQ 的进化搜索会自动找到这种组合。

在用尽他们所知的所有优化手段后陷入瓶颈的团队,现在有了 CompileIQ 这一新手段——编译器本身。

CompileIQ 现已可用,并可使用 pip 安装到你偏好的 Python 环境中,如下一节所示。领先的 AI 实验室已经在生产环境中将其用于性能要求最高的工作负载。

4 步入门

CompileIQ 是一个采用简单工作流的 Python 包:

  • 学习
  • 安装
  • 定义你的目标
  • 运行
pip install compileiq

CompileIQ 随附适用于 PTXAS 和 NVCC 的编译器搜索空间,这些搜索空间会通过 API 自动获取。无需手动下载或配置。

作为开发者,你的任务是定义你的目标函数:例如,一个 Python 可调用对象,它接收一个候选编译器配置,用该配置编译你的内核,对结果进行基准测试,并返回一个分数。如果你能对你的内核进行基准测试,就可以使用 CompileIQ。

下面是一个示例:

import subprocess
from compileiq.ciq import Search
from compileiq.types import SearchConfiguration

# Define your objective: compile with the ACF and measure runtime
def objective(config_blob):
    with open("config.acf", "wb") as f:
        f.write(bytes.fromhex(config_blob))
    result = subprocess.run([
        "ptxas", "-v", "-arch=sm_90a",
        "--apply-controls", "config.acf",
        "my_kernel.ptx"
    ], capture_output=True, text=True)
    return extract_runtime(result.stdout)

# Configure and run the evolutionary search
config = SearchConfiguration(
    pool_size=32, cull_size=24, generations=20,
    mutate_rate=0.1, problem_type="min",
    num_objectives=1
)
search = Search(config, objective)
best_acf = search.run()

上述代码可以分为三个不同的部分:

  • 定义目标:我们定义 objective 函数,该函数接收要在 config_blob 中评估的配置,将其保存到磁盘,编译并运行内核,然后提取指标。
  • 配置搜索:设置将驱动搜索的参数,例如一代中要尝试的候选数量(pool_size)、要运行的代数,以及要优化的目标数量。
  • 运行搜索并提取最佳候选。

就是这样。当搜索开始时,CompileIQ 会初始化一组编译器配置,根据你的目标函数评估每个配置,选择表现最佳的配置,应用变异和交叉来生成新的候选,并在连续多代中收敛到最优 ACF。

你可以在目标函数中定义对你的工作负载而言“更好”意味着什么,CompileIQ 会找到它。

The evolutionary optimization loop: define your objective, and CompileIQ handles the rest – from population initialization through convergence to a deployable ACFThe evolutionary optimization loop: define your objective, and CompileIQ handles the rest – from population initialization through convergence to a deployable ACF
图 1. CompileIQ 工作流:定义目标、初始化、评估、选择并变异、收敛、部署

示例

现在让我们聚焦于你可以尝试的自包含示例。GitHub 仓库中有许多示例,我们将在这里演示两个。首先是单目标示例,它与 GPU 计算无关,但展示了使用 CompileIQ 的原则。

from compileiq.ciq import Search
from compileiq.types import SearchConfiguration
import compileiq.search_spaces.base as ss

def objective(config):
    score = config["x"] ** 2 + config["y"]
    return score

def main():
    dna_config = {
        "x": ss.range(start=1.0, end=20.0, step=0.5),
        "y": ss.choice([1, 2, 3]),
        "z": ss.literal("this is a constant", knockout_prob=0.5),
    }

    main_config = SearchConfiguration(
        generations=5,
        problem_type="min",
        num_objectives=1,
    )

    tuner = Search(
        objective_function=objective,
        search_space=dna_config,
        search_config=main_config,
    )

    results = tuner.start()
    print(f"Entire Results Dataframe:\n {results.get_results()}")
    print(f"Best Result: {results.get_best_result()}")

if __name__ == "__main__":
    main()

首先,目标函数:

def objective(config):
    score = config["x"] ** 2 + config["y"]
    return score

这是一个简单的函数,它对 x 求平方,并将该值加到 y 上。这就是我们将要优化的函数:

    dna_config = {
        "x": ss.range(start=1.0, end=20.0, step=0.5),
        "y": ss.choice([1, 2, 3]),
        "z": ss.literal("this is a constant", knockout_prob=0.5),

该配置指定了变量允许取的值。对于 x,范围在 1.0 到 20.0 之间,步长为 0.5。对于 y,可选值为 1、2 或 3;z 实际上并不参与目标函数的计算,但用于说明 dropout。

  main_config = SearchConfiguration(
        generations=5,
        problem_type="min",
        num_objectives=1,
    )

接下来,我们指定搜索配置。在本例中,我们将运行 5 代,希望最小化目标函数,并且只分析一个目标。

代码的其余部分用于设置参数和搜索。这是一个非常简单的目标函数,你可以很容易地手动计算它;但出于说明目的,下面展示运行代码时会发生什么。

$ python single_objective.py
🧬 Generation:  5/5|█| [elapsed: 00:00 · eta: 00:00] , 🏆 best_score=3.2500
Entire Results Dataframe:
              metadata  ...                                          params
0    {"pid": 2562276}  ...   {'x': 2.5, 'y': 2, 'z': 'this is a constant'}
1    {"pid": 2562276}  ...                              {'x': 8.5, 'y': 3}
2    {"pid": 2562276}  ...  {'x': 11.0, 'y': 1, 'z': 'this is a constant'}
3    {"pid": 2562276}  ...                             {'x': 19.0, 'y': 2}
4    {"pid": 2562276}  ...                             {'x': 13.5, 'y': 3}
..                ...  ...                                             ...
109  {"pid": 2562276}  ...   {'x': 1.5, 'y': 3, 'z': 'this is a constant'}
124  {"pid": 2562276}  ...   {'x': 1.0, 'y': 2, 'z': 'this is a constant'}
126  {"pid": 2562276}  ...   {'x': 2.0, 'y': 1, 'z': 'this is a constant'}
135  {"pid": 2562276}  ...   {'x': 3.0, 'y': 2, 'z': 'this is a constant'}
138  {"pid": 2562276}  ...                              {'x': 1.5, 'y': 3}
[61 rows x 4 columns]
Best Result: {'metadata': '{"pid": 2562276}', 'generation': 4, 'score_1': 3.0, 'params': {'x': 1.0, 'y': 2, 'z': 'this is a constant'}}

请注意,最佳结果列表中 x = 1.0 且 y = 2,这会得到 3.0 的分数。但我们知道,最佳分数实际上是在 x = 1.0 且 y = 1 时得到的,因此在这个例子中,CompileIQ 没有找到最佳答案。由于代数非常少(在我们的例子中为 5),以及搜索的随机性质,我们碰巧没有找到绝对最佳答案。不过,在这个例子中,如果你将代数增加到一个更大的数,比如 15,你几乎总是能得到最佳答案。

让我们转到一个衡量特定 kernel 的 GPU 性能的示例。在 GitHub 仓库中有一个使用 NVCC 构建 reduction kernel 的示例。为简洁起见,我们不会在这里包含完整代码,但会展示一些片段来说明相关概念。

在用于设置搜索的 Python 函数中,我们有以下代码:

# Configure and run search
    search_space = args.search_space if args.search_space else NvccSearchSpace(version=cuda_version)
    config = SearchConfiguration(
        problem_type=ProblemType.MIN,
        generations=args.generations,
        pool_size=args.pool_size,
    )

搜索空间被配置为使用适用于 CUDA 13.3 的 NvccSearchSpace。你可以看到要优化的问题类型是 MIN,这意味着我们希望找到目标函数的最小值。Generations 和 pool size 是命令行参数,在这个 Python 脚本中默认值分别为 10 和 15。GPU 内核代码被设置为运行一次归约,然后打印出时间;目标函数(此处未列出)本质上会构建并运行该内核,搜索 Time = 字符串,而这就是在搜索空间中被最小化的值。

假设你位于 compilers/nvcc_example 文件夹中,下面是运行搜索时的效果。

$ python optimize_reduction.py --arch sm_120
Running baseline...
Baseline: 0.777 ms
Starting optimization (10 generations, pool=15)...
🧬 Generation:  10/10|█| [elapsed: 09:29 · eta: 00:00] , 🏆 best_score=0.7700, a
Baseline:  0.777 ms
Optimized: 0.770 ms
Speedup:   1.01x
Config saved: reduction_best_config.bin
Usage: nvcc --apply-controls reduction_best_config.bin -arch=sm_120 ...

通过搜索得到的性能提升约为 1%,你可以看到,要应用这个已保存的配置,只需使用 –-apply-controls 选项并添加你刚刚生成的 ACF。

多目标优化和 IP 保护

大多数自动调优工具针对单一指标进行优化,通常是运行时间。CompileIQ 更进一步,支持多目标优化,可同时探索运行时间、编译时间和功耗等相互竞争目标之间的权衡。

这一点很重要,因为“尽可能最快”并不总是正确答案。受功耗限制的数据中心可能会接受运行时间的小幅增加,以换取显著更低的功耗。CI/CD 流水线可能会优先考虑编译时间,以保持快速的迭代周期。嵌入式部署可能需要在三者之间取得平衡。

CompileIQ 的进化引擎会计算非支配解的 Pareto 前沿,即在不使另一个目标变差的情况下,无法改进任何单一目标的配置。你的团队可以选择符合自身约束条件的权衡方案,而不是被锁定在单一优化维度上。

这一能力将 CompileIQ 的适用范围大大扩展到 LLM 推理之外。凡是使用 NVIDIA 编译器的场景——科学计算、自动驾驶汽车、图像处理、推荐系统——CompileIQ 都可以探索优化空间,并呈现默认启发式方法会遗漏的配置。

在知识产权保护方面,CompileIQ 的设计确保双方都保持安全。编译器内部机制被封装在搜索空间和 ACF 中。用户无需关心编译器参数。用户工作负载永远不会离开其自身环境;目标函数在本地运行,并且只会生成由此产生的 ACF。ACF 可以安全地提交到版本控制,并在团队之间共享。

Multi-objective optimization produces a Pareto frontier of non-dominated solutions. Teams can select configurations optimized for low runtime (orange), low power (gold), low compile time (cyan), or an optimal trade-off (teal)Multi-objective optimization produces a Pareto frontier of non-dominated solutions. Teams can select configurations optimized for low runtime (orange), low power (gold), low compile time (cyan), or an optimal trade-off (teal)
图 2。3D Pareto 前沿:运行时间 vs 编译时间 vs 功耗

结果与生产采用

CompileIQ 已在生产工作负载上针对 GPU 和 CPU 目标进行了验证。例如,如这场 GTC 演讲所示,Meta 在 TritonBench 和 Helion kernels 上都实现了最高 15% 的性能提升。

这些收益是在已经优化过的基线之上取得的,而这些内核曾被其作者认为已经“完成”。这些改进是 CompileIQ 发现编译器配置的直接结果,而默认启发式方法永远不会选择这些配置。

领先的 AI 实验室已经在生产环境中将 CompileIQ 用于其性能最关键的推理和训练工作负载。它生成的 ACF 完全可复现且可移植:只要相同的基准测试和底层编译器相匹配,同一个 ACF 就会在不同部署中生成相同的优化二进制文件。团队将 ACF 与其内核源代码一起提交到版本控制中,使编译器优化成为开发工作流中一个有版本、可审查的部分。

轮到你了

PTXAS 和 NVCC 均提供编译器搜索空间。确定你影响最大的内核——GEMM 和 attention 是最佳候选——编写一个衡量你的工作负载所关注指标的基准测试,然后运行 CompileIQ。

文档、API 参考和实用示例可在 CompileIQ 文档网站获取。如有问题并需要支持,请在 CompileIQ GitHub 仓库提交 issue。

有一点我们需要明确:CompileIQ 不是一种能自动将编写欠佳的代码变成高性能代码的神奇工具。要从 CompileIQ 中获得最大价值,你需要从性能已经相当高的代码开始,这样才能通过最终的编译器启发式调整将性能提升到最大。

但是,如果你的团队已经用尽了他们所知道的每一种优化手段,CompileIQ 会为他们提供一个新的手段——编译器本身。

下载 CompileIQ,查看 GitHub 中的示例,并立即开始优化你的内核。

Like

标签

原文标题

Extract More Kernel Performance with NVIDIA CompileIQ Auto-Tuning