Làm cách nào để chọn kích thước lưới và kích thước khối cho nhân CUDA?


112

Đây là câu hỏi về cách xác định kích thước lưới, khối và luồng CUDA. Đây là một câu hỏi bổ sung cho câu hỏi được đăng ở đây .

Theo liên kết này, câu trả lời từ talonmies chứa một đoạn mã (xem bên dưới). Tôi không hiểu nhận xét "giá trị thường được chọn bởi điều chỉnh và các ràng buộc phần cứng".

Tôi không tìm thấy lời giải thích hoặc làm rõ giải thích điều này trong tài liệu CUDA. Tóm lại, câu hỏi của tôi là làm thế nào để xác định blocksize(số luồng) tối ưu cho đoạn mã sau:

const int n = 128 * 1024;
int blocksize = 512; // value usually chosen by tuning and hardware constraints
int nblocks = n / nthreads; // value determine by block size and total work
madd<<<nblocks,blocksize>>>mAdd(A,B,C,n);

Câu trả lời:


148

Có hai phần cho câu trả lời đó (tôi đã viết nó). Một phần dễ định lượng, phần còn lại mang tính kinh nghiệm hơn.

Hạn chế phần cứng:

Đây là phần dễ định lượng. Phụ lục F của hướng dẫn lập trình CUDA hiện tại liệt kê một số giới hạn cứng giới hạn số luồng trên mỗi khối mà một lần khởi chạy hạt nhân có thể có. Nếu bạn vượt quá bất kỳ điều nào trong số này, hạt nhân của bạn sẽ không bao giờ chạy. Chúng có thể được tóm tắt một cách đại khái là:

  1. Mỗi khối không được có tổng số hơn 512/1024 luồng ( Khả năng tính toán 1.x hoặc 2.x tương ứng và mới hơn)
  2. Kích thước tối đa của mỗi khối được giới hạn ở [512,512,64] / [1024,1024,64] (Tính 1.x / 2.x trở lên)
  3. Mỗi khối không được tiêu thụ quá 8k / 16k / 32k / 64k / 32k / 64k / 32k / 64k / 32k / 64k tổng số lượt đăng ký (Tính 1.0,1.1 / 1.2,1.3 / 2.x- / 3.0 / 3.2 / 3.5-5.2 / 5,3 / 6-6,1 / 6,2 / 7,0)
  4. Mỗi khối không được tiêu thụ nhiều hơn 16kb / 48kb / 96kb bộ nhớ dùng chung (Tính 1.x / 2.x-6.2 / 7.0)

Nếu bạn ở trong các giới hạn đó, bất kỳ hạt nhân nào bạn có thể biên dịch thành công sẽ khởi chạy mà không bị lỗi.

Điều chỉnh hiệu suất:

Đây là phần thực nghiệm. Số luồng trên mỗi khối bạn chọn trong các ràng buộc phần cứng được nêu ở trên có thể và ảnh hưởng đến hiệu suất của mã chạy trên phần cứng. Cách mỗi mã hoạt động sẽ khác nhau và cách thực sự duy nhất để định lượng nó là bằng cách đo điểm chuẩn và lập hồ sơ cẩn thận. Nhưng một lần nữa, tóm tắt rất đại khái:

  1. Số luồng trên mỗi khối phải là bội số của kích thước sợi dọc, là 32 trên tất cả phần cứng hiện tại.
  2. Mỗi đơn vị đa xử lý phát trực tuyến trên GPU phải có đủ các đường cong hoạt động để ẩn đủ độ trễ của bộ nhớ và đường dẫn lệnh khác nhau của kiến ​​trúc và đạt được thông lượng tối đa. Cách tiếp cận chính thống ở đây là cố gắng đạt được khả năng chiếm dụng phần cứng tối ưu ( câu trả lời của Roger Dahl đang đề cập đến).

Điểm thứ hai là một chủ đề lớn mà tôi nghi ngờ có ai đó sẽ thử và đề cập nó trong một câu trả lời StackOverflow. Có những người viết luận án tiến sĩ xoay quanh việc phân tích định lượng các khía cạnh của vấn đề (xem bài trình bày này của Vasily Volkov từ UC Berkley và bài báo này của Henry Wong từ Đại học Toronto để biết ví dụ về mức độ phức tạp của câu hỏi).

