本文旨在快速介紹GPU的工作原理,詳細(xì)介紹當(dāng)前的Julia GPU生態(tài)系統(tǒng),并讓讀者了解簡(jiǎn)單的GPU編程是多么的容易。
GPU是如何工作的?
首先,什么是GPU?
GPU是一個(gè)大規(guī)模并行處理器,具有幾千個(gè)并行處理單元。 例如,本文中使用的Tesla k80提供4992個(gè)并行CUDA內(nèi)核。 GPU在頻率,延遲和硬件功能方面與CPU完全不同,但有點(diǎn)類似于擁有4992個(gè)內(nèi)核的慢速CPU!
“Tesla K80”
可啟用并行線程的數(shù)量可以大幅提高GPU速度,但也讓它的使用性變得更加困難。讓我們來(lái)詳細(xì)看看在使用這種原始動(dòng)力時(shí),你會(huì)遇到哪些缺點(diǎn):
GPU是一個(gè)獨(dú)立的硬件,具有自己的內(nèi)存空間和不同的架構(gòu)。 因此,從RAM到GPU存儲(chǔ)器(VRAM)的傳輸時(shí)間很長(zhǎng)。 即使在GPU上啟動(dòng)內(nèi)核(換句話說(shuō),調(diào)度函數(shù)調(diào)用)也會(huì)帶來(lái)較大的延遲。 GPU的時(shí)間約為10us,而CPU的時(shí)間則為幾納秒。
在沒(méi)有高級(jí)包裝器的情況下,設(shè)置內(nèi)核會(huì)很快變得復(fù)雜
較低的精度是默認(rèn)值,而較高的精度計(jì)算可以輕松地消除所有性能增益
GPU函數(shù)(內(nèi)核)本質(zhì)上是并行的,所以編寫GPU內(nèi)核至少和編寫并行CPU代碼一樣困難,但是硬件上的差異增加了相當(dāng)多的復(fù)雜性
與上述相關(guān),許多算法都不能很好地移植到GPU上。
內(nèi)核通常是用C/ C++編寫的,這并不是寫算法的最佳語(yǔ)言。
CUDA和OpenCL之間存在分歧,OpenCL是用于編寫低級(jí)GPU代碼的主要框架。雖然CUDA只支持英偉達(dá)硬件,但OpenCL支持所有硬件,但有些粗糙。
Julia的誕生是個(gè)好消息!它是一種高級(jí)腳本語(yǔ)言,允許你在Julia本身編寫內(nèi)核和周圍的代碼,同時(shí)在大多數(shù)GPU硬件上運(yùn)行!
GPUArrays
大多數(shù)高度并行的算法需要通過(guò)相當(dāng)多的數(shù)據(jù)來(lái)克服所有線程和延遲開(kāi)銷。因此,大多數(shù)算法都需要數(shù)組來(lái)管理所有數(shù)據(jù),這需要一個(gè)好的GPU數(shù)組庫(kù)(array library)作為關(guān)鍵基礎(chǔ)。
GPUArrays.jl是Julia的基礎(chǔ)。它提供了一個(gè)抽象數(shù)組實(shí)現(xiàn),專門用于使用高度并行硬件的原始功能。它包含設(shè)置GPU所需的所有功能,啟動(dòng)Julia GPU函數(shù)并提供一些基本的數(shù)組算法。
抽象意味著它需要以CuArrays和CLArrays形式的具體實(shí)現(xiàn)。由于繼承了GPUArrays的所有功能,它們都提供完全相同的接口。唯一的區(qū)別出現(xiàn)在分配數(shù)組時(shí),這會(huì)強(qiáng)制你決定數(shù)組是否位于CUDA或OpenCL設(shè)備上。關(guān)于這一點(diǎn)的更多信息,請(qǐng)參閱內(nèi)存部分。
GPUArrays有助于減少代碼重復(fù),因?yàn)樗试S編寫?yīng)毩⒂谟布腉PU內(nèi)核,可以通過(guò)CuArrays或CLArrays將其編譯為本機(jī)GPU代碼。因此,許多通用內(nèi)核可以在繼承自GPUArrays的所有packages之間共享。
一點(diǎn)選擇建議:CuArrays僅適用于Nvidia GPU,而CLArrays適用于大多數(shù)可用的GPU。CuArrays比CLArrays更穩(wěn)定,并且已經(jīng)可以在Julia 0.7上運(yùn)行。速度上差異不明顯。我建議兩者都試一下,看看哪個(gè)效果最好。
對(duì)于本文,我將選擇CuArrays,因?yàn)楸疚氖菫镴ulia 0.7 / 1.0而寫的,CLArrays仍然不支持。
性能
讓我們用一個(gè)簡(jiǎn)單的交互式代碼示例來(lái)快速說(shuō)明為什么要將計(jì)算轉(zhuǎn)移到GPU上,這個(gè)示例計(jì)算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)
如你所見(jiàn),對(duì)于大型數(shù)組,通過(guò)將計(jì)算移動(dòng)到GPU可以獲得穩(wěn)定的60-80倍的加速。而且非常簡(jiǎn)單,只需將Julia array轉(zhuǎn)換為GPUArray。
有人可能認(rèn)為GPU的性能受到像Julia這樣的動(dòng)態(tài)語(yǔ)言的影響,但Julia的GPU性能應(yīng)該與CUDA或OpenCL的原始性能相當(dāng)。Tim Besard在集成LLVM Nvidia編譯pipeline方面做得非常出色,達(dá)到了與純CUDA C代碼相同(有時(shí)甚至更好)的性能。Tim發(fā)表了一篇非常詳細(xì)的博文,里面進(jìn)一步解釋了這一點(diǎn)[1]。CLArrays方法有點(diǎn)不同,它直接從Julia生成OpenCL C代碼,具有與OpenCL C相同的性能!
為了更好地了解性能并查看與多線程CPU代碼的比較,我收集了一些基準(zhǔn)測(cè)試[2]。
內(nèi)存(Memory)
GPU具有自己的存儲(chǔ)空間,包括視頻存儲(chǔ)器(VRAM),不同的高速緩存和寄存器。無(wú)論你做什么,任何Julia對(duì)象都必須先轉(zhuǎn)移到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類型在轉(zhuǎn)移到GPU或在GPU上創(chuàng)建時(shí)表現(xiàn)都不同。下表概述了預(yù)期結(jié)果:
創(chuàng)建位置描述了對(duì)象是否在CPU上創(chuàng)建然后傳輸?shù)紾PU內(nèi)核,或者是否在內(nèi)核的GPU上創(chuàng)建。這個(gè)表顯示了是否可以創(chuàng)建類型的實(shí)例,并且對(duì)于從CPU到GPU的傳輸,該表還指示對(duì)象是否通過(guò)引用復(fù)制或傳遞。
Garbage Collection
使用GPU時(shí)的一個(gè)很大的區(qū)別是GPU上沒(méi)有垃圾回收( garbage collector, GC)。這不是什么大問(wèn)題,因?yàn)闉镚PU編寫的高性能內(nèi)核不應(yīng)該一開(kāi)始就創(chuàng)建任何GC-tracked memory。
為GPU實(shí)現(xiàn)GC是可能的,但請(qǐng)記住,每個(gè)執(zhí)行的內(nèi)核都是大規(guī)模并行的。在~1000 GPU線程中的每一個(gè)線程創(chuàng)建和跟蹤大量堆內(nèi)存將很快破壞性能增益,因此這實(shí)際上是不值得的。
作為內(nèi)核中堆分配數(shù)組的替代方法,你可以使用GPUArrays。GPUArray構(gòu)造函數(shù)將創(chuàng)建GPU緩沖區(qū)并將數(shù)據(jù)傳輸?shù)絍RAM。如果調(diào)用Array(gpu_array),數(shù)組將被轉(zhuǎn)移回RAM,表示為普通的Julia數(shù)組。這些GPU數(shù)組的Julia句柄由Julia的GC跟蹤,如果它不再使用,GPU內(nèi)存將被釋放。
因此,只能在設(shè)備上使用堆棧分配,并且對(duì)其余的預(yù)先分配的GPU緩沖區(qū)使用。由于傳輸非常昂貴的,因此在編程GPU時(shí)盡可能多地重用和預(yù)分配是很常見(jiàn)的。
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
許多操作是已經(jīng)定義好的。最重要的是,GPUArrays支持Julia的fusing dot broadcasting notation。這種標(biāo)記法允許你將函數(shù)應(yīng)用于數(shù)組的每個(gè)元素,并使用f的返回值創(chuàng)建一個(gè)新數(shù)組。這個(gè)功能通常稱為映射(map)。 broadcast 指的是具有不同形狀的數(shù)組被散布到相同的形狀。
它的工作方式如下:
1x=zeros(4,4)#4x4arrayofzeros2y=zeros(4)#4elementarray3z=2#ascalar4#y's1stdimensiongetsrepeatedforthe2nddimensioninx5#andthescalarzget'srepeatedforalldimensions6#thebelowisequalto`broadcast(+,broadcast(+,xx,y),z)`7x.+y.+z
關(guān)于broadcasting如何工作的更多解釋,可以看看這個(gè)指南:
julia.guide/broadcasting
這意味著在不分配堆內(nèi)存(僅創(chuàng)建isbits類型)的情況下運(yùn)行的任何Julia函數(shù)都可以應(yīng)用于GPUArray的每個(gè)元素,并且多個(gè)dot調(diào)用將融合到一個(gè)內(nèi)核調(diào)用中。由于內(nèi)核調(diào)用延遲很高,這種融合是一個(gè)非常重要的優(yōu)化。
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
現(xiàn)實(shí)世界中的GPUArrays
讓我們直接看看一些很酷的用例。
如下面的視頻所示,這個(gè)GPU加速煙霧模擬是使用GPUArrays + CLArrays創(chuàng)建的,可在GPU或CPU上運(yùn)行,GPU版本的速度提高了15倍:
還有更多的用例,包括求解微分方程,有限元模擬和求解偏微分方程。
讓我們來(lái)看一個(gè)簡(jiǎn)單的機(jī)器學(xué)習(xí)示例,看看如何使用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]))
只需將數(shù)組轉(zhuǎn)換為GPUArrays(使用gpu(array)),我們就可以將整個(gè)計(jì)算轉(zhuǎn)移到GPU并獲得相當(dāng)不錯(cuò)的速度提升。這要?dú)w功于Julia復(fù)雜的AbstractArray基礎(chǔ)架構(gòu),GPUArray可以無(wú)縫地集成到其中。接著,如果你省略了對(duì)轉(zhuǎn)換為GPUArray,代碼也將使用普通的Julia數(shù)組運(yùn)行——但當(dāng)然這是在CPU上運(yùn)行。你可以通過(guò)將use_gpu = true更改為use_gpu = false并重試初始化和訓(xùn)練單元格來(lái)嘗試這個(gè)操作。對(duì)比GPU和CPU,CPU運(yùn)行時(shí)間為975秒,GPU運(yùn)行時(shí)間為29秒 ——加速了約33倍!
另一個(gè)值得關(guān)注的好處是,GPUArrays不需顯式地實(shí)現(xiàn)自動(dòng)微分以有效地支持神經(jīng)網(wǎng)絡(luò)的反向傳播。這是因?yàn)镴ulia的自動(dòng)微分庫(kù)適用于任意函數(shù),并發(fā)出可在GPU上高效運(yùn)行的代碼。這有助于幫助Flux以最少的開(kāi)發(fā)人員在GPU上工作,并使Flux GPU能夠有效地支持用戶定義的函數(shù)。在沒(méi)有GPUArrays + Flux之間協(xié)調(diào)的情況下開(kāi)箱即用是Julia的一個(gè)非常獨(dú)特的特性,詳細(xì)解釋見(jiàn)[3].
編寫GPU內(nèi)核
只需使用GPUArrays的通用抽象數(shù)組接口,而不用編寫任何GPU內(nèi)核,就可以做很多事了。但是,在某些時(shí)候,可能需要實(shí)現(xiàn)一個(gè)需要在GPU上運(yùn)行的算法,并且不能用通用數(shù)組算法的組合來(lái)表示。
好的一點(diǎn)是,GPUArrays通過(guò)一種分層方法減少了大量的工作,這種方法允許你從高級(jí)代碼開(kāi)始編寫低級(jí)內(nèi)核,類似于大多數(shù)OpenCL / CUDA示例里的。它還允許你在OpenCL或CUDA設(shè)備上執(zhí)行內(nèi)核,從而抽象出這些框架中的任何差異。
使這成為可能的函數(shù)名為gpu_call。它可以被稱為 gpu_call(kernel, A::GPUArray, args),并將在GPU上使用參數(shù) (state, args...) 調(diào)用內(nèi)核。State是一個(gè)后端特定對(duì)象,用于實(shí)現(xiàn)獲取線程索引之類的功能。GPUArray需要作為第二個(gè)參數(shù)傳遞,一遍分派到正確的后端并提供啟動(dòng)參數(shù)的缺省值。
讓我們使用gpu_call來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的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
簡(jiǎn)單來(lái)說(shuō),上面的代碼將在GPU上并行調(diào)用julia函數(shù)內(nèi)核length(A) 次。內(nèi)核的每個(gè)并行調(diào)用都有一個(gè)線程索引,我們可以使用它來(lái)安全地索引到數(shù)組A和B。如果我們計(jì)算自己的索引,而不是使用linear_index,我們需要確保沒(méi)有多個(gè)線程讀寫同一個(gè)數(shù)組位置。因此,如果我們使用線程在純Julia中編寫,其對(duì)應(yīng)版本如下:
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")
因?yàn)檫@個(gè)函數(shù)沒(méi)有做很多工作,我們看不到完美的擴(kuò)展,但線程和GPU版本仍然提供了很大的加速。
GPU比線程示例展示的要復(fù)雜得多,因?yàn)橛布€程是在線程塊中布局的——gpu_call在簡(jiǎn)單版本中抽象出來(lái),但它也可以用于更復(fù)雜的啟動(dòng)配置:
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)
在上面的示例中,你可以看到更復(fù)雜的啟動(dòng)配置的迭代順序。確定正確的迭代+啟動(dòng)配置對(duì)于達(dá)到GPU的最佳性能至關(guān)重要。
結(jié)論
在將可組合的高級(jí)編程引入高性能世界方面,Julia取得了長(zhǎng)足的進(jìn)步。現(xiàn)在是時(shí)候?qū)PU做同樣的事情了。
希望Julia降低開(kāi)始在GPU上編程的標(biāo)準(zhǔn),并且我們可以為開(kāi)源GPU計(jì)算發(fā)展可擴(kuò)展的平臺(tái)。第一個(gè)成功案例是通過(guò)Julia packages實(shí)現(xiàn)自動(dòng)微分,這些軟件包甚至不是為GPU編寫,因此這給了我們很多理由相信Julia在GPU計(jì)算領(lǐng)域的可擴(kuò)展和通用設(shè)計(jì)是成功的。
-
gpu
+關(guān)注
關(guān)注
28文章
4760瀏覽量
129130 -
生態(tài)系統(tǒng)
+關(guān)注
關(guān)注
0文章
703瀏覽量
20750
原文標(biāo)題:手把手教你如何用Julia做GPU編程(附代碼)
文章出處:【微信號(hào):AI_era,微信公眾號(hào):新智元】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論