1 Khả năng mở rộng của CUDA

Một phần của tài liệu Ứng dụng công nghệ tính toán đa dụng trên các bộ xử lý đồ họa trong bài toán pagerank (Trang 30)

Sự xuất hiện của các CPU đa lõi và GPU nhiều lõi cho thấy rằng xu thế chủ đạo của

các chip xử lý bây giờ là các hệ thống song song. Hơn nữa, đặc tính song song của

chúng tiếp tục gia tăng theo luật của Moore. Sự thách thức là để phát triển phần mềm

ứng dụng mở rộng (scale) trong suốt về tính song song để tận dụng được sự gia tăng về

số lượng của các lõi xử lý, càng có nhiều các ứng dụng 3D mở rộng (scale) một cách

trong suốt tính song song của nó tới các GPU nhiều lõi với các số lượng thay đổi lớn

của các lõi.

Mô hình lập trình CUDA được thiết kế để vượt qua thách thức này trong khi vẫn duy

trì một độ khó về học tập thấp cho các người lập trình quen thuộc với ngôn ngữ lập

Tại nhân của CUDA là ba khái niệm trừu tượng chính – một cơ chế phân cấp của các

nhóm thread, các bộ nhớ chia sẻ, và rào chắn đồng bộ - đơn giản được bộc lộ tới người

lập trình như là một tập nhỏ các mở rộng về ngôn ngữ.

Các khái niệm trừu tượng này cung cấp cơ chế song song dữ liệu và song song tiến

trình nhỏ được lồng bên trong song song dữ liệu và song song tác vụ lớn. Chúng hướng người lập trình phân chia bài toán thành các bài toán con thô có thể được giải quyết độc

lập một cách song song bởi các block gồm các tiến trình, và mỗi bài toán con thành các

phần mịn hơn mà có thể được giải quyết cùng nhau theo song song bởi tất cả các tiến

trình bên trong block đó. Sự phân chia này biểu hiện khả năng của ngôn ngữ đó là cho

phép các tiến trình phối hợp với nhau để xử lý mỗi bài toán con, và tại cùng một thời

điểm cho phép mở rộng một cách tự động. Quả thực, mỗi block tiến trình có thể được

lập lịch trên bất kỳ số lượng lõi xử lý nào đang có, theo bất cứ thứ tự nào, đồng thời

hay là tuần tự, do đó một chương trình CUDA đã biên dịch có thể chạy trên mọi số

lượng lõi xử lý như được minh họa ở Hình 6, và chỉ vào lúc chạy hệ thống mới cần biết

số lượng lõi xử lý vật lý.

Mô hình lập trình có thể mở rộng này cho phép kiến trúc CUDA mở rộng một phạm vi thương mại rộng lớn băng cách là đơn giản mở rộng số lượng lõi xử lý và các phân vùng bộ nhớ: từ các GPU dành cho tính toán hiệu năng cao GeForce và các sản phẩm

cao cấp Quadro và tính toán Tesla tới một số đa dạng các loại rẻ hơn, dòng chính là các GPU GeForce.

Một chương trình đa luồng được phân chia thành các block tiến trình và chúng được

thực thi độc lập với nhau, do đó một GPU nhiều lõi sẽ tự động thực hiện chương trình

Hình 7: Khả năng tự mở rộng

3.2. Các khái niệm chính

Phần này giới thiệu các khái niệm chính của mô hình lập trình CUDA bằng cách tóm

a. Kernel

CUDA C mở rộng ngôn ngữ C bằng cách cho phép người lập trình định nghĩa các hàm

của C, được gọi là các kernel, mà khi được gọi, sẽ được thực hiện N lần theo song song

bởi N thread CUDA, ngược với chỉ một lần như các hàm thông thường của C.

Một kernel được định nghĩa sử dụng đặc tả khai báo __global__ và số tiến trình CUDA thực hiện kernel đó trong một lần gọi kernel được chỉ ra bằng cách dùng một cú

pháp cấu hình sự thực thi mới <<<...>>> Mỗi tiến trình thực thi kernel được gán một

ID tiến trình duy nhất mà có thể được truy cập bên trong kernel bằng biến có sẵn

threadIdx.

Để minh họa, đoạn mã ví dụ mẫu sau đây thực hiện cộng hai vector A và B có kích

thước N và lưu kết quả vào vector C:

// Kernel definition

__global__ void VecAdd(float* A, float* B, float* C) {

int i = threadIdx.x; C[i] = A[i] + B[i]; }

