0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

快速介绍GPU的工作原理,详细介绍当前的Julia GPU生态系统

DPVg_AI_era 来源:未知 作者:李倩 2018-10-23 09:23 次阅读

本文旨在快速介绍GPU工作原理,详细介绍当前的Julia GPU生态系统,并让读者了解简单的GPU编程是多么的容易。


GPU是如何工作的?

首先,什么是GPU?

GPU是一个大规模并行处理器,具有几千个并行处理单元。 例如,本文中使用的Tesla k80提供4992个并行CUDA内核。 GPU在频率,延迟和硬件功能方面与CPU完全不同,但有点类似于拥有4992个内核的慢速CPU!

“Tesla K80”

可启用并行线程的数量可以大幅提高GPU速度,但也让它的使用性变得更加困难。让我们来详细看看在使用这种原始动力时,你会遇到哪些缺点:

GPU是一个独立的硬件,具有自己的内存空间和不同的架构。 因此,从RAM到GPU存储器(VRAM)的传输时间很长。 即使在GPU上启动内核(换句话说,调度函数调用)也会带来较大的延迟。 GPU的时间约为10us,而CPU的时间则为几纳秒。

在没有高级包装器的情况下,设置内核会很快变得复杂

较低的精度是默认值,而较高的精度计算可以轻松地消除所有性能增益

GPU函数(内核)本质上是并行的,所以编写GPU内核至少和编写并行CPU代码一样困难,但是硬件上的差异增加了相当多的复杂性

与上述相关,许多算法都不能很好地移植到GPU上。

内核通常是用C/ C++编写的,这并不是写算法的最佳语言。

CUDA和OpenCL之间存在分歧,OpenCL是用于编写低级GPU代码的主要框架。虽然CUDA只支持英伟达硬件,但OpenCL支持所有硬件,但有些粗糙。

Julia的诞生是个好消息!它是一种高级脚本语言,允许你在Julia本身编写内核和周围的代码,同时在大多数GPU硬件上运行!

GPUArrays

大多数高度并行的算法需要通过相当多的数据来克服所有线程和延迟开销。因此,大多数算法都需要数组来管理所有数据,这需要一个好的GPU数组库(array library)作为关键基础。

GPUArrays.jl是Julia的基础。它提供了一个抽象数组实现,专门用于使用高度并行硬件的原始功能。它包含设置GPU所需的所有功能,启动Julia GPU函数并提供一些基本的数组算法。

抽象意味着它需要以CuArrays和CLArrays形式的具体实现。由于继承了GPUArrays的所有功能,它们都提供完全相同的接口。唯一的区别出现在分配数组时,这会强制你决定数组是否位于CUDA或OpenCL设备上。关于这一点的更多信息,请参阅内存部分。

GPUArrays有助于减少代码重复,因为它允许编写独立于硬件的GPU内核,可以通过CuArrays或CLArrays将其编译为本机GPU代码。因此,许多通用内核可以在继承自GPUArrays的所有packages之间共享。

一点选择建议:CuArrays仅适用于Nvidia GPU,而CLArrays适用于大多数可用的GPU。CuArrays比CLArrays更稳定,并且已经可以在Julia 0.7上运行。速度上差异不明显。我建议两者都试一下,看看哪个效果最好。

对于本文,我将选择CuArrays,因为本文是为Julia 0.7 / 1.0而写的,CLArrays仍然不支持。

性能

让我们用一个简单的交互式代码示例来快速说明为什么要将计算转移到GPU上,这个示例计算julia set:

