PyCUDA Tutorial
Getting started
在你使用PyCuda之前,你應該import
並且初始化
import pycuda.driver as cuda
import pycuda.autoinit
from pycuda.compiler import Sourcemodule
注意到,你並不需要使用pycuda.autoinit
-如果需要的話,也可以手動執行初始化,上下文創建和清理。
Transferring Data
大多數的程式的下一步是將資料轉移進入設備上,在PyCuda,你主要會從本機端轉換numpy array
資料(實際上,滿足Python緩衝區接口的所有東西都可以執行,即使是str
),我們隨機生成一個4x4的陣列
import numpy
# 亂數生成一個4x4的陣列
a = numpy.random.randh(4, 4)
但是,等等,a
是一個雙精數(double precision numbers),但大部份的Nvidia設備只支援單精數(Single precision)
# 轉換為float32格式
a = a.astype(numpy.float32)
最後,我們需要在某一個地方傳輸資料,因此我們需要在設備上分配記憶體空間:
a_gpu = cuda.mem_alloc(a.nbytes)
numpy.nbytes: Total bytes consumed by the elements of the array.
最後一步,我們需要將資料傳輸到GPU上:
cuda.memcpy_htod(a_gpu, a)
Executing a Kernel
在這個教程中,我們將堅持一些簡單的事情:我們將寫程式將a_gpu
中的每個輸入值乘2。為此,我們要寫一個相對應的CUDA C語言,並將它提供給pycuda.compiler.SourceModule
的構造函數:
mod = SourceModule("""
__global__ void doublify(float *a)
{
int idx = threadIdx.x + threadIdx.y*4;
a[idx] *= 2;
}
""")
threadIdx
是CUDA C語言的變數
如果沒有任何錯誤,程式碼將被編譯並載入至設備上。我們可以利用pycuda.driver.Function
來參照並使用上面定義的C function,指定a_gpu
為參數,設置block=(4, 4)
:
# reference
func = mod.get_function('doublify')
# call it
func(a_gpu, block=(4, 4, 1))
最後,我們取得GPU回饋的資料並呈現它,跟原始的a
放一起來觀察:
a_doubled = numpy.empty_like(a)
cuda.memcpy_dtoh(a_doubled, a_gpu)
print(a_doubled)
print(a)
numpy.empty_like: Return a new array with the same shape and type as a given array.
列印出來的資料如下:(上面是乘2之後的結果,下面是原始結果)
>>> print(a_doubled)
[[ 0.20442943 0.77628285 2.4775066 2.1609256 ]
[ 0.89208996 -0.31590727 -1.8672403 1.1618725 ]
[-2.8112352 0.42963934 3.1333017 -0.6093758 ]
[ 0.6432332 3.4218931 0.45423126 -0.277839 ]]
>>> print(a)
[[ 0.10221472 0.38814142 1.2387533 1.0804628 ]
[ 0.44604498 -0.15795363 -0.93362015 0.58093625]
[-1.4056176 0.21481967 1.5666509 -0.3046879 ]
[ 0.3216166 1.7109466 0.22711563 -0.1389195 ]]
它執行了!這完成了我們的演示,謝天謝地,PyCuda接手並完成所有的清理工作,所以你完成了。
不會翻譯
Stick around for some bonus material in the next section, though.
(你可以在PyCuda資料夾examples/demo.py
找到這段範例程式碼)
Shortcuts for Explicit Memory Copies
pycuda.driver.In
, pycuda.driver.Out
,pycuda.driver.InOut
這三個參數處理程序可以簡化記憶體傳輸作業。舉例來說,如果用a
來代替建立a_gpu
,那下面程式碼可以使用:
func(cuda.InOut(a), block=(4, 4, 1))
Prepared Invocations
函式呼叫使用內建的pycuda.driver.Function.__call__()
方法導致多餘的類型識別開銷(見Device Interface)。為了實現上述無多餘開銷的效果,要將函式綁定參數類別(如Python的標準庫struct),然後呼叫它,這也避免使用numpy.number
顯式分配參數大小。
grid = (1, 1)
block = (4, 4, 1)
func.prepare('P')
func.prepared_call(grid, block, a_gpu)
Bonus: Abstracting Away the Complications
使用[pycuda.gpuarray.GPUArray
](Bonus: Abstracting Away the Complications),寫的更精簡但可以達到一樣的效果。
import pycuda.gpuarray as gpuarray
import pycuda.driver as cuda
import pycuda.autoinit
import numpy
a_gpu = gpuarray.to_gpu(numpy.random.randn(4, 4).astype(numpy.float32))
a_doubled = (2 * a_gpu).get()
print(a_doubled)
print(a_gpu)
Advanced Topics
Structures
(由Nicholas Tung貢獻, 範例程式碼置於examples/demo_struct.py
)
假設我們有下列結構,用來將多個可變長度陣列加倍(乘2):
mod = SourceModule("""
struct DoubleOperation {
int datalen, __padding; // so 64-bit ptrs can be aligned
float *ptr;
};
__global__ void double_array(DoubleOperation *a) {
a = &a[blockIdx.x];
for (int idx = threadIdx.x; idx < a->datalen; idx += blockDim.x) {
a->ptr[idx] *= 2;
}
}
""")
Grid中的每一個Block(參考CUDA文件)將會使其中一個array加倍。for迴圈允許的資料元素比執行緒翻倍更多,但如果可以保證有足夠的執行緒數,則效率不高。下一步,一個基於這個結構的封裝類就被建立,兩個array將會被實作:
class DoubleOpStruct:
mem_size = 8 + numpy.intp(0).nbytes
def __init__(self, array, struct_arr_ptr):
self.data = cuda.to_device(array)
self.shape, self.dtype = array.shape, array.dtype
cuda.memcpy_htod(int(struct_arr_ptr), numpy.getbuffer(numpy.int32(array.size)))
cuda.memcpy_htod(int(struct_arr_ptr) + 8, numpy.getbuffer(numpy.intp(int(self.data))))
def __str__(self):
return str(cuda.from_device(self.data, self.shape, self.dtype))
struct_arr = cuda.mem_alloc(2 * DoubleOpStruct.mem_size)
do2_ptr = int(struct_arr) + DoubleOpStruct.mem_size
array1 = DoubleOpStruct(numpy.array([1, 2, 3], dtype=numpy.float32), struct_arr)
array2 = DoubleOpStruct(numpy.array([0, 4], dtype=numpy.float32), do2_ptr)
print("original arrays", array1, array2)
上面的程式碼使用了pycuda.driver.to_device()
與pycuda.driver.from_device()
兩個函數來分配與複製數值,並演示如何使用已分配的記憶體區塊的偏移量。最後,這程式碼可以執行;下面演示如果對array做加倍,以及只針對第二個array做加倍:
func = mod.get_function("double_array")
func(struct_arr, block = (32, 1, 1), grid=(2, 1))
print("doubled arrays", array1, array2)
func(numpy.intp(do2_ptr), block = (32, 1, 1), grid=(1, 1))
print("doubled second only", array1, array2, "\n")
Where to go from here
當你熟悉上面基礎之後,就儘管去Device Interface。更多的範例,可以檢閱examples/
資料夾底下的資料,這個資料夾也包含了數種關於GPU與CPU之間的計算差異。作為如何完成工作的參考,PyCuda的測試套件置於test/
資料夾底下,也可能有所幫助。
沒有留言:
張貼留言