Ở cấp độ đầu vào, bạn chủ yếu nên biết rằng kích thước khối bạn chọn (trong phạm vi kích thước khối hợp pháp được xác định bởi các ràng buộc ở trên) có thể và có tác động đến tốc độ chạy mã của bạn, nhưng nó phụ thuộc vào phần cứng bạn có và mã bạn đang chạy. Bằng cách đo điểm chuẩn, bạn có thể sẽ thấy rằng hầu hết các mã không tầm thường đều có "điểm ngọt ngào" trong dải 128-512 luồng cho mỗi khối, nhưng nó sẽ yêu cầu một số phân tích từ phía bạn để tìm ra vị trí đó. Tin tốt là vì bạn đang làm việc với nhiều kích thước sợi dọc, không gian tìm kiếm rất hữu hạn và cấu hình tốt nhất cho một đoạn mã nhất định tương đối dễ tìm.


2
"Số luồng trên mỗi khối phải là bội số của kích thước sợi dọc", điều này không bắt buộc nhưng bạn sẽ lãng phí tài nguyên nếu không. Tôi nhận thấy rằng cudaErrorInvalidValue được trả lại bởi cudaGetLastError sau khi khởi chạy hạt nhân với quá nhiều khối (có vẻ như compute 2.0 không thể xử lý 1 tỷ khối, compute 5.0 có thể) - vì vậy cũng có giới hạn ở đây.
masterxilo

4
Liên kết Vasili Volkov của bạn đã chết. Tôi cho rằng bạn thích bài viết của anh ấy vào tháng 9 năm 2010: Hiệu suất tốt hơn ở công suất thấp hơn (hiện được tìm thấy tại nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf ), Có một bitbucket với mã ở đây: bitbucket.org/rvuduc/volkov -gtc10
ofer.sheffer

37

Các câu trả lời ở trên chỉ ra kích thước khối có thể tác động đến hiệu suất như thế nào và đề xuất phương pháp kinh nghiệm chung cho sự lựa chọn của nó dựa trên tối đa hóa công suất thuê. Nếu không muốn để cung cấp các tiêu chí để lựa chọn kích thước khối, nó sẽ là đáng nói đến là CUDA 6.5 (nay trong phiên bản Release Candidate) bao gồm một số chức năng thời gian chạy mới để hỗ trợ trong việc tính toán công suất và cấu hình khởi động, thấy

Mẹo chuyên nghiệp CUDA: API chiếm giữ đơn giản hóa cấu hình khởi chạy

Một trong những chức năng hữu ích là cudaOccupancyMaxPotentialBlockSizetính toán theo kinh nghiệm kích thước khối để đạt được công suất tối đa. Sau đó, các giá trị được cung cấp bởi chức năng đó có thể được sử dụng làm điểm bắt đầu của quá trình tối ưu hóa thủ công các thông số khởi chạy. Dưới đây là một ví dụ nhỏ.

#include <stdio.h>

/************************/
/* TEST KERNEL FUNCTION */
/************************/
__global__ void MyKernel(int *a, int *b, int *c, int N) 
{ 
    int idx = threadIdx.x + blockIdx.x * blockDim.x; 

    if (idx < N) { c[idx] = a[idx] + b[idx]; } 
} 

