人工智能培训

搜索

人工智能技术:教程 | 如何在Julia编程中实现GPU加速

[复制链接]
hacksee 发表于 2018-10-30 09:20:41 | 显示全部楼层 |阅读模式
hacksee 2018-10-30 09:20:41 59 0 显示全部楼层
原标题:教程 | 如何在Julia编程中实现GPU加速
            选自nextjournal
作者:Simon Danisch
参与:高璇、刘晓坤
  GPU 的并行线程可以大幅提升速度,但也使得代码编写变得更复杂。而 Julia 作为一种高级脚本语言,允许在其中编写内核和环境代码,并可在大多数 GPU 硬件上运行。本文旨在介绍 GPU 的工作原理,详细说明当前的 Julia GPU 环境,以及展示如何轻松运行简单 GPU 程序。
为了简化操作,可以在 nextjournal 上注册账户,点击「edit」即可直接运行文章中的简单代码了。
注册地址:http://nextjournal.com/signup
首先,什么是 GPU?
GPU 是一种大型并行处理器,有几千个并行处理单元。例如,本文使用的 Tesla k80,能提供 4992 个并行 CUDA 核。GPU 在频率、延迟和硬件性能方面与 CPU 有很大的不同,但实际上 Tesla k80 有点类似于具有 4992 核的慢速 CPU。
K7b7FLHlruIigUwf.jpg
能够启动的并行线程可以大幅提升速度,但也令使用 GPU 变得更困难。当使用这种未加处理的能量时,会出现以下缺点:


  • GPU 是一种有专属内存空间和不同架构的独立硬件。因此,从 RAM 到 GPU 内存(VRAM,显存)的传输时间很长。甚至在 GPU 上启动内核(调用调度函数)也会带来很大的延迟,对于 GPU 而言是 10us 左右,而对于 CPU 只有几纳秒。  
  • 在没有高级封装的情况下,建立内核会变得复杂。  
  • 低精度是默认值,高精度的计算可以很容易地消除所有性能增益。  
  • GPU 函数(内核)本质上是并行的,所以编写 GPU 内核不比编写并行 CPU 代码容易,而且硬件上的差异增加了一定的复杂性。  
  • 与上述情况相关的很多算法都不能很好地迁移到 GPU 上。想要了解更多的细节,请看这篇博文:http://streamhpc.com/blog/2013-06-03/the application-areas-opencl- cuda-can- used/。  
  • 内核通常是用 C/ C++语言编写的,但这并不是写算法的最好语言。  
  • CUDA 和 OpenCL 之间有差异,OpenCL 是编写底层 GPU 代码的主要框架。虽然 CUDA 只支持英伟达硬件,OpenCL 支持所有硬件,但并不精细。要看个人需求进行选择。