int main() {

...

// Kernel invocation with N threads VecAdd<<<1, N>>>(A, B, C); }

Ở đây, mỗi tiến trình trong N tiến trình thực thi hàm VecAdd() thực hiện phép cộng

cho một cặp.

b. Phân cấp thread

Theo quy ước, biến threadIdx là một vector có 3 thành phần, do đó các thread có thể

được định danh sử dụng một chỉ số thread một chiều, hai chiều, hoặc ba chiều, để tạo

thành block một chiều, hai chiều, hoặc ba chiều. Cơ chế này cung cấp một cách tự

nhiên để thực hiện tính toán trên các phần tử trong một trường như một vector, ma trận,

hoặc khối.

Chỉ số của một thread và ID của nó liên hệ với nhau rất rõ ràng: Với một block một

chiều, chúng là như nhau; với một block hai chiều có kích thước (Dx ,Dy), ID của

một thread có chỉ số (x,y) là (x + yDx); với một block ba chiều có kích thước (Dx ,Dy

,Dz), ID của một thread có chỉ số (x,y,z) là (x + yDx + z Dy Dz).

Ví dụ, đoạn mã sau đây cộng hai ma trận A và B có kích thước NxN và lưu kết quả vào

ma trận C :

// Kernel definition

__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N])

{

int i = threadIdx.x; int j = threadIdx.y;

C[i][j] = A[i][j] + B[i][j]; }

int main() {

...

// Kernel invocation with one block of N * N * 1 threads int numBlocks = 1;

dim3 threadsPerBlock(N, N);

MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C); }

Có sự giới hạn về số thread trong một block, bởi vì tất cả thread của một block được dự

kiến ở trên cùng lõi xử lý và phải chia sẻ các tài nguyên bộ nhớ giới hạn cho lõi đó.

Trên các GPU hiện nay, một block có thể chứa được tới 512 thread.

Tuy nhiên, một kernel có thể được thực hiện bởi nhiều block giống nhau, nên tổng số

thread sẽ được tính bằng số thread trong một block nhân với số block.

Các block được tổ chức thành một grid (lưới) một chiều hoặc hai chiều của các block

như minh họa ở Hình 7. Số block trong một grid thường dựa vào kích thước của dư

liệu cần xử lý hoặc số lõi trong hệ thống (mà có thể tăng rất nhanh).

Số thread trong một block và số block trong một grid được chỉ ra trong cú pháp

<<<...>>> và có thể nhận kiểu int hoặc dim3. Các block hai chiều hoặc các grid có thể được chỉ ra như ở ví dụ trên.

Mỗi block bên trong grid có thể được chỉ ra bằng một chỉ số một chiều hoặc hai chiều

và có thể truy cập từ bên trong kernel thông qua biến có sẵn blockIdx. Chiều của block

Hình 8: Lưới của các block

// Kernel definition

float C[N][N]) {

int i = blockIdx.x * blockDim.x + threadIdx.x; int j = blockIdx.y * blockDim.y + threadIdx.y; if (i < N && j < N)

C[i][j] = A[i][j] + B[i][j]; } int main() { ... // Kernel invocation dim3 threadsPerBlock(16, 16);

dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y); MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);

}

Một block có kích thước 16x16 (256 tiến trình), (mặc dù là tùy ý trong trường hợp

này), là một lựa chọn thông thường. Grid được xây dựng với số block đủ để có một

tiến trình cho một phần tử của ma trận. Để đơn giản, ví dụ này giả sử số tiến trình trên một chiều của grid có thể chia hết cho số tiến trình trên block theo chiều đó, mặc dù có thể khác như vậy.

Các block được thực hiện độc lập: Và có thể thực hiện các block theo bất cứ thứ tự

nào, song song hoặc tuần tự. Yêu cầu độc lập này cho phép các block được lập lịch

theo mọi thứ tự trên mọi số lượng lõi như minh họa ở Hình 8, và cho phép người lập

Các thread bên trong một block có thể phối hợp bằng cách chia sẻ dữ liệu thông qua

một vài bộ nhớ chia sẻ và bằng cách đồng bộ hóa việc thực thi của chúng để phối hợp

truy cập bộ nhớ. Đặc biệt hơn, có thể chỉ ra các điểm đồng bộ hóa trong kernel bằng việc gọi hàm giới hạn__syncthreads() ; __syncthreads() hoạt động như một rào chắn

mà tại đó tất cả các tiến trình bên trong block phải đợi trước khi một tiến trình nào đó được phép tiếp tục.

Để phối hợp có hiệu quả, bộ nhớ chia sẻ được thiết kế là một bộ nhớ có độ trễ truy cập

