Lập trình GPU với C ++

Gpu Programming With C



Trong hướng dẫn này, chúng ta sẽ khám phá sức mạnh của lập trình GPU với C ++. Các nhà phát triển có thể mong đợi hiệu suất đáng kinh ngạc với C ++ và việc truy cập sức mạnh phi thường của GPU với ngôn ngữ cấp thấp có thể mang lại một số tính toán nhanh nhất hiện có.

Yêu cầu

Mặc dù bất kỳ máy nào có khả năng chạy phiên bản Linux hiện đại đều có thể hỗ trợ trình biên dịch C ++, nhưng bạn sẽ cần một GPU dựa trên NVIDIA để làm theo bài tập này. Nếu không có GPU, bạn có thể tạo phiên bản hỗ trợ GPU trong Amazon Web Services hoặc một nhà cung cấp đám mây khác mà bạn chọn.







Nếu bạn chọn một máy vật lý, hãy đảm bảo rằng bạn đã cài đặt các trình điều khiển độc quyền của NVIDIA. Bạn có thể tìm thấy hướng dẫn cho việc này tại đây: https://linuxhint.com/install-nvidia-drivers-linux/



Ngoài trình điều khiển, bạn sẽ cần bộ công cụ CUDA. Trong ví dụ này, chúng tôi sẽ sử dụng Ubuntu 16.04 LTS, nhưng có các bản tải xuống có sẵn cho hầu hết các bản phân phối chính tại URL sau: https://developer.nvidia.com/cuda-downloads



Đối với Ubuntu, bạn sẽ chọn tải xuống dựa trên .deb. Tệp đã tải xuống sẽ không có phần mở rộng .deb theo mặc định, vì vậy tôi khuyên bạn nên đổi tên nó để có đuôi .deb. Sau đó, bạn có thể cài đặt với:





sudo dpkg -tôipackage-name.deb

Bạn có thể sẽ được nhắc cài đặt khóa GPG và nếu có, hãy làm theo hướng dẫn được cung cấp để thực hiện.

Khi bạn đã hoàn thành việc đó, hãy cập nhật kho lưu trữ của bạn:



sudo apt-get cập nhật
sudo apt-get cài đặtphép lạ-và

Sau khi hoàn tất, tôi khuyên bạn nên khởi động lại để đảm bảo mọi thứ được tải đúng cách.

Lợi ích của việc phát triển GPU

CPU xử lý nhiều đầu vào và đầu ra khác nhau và chứa một lượng lớn các chức năng để không chỉ giải quyết nhiều loại nhu cầu của chương trình mà còn để quản lý các cấu hình phần cứng khác nhau. Chúng cũng xử lý bộ nhớ, bộ nhớ đệm, bus hệ thống, phân đoạn và chức năng IO, biến chúng thành một jack cắm của tất cả các giao dịch.

GPU thì ngược lại - chúng chứa nhiều bộ vi xử lý riêng lẻ tập trung vào các hàm toán học rất đơn giản. Do đó, chúng xử lý các tác vụ nhanh hơn nhiều lần so với CPU. Bằng cách chuyên biệt hóa các hàm vô hướng (một hàm nhận một hoặc nhiều đầu vào nhưng chỉ trả về một đầu ra duy nhất), chúng đạt được hiệu suất cực cao với chi phí là chuyên môn hóa cực độ.

Mã mẫu

Trong mã ví dụ, chúng tôi thêm các vectơ lại với nhau. Tôi đã thêm phiên bản mã CPU và GPU để so sánh tốc độ.
gpu-example.cpp nội dung bên dưới:

#include 'cuda_runtime.h'
#bao gồm
#bao gồm
#bao gồm
#bao gồm
#bao gồm

typedefgiờ::thời gian::high_resolution_clockCái đồng hồ;

#define ITER 65535

// Phiên bản CPU của hàm thêm vectơ
vô hiệuvector_add_cpu(NS *đến,NS *NS,NS *NS,NSn) {
NStôi;

// Thêm các phần tử vectơ a và b vào vectơ c
(tôi= 0;tôi<n; ++tôi) {
NS[tôi] =đến[tôi] +NS[tôi];
}
}

