一个可定制的编译器,用于为AI模型生成高效的融合GPU内核 [P]

Reddit r/MachineLearning 工具

摘要

作者介绍了一款用 Python 编写、高度可定制且易于修改的 ML 编译器。该编译器通过多级 IR 流水线将 LLMs 转换为优化的 CUDA 内核,在特定操作上实现了与 PyTorch 相当甚至更优的性能。文章详细阐述了该编译器的优化过程、降级规则以及用于生成高效融合 GPU 内核的 CLI 用法。

现代 ML(LLM)编译器栈非常复杂繁重。TVM 包含超过 50 万行 C++ 代码。PyTorch 则将 Dynamo、Inductor 和 Triton 层层叠加。我从头构建了一个可 Hack 的 LLM 编译器,并正在记录这一过程。它接收小型模型(TinyLlama、Qwen2.5-7B),并通过六层 IR(中间表示)将其降阶为一系列 CUDA 核函数。目前,在 RTX 5090 上,生成的 FP32 核函数运行速度为 **几何平均 PyTorch eager 的 1.11 倍** 以及 **torch.compile 的 1.20 倍**,在 seq=128 时于 TinyLlama-128 和 Qwen2.5-7B 上实现全块性能持平。在小型规约 / SDPA / KV 投影上取得优势(最高达 4.7 倍);在 seq=512 的稠密矩阵乘法上略有落后。[第一部分](https://medium.com/data-science-collective/a-principled-ml-compiler-stack-in-5-000-lines-of-python-17f2db9549d4) 端到端地走通了 RMSNorm 层,并详细梳理了该管线的前半部分。本文作为第二部分将填补这一空白,并深入讲解 Tile IR、Kernel IR 及其相关的降阶规则。完整文章:[用 5000 行 Python 构建一个原则性 ML 编译器栈](https://medium.com/data-science-collective/a-principled-ml-compiler-stack-in-5-000-lines-of-python-2337d208f278) 本文重点介绍如何为以循环嵌套形式编写的操作生成 GPU 调度(Loop IR)。RMSNorm 示例: ```python v0 = reciprocal(2048) for a0 in 0..32: # free for a1 in 0..2048: # reduce in2 = load x[0, a0, a1] v1 = multiply(in2, in2) acc0 <- add(acc0, v1) v2 = multiply(acc0, v0) v3 = add(v2, 1e-06) v4 = rsqrt(v3) for a2 in 0..2048: # free in3 = load x[0, a0, a2] in4 = load p_weight[a2] v5 = multiply(in3, v4) v6 = multiply(v5, in4) merged_n0[0, a0, a2] = v6 ``` 该编译器栈模拟了 CUDA 工程师在优化核函数时会执行的一系列优化步骤:将输入排期(stage)到 smem、减少内存银行冲突、提高占用率(occupancy)等。 ```diff LoopOp │ ▼ [001] tileify — 将外层自由循环提升为线程轴 [002] chunk_matmul_k — 将 K 维规约分块为 K-outer × K-inner(CTA 内部) [003] split_matmul_k — 将 K-outer 分块循环提升为网格维度 [004] cooperative_reduce — 协同规约:让多个线程共享一次规约;使用 Combine 进行树形归并 [005] blockify_launch — 配置启动块:选择块维度;将自由轴划分为 BLOCK 和 THREAD [006] chunk_reduce — 分块规约:对非矩阵乘法的规约操作进行分块,使其加载操作适配共享内存 [007] stage_inputs — 输入排期:将热输入数据块提升(hoist)至 Stage 节点 [008] register_tile — 寄存器分块:复制内层分块,使每个线程独占一个寄存器块 [009] permute_register_tile — 寄存器分块重排:重排寄存器条带,使可能发生银行冲突的加载操作落在较远的列上 [010] double_buffer — 双缓冲:将 K-outer Stage 提升为 BufferedStage(乒乓缓冲) [011] tma_copy — TMA 拷贝:将符合条件的 BufferedStage 细化为 TmaBufferedStage (sm_90+) [012] split_inner_for_swizzle — 为交错分割内层:拆分 TmaBufferedStage 的内层缓存轴以进行交错(swizzle) [013] async_copy — 异步拷贝:将剩余部分细化为 AsyncBufferedStage (cp.async, sm_80+) [014] pad_smem — 共享内存填充:填充共享内存步幅以打破银行冲突 [015] pipeline_k_outer — K-outer 流水线化:将 K-outer 循环轮转为前导(prologue)/稳态(steady-state)/尾声(epilogue)阶段 (cp.async + TMA) [016] mark_unroll — 标记展开:为小型内层循环添加 #pragma unroll 注释 │ ▼ TileOp (fully scheduled) ``` 每个阶段都可以通过 CLI 命令复现。例如,`stage_inputs` 通道会在条件允许且能带来性能收益时(即输入在 CTA 内被多次读取),将输入缓冲区排期到 smem。要查看此效果,可使用以下命令: ```bash deplodock compile \ -c "torch.nn.RMSNorm(2048)(torch.randn(1,32,2048))" \ --ir tile -vv \ | awk '/^>>> t:007/,/^<<< t:007/' ``` ```diff >>> t:007_stage_inputs @@ matched at rms_norm (in-place) @@ @@ -2,6 +2,7 @@ v0 = reciprocal(2048) Tile(axes=(a0:256=THREAD, a1:32=BLOCK)): + x_smem = Stage(x, origin=(0, a1, 0), slab=(a2:2048@2)) StridedLoop(a2 = a0; < 2048; += 256): # reduce - in2 = load x[0, a1, a2] + in2 = load x_smem[a2] v1 = multiply(in2, in2) acc0 <- add(acc0, v1) @@ -11,5 +12,5 @@ v4 = rsqrt(v3) StridedLoop(a2 = a0; < 2048; += 256): # free - in3 = load x[0, a1, a2] + in3 = load x_smem[a2] in4 = load p_weight[a2] v5 = multiply(in3, v4) <<< t:007_stage_inputs ``` RMSNorm 层最终生成的 CUDA 核函数: ```bash deplodock compile \ -c "torch.nn.RMSNorm(2048)(torch.randn(1,32,2048))" \ --target sm_120 --ir cuda ``` ```c extern "C" __global__ __launch_bounds__(256) void k_rms_norm_reduce( const float* x, const float* p_weight, float* rms_norm) { float v0 = 1.0f / 2048.0f; int a1 = blockIdx.x; int a0 = threadIdx.x; int lane = threadIdx.x & 31; int warp = threadIdx.x >> 5; float acc0 = 0.0f; __shared__ float x_smem[2048]; for (int x_smem_flat = a0; x_smem_flat < 2048; x_smem_flat += 256) { float x_smem_v = x[a1 * 2048 + x_smem_flat]; x_smem[x_smem_flat] = x_smem_v; } __syncthreads(); for (int a2 = a0; a2 < 2048; a2 += 256) { float in2 = x_smem[a2]; float v1 = in2 * in2; acc0 += v1; } float acc0_w = acc0; acc0_w = acc0_w + __shfl_xor_sync(0xffffffff, acc0_w, 16); acc0_w = acc0_w + __shfl_xor_sync(0xffffffff, acc0_w, 8); acc0_w = acc0_w + __shfl_xor_sync(0xffffffff, acc0_w, 4); acc0_w = acc0_w + __shfl_xor_sync(0xffffffff, acc0_w, 2); acc0_w = acc0_w + __shfl_xor_sync(0xffffffff, acc0_w, 1); __shared__ float acc0_smem[8]; if (lane == 0) { acc0_smem[warp] = acc0_w; } __syncthreads(); for (int s = 4; s > 0; s >>= 1) { if (warp < s) { acc0_smem[warp] = acc0_smem[warp] + acc0_smem[warp + s]; } __syncthreads(); } float acc0_b = acc0_smem[0]; float v2 = acc0_b * v0; float v3 = v2 + 1e-06f; float v4 = rsqrtf(v3); for (int a2 = a0; a2 < 2048; a2 += 256) { float in3 = x_smem[a2]; float in4 = p_weight[a2]; float v5 = in3 * v4; float v6 = v5 * in4; rms_norm[a1 * 2048 + a2] = v6; } } ```
查看原文

相似文章

KForge:面向AI加速器的LLM驱动跨平台内核生成

arXiv cs.LG

KForge是一个跨平台框架,利用两个协作的基于LLM的智能体,自动生成和优化适用于多种AI加速器的高性能计算内核,在NVIDIA B200和Intel Arc B580硬件上实现了显著的加速效果。

数据并行内核的微型编译器

Hacker News Top

一篇博客文章,描述了一个微型编译器,演示了如何通过将for循环转换为带有通道和掩码的向量化循环来降低数据并行内核,实现代码约180行Python。

使用CUDA内核重写模型推理:瓶颈不仅仅是GEMM [P]

Reddit r/MachineLearning

作者描述了构建FlashRT的过程,这是一个以CUDA为核心的推理运行时,通过使用C++/CUDA内核重写模型推理路径,来解决小批量/实时工作负载中超出GEMM的瓶颈,在Jetson Thor和RTX 5090上实现了显著的延迟改进。文章讨论了关于精度的经验(FP8有帮助,FP4好坏参半)以及绕过通用运行时进行实时推理的必要性。