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

Một phần của tài liệu Nghiên cứu một số thuật toán thám mã sử dụng công nghệ tính toán song song trên các bộ xử lý đồ họa (Trang 53)

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 tắt cách chúng đƣợc mở rộng từ ngôn ngữ C.

53

4.3.2.1.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.

4.3.2.2.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

54

để 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 AB 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); } (adsbygoogle = window.adsbygoogle || []).push({});

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.

55

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 4.5. 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 có thể truy cập bên trong kernel thông qua biến có sẵn blockDim.

56

Đoạn mã sau mở rộng ví dụ MatAdd() trƣớc để xử lý nhiều block.

// Kernel definition

__global__ void MatAdd(float A[N][N], float B[N][N], 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ù có thể 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, trong 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 4.5, và cho phép ngƣời lập trình viết code có thể mở rộng với số lƣợng lõi.

57

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ấp và 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).

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.

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 tính của chúng, và có thể chọn một trong các device để thực hiện kernel.

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

58

int deviceCount; (adsbygoogle = window.adsbygoogle || []).push({});

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");

elseif (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.

59

Hình 4.7. Phân cấp bộ nhớ

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

Nhƣ minh họa ở Hình 4.7, 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.

60

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 memorydevice memory tƣơng ứng. Do đó, một chƣơng trình quản lý các vùng nhớ global, constant, và texture có thể nhìn thấy từ kernel thông qua các lời gọi tới các hàm runtime của CUDA. Bao gồm cả cấp 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.

61

4.3.4.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.

4.4.Hệ thống GPU-Cluster

4.4.1.Phần cứng

Công nghệ CUDA cho phép giảm tải CPU và gửi các tính toán cho GPU mà bằng xây dựng có kiến trúc song song rất cao, do đó đẩy nhanh việc xử lý lƣợng lớn dữ liệu mảng bằng cách sử dụng một tập dòng lệnh giống hệt nhau (đƣợc gọi là nhân) cho từng phần tử trong mảng dữ liệu. Bằng cách này, bộ vi xử lý chỉ bận rộn với việc cung cấp các dữ liệu và hạt nhân cho GPU và sau đó thu thập kết quả. Mặc dù việc chuyển giao giữa CPU (máy chủ) và GPU (thiết bị) có thể xuất hiện nhƣ là một nút cổ chai do làm chậm tốc độ của bus. Một cách có thể đạt đƣợc kết quả rất tốt nếu nó làm cho song song dữ liệu nhiều nhất có thể trong thuật toán và cũng sẽ ánh xạ có thể tới phần cứng hiệu quả nhất có thể theo cách mà các giao dịch chuyển dữ liệu đƣợc giảm thiểu.

Mong mỏi cho sức mạnh tính toán nhiều hơn dẫn đến ý tƣởng của đƣa công nghệ CUDA vào môi trƣờng giao diện truyền thông điệp (MPI), do đó phát sinh khái niệm về cluser của GPU. Bên trong cluster mỗi nút của mạng MPI sẽ truyền hầu hết các nhiệm vụ song song tới các GPU, giảm tải để CPU chỉ để xử lý các thông tin liên lạc giữa các nút mạng.

62

Có ba thành phần chính đƣợc sử dụng trong một cụm GPU: các nút chủ, GPU, và kết nối. Từ kỳ vọng là cho GPU thực hiện một phần đáng kể các tính toán, bộ nhớ lƣu trữ, bus PCIe, và mạng lƣới kết nối cần phải đƣợc phù hợp với hiệu suất GPU để duy trì một hệ thống cân bằng tốt. InfiniBand QDR kết nối đƣợc kỳ vòng để phù hợp với băng thông GPU tới máy chủ. Bộ nhớ lƣu trữ cũng cần phù hợp với số lƣợng bộ nhớ trên GPU để sử dụng tất cả bộ nhớ của chúng.

4.4.2.Phần mềm (adsbygoogle = window.adsbygoogle || []).push({});

Giao diện truyền thông điệp cần đƣợc sử dụng để vận chuyển dữ liệu và thông tin giữa các nút và tại mỗi nút CUDA đƣợc sử dụng để giao tiếp giữa CPU và GPU và để ra lệnh bộ xử lý đồ họa thực hiện tính toán mong muốn. Để thực hiện điều này mã lai MPI và CUDA cần đƣợc phát triển, biên dịch và triển khai trên cluster.

MPICH2 sử dụng một kịch bản gọi là mpicc cho biên dịch mã C. Ở một mức độ kịch bản này không làm gì nhiều hơn việc gọi trình biên dịch gcc với một vài thông số để bao gồm header mpich và liên kết các thƣ viện mpich. Do đó biên dịch một mã MPI/C có thể đƣợc thực hiện bằng cách chỉ sử dụng trình biên dịch gcc với các thông số thích hợp.

Trình biên dịch nvcc sẽ phân chia mã C / C + + (bao gồm cả MPI) và sẽ truyền nó tới trình biên dịch gcc cùng với các thông số nvcc, dẫn tới trong một biên dịch MPICH bình thƣờng mà sau này trong giai đoạn sẽ đƣợc liên kết đến mã CUDA. Một mẫu Makfile đƣợc sử dụng để biên dịch mã hỗn hợp MPICH CUDA nhƣ hình sau:

CPU PCIe Bus GPU CPU PCIe Bus GPU

InfiniBand/ Gigabit Ethernet

63

CC= nvcc

CFLAGS = -I. -I/usr / local /mpich2 -1.0.6. p1/ include \ -I/ usr/ local / cuda / include \

-I/ home / noaje_gab / NVIDIA_CUDA_SDK / common /inc

LDFLAGS =-L/usr / local /mpich2 -1.0.6. p1/lib \

-L/ usr/ local / cuda /lib \

-L/ home / noaje_gab / NVIDIA_CUDA_SDK /lib \

-L/ home / noaje_gab / NVIDIA_CUDA_SDK / common /lib

LIB= -lcuda -lcudart -lm -lmpich -lpthread -lrt

MPICH_FLAG = - DMPICH_IGNORE_CXX_SEEK \

- DMPICH_SKIP_MPICXX

SOURCES = CalcCirc .cpp Init .cu main .cpp

EXECNAME = MatrixProduct

all:

$(CC) -v -o $( EXECNAME )$( SOURCES ) $(LIB) \

$( LDFLAGS )$( CFLAGS )$( MPICH_FLAG )

clean :

rm -f *.o core

Bảng 4.1. Ví dụ Makefile trộn mã CUDA + MPI

4.5.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 dƣới các giới hạn đƣợc trình bày ở phần 4.4.

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. 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.

64

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ộ nhớ thiết bị, quản lý hệ thống có nhiều thiết bị, v..v. (adsbygoogle = window.adsbygoogle || []).push({});

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.

4.5.1.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.

Một phần của tài liệu Nghiên cứu một số thuật toán thám mã sử dụng công nghệ tính toán song song trên các bộ xử lý đồ họa (Trang 53)