Dawn 的 cuTile 后端

Dawn 里的 GPU kernel 是一个带 !Dev 效果的普通函数。运行一次,它发出的每个操作都被记录成 CUDA Tile IR,再由 NVIDIA 的 tileiras 汇编成 cubin。

  • !Dev · Tile IR · tileiras
  • 208 个 golden kernel
  • 3 块 GPU 有记录

两层,两个效果

宿主持有缓冲区,设备在上面计算;两者只在宿主按名字启动 kernel 的那一处相遇。

宿主std/gpu.dawn!Gpu
  1. main
  2. launch_entry3
  3. with_gpu_realwith_gpu_fake

std/gpu 的 Gpu 效果:分配、上传、启动、下载。with_gpu_real 驱动 CUDA driver,with_gpu_fake 不需要 GPU 也能跑同一个程序。

设备packages/tileir!Dev
  1. vadd
  2. record
  3. Tile IR
  4. tileiras
  5. cubin

packages/tileir 的 Dev 效果。kernel 被记录的一次运行,变成 Tile IR 文本,或交给 tileiras 的字节码。

写一个 kernel

kernel 在参数标记上一次说清它读哪里、写哪里,凡是能从操作数读出的形状一概不写。下面每段代码都在建站时从 golden kernel 里切出来。

学着写一个

标记就是寻址

vadd

In 与 Out 把每个张量切成格子,Out 的格子就是 launch 的网格。函数体读本块的格子、写本块的格子,不写任何偏移。

fn vadd(a: Param[Float], b: Param[Float], out: Param[Float]) -> Unit !Dev =  store_cell(out, load_cell(a) + load_cell(b)) "vadd" -> {  let (p, _e) = trace3("vadd", In(F64, run_of(128)), In(F64, run_of(128)), Out(F64, run_of(128)), vadd)  p} fn run_of(tile: Int) -> Cells = cells([DYN_DIM], [tile])

kernels.dawn:48–49 · kernels.dawn:5035–5038 · kernels.dawn:4934 · vadd.mlir

形状从操作数来

softmax

没有一个操作收形状或格式。常量、以及对一维 tile 的归约,都是 0 秩 tile:遇到更宽的操作数时自己加宽,别处一律不加宽。

fn softmax(x: Param[Float], out: Param[Float]) -> Unit !Dev = {  let t = load_cell(x)  let ex = exp(t - reduce_max(t))  store_cell(out, ex / reduce_sum(ex))}

kernels.dawn:224–228 · softmax.mlir

写只经 Out

matmul

Out 只接受 store_cell 与 store_sub 两种写,累加器从它的 zeros 起步。FREE_AXIS 把一维留给 kernel 自己挑,这里是循环的 k。

fn matmul(a: Param[Float], b: Param[Float], c: Param[Float]) -> Unit !Dev = {  var acc = zeros(c)  for k in d_range(0, MM_K / MM_TK) { acc = mma(load_at(a, [k]), load_at(b, [k]), acc) }  store_cell(c, acc)} "matmul" -> {  let (p, _e) = trace3("matmul",    In(F64, cells([MM_M, MM_K], [MM_TM, MM_TK], along: [0, FREE_AXIS])),    In(F64, cells([MM_K, MM_N], [MM_TK, MM_TN], along: [FREE_AXIS, 1])),    Out(F64, cells([MM_M, MM_N], [MM_TM, MM_TN])), matmul)  p}

kernels.dawn:389–393 · kernels.dawn:5159–5165 · matmul.mlir

Shared 是逃生口

histogram

原子操作不是格子写,地址由数据决定的 scatter、一块写两个区域也不是。它们经 Shared 写,标记把这件事写在读者找得到的地方。

fn histogram(input: Param[I32], hist: Param[I32]) -> Unit !Dev = {  let x = load_cell(input)  let live = tail_mask(tile_at(block_id(0), HIST_TILE), [HIST_TILE], HIST_N)  let ok = and_mask(live, in_range(x, 0, HIST_BINS))  atomic_add(hist, and_i(x, i_const(HIST_BINS - 1)), i_const(1), mask: Some(ok))} "histogram" -> {  let (p, _e) = trace2("histogram", In(I32, cells([HIST_N], [HIST_TILE])), Shared(I32), histogram)  p}

kernels.dawn:1613–1618 · kernels.dawn:5357–5360 · histogram.mlir

在 sm_86 上,128×128 的 f16 矩阵乘要带 hint_occupancy(2):每个 SM 放得下两块,而不是一块。 怎么测的。

这个包的完整公开面 · 0.9.0 改了什么

一个 kernel,逐行对照