1usingCuArrays,FileIO,Colors,GPUArrays,BenchmarkTools 2usingCuArrays:CuArray 3""" 4ThefunctioncalculatingtheJuliaset 5""" 6functionjuliaset(z0,maxiter) 7c=ComplexF32(-0.5,0.75) 8z=z0 9foriin1:maxiter10abs2(z)>4f0&&return(i-1)%UInt811z=z*z+c12end13returnmaxiter%UInt8#%isusedtoconvertwithoutoverflowcheck14end15range=100:50:2^1216cutimes,jltimes=Float64[],Float64[]17functionrun_bench(in,out)18#usedotsyntaxtoapply`juliaset`toeachelemtofq_converted19#andwritetheoutputtoresult20out.=juliaset.(in,16)21#allcallstotheGPUarescheduledasynchronous,22#soweneedtosynchronize23GPUArrays.synchronize(out)24end25#storeareferencetothelastresultsforplotting26last_jl,last_cu=nothing,nothing27forNinrange28w,h=N,N29q=[ComplexF32(r,i)fori=1:-(2.0/w):-1,r=-1.5:(3.0/h):1.5]30for(times,Typ)in((cutimes,CuArray),(jltimes,Array))31#converttoArrayorCuArray-movingthecalculationtoCPU/GPU32q_converted=Typ(q)33result=Typ(zeros(UInt8,size(q)))34foriin1:10#5samplespersize35#benchmarkingmacro,allvariablesneedtobeprefixedwith$36t=Base.@elapsedbegin37run_bench(q_converted,result)38end39globallast_jl,last_cu#we'reinlocalscope40ifresultisaCuArray41last_cu=result42else43last_jl=result44end45push!(times,t)46end47end48end4950cu_jl=hcat(Array(last_cu),last_jl)51cmap=colormap("Blues",16+1)52color_lookup(val,cmap)=cmap[val+1]53save("results/juliaset.png",color_lookup.(cu_jl,(cmap,)))

1usingPlots;plotly()2x=repeat(range,inner=10)3speedup=jltimes./cutimes4Plots.scatter(5log2.(x),[speedup,fill(1.0,length(speedup))],6label=["cuda""cpu"],markersize=2,markerstrokewidth=0,7legend=:right,xlabel="2^N",ylabel="speedup"8)

如你所见,对于大型数组,通过将计算移动到GPU可以获得稳定的60-80倍的加速。而且非常简单,只需将Julia array转换为GPUArray。

有人可能认为GPU的性能受到像Julia这样的动态语言的影响,但Julia的GPU性能应该与CUDA或OpenCL的原始性能相当。Tim Besard在集成LLVM Nvidia编译pipeline方面做得非常出色,达到了与纯CUDA C代码相同(有时甚至更好)的性能。Tim发表了一篇非常详细的博文,里面进一步解释了这一点[1]。CLArrays方法有点不同,它直接从Julia生成OpenCL C代码,具有与OpenCL C相同的性能!

为了更好地了解性能并查看与多线程CPU代码的比较,我收集了一些基准测试[2]。

内存(Memory)

GPU具有自己的存储空间,包括视频存储器(VRAM),不同的高速缓存和寄存器。无论你做什么,任何Julia对象都必须先转移到GPU才能使用。并非Julia中的所有类型都可以在GPU上工作。

首先让我们看一下Julia的类型:

1structTest#animmutablestruct 2#thatonlycontainsotherimmutable,whichmakes 3#isbitstype(Test)==true 4x::Float32 5end 6 7#theisbitspropertyisimportant,sincethosetypescanbeused 8#withoutconstraintsontheGPU! 9@assertisbitstype(Test)==true10x=(2,2)11isa(x,Tuple{Int,Int})#tuplesarealsoimmutable12mutablestructTest2#->mutable,isbits(Test2)==false13x::Float3214end15structTest316#containsaheapallocation/reference,notisbits17x::Vector{Float32}18y::Test2#Test2ismutableandalsoheapallocated/areference19end20Vector{Test}#<-  An Array with isbits elements is contigious in memory21Vector{Test2} # <- An Array with mutable elements is basically an array of heap pointers. Since it just contains cpu heap pointers, it won't work on the GPU.

"Array{Test2,1}"

所有这些Julia类型在转移到GPU或在GPU上创建时表现都不同。下表概述了预期结果:

