Julia是一種用于數(shù)學(xué)計算的高級編程語言,它不僅與Python一樣易于使用,而且還與C一樣快。Julia是出于性能考慮而創(chuàng)建的,它的語法與其他編程語言相似,但是卻擁有和編譯型語言相媲美的性能。
如今,在多核CPU和大型并行計算系統(tǒng)的編程中,Julia已經(jīng)非常受歡迎了。隨著Julia的發(fā)展,其在GPU計算中也受到了眾多青睞。GPU的性能可以通過提供更多的高級工具進行民主化,這些工具更易于被應(yīng)用數(shù)學(xué)家和機器學(xué)習程序員使用。在這篇文章中,我將重點介紹使用Julia軟件包CUDAnative.jl進行本地GPU編程,通過本地PTX代碼生成功能增強Julia編譯器。
圖1.使用不同抽象級別的庫和Julia包進行GPU編程
現(xiàn)有的GPU-Accelerated Julia Packages
Julia包生態(tài)系統(tǒng)已經(jīng)包含了相當多的與GPU相關(guān)且針對不同抽象級別的軟件包,如圖1所示。在最高抽象級別包括類似 和 TensorFlow.jl的域特定包,可以透明地使用系統(tǒng)中的GPU。使用ArrayFire 可以進行更通用的開發(fā) ,如果需要線性代數(shù)或深層神經(jīng)網(wǎng)絡(luò)算法等專門的CUDA實現(xiàn),可以使用供應(yīng)商特定的軟件包,如 或 cuDNN.jl。所有這些軟件包本質(zhì)上都是原生庫的包裝器,利用Julia的外部函數(shù)接口(FFI)以最小的開銷調(diào)用庫的API。如果想要了解更多信息,可以點擊下面的GitHub地址:https://github.com/JuliaGPU/
這個列表中缺少的是最低的抽象級別,你可以在CUDA C ++中編寫內(nèi)核并管理執(zhí)行。靈活的實現(xiàn)或優(yōu)化現(xiàn)有軟件包不能實現(xiàn)的抽象表達算法,是CUDAnative.jl的魅力所在。
基于CUDAnative.jl的Native GPU Programming
CUDAnative.jl包添加了原生GPU編程能力的Julia編程語言。與CUDAdrv.jl 或CUDArt.jl 包一起使用時,要分別與CUDA驅(qū)動程序和運行時庫進行連接,之后就可以在Julia中進行低級別CUDA開發(fā),而無需外部語言或編譯器。下面的程序是演示如何計算兩個向量的和:
這個例子的真正主力是@cuda macro,它生成專門的代碼,用于將內(nèi)核函數(shù)編譯成GPU匯編,將其上傳到驅(qū)動程序,并準備執(zhí)行環(huán)境。與Julia的即時(JIT)編譯器一起,形成非常有效的內(nèi)核啟動序列,從而避免了與動態(tài)語言相關(guān)聯(lián)的運行時開銷。生成的代碼也有特定的參數(shù)類型; 使用不同類型的參數(shù)可以調(diào)用相同的內(nèi)核。
圖2. CUDAnative.jl編譯器的原理圖。
Julia GPU編譯器的設(shè)計避免提供自定義工具鏈來編譯(GPU兼容的)Julia源代碼到GPU匯編的子代。如圖2所示,它盡可能地與現(xiàn)有的Julia編譯器集成。只有在生成機器代碼的最后一步才會完全支持GPU包。
重用Julia編譯器有很多優(yōu)點,首先硬件支持微小包:CUDAnative.jl只有1300行代碼。其次,CUDAnative.jl避免了多個獨立的語言實現(xiàn),雖然可能會導(dǎo)致語法略有些不同,但同時你也可以像使用GPU代碼一樣使用Julia實現(xiàn)GPU功能。第三,你可以使用動態(tài)類型多方法,元編程,任意類型和對象等。當然這其中也會有一些不兼容或者限制,例如,限制缺乏Julia GPU運行時庫、垃圾回收器,不過值得欣慰的是,Julia團隊正在不斷努力來覆蓋更多語言和庫。
示例: Parallel Reduction
Parallel Reduction是一個很有趣的例子,它是一種用于從輸入值序列中計算總和或其他單個值的協(xié)作并行算法。本文中列舉了Julia代碼在一部分。
以下代碼片段顯示了使用GPU隨機播放指令縮減內(nèi)部函數(shù)。
該函數(shù)有兩個參數(shù),縮減運算符函數(shù)和減小值。Julia編譯器基于這些參數(shù)的類型生成專用代碼,不僅避免了運行時類型檢查,而且完全內(nèi)聯(lián)函數(shù)參數(shù)。這可以在下面的代碼清單中看到,當與+操作符一起調(diào)用此函數(shù)時生成PTX代碼,以及作為參數(shù)的32位整數(shù)。
功能專業(yè)化是自動的,其賦予了Julia標準庫中的大部分通用代碼。在這個例子中,它生成了類似于nvcc的非常干凈緊湊的PTX代碼。
性能
前文我們介紹道CUDAnative.jl可以為小功能生成高效的PTX代碼,但是其如何支持現(xiàn)實生活中的應(yīng)用程序?為了評估這一點,Julia團隊移植了 來異構(gòu)加速Julia。因為移植這樣的應(yīng)用程序需要很大的努力,所以我們專注10個小基準。
圖3中的圖表比較了這些基準測試的原始CUDA C ++實現(xiàn)與Julia端口的性能。Julia版本幾乎是逐字的端口,也就是說,沒有算法變化,也沒有引入高級概念。如圖所示,使用Julia進行GPU計算并不會遭受任何廣泛的性能損失。唯一的異常值是nn基準,與CUDAnative.jl相比,由于稍微更好的寄存器使用情況,其性能顯著提高。平均來說,CUDAnative.jl端口與靜態(tài)編譯的CUDA C ++相同。
圖3.來自Rodinia benchmark suite 幾個基準測試的CUDA C ++和CUD
NVIDIA工具
CUDAnative.jl還旨在與CUDA工具包中的現(xiàn)有工具兼容。例如,它會生成必要的行號信息,以便NVIDIA Visual Profiler按預(yù)期的方式工作,并將 以進行更細粒度的控制。行號信息還可以與cuda-memcheck這樣的工具相結(jié)合,實現(xiàn)精確的回溯:
LLVM NVPTX后端不支持完整調(diào)試信息 ,因此cuda-gdb無法正常工作。
高級編程
Julia動態(tài)語言語義的組合、專門的JIT編譯器和一級元編程有可能創(chuàng)建非常強大的高級抽象,這在其他動態(tài)語言中是很難實現(xiàn)的。例如, CuArrays.jl 包將CUBLAS.jl的性能和CUDAnative.jl的靈活性相結(jié)合,提供了與Julia中其他陣列一樣的CUDA加速數(shù)組。它建立在Julia對高階函數(shù)的支持之上,這些功能是自動且專用的:在CuArray上調(diào)用映射或廣播會生成專門針對操作和數(shù)組類型的內(nèi)核,與上述reduce_warp示例非常相似。
此外,Julia最近已經(jīng)獲得了syntactic loop fusion的支持,根據(jù)介紹文檔顯示,假設(shè)我們實現(xiàn)了,現(xiàn)在要計算并將結(jié)果存儲在X中。你可以執(zhí)行以下操作:
整個計算將融合到單一的GPU內(nèi)核,性能可以與hand-written 相當。代碼是動態(tài)類型的,對于CPU陣列甚至是分布式容器,只要實現(xiàn)相關(guān)方法,它也同樣適用。這被稱為 duck typin,它更容易實現(xiàn)真正的通用代碼。目前Julia團隊正在努力實現(xiàn)所有必需的接口,以使用GPU陣列與ForwardDiff.jl或Knet.jl等軟件包。
除此之外,Julia具有強大的外部功能界面,用于調(diào)用其他語言環(huán)境。它允許Cxx.jl這樣的 ,和用于C ++的FFI,解析相應(yīng)地頭文件并調(diào)用函數(shù)。同時團隊還在使用該接口來創(chuàng)建 CUDAnativelib.jl 來調(diào)用CUDA設(shè)備庫,其中一些庫作為模板化的C ++頭文件發(fā)布。
前文中在GitHub中列出的許多軟件包基本都已經(jīng)成熟了,支持各種平臺和Julia版本。但CUDAnative.jl及其依賴關(guān)系還沒有那么成熟,例如兼容性方面僅支持Linux和MacOS。感興趣的朋友可以去嘗試!