2019年7月11日 星期四

PyCUDA Tutorial

PyCUDA Tutorial

官方文檔_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/資料夾底下,也可能有所幫助。

沒有留言:

張貼留言