创建位置描述了对象是否在CPU上创建然后传输到GPU内核,或者是否在内核的GPU上创建。这个表显示了是否可以创建类型的实例,并且对于从CPU到GPU的传输,该表还指示对象是否通过引用复制或传递。

Garbage Collection

使用GPU时的一个很大的区别是GPU上没有垃圾回收( garbage collector, GC)。这不是什么大问题,因为为GPU编写的高性能内核不应该一开始就创建任何GC-tracked memory。

为GPU实现GC是可能的,但请记住,每个执行的内核都是大规模并行的。在~1000 GPU线程中的每一个线程创建和跟踪大量堆内存将很快破坏性能增益,因此这实际上是不值得的。

作为内核中堆分配数组的替代方法,你可以使用GPUArrays。GPUArray构造函数将创建GPU缓冲区并将数据传输到VRAM。如果调用Array(gpu_array),数组将被转移回RAM,表示为普通的Julia数组。这些GPU数组的Julia句柄由Julia的GC跟踪,如果它不再使用,GPU内存将被释放。

因此,只能在设备上使用堆栈分配,并且对其余的预先分配的GPU缓冲区使用。由于传输非常昂贵的,因此在编程GPU时尽可能多地重用和预分配是很常见的。

The GPUArray Constructors

1usingCuArrays,LinearAlgebra 2 3#GPUArrayscanbeconstructedfromallJuliaarrayscontainingisbitstypes! 4A1D=cu([1,2,3])#clforCLArrays 5A1D=fill(CuArray{Int},0,(100,))#CLArrayforCLArrays 6#Float32array-Float32isusuallypreferredandcanbeupto30xfasteronmostGPUsthanFloat64 7diagonal_matrix=CuArray{Float32}(I,100,100) 8filled=fill(CuArray,77f0,(4,4,4))#3DarrayfilledwithFloat3277 9randy=rand(CuArray,Float32,42,42)#randomnumbersgeneratedontheGPU10#Thearrayconstructoralsoacceptsisbitsiteratorswithaknownsize11#Note,thatsinceyoucanalsopassisbitstypestoagpukerneldirectly,inmostcasesyouwon'tneedtomaterializethemasangpuarray12from_iter=CuArray(1:10)13#let'screateapointtypetofurtherillustratewhatcanbedone:14structPoint15x::Float3216y::Float3217end18Base.convert(::Type{Point},x::NTuple{2,Any})=Point(x[1],x[2])19#becausewedefinedtheaboveconvertfromatupletoapoint20#[Point(2,2)]canbewrittenasPoint[(2,2)]sinceallarray21#elementswillgetconvertedtoPoint22custom_types=cu(Point[(1,2),(4,3),(2,2)])23typeof(custom_types)

"CuArray{Point, 1}"

Array Operations

许多操作是已经定义好的。最重要的是,GPUArrays支持Julia的fusing dot broadcasting notation。这种标记法允许你将函数应用于数组的每个元素,并使用f的返回值创建一个新数组。这个功能通常称为映射(map)。 broadcast 指的是具有不同形状的数组被散布到相同的形状。

它的工作方式如下:

1x=zeros(4,4)#4x4arrayofzeros2y=zeros(4)#4elementarray3z=2#ascalar4#y's1stdimensiongetsrepeatedforthe2nddimensioninx5#andthescalarzget'srepeatedforalldimensions6#thebelowisequalto`broadcast(+,broadcast(+,xx,y),z)`7x.+y.+z

关于broadcasting如何工作的更多解释,可以看看这个指南:

julia.guide/broadcasting

这意味着在不分配堆内存(仅创建isbits类型)的情况下运行的任何Julia函数都可以应用于GPUArray的每个元素,并且多个dot调用将融合到一个内核调用中。由于内核调用延迟很高,这种融合是一个非常重要的优化。

