三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Tullio.jl GPU计算教程:使用KernelAbstractions实现高效并行

Tullio.jl GPU计算教程:使用KernelAbstractions实现高效并行

Tullio.jl GPU计算教程:使用KernelAbstractions实现高效并行

【免费下载链接】Tullio.jl项目地址: https://gitcode.com/gh_mirrors/tu/Tullio.jl

Tullio.jl 是一个极其灵活的 Julia 张量运算宏,本教程将带你掌握 Tullio.jl GPU计算的核心用法:只需加载 CUDA 与 KernelAbstractions,就能把同样的索引记号代码自动编译成 GPU 内核,实现高效并行计算。无论是矩阵乘法、数组置换还是卷积、广播,一套代码即可在 CPU 与 GPU 之间无缝切换。

什么是 Tullio.jl?为何它适合 GPU 并行计算?

Tullio.jl 本质上是一个"超级 einsum 宏",它把类似数学公式的索引记号(index notation)翻译成高性能的嵌套循环。它不仅能做矩阵乘法和数组置换,还能处理卷积、模板计算(stencil)、散射/聚集(scatter/gather)和广播等复杂运算。

它实现高效并行的两条关键路径:

  • CPU 端:借助 LoopVectorization.@avx 自动向量化,配合多线程与递归分块(tiling);
  • GPU 端:借助 KernelAbstractions.@kernel 自动生成 GPU 内核,在 CUDA 显卡上并行执行。

这套自动生成 GPU 内核的逻辑位于 src/macro.jl,而 CUDA 的接入扩展则在 ext/TullioCUDAExt.jl。

快速开始:安装与环境准备

在开始 Tullio.jl GPU计算之前,请确保 Julia 环境就绪(要求 Julia 1.10+)。在 Julia 的 REPL 中执行:

using Pkg Pkg.add("Tullio")

要让 GPU 内核生效,还必须同时加载CUDAKernelAbstractions两个包。Tullio 的 GPU 内核只有在这两个包可见时才会被构造,这与它的弱依赖机制(见 Project.toml)密切相关:

using Tullio using CUDA, KernelAbstractions

如果你的机器没有 NVIDIA 显卡,也别担心——Tullio 会自动回退到 CPU 多线程路径,功能照样可用。

核心概念:用索引记号描述张量运算

先看一个最经典的例子——矩阵乘法。数学上写成 C[i,k] = Σⱼ A[i,j]·B[j,k],用 Tullio 表达几乎一模一样:

mul(A, B) = @tullio C[i,k] := A[i,j] * B[j,k] A = rand(3, 40); B = rand(40, 500); A * B ≈ mul(A, B) # true

宏会自动推断每个索引的取值范围:j 不在左侧出现,因此自动对 j 求和;i、k 在左侧出现,则成为输出维度。同样的语法还能描述置换、求和、点乘、卷积等大量操作,例如求和@tullio S[c] := M[r,c](对 r 求和)等价于sum(M, dims=1)

一键切换到 GPU:高效并行的关键一步

Tullio.jl GPU计算的神奇之处在于:代码完全不用改,只要把输入换成 CuArray。继续使用上面的mul函数:

cu(A * B) ≈ mul(cu(A), cu(B)) # true,结果同样是 CuArray

宏在展开时发现输入是CuArray,就会自动通过 KernelAbstractions 构造 GPU 内核并分发到显卡上执行。整个切换逻辑发生在 src/macro.jl 的 KernelAbstractions 分支中,测试用例可见 test/cuda.jl。

更妙的是,连反向传播梯度都能在 GPU 上运行。配合 Tracker 或 Zygote,只需加载即可:

using Tracker ΔA = Tracker.gradient((A,B) -> sum(mul(A, B)), cu(A), cu(B))[1]

性能实测:与 MKL、OpenBLAS 的对比

Tullio 的目标之一是在简单矩阵乘法上追平 BLAS 库。下面这张来自官方基准(benchmarks/02/matmul.jl)的对比图显示,在中等矩阵规模下,Tullio 的 GFLOPS 与 Intel MKL 相当接近:

而在数组置换(permute)这类"奇怪"的张量操作上,Tullio 优势更加明显——它不需要先permutedims再计算,而是直接在索引级别完成。下图中红色点线(tullio)在各维度规模下耗时都显著更低:

转置操作同样如此,大尺寸下 Tullio 的表现优于 einsum、arraymeta 甚至 MKL 的矩阵复制转置:

性能结果与硬件、Tullio 版本相关,以上图表来自官方基准目录 benchmarks/02,仅作参考。

常用参数调优:让并行更高效

@tullio宏支持多个关键词参数,帮助你控制 CPU 与 GPU 的并行策略:

参数作用推荐用法
cuda控制 GPU 内核生成与工作组大小cuda=true自动调优,cuda=256指定块大小,cuda=false禁用
threads是否启用 CPU 多线程默认按数组大小自动判断,可用threads=n强制
avx是否使用 LoopVectorization 向量化默认开启,复杂数值类型失效时自动回退
verbose打印索引范围与内核信息调试时用verbose=trueverbose=2打印全部细节

典型的"满血"配置长这样:

@tullio threads=true fastmath=true avx=true cuda=256 grad=Base verbose=false C[i,k] := A[i,j] * B[j,k]

其中cuda=256会传递给 KernelAbstractions 的kernel(CUDA(), 256),以 256 为工作组大小启动内核。

避坑指南:GPU 计算的注意事项

初学 Tullio.jl GPU计算,下面几个坑最容易踩到:

  1. 必须同时加载 CUDA 与 KernelAbstractions,否则宏根本不会生成 GPU 分支,只会静默使用 CPU 路径;
  2. 完整归约到标量目前在 GPU 上不可用(如@tullio s := A[i,j]^2这种归约到单个数字的操作),这类操作在 GPU 上极慢且已被移除,建议改写为输出数组形式;
  3. 标量不参与梯度计算gradient(a -> (@tullio _ := $a * A[i]), 3.14)返回的梯度是零,梯度只对数组计算;
  4. GPU 路径不支持 OffsetArrays,内核会主动抛出"KernelAbstractions can't handle OffsetArrays here"的错误提示;
  5. CPU 版 KernelAbstractions 内核速度一般,仅当threads=false时才会被调用,日常 CPU 计算建议交给 LoopVectorization 路径,因此默认情况下 GPU 分支的 CPU 回退不会生效。

总结

Tullio.jl 提供了一条从"数学公式"到"高效并行"的捷径:同一份索引记号代码,在 CPU 上由 LoopVectorization + 多线程加速,在 GPU 上由 KernelAbstractions 自动生成内核。对于矩阵乘法、置换、卷积等张量运算,这套方案既简洁又高效,尤其适合需要一套代码多端部署的科学计算与深度学习场景。

想深入源码?可以从宏展开的核心 src/macro.jl、CUDA 扩展 ext/TullioCUDAExt.jl 以及 GPU 测试 test/cuda.jl 开始读起,结合 README.md 中的 Notation 章节,很快就能写出自己的高性能并行代码。

【免费下载链接】Tullio.jl项目地址: https://gitcode.com/gh_mirrors/tu/Tullio.jl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

← 返回列表