而 Julia 作为一种高级脚本语言,允许在其中编写内核和环境代码,同时可在大多数 GPU 硬件上运行!
GPUArrays
大多数高度并行的算法都需要同时处理大量数据,以克服所有的多线程和延迟损耗。因此,大多数算法都需要数组来管理所有数据,这就需要一个好的 GPU 数组库作为关键的基础。
GPUArrays.jl 是 Julia 为此提供的基础。它实现了一个专门用于高度并行硬件的抽象数组。它包含了设置 GPU、启动 Julia GPU 函数、提供一些基本数组算法等所有必要功能。
抽象意味着它需要以 CuArrays 和 CLArrays 的形式实现。由于继承了 GPUArrays 的所有功能,它们提供的接口完全相同。唯一的区别出现在分配数组时,这会强制用户决定这一数组是存在于 CUDA 还是 OpenCL 设备上。关于这一点的更多信息,请参阅「内存」部分。
GPUArrays 有助于减少代码重复,因为它允许编写独立于硬件的 GPU 内核,这些内核可以通过 CuArrays 或 CLArrays 编译到本地的 GPU 代码。因此,大多通用内核可以在从 GPUArrays 继承的所有包之间共享。
选择小贴士:CuArrays 只支持 Nvidia GPU,而 CLArrays 支持大多数可用的 GPU。CuArrays 比 CLArrays 更稳定,可以在 Julia 0.7 上使用。速度上两者大同小异。我建议都试一试,看看哪种最有效。
本文中,我将选择 CuArrays,因为本文是在 Julia 0.7 / 1.0 上编写的,CLArrays 暂不支持。
性能
用一个简单的交互式代码示例来快速说明:为了计算 julia 集合(曼德勃罗集合),我们必须要将计算转移到 GPU 上。
using CuArrays, FileIO, Colors, GPUArrays, BenchmarkTools
using CuArrays:CuArray
"""
The function calculating the Julia set
"""
function juliaset(z 0, maxiter)
c = ComplexF32(- 0. 5, 0. 75)
z = z 0
fori in1:maxiter
abs2(z) > 4f 0&& return(i - 1) % UInt8
z = z * z + c
end
returnmaxiter % UInt8 # % is used to convert without overflow check
end
range = 100:50:2^ 12
cutimes, jltimes = Float64[], Float64[]
function run_bench( in, out)
# use dot syntax to apply `juliaset` to each elemt of q_converted
# and write the output to result
out .= juliaset.( in, 16)
# all calls to the GPU are scheduled asynchronous,
# so we need to synchronize
GPUArrays.synchronize(out)
end
# store a reference to the last results for plotting
last_jl, last_cu = nothing, nothing
forN inrange
w, h = N, N
q = [ComplexF32(r, i) fori= 1:-( 2.0/w) :-1, r=- 1.5:( 3.0/h) :1.5]
for(times, Typ) in((cutimes, CuArray), (jltimes, Array))
# convert to Array or CuArray - moving the calculation to CPU/GPU
q_converted = Typ(q)
result = Typ(zeros(UInt8, size(q)))
fori in1:10# 5 samples per size
# benchmarking macro, all variables need to be prefixed with $
t = Base.@elapsed begin
run_bench(q_converted, result)
end
global last_jl, last_cu # we're in local scope
ifresult isa CuArray
last_cu = result
else
last_jl = result
end
push!(times, t)
end
end
end
cu_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,)))
BxOMk5G2o5mnTMHt.jpg
using Plots; plotly()
x = repeat(range, inner = 10)
speedup = jltimes ./ cutimes
Plots.scatter(
log2.(x), [speedup, fill(1.0, length(speedup))],
label = [ "cuda""cpu"], markersize = 2, markerstrokewidth = 0,
legend = :right, xlabel = "2^N", ylabel = "speedup"
)
tV65GUV586O64205.jpg
对于大型数组,通过将计算转移到 GPU,可以稳定地将速度提高 60-80 倍。获得此加速和将 Julia 数组转换为 GPUArray 一样简单。
有人可能认为 GPU 性能会受到像 Julia 这样的动态语言影响,但 Julia 的 GPU 性能应该与 CUDA 或 OpenCL 的原始性能相当。Tim Besard 在集成 LLVM Nvidia 编译流程方面做得很好,能够实现与纯 CUDA C 语言代码相同(有时甚至更好)的性能。他在博客(http://devblogs.nvidia.com/gpu-computing-julia-programming-language/)中作了进一步解释。CLArrays 方法有点不同,它直接从 Julia 生成 OpenCL C 代码,代码性能与 OpenCL C 相同!
为了更好地了解性能并与多线程 CPU 代码进行比对,我整理了一些基准:http://github.com/JuliaGPU/GPUBenchmarks.jl/blob/master/results/results.md
内存
GPU 具有自己的存储空间,包括显存(VRAM)、不同的高速缓存和寄存器。无论做什么,运行前都要先将 Julia 对象转移到 GPU。并非 Julia 中的所有类型都可以在 GPU 上运行。
首先让我们看一下 Julia 的类型:
struct Test # an immutable struct
# that only contains other immutable, which makes
# isbitstype(Test) == true
x::Float32
end
# the isbits property is important, since those types can be used
# without constraints on the GPU!
@assert isbitstype( Test) == true
x = ( 2, 2)
isa(x, Tuple{ Int, Int}) # tuples are also immutable
mutable structTest2 #-> mutable, isbits(Test2) == false
x::Float32
end
structTest3
# contains a heap allocation/ reference, not isbits
x::Vector{Float32}
y::Test2 # Test2 is mutable and also heap allocated / a reference
end

Vector{ Test} #  x^2, xs, dims = 1)  缩减为标量 (reduce(*, xs), sum(xs), prod(xs))
  各种 BLAS 操作 (matrix*matrix, matrix*vector)
  FFT,使用与 julia 的 FFT 相同的 API
</ul>GPUArrays 实际应用
让我们直接看一些很酷的实例。
GPU 加速烟雾模拟器是由 GPUArrays + CLArrays 创建的,可在 GPU 或 CPU 上运行,GPU 版本速度提升 15 倍:

还有更多的例子,包括求微分方程、FEM 模拟和求解偏微分方程。
演示地址:http://juliagpu.github.io/GPUShowcases.jl/latest/index.html
让我们通过一个简单的机器学习示例,看看如何使用 GPUArrays:
using Flux, Flux.Data.MNIST, Statistics
using Flux: onehotbatch, onecold, crossentropy, throttle
using Base.Iterators: repeated, partition
using CuArrays
# Classify MNIST digits with a convolutional network
imgs = MNIST.images()
labels = onehotbatch(MNIST.labels(), 0:9)
# Partition into batches of size 1,000
train = [(cat(float.(imgs)..., dims = 4), labels[:,i])
for i in partition(1:60_000, 1000)]
use_gpu = true # helper to easily switch between gpu/cpu
todevice(x) = use_gpu ? gpu(x) : x
train = todevice.(train)
# Prepare test set (first 1,000 images)
tX = cat(float.(MNIST.images(:test)[1:1000])..., dims = 4) |> todevice
tY = onehotbatch(MNIST.labels(:test)[1:1000], 0:9) |> todevice
m = 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)),
Dense(288, 10), softmax) |> todevice
m(train[1][1])
loss(x, y) = crossentropy(m(x), y)
accuracy(x, y) = mean(onecold(m(x)) .== onecold(y))
evalcb = throttle(() -> @show(accuracy(tX, tY)), 10)
opt = ADAM(Flux.params(m));
# train
fori = 1:10
Flux.train!(loss, train, opt, cb = evalcb)
end
HqwYlqk4nT24UUfy.jpg
using Colors, FileIO, ImageShow
N = 22
img = tX[:, :, 1:1, N:N]
println("Predicted: ", Flux.onecold(m(img)) .- 1)
Gray.(collect(tX[:, :, 1, N]))
PAeaOWJcSXA6n5wG.jpg
只需将数组转换为 GPUArrays(使用 gpu(array),就可以将整个计算移动到 GPU 并获得可观的速度提升。这要归功于 Julia 复杂的 AbstractArray 基础架构,使 GPUArray 可以无缝集成。随后,如果省略转换为 GPUArray 这一步,代码会按普通的 Julia 数组处理,但仍在 CPU 上运行。可以尝试将 use_gpu = true 改为 use_gpu = false,重新运行初始化和训练单元格。对比 GPU 和 CPU,CPU 运行时间为 975 秒,GPU 运行时间为 29 秒,速度提升约 33 倍。
另一个优势是为了有效地支持神经网络的反向传播,GPUArrays 无需明确地实现自动微分。这是因为 Julia 的自动微分库适用于任意函数,并存有可在 GPU 上高效运行的代码。这样即可利用最少的开发人员就能在 GPU 上实现 Flux,并使 Flux GPU 能够高效实现用户定义的功能。这种开箱即用的 GPUArrays + Flux 不需要协调,这是 Julia 的一大特点,详细解释如下:为什么 Numba 和 Cython 不能代替 Julia(http://www.stochasticlifestyle.com/why)。
编写 GPU 内核
一般情况,只使用 GPUArrays 的通用抽象数组接口即可,而不需要编写任何 GPU 内核。但是有些时候,可能需要在 GPU 上实现一个无法通过一般数组算法组合表示的算法。
好消息是,GPUArrays 通过分层法消除了大量工作,可以实现从高级代码开始,编写类似于大多数 OpenCL / CUDA 示例的低级内核。同时可以在 OpenCL 或 CUDA 设备上执行内核,从而提取出这些框架中的所有差异。
实现上述功能的函数名为 gpu_call。调用语句为 gpu_call(kernel, A::GPUArray, args),在 GPU 上使用参数 (state, args...) 调用 kernel。State 是一个用于实现获取线程索引等功能的后端特定对象。GPUArray 需要作为第二个参数传递,以分配到正确的后端并提供启动参数的默认值。
让我们使用 gpu_call 来实现一个简单的映射内核:
using GPUArrays, CuArrays
# Overloading the Julia Base map! function for GPUArrays
function Base.map!(f::Function, A::GPUArray, B::GPUArray)
# our function that will run on the gpu
function kernel(state, f, A, B)
# If launch parameters aren't specified, linear_index gets the index
# into the Array passed as second argument to gpu_call (`A`)
i = linear_index(state)

ifi
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则 返回列表 发新帖

hacksee当前离线
新手上路

查看:59 | 回复:0

快速回复 返回顶部 返回列表