1usingCuArrays 2A=cu([1,2,3]) 3B=cu([1,2,3]) 4C=rand(CuArray,Float32,3) 5result=A.+B.-C 6test(a::T)whereT=a*convert(T,2)#converttosametypeas`a` 7 8#inplacebroadcast,writesdirectlyinto`result` 9result.=test.(A)#customfunctionwork1011#Thecoolthingisthatthiscomposeswellwithcustomtypesandcustomfunctions.12#Let'sgobacktoourPointtypeanddefineadditionforit13Base.:(+)(p1::Point,p2::Point)=Point(p1.x+p2.x,p1.y+p2.y)1415#nowthisworks:16custom_types=cu(Point[(1,2),(4,3),(2,2)])1718#Thisparticularexamplealsoshowsthepowerofbroadcasting:19#Nonarraytypesarebroadcastedandrepeatedforthewholelength20result=custom_types.+Ref(Point(2,2))2122#Sotheaboveisequalto(minusalltheallocations):23#thisallocatesanewarrayonthegpu,whichwecanavoidwiththeabovebroadcast24broadcasted=fill(CuArray,Point(2,2),(3,))2526result==custom_types.+broadcasted

ture

现实世界中的GPUArrays

让我们直接看看一些很酷的用例。

如下面的视频所示,这个GPU加速烟雾模拟是使用GPUArrays + CLArrays创建的,可在GPU或CPU上运行,GPU版本的速度提高了15倍:

还有更多的用例,包括求解微分方程,有限元模拟和求解偏微分方程。

让我们来看一个简单的机器学习示例,看看如何使用GPUArrays:

1usingFlux,Flux.Data.MNIST,Statistics 2usingFlux:onehotbatch,onecold,crossentropy,throttle 3usingBase.Iterators:repeated,partition 4usingCuArrays 5 6#ClassifyMNISTdigitswithaconvolutionalnetwork 7 8imgs=MNIST.images() 910labels=onehotbatch(MNIST.labels(),0:9)1112#Partitionintobatchesofsize1,00013train=[(cat(float.(imgs[i])...,dims=4),labels[:,i])14foriinpartition(1:60_000,1000)]1516use_gpu=true#helpertoeasilyswitchbetweengpu/cpu1718todevice(x)=use_gpu?gpu(x):x1920train=todevice.(train)2122#Preparetestset(first1,000images)23tX=cat(float.(MNIST.images(:test)[1:1000])...,dims=4)|>todevice24tY=onehotbatch(MNIST.labels(:test)[1:1000],0:9)|>todevice2526m=Chain(27Conv((2,2),1=>16,relu),28x->maxpool(x,(2,2)),29Conv((2,2),16=>8,relu),30x->maxpool(x,(2,2)),31x->reshape(x,:,size(x,4)),32Dense(288,10),softmax)|>todevice3334m(train[1][1])3536loss(x,y)=crossentropy(m(x),y)3738accuracy(x,y)=mean(onecold(m(x)).==onecold(y))3940evalcb=throttle(()->@show(accuracy(tX,tY)),10)41opt=ADAM(Flux.params(m));

1#train2fori=1:103Flux.train!(loss,train,opt,cb=evalcb)4end

accuracy(tX, tY) = 0.101

accuracy(tX, tY) = 0.888

accuracy(tX, tY) = 0.919