一个融合注意力 kernel(也就是上面那两行行统计的出处)的每一行,都摆在它的调用写出的 Tile IR 旁边,每一段挂在写出它的那个调用名下。配对来自记录本身和 Dawn 自己的解析器,不来自手写的对照表。每段里加粗的那一行是这个调用真正要做的操作,它上面的几行是 lowering 补上的寻址。每一行起初是折叠的,只露出最外层调用的那一行加粗行;点一下这一行,其余的就展开。同一个函数连同它编出来的 C 与 JVM 字节码,在对照页上。

flash_attn一个循环,里面两次归约

38 个调用 50 个操作 62 行 Tile IR · 691 字节

  1. 2176fn flash_attn(q: Param[Float], k: Param[Float], v: Param[Float], o: Param[Float], scale: Param[Float]) -> Unit !Dev = {
    1cuda_tile.module @m {2  entry @flash_attn(%arg0: tile<ptr<f64>>, %arg1: tile<ptr<f64>>, %arg2: tile<ptr<f64>>, %arg3: tile<ptr<f64>>, %arg4: tile<f64>) {3    %0 = make_token : token
  2. 2177  let tq = load_cell(q)
    load_cell
    4    %1 = assume div_by<16>, %arg0 : tile<ptr<f64>>5    %2 = make_tensor_view %1, shape = [64, 32], strides = [32, 1] : tensor_view<64x32xf64, strides=[32, 1]>6    %3 = make_partition_view %2 : partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>7    %4, %5, %6 = get_tile_block_id : tile<i32>8    %7, %8, %9 = get_tile_block_id : tile<i32>9    %10, %11 = load_view_tko weak %3[%4, %8] token=%0 : partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>, tile<i32> -> tile<32x32xf64>, token
  3. 2178  var m: Tile[Float] = full([FA_BQ, 1], -INFINITY)
    full
    10    %12 = constant <f64: -Infinity> : tile<32x1xf64>
  4. 2179  var l: Tile[Float] = full([FA_BQ, 1], 0.0)
    full
    11    %13 = constant <f64: 0.0> : tile<32x1xf64>
  5. 2180  var acc = zeros(o)
    zeros
    12    %14 = constant <f64: 0.0> : tile<32x32xf64>
  6. 2181  for j in d_range(0, ATT_N / FA_BK) {
    d_range
    13    %15 = constant <i32: 0> : tile<i32>14    %16 = constant <i32: 2> : tile<i32>15    %17 = constant <i32: 1> : tile<i32>16    %18, %19, %20, %21 = for %22 in (%15 to %16, step %17) : tile<i32> iter_values(%23 = %12, %24 = %13, %25 = %14, %26 = %11) -> (tile<32x1xf64>, tile<32x1xf64>, tile<32x32xf64>, token) {
  7. 2182    let s: Tile[Float] = mma(tq, load_at(k, [j]).transpose(), 0.0) * scalar(scale)
    load_at
    17      %27 = assume div_by<16>, %arg1 : tile<ptr<f64>>18      %28 = make_tensor_view %27, shape = [64, 32], strides = [32, 1] : tensor_view<64x32xf64, strides=[32, 1]>19      %29 = make_partition_view %28 : partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>20      %30, %31 = load_view_tko weak %29[%22, %8] token=%26 : partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>, tile<i32> -> tile<32x32xf64>, token
    transpose
    21      %32 = permute %30 [1, 0] : tile<32x32xf64> -> tile<32x32xf64>
    mma
    22      %33 = constant <f64: 0.0> : tile<32x32xf64>23      %34 = mmaf %10, %32, %33 : tile<32x32xf64>, tile<32x32xf64>, tile<32x32xf64>
    mul
    24      %35 = reshape %arg4 : tile<f64> -> tile<1x1xf64>25      %36 = broadcast %35 : tile<1x1xf64> -> tile<32x32xf64>26      %37 = mulf %34, %36 rounding<nearest_even> : tile<32x32xf64>
  8. 2183    let m_new = max(m, reduce_max(s, keepdims: true))
    reduce_max
    27      %38 = reduce %37 dim=1 identities=[-Infinity : f64] : tile<32x32xf64> -> tile<32xf64> (%39: tile<f64>, %40: tile<f64>) {28        %41 = maxf %39, %40 : tile<f64>29        yield %41 : tile<f64>30      }31      %42 = reshape %38 : tile<32xf64> -> tile<32x1xf64>
    max
    32      %43 = maxf %23, %42 : tile<32x1xf64>
  9. 2184    let p = exp(s - m_new)
    sub
    33      %44 = broadcast %43 : tile<32x1xf64> -> tile<32x32xf64>34      %45 = subf %37, %44 rounding<nearest_even> : tile<32x32xf64>
    exp
    35      %46 = exp %45 : tile<32x32xf64>
  10. 2185    let alpha = exp(m - m_new)
    sub
    36      %47 = subf %23, %43 rounding<nearest_even> : tile<32x1xf64>
    exp
    37      %48 = exp %47 : tile<32x1xf64>
  11. 2186    l = l * alpha + reduce_sum(p, keepdims: true)
    mul
    38      %49 = mulf %24, %48 rounding<nearest_even> : tile<32x1xf64>
    reduce_sum
    39      %50 = reduce %46 dim=1 identities=[0.0 : f64] : tile<32x32xf64> -> tile<32xf64> (%51: tile<f64>, %52: tile<f64>) {40        %53 = addf %51, %52 rounding<nearest_even> : tile<f64>41        yield %53 : tile<f64>42      }43      %54 = reshape %50 : tile<32xf64> -> tile<32x1xf64>
    add
    44      %55 = addf %49, %54 rounding<nearest_even> : tile<32x1xf64>
  12. 2187    acc = mma(p, load_at(v, [j]), acc * alpha)
    load_at
    45      %56 = assume div_by<16>, %arg2 : tile<ptr<f64>>46      %57 = make_tensor_view %56, shape = [64, 32], strides = [32, 1] : tensor_view<64x32xf64, strides=[32, 1]>47      %58 = make_partition_view %57 : partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>48      %59, %60 = load_view_tko weak %58[%22, %8] token=%31 : partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>, tile<i32> -> tile<32x32xf64>, token
    mul
    49      %61 = broadcast %48 : tile<32x1xf64> -> tile<32x32xf64>50      %62 = mulf %25, %61 rounding<nearest_even> : tile<32x32xf64>
    mma
    51      %63 = mmaf %46, %59, %62 : tile<32x32xf64>, tile<32x32xf64>, tile<32x32xf64>
  13. 2188    m = m_new
  14. 2189  }
    d_range
    52      continue %43, %55, %63, %60 : tile<32x1xf64>, tile<32x1xf64>, tile<32x32xf64>, token53    }
  15. 2190  store_cell(o, acc / l)
    div
    54    %64 = broadcast %19 : tile<32x1xf64> -> tile<32x32xf64>55    %65 = divf %20, %64 rounding<nearest_even> : tile<32x32xf64>
    store_cell
    56    %66 = assume div_by<16>, %arg3 : tile<ptr<f64>>57    %67 = make_tensor_view %66, shape = [64, 32], strides = [32, 1] : tensor_view<64x32xf64, strides=[32, 1]>58    %68 = make_partition_view %67 : partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>59    %69 = store_view_tko weak %65, %68[%4, %8] token=%21 : tile<32x32xf64>, partition_view<tile=(32x32), padding_value = zero, tensor_view<64x32xf64, strides=[32, 1]>, dim_map=[0, 1]>, tile<i32> -> token
  16. 2191}
    60    return61  }62}

