这篇文章给大家聊聊关于教程|在Julia 编程中实现GPU 加速,以及对应的知识点,希望对各位有所帮助,不要忘了收藏本站哦。
首先,什么是GPU?
能够启动并行线程可以显着提高速度,但也会使GPU 的使用变得更加困难。当使用这种未经处理的能源时,会出现以下缺点:
GPU 是一个独立的硬件,拥有自己的内存空间和不同的架构。因此,从RAM 到GPU 内存(VRAM、视频内存)的传输时间较长。即使在GPU 上启动内核(调用调度函数)也会带来很大的延迟,GPU 大约需要10us,而CPU 则只有几纳秒。如果没有高级封装,构建核心就会变得复杂。低精度是默认值,高精度计算可以轻松消除所有性能增益。 GPU 函数(内核)本质上是并行的,因此编写GPU 内核并不比编写并行CPU 代码容易,而且硬件差异增加了一些复杂性。内核通常用C/C++ 编写,但这并不是编写算法的最佳语言。 CUDA 和OpenCL 之间存在差异,后者是编写低级GPU 代码的主要框架。虽然CUDA 仅支持NVIDIA 硬件,但OpenCL 支持所有硬件,但并不细粒度。选择取决于个人需求。 Julia 作为一种高级脚本语言,允许在大多数GPU 硬件上运行时编写内核和环境代码!
GPU阵列
大多数高度并行的算法需要同时处理大量数据,以克服所有多线程和延迟问题。因此,大多数算法都需要数组来管理所有数据,这就需要一个好的GPU数组库作为关键基础。
GPUArrays.jl 是Julia 为此提供的基础。它实现了一个设计用于高度并行硬件的抽象数组。它包含设置GPU、启动Julia GPU 函数以及提供一些基本数组算术所需的所有功能。
抽象意味着它需要以CuArrays 和CLArrays 的形式实现。由于它们继承了GPUArray 的所有功能,因此它们提供完全相同的接口。唯一的区别发生在分配数组时,这迫使用户决定该数组是否存在于CUDA 或OpenCL 设备上。有关这方面的更多信息,请参阅内存部分。
GPUArray 有助于减少代码重复,因为它允许编写独立于硬件的GPU 内核,这些内核可以通过CuArray 或CLArray 编译为本机GPU 代码。因此,大多数通用内核可以在从GPUArrays 继承的所有包之间共享。
选择提示:CuArrays 仅支持Nvidia GPU,而CLArrays 支持大多数可用的GPU。 CuArrays 比CLArrays 更稳定,可以在Julia 0.7 上使用。两者的速度差不多。我建议尝试两者,看看哪种效果最好。
表现
通过一个简单的交互式代码示例进行快速解释:为了计算julia 集(Mandelbrot 集),我们必须将计算卸载到GPU。
使用CuArrays、FileIO、Colors、GPUArrays、BenchmarkToolsusing CuArrays: CuArray'''计算Julia 集的函数'''函数juliaset(z0, maxiter) c=ComplexF32(-0.5, 0.75) z=z0 for i in 1:maxiter abs2(z ) 4f0 return (i - 1) % UInt8 z=z * z + c end return maxiter % UInt8 # %用于转换不溢出checkendrange=100:50:2^12cutimes, jltimes=Float64[], Float64[]function run_bench(in, out) # 使用点语法将`juliaset` 应用于q_converted 的每个元素# 并将输出写入结果out .=juliaset.(in, 16) # 所有对GPU 的调用都是异步调度的, # 所以我们需要同步GPUArrays .synchronize(out)end# 存储对最后绘图结果的引用last_jl, last_cu=Nothing, Nothingfor N in range w, h=N, N q=[ComplexF32(r, i) for i=1:-(2.0/w ):-1, r=-1.5:(3.0/h):1.5] for (times, Typ) in ((cutimes, CuArray), (jltimes, Array)) # 转换为Array 或CuArray - 将计算移至CPU/GPU q_converted=Typ(q) result=Typ(zeros(UInt8, size(q))) for i in 1:10 # 每个大小5 个样本# 基准测试宏,所有变量都需要以$ t=Base.@elapsed begin run_bench( q_converted, result) end global last_jl, last_cu # 我们在本地范围内if result isa CuArray last_cu=result else last_jl=result end push!(times, t) end endendcu_jl=hcat(Array(last_cu), last_jl)cmap=colormap ('Blues', 16 + 1)color_lookup(val, cmap)=cmap[val + 1]save('results/juliaset.png', color_lookup.(cu_jl, (cmap,)))using Plots; plotly()x=重复(范围, 内部=10)加速=jltimes ./cutimesPlots.scatter( log2.(x), [加速, 填充(1.0, 长度(加速))], 标签=['cuda' 'cpu '],markersize=2,markertripwidth=0, legend=:right, xlabel='2^N', ylabel='speedup') 对于大型数组,通过将计算卸载到GPU 可以实现稳定的60-80 倍加速。获得这种加速就像将Julia 数组转换为GPUArray 一样简单。
人们可能认为GPU 性能会受到Julia 这样的动态语言的影响,但Julia 的GPU 性能应该与CUDA 或OpenCL 的原始性能相当。 Tim Besard 在集成LLVM Nvidia 编译管道方面做得非常出色,以实现与纯CUDA C 代码相同(有时甚至更好)的性能。他在博客(https://devblogs.nvidia.com/gpu-computing-julia-programming-language/) 上进行了进一步解释。 CLArrays 方法有点不同,它直接从Julia 生成OpenCL C 代码,具有与OpenCL C 相同的代码性能!
为了更好地了解性能并将其与多线程CPU 代码进行比较,我整理了一些基准测试:https://github.com/JuliaGPU/GPUBenchmarks.jl/blob/master/results/results.md
记忆
GPU有自己的存储空间,包括视频内存(VRAM)、不同的缓存和寄存器。无论您做什么,请在运行之前将Julia 对象传输到GPU。并非Julia 中的所有类型都在GPU 上运行。
首先我们看一下Julia 的类型:
struct Test # 一个不可变的结构体# 只包含其他不可变的结构体,这使得# isbitstype(Test)==true x:Float32 end# isbits 属性很重要,因为这些类型可以在不受GPU 限制的情况下使用!@assert isbitstype(Test )==truex=(2, 2)isa(x, Tuple{Int, Int}) # 元组也是不可变的mutable struct Test2 #-mutable, isbits(Test2)==false x:Float32endstruct Test3 # 包含堆分配/引用,而不是因为它只包含cpu 堆指针,所以它不能在GPU 上工作。'Array{Test2,1}'

所有这些Julia 类型在传输到GPU 或在GPU 上创建时的行为都不同。下表概述了预期结果:
垃圾收集
使用GPU 时,请注意GPU 上没有垃圾收集器(GC)。这应该不会产生太大影响,因为写入GPU 的高性能内核一开始就不应该创建任何GC 跟踪的内存。
在GPU 上实现GC 并非不可能,但请记住每个执行核心都是大规模并行的。在大约1000 个GPU 线程中的每个线程中创建和跟踪大量堆内存会立即消除性能增益,因此实现GC 是不值得的。
使用GPUArrays 可以作为在内核中分配数组的替代方案。 GPUArray 构造函数创建GPU 缓冲区并将数据传输到VRAM。如果调用Array(gpu_array),该数组将被传输回RAM 并成为普通的Julia 数组。 Julia 的GC 会跟踪这些GPU 阵列上的Julia 操作,并且如果不再使用GPU 内存,则会释放该内存。
因此,堆栈分配只能在设备上使用,并且只能由其他预分配的GPU 缓冲区使用。由于传输成本高昂,因此在为GPU 编写时,您倾向于尽可能多地重用和预分配。
GPUArray 构造函数
使用CuArrays、LinearAlgebra# GPU 数组可以从所有包含isbits 类型的Julia 数组构建!A1D=cu([1, 2, 3]) # cl for CLArraysA1D=fill(CuArray{Int}, 0, (100,)) # CLArray for CLArrays# Float32 数组- Float32 通常是首选,在大多数GPU 上,其速度比Float64diagonal_matrix=CuArray{Float32}(I, 100, 100)filled=fill(CuArray, 77f0, (4, 4, 4) 快30 倍) # 用Float32 填充的3D 数组77randy=rand(CuArray, Float32, 42, 42) # 在GPU 上生成的随机数# 数组构造函数还接受已知大小的isbits 迭代器# 注意,因为您还可以将isbits 类型传递给直接GPU 内核,在大多数情况下,您不需要将它们具体化为GPU 数组from_iter=CuArray(1:10)# 让我们创建一个点类型来进一步说明可以做什么:struct Point x:Float32 y:Float32endBase.convert(:Type{Point}, x3336 0:NTuple {2, Any})=Point(x[1], x[2])# 因为我们定义了上面从元组到点的转换# [Point(2, 2)] 可以写成Point[(2 , 2)] 因为所有数组# 元素都将转换为Pointcustom_types=cu(Point[(1, 2), (4, 3), (2, 2)])typeof(custom_types)'CuArray{point,1}'
数组运算
我们定义了一些操作。最重要的是,GPUArrays 支持Julia 的融合点广播表示法。此表示法允许您将函数应用于数组的每个元素,并使用f 的返回值创建一个新数组。此功能通常称为地图。广播是指将不同形状的数组广播成相同的形状。
它的工作原理如下:
x=Zeros(4, 4) # 4x4 零数组sy=Zeros(4) # 4 个元素数组z=2 # 一个标量# y 的第一个维度在x 中的第二个维度上重复# 标量z 在所有维度上重复# the下面等于`broadcast(+, Broadcast(+, xx, y), z)`x .+ y .+ z 发生“融合”是因为Julia 编译器会将表达式重写为惰性传递调用树广播调用,然后,在循环数组之前,可以将整个调用树融合到单个函数中。
如果您想了解有关广播的更多信息,请查看本指南:julia.guide/broadcasting。
这意味着任何在不分配堆内存(仅创建isbits 类型)的情况下运行的Julia 函数都可以应用于GPUArray 的每个元素,并且多个调用会融合到单个内核调用中。由于内核调用可能具有显着的延迟,因此这种融合是一项非常重要的优化。
使用CuArraysA=cu([1, 2, 3])B=cu([1, 2, 3])C=rand(CuArray, Float32, 3)结果=A .+ B .- Ctest(a:T) 其中T=a * Convert(T, 2) # 转换为与`a` 相同的类型# 就地广播,直接写入`result` result .=test.(A) # 自定义函数work# 最酷的是,这与自定义函数组合得很好类型和自定义函数。# 让我们回到Point 类型并为其定义加法Base.(+)(p1:Point, p2:Point)=Point(p1.x + p2.x, p1.y + p2.y)# 现在是这个works:custom_types=cu(Point[(1, 2), (4, 3), (2, 2)])# 这个特殊的例子也展示了广播的力量: # 非数组类型被广播并在整个长度内重复result=custom_types. + Ref(Point(2, 2))# 所以上面等于(减去所有分配):# 这会在GPU 上分配一个新数组,我们可以通过上面的广播广播=fill(CuArray, Point(2) 来避免这种情况, 2), (3,))结果==custom_types.+broadcastedtrue
GPUArrays支持更多操作:
多维索引和切片(xs[1:2, 5,]) 排列串联(vcat(x, y), cat(3, xs, ys, zs)) 映射、融合广播(zs .=xs.^2 .+ ys .* 2) fill (CuArray, 0f0, dims), fill (gpu_array, 0) 减少大小(reduce(+, xs, dims=3), sum(x -x^2, xs, dims=1) 减少到标量(reduce(*, xs), sum(xs), prod(xs))各种BLAS运算(矩阵*矩阵,矩阵*向量)FFT,使用与julia的FFT相同的APIGPUArrays实际应用
让我们直接来看一些很酷的例子。

GPU 加速的烟雾模拟器是由GPUArrays + CLArrays 创建的,可以在GPU 或CPU 上运行,GPU 版本速度快15 倍:
还有更多示例,包括查找微分方程、FEM 模拟和求解偏微分方程。
演示地址:https://juliagpu.github.io/GPUShowcases.jl/latest/index.html
让我们通过一个简单的机器学习示例来看看如何使用GPUArrays:
using Flux、Flux.Data.MNIST、Statisticsusing Flux: onehotbatch、onecold、crossentropy、throttleusing Base.Iterators: 重复、partitionusing CuArrays# 使用卷积网络对MNIST 数字进行分类imgs=MNIST.images()labels=onehotbatch(MNIST.labels(), 0:9 )# 划分为大小为1,000 的批次train=[(cat(float.(imgs[i]). dims=4), labels[:i]) for i in partition(1:60_000, 1000)]use_gpu=true # 在gpu/cputodevice 之间轻松切换的帮助器(x)=use_gpu ? gpu(x) : xtrain=todevice.(train)# 准备测试集(前1,000 张图像)tX=cat(float.(MNIST.images(:test) [1:1000]). dims=4) |todevicetY=onehotbatch (MNIST.labels(:test)[1:1000], 0:9) |todevicem=Chain( Conv((2,2), 1=16, relu), x -maxpool(x, (2,2)), Conv((2 ,2), 16=8, relu), x -maxpool(x, (2,2)), x -reshape(x, size(x, 4)), 密集(288, 10), softmax) | todevicem(train[1][1])loss(x, y)=交叉熵(m(x), y)准确度(x, y)=均值(onecold(m(x)) .==onecold(y)) evalcb=throttle(() -@show(accuracy(tX, tY)), 10)opt=ADAM(Flux.params(m));# trainfor i=1:10 Flux.train!(loss, train, opt, cb=evalcb)endusing Colors, FileIO, ImageShowN=22img=tX[: 1:1, N:N]println('Predicted: ', Flux.onecold(m(img )) .- 1)Gray.(collect(tX[:) , 1, N])) 只需将数组转换为GPUArrays(使用gpu(array)),就可以将整个计算移至GPU 并获得可观的速度提升。这要归功于Julia 复杂的AbstractArray 基础设施,它允许GPUArray 的无缝集成。稍后,如果省略转换为GPUArray 步骤,代码会将其视为普通的Julia 数组,但仍然在CPU 上运行。您可以尝试将use_gpu=true 更改为use_gpu=false 并重新运行初始化和训练单元。对比GPU和CPU,CPU运行时间为975秒,GPU运行时间为29秒,快了约33倍。
另一个优点是,为了有效支持神经网络的反向传播,GPUArray 不需要显式实现自动微分。这是因为Julia 的自动微分库适用于任意函数,并且包含在GPU 上高效运行的代码。这使得Flux能够以最少的开发人员数量在GPU上实现,并使Flux GPU能够高效地实现用户定义的功能。这种开箱即用的GPUArrays + Flux 不需要协调,这是Julia 的一大特色,详细解释如下:为什么Numba 和Cython 无法取代Julia(http://www.stochasticlifestyle.com/why)。
编写GPU 内核
一般来说,只需使用GPUArrays的通用抽象数组接口即可,无需编写任何GPU内核。但有时,可能需要在GPU上实现一种无法用通用数组算法组合来表示的算法。
好消息是GPUArray 通过分层方法消除了大量工作,该方法允许您从高级代码开始并编写类似于大多数OpenCL/CUDA 示例的低级内核。还可以在OpenCL 或CUDA 设备上执行内核,从而提取这些框架中的所有差异。
实现上述功能的函数称为gpu_call。调用语句为gpu_call(kernel, A:GPUArray, args),使用参数(state, args.)在GPU上调用kernel。 State是后端特定的对象,用于实现获取线程索引等功能。需要传递一个GPUArray 作为第二个参数来分配给正确的后端并为启动参数提供默认值。
让我们使用gpu_call 来实现一个简单的映射内核:
使用GPUArrays、CuArrays# 重载Julia 底图! function for GPUArraysfunction Base.map!(f:Function, A:GPUArray, B:GPUArray) # 将在GPU 上运行的函数kernel(state, f, A, B) # 如果未指定启动参数,则Linear_index 获取索引#作为第二个参数传递给gpu_call (`A`) i=Linear_index(state) if i=length(A) @inbounds A[i]=f(B[i]) end return end # 在GPU 上调用内核gpu_call(kernel, A, (f, A, B))end 简而言之,这将在GPU 上并行调用julia 函数kernel length(A) 次。对内核的每个并行调用都有一个线程索引,可用于索引数组A 和B。如果您在不使用Linear_index 的情况下计算索引,则需要确保没有多个线程正在读取和写入同一数组位置。因此,如果使用线程以纯Julia 编写,则相当于:
使用BenchmarkToolsfunction threadded_map!(f:Function, A:Array, B:Array) Threads.@threads for i in 1:length(A) A[i]=f(B[i]) end Aendx, y=rand(10^7), rand(10^ 7)kernel(y)=(y/33f0) * (732.f0/y)# 在没有线程的CPU 上:single_t=@belapsed map!($kernel, $x, $y)# '在有4 个线程的CPU 上( 2 个真实核心):thread_t=@belapsed threadded_map!($kernel, $x, $y)# 在GPU:xgpu, ygpu=cu(x), cu(y)gpu_t=@belapsed begin map!($kernel, $xgpu, $ygpu) GPUArrays.synchronize($xgpu)endtimes=[single_t, thread_t, gpu_t]speedup=最大(次) ./timesprintln('speedup: $speedup')bar(['1 核', '2 核', 'gpu '], speedup, legend=false, fillcolor=:grey, ylabel='speedup')由于这个函数实现的内容不多,所以没有得到更多的扩展,但是线程和GPU版本仍然有不错的加速比。
GPU 比线程示例更复杂,因为硬件线程分布在线程块中,并且gpu_call 是从简单版本中提取的,但它也可用于更复杂的启动配置:
使用CuArraysthreads=(2, 2)blocks=(2, 2)T=fill(CuArray, (0, 0), (4, 4))B=fill(CuArray, (0, 0), (4, 4) )gpu_call(T, (B, T), (blocks,threads)) do state, A, B # 这些名称几乎指的是cuda 名称b=(blockidx_x(state), blockidx_y(state)) bdim=(blockdim_x (状态), blockdim_y(状态)) t=(threadidx_x(状态), threadidx_y(状态)) idx=(bdim .* (b .- 1)) .+ t A[idx.]=b B[idx .]=t returnendprintln('Threads index: \n', T)println('Block index: \n', B) 上例中启动配置的迭代顺序比较复杂。确定适当的迭代+启动配置对于实现最佳GPU 性能至关重要。许多关于CUDA 和OpenCL 的GPU 教程对此进行了非常详细的解释,并且在Julia 中对GPU 进行编程时的原理是相同的。
综上所述
Julia 将可组合的高级编程带入高性能世界。现在是时候对GPU 做同样的事情了。
希望Julia 能够降低GPU 编程的入门门槛,我们可以为开源GPU 计算开发一个可扩展的平台。第一个成功故事是通过Julia 包实现自动微分解决方案,这些包甚至不是为GPU 编写的,因此相信Julia 将在GPU 计算的可扩展性和通用设计方面大放异彩。






























用户评论
endlich! Endlich etwas GPU-beschleunigte Programmierung in Julia. Ich hab schon ewig darauf gewartet, dass jemand das macht. Es wird mich freuen, meine Berechnungen mit dem GPU-Boost zu beschleunigen!
有11位网友表示赞同!
Julia auf der GPU? Das klingt nach ner genialen Idee! Schade, in meinem alten Laptop hat die Grafikkarte eh nicht wirklich viel Power... mal schauen, ob ich mir irgendwann einen richtigen Performance-Booster besorgen kann.
有14位网友表示赞同!
Super, das gefällt mir! Bin schon gespannt, wie sich den ganzen Stuff hier mit meiner GPU zusammenfinden lässt. Das Tutorium schaut gut aus, werde es mir mal genauer ansehen.
有18位网友表示赞同!
GPU-Beschleunigung in Julia? Klingt spannend, aber ich bin ehrlich gesagt etwas skeptisch. Hat doch irgendwie nicht so geklappt, als andere Sprachen wie C++ oder CUDA versucht haben?
有20位网友表示赞同!
Hoffe das Tutorial erklärt die ganzen komplizierten Details gut. Und ob man dafür auch spezielle Hardware braucht, würde mich interessieren.
有8位网友表示赞同!
Bisher habe ich Julia hauptsächlich für meine wissenschaftlichen Berechnungen verwendet, aber GPU-Beschleunigung wäre wirklich ein Gamechanger im Bereich der Datenanalyse! Ist es überhaupt einfach, die CUDA-Kerne in Julia zu implementieren?
有18位网友表示赞同!
Ein guter Schritt nach vorne für Julia! Immer mehr Tools und Frameworks werden verfügbar, das macht den Einstieg leichter und attraktiver. GPU Beschleunigung ist natürlich auch ein super Feature für Performance-kritische Anwendungen.
有17位网友表示赞同!
Da hätte ich ja was sagen, aber leider brauche ich dafür ein bisschen tieferes technisches Wissen. Vielleicht kann mir ja jemand aufklären, wie das genau funktioniert?
有13位网友表示赞同!
Das Tutorial sieht echt gut aus! Mal schauen ob die GPU in Julia genauso schnell ist wie in anderen Sprachen wie Fortran oder Python. Würde mich freuen.
有17位网友表示赞同!
GPU-Beschleunigung klingt super spannend. Gibt es vielleicht auch Tutorials für Anfänger, die nicht schon so viel Ahnung von dem technischen Kram haben?
有9位网友表示赞同!
Bin gespannt, welche Anwendungen mit GPU-Beschleunigung in Julia möglich sind! Vielleicht kann man ja sogar Spiele oder grafisch anspruchsvolle Programme damit erstellen.
有15位网友表示赞同!
Cool! GPU Acceleration in Julia klingt nach einer genialen Idee. Habe mir das Tutorium schon mal angesehen, und die Erklärungen sind wirklich gut verständlich.
有7位网友表示赞同!
GPU-Beschleunigung ist echt ein Game Changer für Anwendungen wie Deep Learning oder wissenschaftliche Simulationen. In Kombination mit Julia - das wären echt neue Möglichkeiten.
有16位网友表示赞同!
Ich bin ja kein absoluter Programmierer, aber GPU-Beschleunigung klingt nach etwas, das wirklich den Unterschied machen kann. Vielleicht schau ich mir ja das Tutorial mal genauer an.
有14位网友表示赞同!
Interessant! Mal sehen, ob sich Julia in Zeiten des AI Booms durch die GPU-Funktion besser positionieren kann. Bisher ist Ruby doch ein beliebter Kandidat für ML und KI.
有7位网友表示赞同!
Hat jemand schon Erfahrungen gemacht mit Julia und GPU-Beschleunigung? Würde mich freuen auf Feedback im Kommentarbereich.
有9位网友表示赞同!
Bin echt begeistert von dem Entwicklung in Richtung GPU-Beschleunigung! Vielleicht kann ich ja bald meine eigenen Projekte schneller ausführen.
有17位网友表示赞同!