JuliaでGPU計算、やってみたいですよね。そして、どうせGPUを使うなら、色々なGPUを使いたいですよね。
さらに、どうせGPU用のコードを書くなら、一回コードを書いたらそれがどのアーキテクチャでも動いてくれるといいですよね。NVIDIA製GPUでも、AMD製GPUでも、ついでに普通の並列も動いてくれるといいですよね。
そんなことできるの?とお思いの方に、JACC.jlというパッケージを紹介します。
このパッケージを使うと、自分の書いたコードが、NVIDIA製GPUでも、AMD製GPUでも、Intel製GPUでも、そして、CPUのマルチコアでも、並列に動いてしまいます。
インストール
インストールは、おすすめとしては、これをaddする前に、]キーを押してパッケージモードにしてから、
activate .
add JACC
です。これで、今コードを書こうとしているディレクトリ専用の仮想環境が用意され、そこにJACCが入ります。activate .は便利です。Pythonのvenvみたいなものですね。JACCは2025年12月7日現在0.6となっています。
次に、どのハードウェアを使うか、バックエンドを指定するために、
using JACC
JACC.set_backend("cuda")
のようにしてください。cudaだとNVIDIA製、amdgpuだとAMD製、threadsだとマルチコアです。
そのあとREPLを一度終了してください。これによって、LocalPreferences.tomlというファイルがディレクトリにできていると思います。中身は
[JACC]
backends = ["threads", "cuda"]
default_backend = "cuda"
[JACC.placement]
cuda = "none"
こんな感じです。
対象とする計算
次に、GPU計算のコードを書いてみます。
JACCを使う場合、どのハードウェアを使うかという関連の初期化をするために、最初に
julia
using JACC
JACC.@init_backend
とします。
さて、今回は、
C_{ij}(n) = \sum_{k=1}^{NC} A_{ik}(n)B_{kj}(n)
という計算をやってみることにします。ここで、インデックスnは実空間のインデックスで、つまり、n番目の格子点の上に$NC \times NC$の行列が定義されており、その行列の積を計算します。
この計算をGPUでやるにあたって、インデックスnの部分を並列化します。それぞれのnで計算は別々にできますので。ということで、以下のような関数を定義します。
@inline function kernel_mul!(n, C, A, B, NC)
for jc = 1:NC
for ic = 1:NC
C[ic, jc, n] = 0.0
for kc = 1:NC
C[ic, jc, n] += A[ic, kc, n] * B[kc, jc, n]
end
end
end
return
end
最初の引数は$n$で、n番目の格子を意味しています。そして、C,A,Bは
NC = 5
N = 1000000
C = zeros(Float64, NC, NC, N)
A = rand(Float64, NC, NC, N)
B = rand(Float64, NC, NC, N)
のように定義します。ここで1000000個の格子点の上に5x5行列が定義されています。
普通にCPUで計算するのであれば、
function multCPU!(C, A, B, NC, N)
for n = 1:N
kernel_mul!(n, C, A, B, NC)
end
end
ですね。
JACCの書き方
次に、JACCを使って並列計算をしてみます。JACCでは、ハードウェアが何であるかを問わない書き方ができて、例えば、
NC = 5
N = 1000000
C = zeros(Float64, NC, NC, N)
A = rand(Float64, NC, NC, N)
B = rand(Float64, NC, NC, N)
Cj = JACC.array(C)
Aj = JACC.array(A)
Bj = JACC.array(B)
とします。JACC.array(C)とした瞬間、もしGPUを使っている場合には、配列CがGPUメモリーに転送されることになります。マルチCPUの場合はそのままです。
並列計算は、
function multGPU!(C, A, B, NC, N)
JACC.parallel_for(
N, kernel_mul!, C, A, B, NC
)
end
です。最初の引数は、ループの長さを、第二引数は関数を、第三以降はその関数の引数を並べます。これで、kernel_mul!の第一引数を並列化することになります。
以上です。このコードで、どんなGPUでも並列化できてしまいます。
全体のコードは
using JACC
JACC.@init_backend
@inline function kernel_mul!(n, C, A, B, NC)
for jc = 1:NC
for ic = 1:NC
C[ic, jc, n] = 0.0
for kc = 1:NC
C[ic, jc, n] += A[ic, kc, n] * B[kc, jc, n]
end
end
end
return
end
function multGPU!(C, A, B, NC, N)
JACC.parallel_for(
N, kernel_mul!, C, A, B, NC
)
end
function multCPU!(C, A, B, NC, N)
for n = 1:N
kernel_mul!(n, C, A, B, NC)
end
end
function main()
# Your main application logic goes here
NC = 5
N = 1000000
C = zeros(Float64, NC, NC, N)
A = rand(Float64, NC, NC, N)
B = rand(Float64, NC, NC, N)
Cj = JACC.array(C)
Aj = JACC.array(A)
Bj = JACC.array(B)
multGPU!(Cj, Aj, Bj, NC, N)
display(Cj[:, :, 1])
multCPU!(C, A, B, NC, N)
display(C[:, :, 1])
C = zeros(Float64, NC, NC, N)
A = rand(Float64, NC, NC, N)
B = rand(Float64, NC, NC, N)
Cj = JACC.array(C)
Aj = JACC.array(A)
Bj = JACC.array(B)
@time multGPU!(Cj, Aj, Bj, NC, N)
display(Cj[:, :, 1])
@time multCPU!(C, A, B, NC, N)
display(C[:, :, 1])
end
main()
です。CPUとJACCの比較をしています。
ベンチマーク
それでは、計算がちゃんと並列化されているかみてみましょう。
スレッド並列
まず、マルチコアCPUを使ってみます。使うものはMacBookPro 14インチ(2023)、CPUはApple M2 Maxで、12コア(パフォーマンスコア8コア、効率化コア4コア)です。
Juliaでスレッド並列をするには、-tをつけます。先ほどのディレクトリの環境を使うには、--project=.とします。
つまり、
julia -t 2 --project=. main.jl
で2スレッド並列ですね。
実行結果は
0.129510 seconds (12 allocations: 1.141 KiB)
5×5 Matrix{Float64}:
1.05593 0.953626 0.525468 1.03582 1.05768
1.46264 2.52129 1.65358 1.38293 2.02142
0.855354 1.10988 0.747243 0.669073 1.19669
0.31872 0.470203 0.364287 0.323551 0.381042
0.668523 1.09341 0.501557 0.68106 0.855375
0.213086 seconds
5×5 Matrix{Float64}:
1.05593 0.953626 0.525468 1.03582 1.05768
1.46264 2.52129 1.65358 1.38293 2.02142
0.855354 1.10988 0.747243 0.669073 1.19669
0.31872 0.470203 0.364287 0.323551 0.381042
0.668523 1.09341 0.501557 0.68106 0.855375
で大体2倍速になっています。4スレッドだと
0.062821 seconds (22 allocations: 2.219 KiB)
5×5 Matrix{Float64}:
1.25792 1.32799 0.940223 0.781699 1.55357
0.878267 1.17095 0.992576 0.669238 1.12706
1.20005 1.18871 0.607388 0.556967 1.44924
0.562391 1.14431 0.880372 0.638255 0.804898
0.292237 0.692637 0.408039 0.250453 0.316187
0.205496 seconds
5×5 Matrix{Float64}:
1.25792 1.32799 0.940223 0.781699 1.55357
0.878267 1.17095 0.992576 0.669238 1.12706
1.20005 1.18871 0.607388 0.556967 1.44924
0.562391 1.14431 0.880372 0.638255 0.804898
0.292237 0.692637 0.408039 0.250453 0.316187
8スレッドだと、
0.027700 seconds (42 allocations: 4.375 KiB)
5×5 Matrix{Float64}:
1.22701 0.96939 0.506475 0.489596 1.16823
2.23987 1.83774 0.572637 1.05251 1.89627
1.67783 1.38032 0.404913 1.03638 1.52376
1.55576 1.05758 0.40458 0.998169 1.32818
1.95967 1.66258 0.589071 0.606719 1.62389
0.186760 seconds
5×5 Matrix{Float64}:
1.22701 0.96939 0.506475 0.489596 1.16823
2.23987 1.83774 0.572637 1.05251 1.89627
1.67783 1.38032 0.404913 1.03638 1.52376
1.55576 1.05758 0.40458 0.998169 1.32818
1.95967 1.66258 0.589071 0.606719 1.62389
で、8倍速にはなっていませんが結構速くなっています。なお、スレッドを使うと、
[ Info: Threads backend loaded with 8 threads
のようなメッセージが出ますので、8スレッドで動いていることがわかります。
NVIDIA GeForce RTX 4090
0.006675 seconds (74 allocations: 3.891 KiB)
5×5 CUDA.CuArray{Float64, 2, CUDA.DeviceMemory}:
1.17201 1.59068 1.72769 1.99373 1.59829
0.998139 1.32721 1.3987 1.49103 1.54037
1.8172 1.79746 1.7754 2.86208 1.72581
0.972047 1.48182 1.53006 1.77494 1.38059
0.609081 0.994494 0.867954 1.39835 0.772122
0.291615 seconds
5×5 Matrix{Float64}:
1.17201 1.59068 1.72769 1.99373 1.59829
0.998139 1.32721 1.3987 1.49103 1.54037
1.8172 1.79746 1.7754 2.86208 1.72581
0.972047 1.48182 1.53006 1.77494 1.38059
0.609081 0.994494 0.867954 1.39835 0.772122
です。速いですね。倍精度演算なので単精度の1/64しか速度が出ないはずですが、今回みたいな小さな行列を何回も計算するやつは、多分GPUメモリ律速になっていると思われます。
また、CUDAが使われている場合には、
[ Info: CUDA backend loaded
と出ます。これが出ていないと、GPUが使えていませんので注意してください。
AMD Radeon RX 7800 XT
次は、AMD製のGPUです。コードは全く同じで構いません。

実行結果は、
0.029817 seconds (334 allocations: 9.016 KiB)
5×5 AMDGPU.ROCArray{Float64, 2, AMDGPU.Runtime.Mem.HIPBuffer}:
0.37837 0.446957 0.590148 0.72041 0.747445
0.552329 0.615178 0.729361 1.02719 0.71334
1.15405 0.673195 0.86338 1.78428 1.31944
1.02976 0.759977 1.3385 2.15536 0.703746
1.16309 0.899828 1.27847 2.23525 0.78781
0.153014 seconds
5×5 Matrix{Float64}:
0.37837 0.446957 0.590148 0.72041 0.747445
0.552329 0.615178 0.729361 1.02719 0.71334
1.15405 0.673195 0.86338 1.78428 1.31944
1.02976 0.759977 1.3385 2.15536 0.703746
1.16309 0.899828 1.27847 2.23525 0.78781
です。RTXと比べるとメモリ帯域が確か半分だったので、メモリ律速を裏付けているような感じです。
AMDの場合は、実行すると、
[ Info: AMDGPU backend loaded
と出ます。
NVIDIA H100 NVL
最後に、NVIDIA H100 NVLが手元で使えたので、こちらでも試してみます。
実行結果は、
0.007441 seconds (323 allocations: 7.547 KiB)
5×5 CUDA.CuArray{Float64, 2, CUDA.DeviceMemory}:
0.615174 1.01216 1.25659 1.30054 0.872642
1.47844 1.6595 2.11988 2.18352 2.0313
0.963653 1.17193 1.48219 1.6469 1.15857
1.4952 1.77438 1.98736 1.96495 1.3483
0.992886 1.19461 1.4507 1.06382 1.55244
0.233601 seconds
5×5 Matrix{Float64}:
0.615174 1.01216 1.25659 1.30054 0.872642
1.47844 1.6595 2.11988 2.18352 2.0313
0.963653 1.17193 1.48219 1.6469 1.15857
1.4952 1.77438 1.98736 1.96495 1.3483
0.992886 1.19461 1.4507 1.06382 1.55244
です。RTXとあまり変わらない結果というのは、やはりGPUメモリ律速なのかなと思います。