1usingColors,FileIO,ImageShow2N=223img=tX[:,:,1:1,N:N]4println("Predicted:",Flux.onecold(m(img)).-1)5Gray.(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倍!

另一个值得关注的好处是,GPUArrays不需显式地实现自动微分以有效地支持神经网络的反向传播。这是因为Julia的自动微分库适用于任意函数,并发出可在GPU上高效运行的代码。这有助于帮助Flux以最少的开发人员在GPU上工作,并使Flux GPU能够有效地支持用户定义的函数。在没有GPUArrays + Flux之间协调的情况下开箱即用是Julia的一个非常独特的特性,详细解释见[3].

编写GPU内核

只需使用GPUArrays的通用抽象数组接口,而不用编写任何GPU内核,就可以做很多事了。但是,在某些时候,可能需要实现一个需要在GPU上运行的算法,并且不能用通用数组算法的组合来表示。

好的一点是,GPUArrays通过一种分层方法减少了大量的工作,这种方法允许你从高级代码开始编写低级内核,类似于大多数OpenCL / CUDA示例里的。它还允许你在OpenCL或CUDA设备上执行内核,从而抽象出这些框架中的任何差异。

使这成为可能的函数名为gpu_call。它可以被称为 gpu_call(kernel, A::GPUArray, args),并将在GPU上使用参数 (state, args...) 调用内核。State是一个后端特定对象,用于实现获取线程索引之类的功能。GPUArray需要作为第二个参数传递,一遍分派到正确的后端并提供启动参数的缺省值。

让我们使用gpu_call来实现一个简单的map kernel:

1usingGPUArrays,CuArrays 2#OverloadingtheJuliaBasemap!functionforGPUArrays 3functionBase.map!(f::Function,A::GPUArray,B::GPUArray) 4#ourfunctionthatwillrunonthegpu 5functionkernel(state,f,A,B) 6#Iflaunchparametersaren'tspecified,linear_indexgetstheindex 7#intotheArraypassedassecondargumenttogpu_call(`A`) 8i=linear_index(state) 9ifi<= length(A)10          @inbounds A[i] = f(B[i])11        end12        return13    end14    # call kernel on the gpu15    gpu_call(kernel, A, (f, A, B))16end

简单来说,上面的代码将在GPU上并行调用julia函数内核length(A) 次。内核的每个并行调用都有一个线程索引,我们可以使用它来安全地索引到数组A和B。如果我们计算自己的索引,而不是使用linear_index,我们需要确保没有多个线程读写同一个数组位置。因此,如果我们使用线程在纯Julia中编写,其对应版本如下:

1usingBenchmarkTools 2functionthreadded_map!(f::Function,A::Array,B::Array) 3Threads.@threadsforiin1:length(A) 4A[i]=f(B[i]) 5end 6A 7end 8x,y=rand(10^7),rand(10^7) 9kernel(y)=(y/33f0)*(732.f0/y)10#onthecpuwithoutthreads:11single_t=@belapsedmap!($kernel,$x,$y)1213#"ontheCPUwith4threads(2realcores):14thread_t=@belapsedthreadded_map!($kernel,$x,$y)1516#ontheGPU:17xgpu,ygpu=cu(x),cu(y)18gpu_t=@belapsedbegin19map!($kernel,$xgpu,$ygpu)20GPUArrays.synchronize($xgpu)21end22times=[single_t,thread_t,gpu_t]23speedup=maximum(times)./times24println("speedup:$speedup")25bar(["1core","2cores","gpu"],speedup,legend=false,fillcolor=:grey,ylabel="speedup")

因为这个函数没有做很多工作,我们看不到完美的扩展,但线程和GPU版本仍然提供了很大的加速。

GPU比线程示例展示的要复杂得多,因为硬件线程是在线程块中布局的——gpu_call在简单版本中抽象出来,但它也可以用于更复杂的启动配置:

1usingCuArrays 2 3threads=(2,2) 4blocks=(2,2) 5T=fill(CuArray,(0,0),(4,4)) 6B=fill(CuArray,(0,0),(4,4)) 7gpu_call(T,(B,T),(blocks,threads))dostate,A,B 8#thosenamesprettymuchrefertothecudanames 9b=(blockidx_x(state),blockidx_y(state))10bdim=(blockdim_x(state),blockdim_y(state))11t=(threadidx_x(state),threadidx_y(state))12idx=(bdim.*(b.-1)).+t13A[idx...]=b14B[idx...]=t15return16end17println("Threadsindex: ",T)18println("Blockindex: ",B)

在上面的示例中,你可以看到更复杂的启动配置的迭代顺序。确定正确的迭代+启动配置对于达到GPU的最佳性能至关重要。

结论

在将可组合的高级编程引入高性能世界方面,Julia取得了长足的进步。现在是时候对GPU做同样的事情了。

希望Julia降低开始在GPU上编程的标准,并且我们可以为开源GPU计算发展可扩展的平台。第一个成功案例是通过Julia packages实现自动微分,这些软件包甚至不是为GPU编写,因此这给了我们很多理由相信Julia在GPU计算领域的可扩展和通用设计是成功的。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • gpu
    gpu
    +关注

    关注

    27

    文章

    4417

    浏览量

    126689
  • 生态系统
    +关注

    关注

    0

    文章

    681

    浏览量

    20525

原文标题:手把手教你如何用Julia做GPU编程(附代码)

文章出处:【微信号:AI_era,微信公众号:新智元】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    基于STM32嵌入式系统设计_[3.2.1]--STM32Cube生态系统介绍#硬声创作季

    ST生态系统
    Hello,World!
    发布于 :2022年10月26日 20:22:34

    嵌入式技术:STM32Cube生态系统介绍#嵌入式系统

    嵌入式生态系统
    学习电子
    发布于 :2022年11月12日 23:59:12

    [3.2.1]--STM32Cube生态系统介绍

    嵌入式微机原理生态系统
    jf_90840116
    发布于 :2022年11月15日 00:35:03

    大数据hadoop生态系统概念简单介绍

    零基础大数据入门3--大数据生态系统及其各组件简介
    发表于 05-05 14:34

    工厂生产系统能效的生态系统优化设计

    优化工厂生产系统能效的生态系统
    发表于 07-12 08:44

    everspin生态系统和制造工艺创新解析

    everspin生态系统和制造工艺创新
    发表于 01-01 07:55

    gpu工作原理是什么?

    gpu工作原理是什么?
    发表于 05-31 06:17

    STM32单片机基础01——初识 STM32Cube 生态系统 精选资料分享

    本篇文章主要介绍STM32Cube生态系统。STM32Cube EcosystemSTM32Cube是ST公司开发的一套生态系统,致力于使STM32的开发变的更简单,并且100%开源免费。在开始
    发表于 08-03 07:15

    STM32Cube生态系统更新

    前不久STM32Cube生态系统进行了一次“大更新”,STM32CubeMX升级至V6.0.0,STM32CubeIDE升级至V1.4.0,STM32CubeProg升级至V2.5.0。...
    发表于 08-03 06:05

    什么是STM32Cube生态系统

    什么是STM32Cube生态系统
    发表于 09-29 06:12

    IT的生态系统概述

    一、IT的生态系统概述1.1 计算机CPU四大体系结构1、以PC机为代表的X86结构2、以嵌入式计算机为代表的ARM结构3、以国产龙芯为代表的MIPS结构4、以IBM巨型机为代表的POWPC结构
    发表于 12-22 06:47

    GaN功率半导体与高频生态系统

    GaN功率半导体与高频生态系统(氮化镓)
    发表于 06-25 09:38

    STM32Cube生态系统助力开发者释放创造力

    STM32Cube生态系统,助力开发者释放创造力,本片文档主要介绍选型、原型评估、配置、编译、软件包、调试、烧录、监测和总结。
    发表于 09-05 06:10

    GPU服务器的详细介绍工作原理说明

    众所周知,GPU是图形处理器,在很多任务中,GPU占据着重要作用,如深度学习领域。为增进大家对GPU的了解,本文将对GPU服务器加以介绍,并
    的头像 发表于 11-28 10:01 6643次阅读

    GPU和FPGA的工作原理及其区别

      GPU和FPGA都是现代计算机技术中的高性能计算设备,具有不同的特点和应用场景。本文将详细介绍GPU和FPGA的工作原理及其区别。
    的头像 发表于 08-06 16:50 1540次阅读