/********/
/* MAIN */
/********/
void main() 
{ 
    const int N = 1000000;

    int blockSize;      // The launch configurator returned block size 
    int minGridSize;    // The minimum grid size needed to achieve the maximum occupancy for a full device launch 
    int gridSize;       // The actual grid size needed, based on input size 

    int* h_vec1 = (int*) malloc(N*sizeof(int));
    int* h_vec2 = (int*) malloc(N*sizeof(int));
    int* h_vec3 = (int*) malloc(N*sizeof(int));
    int* h_vec4 = (int*) malloc(N*sizeof(int));

    int* d_vec1; cudaMalloc((void**)&d_vec1, N*sizeof(int));
    int* d_vec2; cudaMalloc((void**)&d_vec2, N*sizeof(int));
    int* d_vec3; cudaMalloc((void**)&d_vec3, N*sizeof(int));

    for (int i=0; i<N; i++) {
        h_vec1[i] = 10;
        h_vec2[i] = 20;
        h_vec4[i] = h_vec1[i] + h_vec2[i];
    }

    cudaMemcpy(d_vec1, h_vec1, N*sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(d_vec2, h_vec2, N*sizeof(int), cudaMemcpyHostToDevice);

    float time;
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start, 0);

    cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, MyKernel, 0, N); 

    // Round up according to array size 
    gridSize = (N + blockSize - 1) / blockSize; 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Occupancy calculator elapsed time:  %3.3f ms \n", time);

    cudaEventRecord(start, 0);

    MyKernel<<<gridSize, blockSize>>>(d_vec1, d_vec2, d_vec3, N); 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Kernel elapsed time:  %3.3f ms \n", time);

    printf("Blocksize %i\n", blockSize);

    cudaMemcpy(h_vec3, d_vec3, N*sizeof(int), cudaMemcpyDeviceToHost);

    for (int i=0; i<N; i++) {
        if (h_vec3[i] != h_vec4[i]) { printf("Error at i = %i! Host = %i; Device = %i\n", i, h_vec4[i], h_vec3[i]); return; };
    }

    printf("Test passed\n");

}

BIÊN TẬP

Giá trị cudaOccupancyMaxPotentialBlockSizeđược định nghĩa trong cuda_runtime.htệp và được định nghĩa như sau:

template<class T>
__inline__ __host__ CUDART_DEVICE cudaError_t cudaOccupancyMaxPotentialBlockSize(
    int    *minGridSize,
    int    *blockSize,
    T       func,
    size_t  dynamicSMemSize = 0,
    int     blockSizeLimit = 0)
{
    return cudaOccupancyMaxPotentialBlockSizeVariableSMem(minGridSize, blockSize, func, __cudaOccupancyB2DHelper(dynamicSMemSize), blockSizeLimit);
}

Ý nghĩa của các tham số như sau

minGridSize     = Suggested min grid size to achieve a full machine launch.
blockSize       = Suggested block size to achieve maximum occupancy.
func            = Kernel function.
dynamicSMemSize = Size of dynamically allocated shared memory. Of course, it is known at runtime before any kernel launch. The size of the statically allocated shared memory is not needed as it is inferred by the properties of func.
blockSizeLimit  = Maximum size for each block. In the case of 1D kernels, it can coincide with the number of input elements.

Lưu ý rằng, kể từ CUDA 6.5, người ta cần tính toán kích thước khối 2D / 3D của riêng mình từ kích thước khối 1D do API đề xuất.

Cũng lưu ý rằng API trình điều khiển CUDA chứa các API tương đương về chức năng để tính toán tỷ lệ sử dụng, do đó, có thể sử dụng cuOccupancyMaxPotentialBlockSizetrong mã API trình điều khiển theo cách tương tự được hiển thị cho API thời gian chạy trong ví dụ trên.


2
Tôi có hai câu hỏi. Đầu tiên khi nào người ta nên chọn kích thước lưới là minGridSize trên gridSize được tính toán thủ công. Thứ hai, bạn đã đề cập rằng "Các giá trị được cung cấp bởi chức năng đó sau đó có thể được sử dụng làm điểm bắt đầu của quá trình tối ưu hóa thủ công các thông số khởi chạy." - ý bạn là các thông số khởi chạy vẫn cần được tối ưu hóa theo cách thủ công?
nurabha

Có hướng dẫn nào về cách tính kích thước khối 2D / 3D không? Trong trường hợp của tôi, tôi đang tìm kích thước khối 2D. Có phải đó chỉ là trường hợp tính thừa số x và y khi nhân với nhau cho ra kích thước khối ban đầu?
Graham Dawes

1
@GrahamDawes điều này có thể được quan tâm.
Robert Crovella

9

Kích thước khối thường được chọn để tối đa hóa "công suất". Tìm kiếm trên CUDA Occupancy để biết thêm thông tin. Đặc biệt, hãy xem bảng tính Công suất thuê CUDA.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.