thấpvà nằm gần lõi xử lý (phần nhiều giống như một bộ đệm mức 1) và hàm __syncthreads() là một hàm gọn nhẹ (lightweight).

c. Phân cấp bộ nhớ

Các tiến trình CUDA có thể truy cập dữ liệu từ nhiều không gian bộ nhớ trong suốt quá trình thực hiện của chúng. Mỗi tiến trình có bộ nhớ cục bộ riêng. Mỗi block có bộ nhớ

chia sẻ được tất cả các tiến trình của block đó nhìn thấy trong cùng thời gian sống với

block. Tất cả các thread có thể truy cập tới bộ nhớ toàn cục chung (global memory).

Có hai không gian bộ nhớ chỉ đọc (read-only) được bổ sung và có thể truy cập bởi tất

cả các tiến trình: đó là bộ nhớ constant và bộ nhớ texture. Các không gian bộ nhớ

global, constant, và texture được tối ưu cho các mục đích sử dụng bộ nhớ khác nhau.

Bộ nhớ texture cũng đưa ra các chế độ địa chỉ khác nhau, cũng như là việc lọc dư liệu,

cho một vài định dạng dữ liệu đặc biệt

Các bộ nhớ global, constant, và texture được ràng buộc với các khởi chạy (launch) của

kernel bởi cùng ứng dụng.

d. Hệ thống nhiều device

Một hệ thống host có thể có nhiều device. Có thể liệt kê các device, truy vấn các đặc

Một vài các tiến trình host có thể thực thi phần device code trên cùng thiết bị, nhưng

theo thiết kế, một tiến trình host có thể thực thi device code chỉ trên một thiết bị tại một

thời điểm. Do đó, để thực hiện device code trên nhiều thiết bị cần phải có nhiều tiến

trình host. Hơn nữa, mọi tài nguyên CUDA được tạo ra thông qua runtime trong một

tiến trình host không thể được dùng bởi runtime từ tiến trình host khác.

Đoạn mã sau liệt kê tất cả các device trong hệ thống và truy lục các đặc tính của nó.

Nó cũng xác định số thiết bị hỗ trợ CUDA

int deviceCount;

cudaGetDeviceCount(&deviceCount); int device;

for (device = 0; device < deviceCount; ++device) { cudaDeviceProp deviceProp;

cudaGetDeviceProperties(&deviceProp, device); if (dev == 0) {

if (deviceProp.major == 9999 && deviceProp.minor == 9999) printf("There is no device supporting CUDA.\n");

else if (deviceCount == 1)

printf("There is 1 device supporting CUDA\n"); else

printf("There are %d devices supporting CUDA\n", deviceCount);

} }

Theo mặc định, thiết bị được liên kết với tiến trình host được ngầm chọn là device 0 ngay khi một hàm runtime quản lý được gọi. Mọi thiết bị khác có thể được chọn bằng cách gọi hàm cudaSetDevice() trước. Sau khi một device đã được chọn, hoặc

là ngầm hoặc rõ, mọi lời gọi rõ ràng tiếp theo tới cudaSetDevice() sẽ không hiệu

lực cho đến khi hàm cudaThreadExit() được gọi.

cudaThreadExit() xóa bỏ tất cả các tài nguyên liên quan đến runtime đã liên kết với

tiến trình host. Mọi lời gọi API tiếp theo khởi tạo lại runtime.

3.2. Lập trình không đồng nhất

Như minh họa ở Hình 10, mô hình lập trình CUDA cho phép các tiến trình CUDA thực

hiện trên một thiết bị (device) vật lý tách rời và có thể hoạt động như một bộ đồng xử

lý với tiến trình chủ (host) chạy chương trình C. Trong trường hợp này, ví dụ, khi các

kernel thực hiện trên một GPU và phần chương trình C còn lại thực hiện trên một CPU.

Mô hình lập trình CUDA cũng cho phép cả host và device duy trì các vùng bộ nhớ

DRAM tách rời của chúng, được gọi là host memory và device memory tương ứng.

Hình 10: Lập trình không đồng nhất

Do đó, một chương trình quản lý các vùng nhớ global, constant, và texture có thể nhìn

phát bộ nhớ và giải phóng bộ nhớ cũng như truyền dữ liệu giữa bộ nhớ host và bộ nhớ

device.

3.3. Khả năng tính toán.

Khả năng tính toán của một thiết bị được xác định bằng một số revision chính và một

số revision phụ.

Các thiết bị với cùng con số chính là có cùng kiến trúc lõi. Con số chính của các thiết

bị dựa trên kiến trúc Fermi là 2. Các thiết bị đầu tiên đều có khả năng tính toán 1.x