后端覆盖了什么

Tile IR 版本里每个公开 opcode、类型 tag 和属性值都有一行,记着状态和证据。指向卡片可以看到还没实现的那几行。

102/105个公开 opcode 已实现3 structuralfeatures.txt
  • entry structural · 3
  • global structural · T7
  • module structural · 3
24个类型 tag 已实现types.txt
52/53个属性值已实现1 unimplementedattrs.txt
  • ptr_attr.none unimplemented · T21
208个 golden kernel,每个都以文本和字节码两种形式钉住tile-golden/

三道门

每一层都抓得到前一层抓不到的问题,而且每一层都必须让自己的变异体变红。每个点是其中一个,落在它所针对的那一道门。

  1. 0

    文本与字节

    10 个变异体在这里被抓住

    每个 golden kernel 都被 trace 两次,与记录下的文本和字节码逐字节比对。它只说明有没有变化,不说明对不对。

    scripts/tile-golden/run.sh
  2. 1

    汇编器接受

    83 个变异体在这里被抓住

    tileiras 必须把每个字节码 golden 汇编成带有该 kernel 名字的 cubin。它抓编码错误和类型错误,抓不到算错的答案。

    scripts/tile-golden/run.sh
  3. 2

    设备一致

    43 个变异体在这里被抓住

    在有 GPU 的机器上,每个输出缓冲区都与宿主参考比对,逐位一致或在声明的容差内。每次运行在台账追加一行,CI 拒绝最后一行没测过的源码树。

    scripts/tile-gpu-diff/run.sh

在真实硬件上

每台机器台账的最后一行。CI 读第一行;sm_90 与 sm_100 两本覆盖了 Ampere 卡加载不了的 fp8、fp4 与 block-scaled 那几行。

GPU日期驱动tileiras结果逐位一致容差内源码树
sm_862026-10-14616.5613.4.92pass106459fabf4af78f2
sm_902026-10-11580.95.0513.4.92pass10945b88bc2b4a1cb
sm_1002026-10-11580.159.0413.4.92pass11245b88bc2b4a1cb

按档位统计比对过的 kernel 数,对该行涉及的各族求和。