Áp phích đã tìm thấy câu trả lời cho vấn đề của riêng mình. Tuy nhiên, trong đoạn mã bên dưới, tôi đang cung cấp một khung chung để triển khai một phần quan trọng trong CUDA. Cụ thể hơn, mã thực hiện đếm khối, nhưng nó có thể dễ dàng sửa đổi để lưu trữ các hoạt động khác được thực hiện trong một phần quan trọng . Dưới đây, tôi cũng báo cáo một số giải thích về mã, với một số lỗi "điển hình" trong việc thực hiện các phần quan trọng trong CUDA.
BỘ LUẬT
#include <stdio.h>
#include "Utilities.cuh"
#define NUMBLOCKS 512
#define NUMTHREADS 512 * 2
/***************/
/* LOCK STRUCT */
/***************/
struct Lock {
int *d_state;
// --- Constructor
Lock(void) {
int h_state = 0; // --- Host side lock state initializer
gpuErrchk(cudaMalloc((void **)&d_state, sizeof(int))); // --- Allocate device side lock state
gpuErrchk(cudaMemcpy(d_state, &h_state, sizeof(int), cudaMemcpyHostToDevice)); // --- Initialize device side lock state
}
// --- Destructor
__host__ __device__ ~Lock(void) {
#if !defined(__CUDACC__)
gpuErrchk(cudaFree(d_state));
#else
#endif
}
// --- Lock function
__device__ void lock(void) { while (atomicCAS(d_state, 0, 1) != 0); }
// --- Unlock function
__device__ void unlock(void) { atomicExch(d_state, 0); }
};
/*************************************/
/* BLOCK COUNTER KERNEL WITHOUT LOCK */
/*************************************/
__global__ void blockCountingKernelNoLock(int *numBlocks) {
if (threadIdx.x == 0) { numBlocks[0] = numBlocks[0] + 1; }
}
/**********************************/
/* BLOCK COUNTER KERNEL WITH LOCK */
/**********************************/
__global__ void blockCountingKernelLock(Lock lock, int *numBlocks) {
if (threadIdx.x == 0) {
lock.lock();
numBlocks[0] = numBlocks[0] + 1;
lock.unlock();
}
}
/****************************************/
/* BLOCK COUNTER KERNEL WITH WRONG LOCK */
/****************************************/
__global__ void blockCountingKernelDeadlock(Lock lock, int *numBlocks) {
lock.lock();
if (threadIdx.x == 0) { numBlocks[0] = numBlocks[0] + 1; }
lock.unlock();
}
/********/
/* MAIN */
/********/
int main(){
int h_counting, *d_counting;
Lock lock;
gpuErrchk(cudaMalloc(&d_counting, sizeof(int)));
// --- Unlocked case
h_counting = 0;
gpuErrchk(cudaMemcpy(d_counting, &h_counting, sizeof(int), cudaMemcpyHostToDevice));
blockCountingKernelNoLock << <NUMBLOCKS, NUMTHREADS >> >(d_counting);
gpuErrchk(cudaPeekAtLastError());
gpuErrchk(cudaDeviceSynchronize());
gpuErrchk(cudaMemcpy(&h_counting, d_counting, sizeof(int), cudaMemcpyDeviceToHost));
printf("Counting in the unlocked case: %i\n", h_counting);
// --- Locked case
h_counting = 0;
gpuErrchk(cudaMemcpy(d_counting, &h_counting, sizeof(int), cudaMemcpyHostToDevice));
blockCountingKernelLock << <NUMBLOCKS, NUMTHREADS >> >(lock, d_counting);
gpuErrchk(cudaPeekAtLastError());
gpuErrchk(cudaDeviceSynchronize());
gpuErrchk(cudaMemcpy(&h_counting, d_counting, sizeof(int), cudaMemcpyDeviceToHost));
printf("Counting in the locked case: %i\n", h_counting);
gpuErrchk(cudaFree(d_counting));
}
MÃ GIẢI THÍCH
Phần quan trọng là trình tự của các hoạt động đó phải được thực hiện tuần tự theo chủ đề CUDA.
Giả sử để tạo hạt nhân có nhiệm vụ tính toán số lượng khối chuỗi của lưới ren. Một ý tưởng có thể là để cho mỗi luồng trong mỗi khối có threadIdx.x == 0
tăng bộ đếm toàn cục. Để ngăn chặn các điều kiện chủng tộc, tất cả các gia tăng phải xảy ra tuần tự, vì vậy chúng phải được kết hợp trong một phần quan trọng.
Mã trên có hai hàm hạt nhân: blockCountingKernelNoLock
và blockCountingKernelLock
. Trước đây không sử dụng một phần quan trọng để tăng bộ đếm và, như người ta có thể thấy, trả về kết quả sai. Sau này đóng gói sự gia tăng truy cập trong một phần quan trọng và do đó tạo ra kết quả chính xác. Nhưng phần quan trọng hoạt động như thế nào?
Phần quan trọng được điều chỉnh bởi trạng thái toàn cầu d_state
. Ban đầu, tiểu bang là 0
. Hơn nữa, hai phương thức __device__
, lock
và unlock
, có thể thay đổi trạng thái này. Các phương thức lock
và unlock
chỉ có thể được gọi bởi một chuỗi đơn trong mỗi khối và đặc biệt là bởi chuỗi có chỉ mục chuỗi địa phương threadIdx.x == 0
.
Ngẫu nhiên trong khi thực hiện, một trong các chuỗi có chỉ mục chuỗi địa phương threadIdx.x == 0
và chỉ mục chuỗi tổng thể, giả sử, t
sẽ là phương thức đầu tiên gọi phương thức lock
. Đặc biệt, nó sẽ khởi chạy atomicCAS(d_state, 0, 1)
. Kể từ lúc đầu tiên d_state == 0
, sau đó d_state
sẽ được cập nhật thành 1
, atomicCAS
sẽ trả lại 0
và chuỗi sẽ thoát khỏi chức năng lock
, chuyển đến hướng dẫn cập nhật. Trong khi đó một luồng như vậy thực hiện các hoạt động đã đề cập, tất cả các luồng khác của tất cả các khối khác có threadIdx.x == 0
sẽ thực hiện phương thức lock
. Tuy nhiên, họ sẽ tìm thấy giá trị là d_state
bằng 1
, để atomicCAS(d_state, 0, 1)
sẽ không thực hiện cập nhật và sẽ trả về 1
, vì vậy, để lại các chuỗi này chạy vòng lặp while. Sau đó thread t
hoàn thành bản cập nhật, sau đó nó thực thi chức năng unlock
, cụ thể là atomicExch(d_state, 0)
, do đó khôi phục d_state
thành 0
. Tại thời điểm này, một cách ngẫu nhiên, một chủ đề khác với threadIdx.x == 0
sẽ khóa lại trạng thái.
Mã trên cũng chứa hàm hạt nhân thứ ba, cụ thể là blockCountingKernelDeadlock
. Tuy nhiên, đây là một thực hiện sai khác của phần quan trọng, dẫn đến deadlocks. Thật vậy, chúng tôi nhớ lại rằng warps hoạt động ở chế độ khóa và chúng đồng bộ hóa sau mỗi lệnh. Vì vậy, khi chúng tôi thực hiện blockCountingKernelDeadlock
, có khả năng là một trong các chủ đề trong một sợi dọc, nói một sợi chỉ với chỉ số thread địa phương t≠0
, sẽ khóa trạng thái. Trong trường hợp này, các chủ đề khác trong cùng một sợi dọc của t
, bao gồm cả với threadIdx.x == 0
, sẽ thực hiện cùng một câu lệnh vòng lặp while như luồng t
, là việc thực hiện các luồng trong cùng một đường dọc được thực hiện trong khóa. Theo đó, tất cả các chủ đề sẽ chờ đợi một người nào đó để mở khóa nhà nước, nhưng không có chủ đề khác sẽ có thể làm như vậy, và mã sẽ bị mắc kẹt trong một bế tắc.
Điều này đã được thảo luận nhiều lần trên diễn đàn NVIDIA.Tôi nghĩ rằng kết luận là điều này chỉ hoạt động nếu bạn có thể đảm bảo rằng số lượng các khối nhỏ hơn hoặc bằng số lượng các bộ xử lý đa. Nếu không, nó có thể dẫn đến bế tắc. Nói cách khác, hãy thử tìm cách khác để triển khai thuật toán của bạn mà không yêu cầu các phần quan trọng. – Eric
Tôi không hiểu lời giải thích của bạn lúc đầu, nghĩ rằng sự phân kỳ dọc đã thực sự cho phép các luồng trong cùng một sợi dọc làm những việc khác nhau. Đối với các độc giả tương lai trong tình huống tương tự của tôi, tôi muốn thêm rằng các luồng trong cùng một warp có thể thực hiện các lệnh khác nhau, nhưng trong khi một số luồng thực hiện một nhánh, thì * các luồng khác bị vô hiệu hóa cho đến khi nhánh đó hoàn thành *. – AkiRoss
Tại sao tôi không thể thay thế 'atomicExch (& (khóa [id]), 0u);' bằng 'khóa [id] = 0u;'? (Đã thử, không hoạt động) – whenov