// Phiên bản GPU của hàm thêm vectơ
__toàn cầu__vô hiệuvector_add_gpu(NS *gpu_a,NS *gpu_b,NS *gpu_c,NSn) {
NStôi=threadIdx.NS;
// Không cần vòng lặp for vì thời gian chạy CUDA
// sẽ xâu chuỗi ITER này lần
gpu_c[tôi] =gpu_a[tôi] +gpu_b[tôi];
}

NSchủ chốt() {

NS *đến,*NS,*NS;
NS *gpu_a,*gpu_b,*gpu_c;

đến= (NS *)malloc(ITER* kích thước(NS));
NS= (NS *)malloc(ITER* kích thước(NS));
NS= (NS *)malloc(ITER* kích thước(NS));

// Chúng tôi cần các biến có thể truy cập vào GPU,
// vì vậy cudaMallocManaged cung cấp những
cudaMallocManaged(&gpu_a, ITER* kích thước(NS));
cudaMallocManaged(&gpu_b, ITER* kích thước(NS));
cudaMallocManaged(&gpu_c, ITER* kích thước(NS));

(NStôi= 0;tôi<ITER; ++tôi) {
đến[tôi] =tôi;
NS[tôi] =tôi;
NS[tôi] =tôi;
}

// Gọi hàm CPU và thời gian cho nó
tự độngcpu_start=Cái đồng hồ::hiện nay();
vector_add_cpu(a, b, c, ITER);
tự độngcpu_end=Cái đồng hồ::hiện nay();
giờ::Giá cả << 'vector_add_cpu:'
<<giờ::thời gian::thời_trình<giờ::thời gian::nano giây>(cpu_end-cpu_start).đếm()
<< 'nano giây. ';

// Gọi hàm GPU và thời gian cho nó
// Vòng đệm ba góc là một phần mở rộng thời gian chạy CUDA cho phép
// các tham số của một cuộc gọi hạt nhân CUDA sẽ được truyền.
// Trong ví dụ này, chúng ta đang truyền một khối luồng với các luồng ITER.
tự độnggpu_start=Cái đồng hồ::hiện nay();
vector_add_gpu<<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
tự độnggpu_end=Cái đồng hồ::hiện nay();
giờ::Giá cả << 'vector_add_gpu:'
<<giờ::thời gian::thời_trình<giờ::thời gian::nano giây>(gpu_end-gpu_start).đếm()
<< 'nano giây. ';

// Giải phóng phân bổ bộ nhớ dựa trên chức năng GPU
cudaFree(đến);
cudaFree(NS);
cudaFree(NS);

// Giải phóng phân bổ bộ nhớ dựa trên chức năng CPU
miễn phí(đến);
miễn phí(NS);
miễn phí(NS);

trở lại 0;
}

Makefile nội dung bên dưới:

INC= -Tôi/usr/địa phương/phép lạ/bao gồm
NVCC=/usr/địa phương/phép lạ//nvcc
NVCC_OPT= -std = c ++mười một

tất cả các:
$(NVCC)$(NVCC_OPT)gpu-example.cpp-hoặcgpu-example

dọn dẹp:
-rm -NSgpu-example

Để chạy ví dụ, hãy biên dịch nó:

chế tạo

Sau đó chạy chương trình:

./gpu-example

Như bạn có thể thấy, phiên bản CPU (vector_add_cpu) chạy chậm hơn đáng kể so với phiên bản GPU (vector_add_gpu).

Nếu không, bạn có thể cần điều chỉnh ITER xác định trong gpu-example.cu thành một số cao hơn. Điều này là do thời gian thiết lập GPU dài hơn một số vòng lặp sử dụng CPU nhỏ hơn. Tôi thấy 65535 hoạt động tốt trên máy của mình, nhưng số dặm của bạn có thể thay đổi. Tuy nhiên, khi bạn xóa ngưỡng này, GPU sẽ nhanh hơn đáng kể so với CPU.

Phần kết luận

Tôi hy vọng bạn đã học được nhiều điều từ phần giới thiệu của chúng tôi về lập trình GPU với C ++. Ví dụ ở trên không thực hiện được nhiều, nhưng các khái niệm được trình bày cung cấp một khuôn khổ mà bạn có thể sử dụng để kết hợp các ý tưởng của mình nhằm giải phóng sức mạnh của GPU.