(con số revision chính là 1).

Con số revison phụ tương ứng với một sự cải tiến cho kiến trúc lõi đó, có thể bao gồm các đặc tính mới.

3.4. Giao diện lập trình

Hai giao diện lập trình hiện tại được hỗ trợ để viết các chương trình CUDA là: CUDA C và CUDA driver API. Một ứng dụng thông thường sử dụng một trong hai, nhưng

cũng có thể dử dụng cả hai..

CUDA C thể hiện mô hình lập trình CUDA như là một tập nhỏ các mở rộng của ngôn

ngữ C. Mọi tệp nguồn chứa một số mở rộng này phải được biên dịch bằng nvcc như

tóm tắt trong phần 3.5. Các giới hạn này cho phép người lập trình định nghĩa một

kernel như một hàm của C và sử dụng một số cú pháp mới để chỉ ra chiều của grid và

block mỗi lần hàm kernel được gọi.

CUDA driver API là một API C ở mức thấp hơn cung cấp các hàm để tải các kernel như là các module của mã CUDA dạng nhị phân hoặc các mã assembly, để kiểm tra

các tham số của chúng, và để thực hiện chúng. Các mã nhị phân và mã assembly

thường có được sau khi biên dịch các kernel được viết bằng C.

CUDA C đi cùng với một API runtime và cả API runtime và API driver đều cung cấp

các hàm để cấp phát, giải phóng bộ nhớ thiết bị, truyền dữ liệu dư bộ nhớ host và bộ

API runtime được xây dựng trên đỉnh của CUDA driver API. Việc quản lý khởi tạo,

quản lý ngữ cảnh (context), quản lý module hoàn toàn được ẩn đi và do đó các đoạn mã

rất ngắn gọn. CUDA C cũng hỗ trợ chế độ mô phỏng thiết bị, thường có ích trong việc

gỡ rối

Ngược lại, CUDA driver API yêu cầu nhiều code hơn, khó lập trình và gỡ rối hơn,

nhưng nó đưa ra một sự điều khiển ở mức tốt hơn và độc lập với ngôn ngữ do đó có thể

quản lý mã nhị phân hoặc assembly.

3.5. Biên dịch với NVCC

Các kernel có thể được viết sử dụng kiến trúc tập lệnh CUDA, được gọi là PTX (được

mô tả trong tài liệu PTX reference manual). Tuy nhiên thường thì sử dụng một ngôn

ngữ lập trình cấp cao như là C sẽ hiệu quả hơn. Trong cả hai trường hợp,

các kernel phải được biên dịch thành mã nhị phân bởi nvcc để có thể thực hiện được

trên thiết bị (device).

nvcc là một trình biên dịch đơn giản hóa việc biên dịch mã nguồn C hoặc mã PTX: Nó cung cấp các tùy chọn dòng lệnh đơn giản và quen thuộc và thực thi chúng bằng cách gọi tập các công cụ để thực hiện các pha biên dịch khác nhau. Phần này trình bày

tổng quan về luồng công việc và các tùy chọn dòng lệnh của nvcc. Một mô tả đầy đủ

có thể tìm trong tìm trong tài liệu nvcc user manual.

3.6. CUDA C

CUDA C cung cấp một cách thức đơn giản cho các người sử dụng quen thuộc với ngôn ngữ lập trình C để dễ dàng viết các chương trình để thực thi trên thiết bị.

Nó bao gồm một tập nhỏ các mở rộng của ngôn ngữ C và một thư viện thời gian chạy

thư viện runtime. Một mô tả hoàn chỉnh cho runtime có thể tìm thấy ở trong tài liệu

CUDA reference manual.

runtime được thực hiện trong thư viện động cudart và tất cả các mục của nó đều có tiền

tố cuda.

Không có hàm khởi tạo rõ ràng cho runtime; nó khởi tạo khi một hàm runtime được

gọi lần đầu tiên (chính xác hơn là mọi hàm trừ các hàm từ các phần quản lý phiên bản

và thiết bị trong tài liệu reference manual). Cần chú ý đến điều này khi lựa chọn đúng

lúc (timing) các lời gọi hàm runtime và khi thông dịch mã lỗi từ lời gọi đầu tiên trong runtime.

Một khi runtime đã được khởi tạo trong một tiến trình của host, mọi tài nguyên (bộ

Một phần của tài liệu Ứng dụng công nghệ tính toán đa dụng trên các bộ xử lý đồ họa trong bài toán pagerank (Trang 30)

Tải bản đầy đủ (PDF)

(67 trang)