Julia 教程 / Julia GPU 计算(CUDA.jl)
Julia GPU 计算(CUDA.jl)
CUDA.jl 是 Julia 的 NVIDIA GPU 计算库,提供与 CUDA C 类似的编程接口,同时保持 Julia 的高级抽象和类型系统。无需编写任何 C/CUDA 代码,即可在 GPU 上运行 Julia 代码。
1. 安装与配置
# 安装 CUDA.jl
using Pkg
Pkg.add("CUDA")
# 验证安装
using CUDA
CUDA.functional() # true 表示可用
CUDA.version() # CUDA 驱动版本
CUDA.runtime_version() # CUDA 运行时版本
# GPU 信息
dev = CUDA.device()
CUDA.name(dev) # GPU 名称
CUDA.totalmem(dev) # 显存总量
CUDA.capability(dev) # 计算能力
# 多 GPU 环境
CUDA.ndevices() # GPU 数量
CUDA.devices() # 所有设备列表
| 函数 | 说明 |
|---|---|
CUDA.functional() | 检查 GPU 是否可用 |
CUDA.device() | 当前设备 |
CUDA.device!(id) | 切换到指定设备 |
CUDA.memory_status() | 内存使用状态 |
CUDA.synchronize() | 同步 GPU 操作 |
⚠️ 注意: CUDA.jl 需要 NVIDIA GPU 和 CUDA 驱动程序。确保驱动版本 ≥ CUDA Toolkit 要求的最低版本。AMD GPU 使用 AMDGPU.jl,Apple Silicon 使用 Metal.jl。
2. CuArray 创建与操作
using CUDA
# 从普通数组创建
A_cpu = rand(Float32, 1000, 1000)
A_gpu = CuArray(A_cpu) # CPU → GPU
A_back = Array(A_gpu) # GPU → CPU
# 直接在 GPU 上创建
B_gpu = CUDA.zeros(Float32, 1000, 1000) # 全零
C_gpu = CUDA.ones(Float32, 1000, 1000) # 全一
D_gpu = CUDA.rand(Float32, 1000, 1000) # 随机
E_gpu = CUDA.randn(Float32, 1000, 1000) # 正态随机
# GPU 数组操作(与 CPU 数组语法完全一致)
F_gpu = A_gpu * B_gpu # 矩阵乘法
G_gpu = A_gpu .+ C_gpu # 逐元素加法
H_gpu = sin.(D_gpu) # 逐元素 sin
sum_gpu = sum(D_gpu) # 求和(结果在 GPU 上)
max_gpu = maximum(D_gpu) # 最大值
# 类型转换
A_f64 = CuArray{Float64}(A_gpu) # 转换精度
| 操作 | CPU 数组 | GPU 数组 |
|---|---|---|
| 创建 | zeros(n) | CUDA.zeros(n) |
| 随机 | rand(n) | CUDA.rand(n) |
| 转换 | — | CuArray(cpu_arr) |
| 回传 | — | Array(gpu_arr) |
💡 提示: CuArray 和 Array 的 API 几乎完全一致。大多数 Julia 代码只需要将 Array 替换为 CuArray 就能在 GPU 上运行。
3. GPU 核函数(@cuda)
using CUDA
# 自定义 GPU 核函数
# 向量加法
function vector_add!(C, A, B)
i = (blockIdx().x - 1) * blockDim().x + threadIdx().x
if i <= length(C)
C[i] = A[i] + B[i]
end
return nothing
end
n = 1024 * 1024
A = CUDA.rand(Float32, n)
B = CUDA.rand(Float32, n)
C = CUDA.zeros(Float32, n)
# 启动核函数
threads = 256
blocks = ceil(Int, n / threads)
@cuda threads=threads blocks=blocks vector_add!(C, A, B)
# 等待 GPU 完成
CUDA.synchronize()
# 验证
@assert Array(C) ≈ Array(A) + Array(B)
矩阵乘法核函数
# 简单的矩阵乘法(非优化版本)
function matmul_kernel!(C, A, B)
row = (blockIdx().y - 1) * blockDim().y + threadIdx().y
col = (blockIdx().x - 1) * blockDim().x + threadIdx().x
if row <= size(C, 1) && col <= size(C, 2)
sum_val = 0.0f0
for k in 1:size(A, 2)
sum_val += A[row, k] * B[k, col]
end
C[row, col] = sum_val
end
return nothing
end
M, K, N = 256, 128, 256
A = CUDA.rand(Float32, M, K)
B = CUDA.rand(Float32, K, N)
C = CUDA.zeros(Float32, M, N)
# 2D 线程块
threads = (16, 16)
blocks = (ceil(Int, N/16), ceil(Int, M/16))
@cuda threads=threads blocks=blocks matmul_kernel!(C, A, B)
CUDA.synchronize()
4. 线程与块索引
using CUDA
# CUDA 线程层次结构:
# Grid → Block → Thread
# blockIdx().x/y/z : 块索引
# blockDim().x/y/z : 块维度
# threadIdx().x/y/z : 线程索引
# gridDim().x/y/z : 网格维度
# 全局线程索引
function global_index_1d()
return (blockIdx().x - 1) * blockDim().x + threadIdx().x
end
function global_index_2d()
row = (blockIdx().y - 1) * blockDim().y + threadIdx().y
col = (blockIdx().x - 1) * blockDim().x + threadIdx().x
return row, col
end
# 通用核函数模板
function generic_kernel!(output, input)
idx = (blockIdx().x - 1) * blockDim().x + threadIdx().x
if idx <= length(input)
output[idx] = some_operation(input[idx])
end
return nothing
end
| 索引函数 | 说明 |
|---|---|
threadIdx().x | 块内线程 x 索引(1-based) |
blockIdx().x | 块 x 索引(1-based) |
blockDim().x | 块 x 维度 |
gridDim().x | 网格 x 维度 |
⚠️ 注意: Julia 的 CUDA 索引从 1 开始(不是 C CUDA 的 0 开始)。这是一个重要的区别!
5. 共享内存
using CUDA
# 使用共享内存优化计算
# 共享内存是块内线程共享的高速缓存,比全局内存快 100 倍
function reduce_shared!(output, input)
TILE = 256
shared = @cuStaticSharedMem(Float32, 256)
tid = threadIdx().x
i = (blockIdx().x - 1) * blockDim().x + threadIdx().x
# 加载到共享内存
shared[tid] = i <= length(input) ? input[i] : 0.0f0
sync_threads()
# 归约(树形求和)
stride = blockDim().x ÷ 2
while stride > 0
if tid <= stride
shared[tid] += shared[tid + stride]
end
sync_threads()
stride ÷= 2
end
if tid == 1
output[blockIdx().x] = shared[1]
end
return nothing
end
6. GPU 广播与高级操作
using CUDA
# GPU 广播(自动并行化)
A = CUDA.rand(Float32, 1000, 1000)
B = CUDA.rand(Float32, 1000, 1000)
# Julia 广播语法自动在 GPU 上并行执行
C = sin.(A) .+ cos.(B) .+ 1.0f0
D = A .^ 2 .- B .* 2.0f0
# reduce 操作
total = sum(A)
max_val = maximum(A)
min_val = minimum(A)
# mapreduce
result = mapreduce(x -> x^2, +, A) # 所有元素平方和
# GPU 上的逻辑索引
mask = A .> 0.5f0
count_true = sum(mask)
# 条件赋值
C = CUDA.zeros(Float32, 1000, 1000)
C[A .> 0.5f0] .= 1.0f0
# 多维规约
col_sums = sum(A, dims=1) # 按列求和
row_means = mean(A, dims=2) # 按行求均值
7. GPU 与 CPU 数据传输
using CUDA
# CPU → GPU
A_cpu = rand(Float32, 1000, 1000)
A_gpu = CuArray(A_cpu) # 方式1:显式转换
A_gpu = CUDA.cu(A_cpu) # 方式2:cu 函数
# GPU → CPU
A_cpu = Array(A_gpu) # 方式1
A_cpu = collect(A_gpu) # 方式2(同 Array)
# 原地传输(避免分配)
A_cpu = rand(Float32, 100)
A_gpu = CUDA.zeros(Float32, 100)
copyto!(A_gpu, A_cpu) # CPU → GPU 原地
copyto!(A_cpu, A_gpu) # GPU → CPU 原地
# 异步传输(使用 CUDA.stream)
A_gpu = CUDA.zeros(Float32, 1000, 1000)
CUDA.@sync copyto!(A_gpu, A_cpu) # 等待完成
# 检查数据所在设备
CUDA.device(A_gpu) # 返回设备 ID
⚠️ 注意: CPU↔GPU 数据传输是性能瓶颈。应尽量减少传输次数,尽量让数据留在 GPU 上。
8. 内存管理
using CUDA
# 查看显存使用
CUDA.memory_status() # 显示已用/总显存
# 手动垃圾回收
CUDA.reclaim() # 释放 GPU 内存
# 内存池管理
CUDA.pool_status() # 内存池状态
CUDA.memory_pool() # 当前内存池
# 预分配避免频繁分配
function good_practice()
# 预分配输出
output = CUDA.zeros(Float32, 1000, 1000)
input = CUDA.rand(Float32, 1000, 1000)
for _ in 1:100
output .= sin.(input) # 原地操作,不分配新内存
end
end
# 避免不必要的 GPU→CPU 传输
function bad_practice(A_gpu)
A_cpu = Array(A_gpu) # 传输到 CPU
result = sum(A_cpu) # 在 CPU 上计算
return CuArray(result) # 传回 GPU(完全不必要!)
end
function good_practice(A_gpu)
return sum(A_gpu) # 直接在 GPU 上计算
end
9. 性能分析
using CUDA, BenchmarkTools
# GPU 计时(必须同步)
function gpu_time()
A = CUDA.rand(Float32, 10000, 10000)
B = CUDA.rand(Float32, 10000, 10000)
# 预热(第一次运行会触发编译)
C = A * B
CUDA.synchronize()
# 计时
t = CUDA.@elapsed begin
C = A * B
CUDA.synchronize()
end
println("GPU 矩阵乘法: $(round(t*1000, digits=2)) ms")
return C
end
# CPU 对比
function cpu_time()
A = rand(Float32, 10000, 10000)
B = rand(Float32, 10000, 10000)
t = @elapsed C = A * B
println("CPU 矩阵乘法: $(round(t*1000, digits=2)) ms")
return C
end
# GPU 性能分析
CUDA.@profile A * B
💡 提示: GPU 加速在计算密集型、数据并行的任务上效果最好。对于小数据量或高度串行的任务,CPU 可能更快(因为 GPU 启动开销)。
10. 多 GPU 编程
using CUDA
# 列出所有 GPU
for dev in CUDA.devices()
println("$(CUDA.name(dev)): $(CUDA.totalmem(dev) ÷ 1024^3) GB")
end
# 在指定 GPU 上工作
CUDA.device!(0) do
A = CUDA.rand(Float32, 1000, 1000)
result = sum(A * A)
println("GPU 0 结果: $result")
end
# 多 GPU 数据并行
function multi_gpu_sum(data_chunks)
n = CUDA.ndevices()
results = Vector{Float32}(undef, n)
Threads.@threads for i in 1:n
CUDA.device!(i - 1) do
results[i] = sum(CuArray(data_chunks[i]))
end
end
return sum(results)
end
11. 实际案例
11.1 GPU 加速矩阵乘法
using CUDA, BenchmarkTools
n = 4096
A_cpu = rand(Float32, n, n)
B_cpu = rand(Float32, n, n)
# CPU
A_gpu = CuArray(A_cpu)
B_gpu = CuArray(B_cpu)
t_cpu = @elapsed A_cpu * B_cpu
t_gpu = CUDA.@elapsed begin
A_gpu * B_gpu; CUDA.synchronize()
end
println("CPU: $(round(t_cpu*1000, digits=1))ms, GPU: $(round(t_gpu*1000, digits=1))ms")
println("加速比: $(round(t_cpu/t_gpu, digits=1))x")
11.2 GPU 加速 Monte Carlo 模拟
using CUDA
function monte_carlo_pi_gpu(n::Int)
x = CUDA.rand(Float32, n)
y = CUDA.rand(Float32, n)
inside = (x .^ 2 .+ y .^ 2) .<= 1.0f0
return 4.0 * sum(inside) / n
end
n = 100_000_000
t_gpu = CUDA.@elapsed pi_gpu = monte_carlo_pi_gpu(n)
t_cpu = @elapsed pi_cpu = 4.0 * sum(rand(n).^2 + rand(n).^2 .<= 1.0) / n
println("GPU π ≈ $pi_gpu ($(round(t_gpu*1000, digits=1))ms)")
println("CPU π ≈ $pi_cpu ($(round(t_cpu*1000, digits=1))ms)")
12. GPU 编程最佳实践
# ❌ 避免:频繁传输
for i in 1:100
A_cpu = Array(A_gpu) # GPU → CPU
result = process(A_cpu)
A_gpu = CuArray(result) # CPU → GPU
end
# ✅ 推荐:数据留在 GPU
for i in 1:100
A_gpu .= process_gpu.(A_gpu) # 全在 GPU 上完成
end
| 实践 | 说明 |
|---|---|
| 减少数据传输 | 数据尽量留在 GPU 上 |
| 预分配内存 | 避免核函数中频繁分配 |
| 合并访存 | 相邻线程访问相邻内存地址 |
| 利用共享内存 | 减少全局内存访问 |
| 合理设置线程块 | 通常 128-512 线程/块 |
扩展阅读
- CUDA.jl 官方文档
- AMDGPU.jl — AMD GPU 支持
- Metal.jl — Apple Silicon GPU
- oneAPI.jl — Intel GPU 支持
- KernelAbstractions.jl — 跨 GPU 平台抽象
- NVIDIA, CUDA C Programming Guide
- JuliaGPU.org
- Sanders & Kandrot, CUDA by Example