ฉันจะเลือกขนาดกริดและบล็อกสำหรับเมล็ด CUDA ได้อย่างไร


113

นี่เป็นคำถามเกี่ยวกับวิธีกำหนดขนาดกริดบล็อกและเธรด CUDA นี่เป็นคำถามที่เพิ่มเติมให้กับคนที่โพสต์ที่นี่

ตามลิงค์นี้คำตอบจากกรงเล็บมีข้อมูลโค้ด (ดูด้านล่าง) ฉันไม่เข้าใจความคิดเห็น "ค่ามักจะถูกเลือกโดยการปรับแต่งและข้อ จำกัด ของฮาร์ดแวร์"

ฉันไม่พบคำอธิบายที่ดีหรือคำชี้แจงที่อธิบายเรื่องนี้ในเอกสาร CUDA โดยสรุปคำถามของฉันคือวิธีกำหนดblocksize(จำนวนเธรด) ที่เหมาะสมที่สุดโดยใช้รหัสต่อไปนี้:

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

คำตอบ:


148

คำตอบนั้นมีสองส่วน (ฉันเขียนไว้) ส่วนหนึ่งหาปริมาณได้ง่ายส่วนอีกส่วนหนึ่งเป็นเชิงประจักษ์มากกว่า

ข้อ จำกัด ของฮาร์ดแวร์:

นี่เป็นส่วนที่ง่ายต่อการหาปริมาณ ภาคผนวก F ของคู่มือการเขียนโปรแกรม CUDA ปัจจุบันจะแสดงรายการขีด จำกัด ที่ยากซึ่ง จำกัด จำนวนเธรดต่อบล็อกที่การเรียกใช้เคอร์เนลสามารถมีได้ หากคุณทำเกินจำนวนนี้เคอร์เนลของคุณจะไม่ทำงาน สามารถสรุปได้คร่าวๆดังนี้:

  1. แต่ละบล็อกต้องมีเธรดทั้งหมดไม่เกิน 512/1024 เธรด ( Compute Capability 1.x หรือ 2.x และใหม่กว่าตามลำดับ)
  2. ขนาดสูงสุดของแต่ละบล็อกถูก จำกัด ไว้ที่ [512,512,64] / [1024,1024,64] (คำนวณ 1.x / 2.x หรือใหม่กว่า)
  3. แต่ละบล็อกไม่สามารถกินได้มากกว่า 8k / 16k / 32k / 64k / 32k / 64k / 32k / 64k / 32k / 64k รีจิสเตอร์ทั้งหมด (Compute 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. แต่ละบล็อกไม่สามารถใช้หน่วยความจำที่ใช้ร่วมกันได้มากกว่า 16kb / 48kb / 96kb (Compute 1.x / 2.x-6.2 / 7.0)

หากคุณอยู่ภายในขีด จำกัด เหล่านั้นเคอร์เนลใด ๆ ที่คุณสามารถคอมไพล์ได้สำเร็จจะเริ่มทำงานโดยไม่มีข้อผิดพลาด

การปรับแต่งประสิทธิภาพ:

นี่คือส่วนที่เป็นเชิงประจักษ์ จำนวนเธรดต่อบล็อกที่คุณเลือกภายในข้อ จำกัด ของฮาร์ดแวร์ที่ระบุไว้ข้างต้นสามารถและมีผลต่อประสิทธิภาพของโค้ดที่ทำงานบนฮาร์ดแวร์ วิธีการทำงานของรหัสแต่ละตัวจะแตกต่างกันและวิธีเดียวที่แท้จริงในการหาปริมาณคือการเปรียบเทียบและการสร้างโปรไฟล์อย่างรอบคอบ แต่สรุปอย่างคร่าวๆอีกครั้ง:

  1. จำนวนเธรดต่อบล็อกควรเป็นจำนวนรอบของขนาดวิปริตซึ่งเท่ากับ 32 สำหรับฮาร์ดแวร์ปัจจุบันทั้งหมด
  2. ยูนิตมัลติโปรเซสเซอร์สตรีมมิ่งแต่ละยูนิตบน GPU จะต้องมีวาร์ปที่ใช้งานอยู่เพียงพอเพื่อซ่อนหน่วยความจำที่แตกต่างกันและเวลาแฝงของไปป์ไลน์คำสั่งของสถาปัตยกรรมและบรรลุปริมาณงานสูงสุด แนวทางดั้งเดิมในที่นี้คือการพยายามบรรลุการครอบครองฮาร์ดแวร์ที่เหมาะสมที่สุด ( คำตอบของ Roger Dahlหมายถึงอะไร)

ประเด็นที่สองเป็นหัวข้อใหญ่ที่ฉันสงสัยว่าจะมีใครพยายามพูดให้ครอบคลุมในคำตอบเดียวของ StackOverflow มีคนเขียนวิทยานิพนธ์ระดับปริญญาเอกเกี่ยวกับการวิเคราะห์เชิงปริมาณในแง่มุมของปัญหา (ดูการนำเสนอนี้โดย Vasily Volkov จาก UC Berkley และบทความนี้โดย Henry Wong จากมหาวิทยาลัยโตรอนโตเพื่อเป็นตัวอย่างว่าคำถามซับซ้อนเพียงใด)

ในระดับเริ่มต้นคุณควรทราบเป็นส่วนใหญ่ว่าขนาดบล็อกที่คุณเลือก (ภายในช่วงของขนาดบล็อกทางกฎหมายที่กำหนดโดยข้อ จำกัด ด้านบน) สามารถและมีผลกระทบต่อความเร็วในการทำงานของโค้ด แต่ขึ้นอยู่กับฮาร์ดแวร์ คุณมีและรหัสที่คุณกำลังเรียกใช้ จากการเปรียบเทียบคุณอาจพบว่าโค้ดที่ไม่สำคัญส่วนใหญ่มี "จุดที่น่าสนใจ" ใน 128-512 เธรดต่อช่วงบล็อก แต่จะต้องมีการวิเคราะห์ในส่วนของคุณเพื่อค้นหาว่าอยู่ที่ไหน ข่าวดีก็คือเนื่องจากคุณกำลังทำงานในขนาดวิปริตหลายเท่าพื้นที่การค้นหาจึงมี จำกัด มากและเป็นการกำหนดค่าที่ดีที่สุดสำหรับโค้ดที่ระบุซึ่งหาได้ง่าย


2
"จำนวนเธรดต่อบล็อกต้องเป็นตัวคูณรอบของขนาดวาร์ป" นี่ไม่ใช่สิ่งที่จำเป็น แต่คุณจะเสียทรัพยากรหากไม่เป็นเช่นนั้น ฉันสังเกตเห็นว่า cudaErrorInvalidValue ถูกส่งกลับโดย cudaGetLastError หลังจากการเปิดตัวเคอร์เนลที่มีบล็อกมากเกินไป (ดูเหมือนว่า compute 2.0 ไม่สามารถรองรับ 1 พันล้านบล็อกได้คำนวณ 5.0 ได้) - ดังนั้นจึงมีข้อ จำกัด ที่นี่เช่นกัน
masterxilo

4
ลิงค์ Vasili Volkov ของคุณตายแล้ว ฉันคิดว่าคุณชอบบทความกันยายน 2010: Better Performance at Lower Occupancy (ปัจจุบันอยู่ที่nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf ) มี bitbucket พร้อมรหัสที่นี่: bitbucket.org/rvuduc/volkov -gtc10
ofer.sheffer

37

คำตอบข้างต้นชี้ให้เห็นว่าขนาดบล็อกสามารถส่งผลกระทบต่อประสิทธิภาพการทำงานได้อย่างไรและแนะนำวิธีการฮิวริสติกทั่วไปสำหรับทางเลือกตามการเพิ่มจำนวนผู้เข้าพัก โดยไม่ต้องการที่จะให้เกณฑ์การเลือกขนาดบล็อกมันจะคุ้มค่าการกล่าวขวัญว่า CUDA 6.5 (ตอนนี้ในรุ่นสมัครอิสระ) รวมถึงหลายฟังก์ชั่นรันไทม์ใหม่ที่จะช่วยในการคำนวณอัตราการเข้าพักและการกำหนดค่าการเปิดดู

เคล็ดลับ CUDA Pro: Occupancy API ช่วยลดความซับซ้อนของการกำหนดค่าการเปิดตัว

หนึ่งในฟังก์ชันที่มีประโยชน์คือการcudaOccupancyMaxPotentialBlockSizeคำนวณขนาดบล็อกแบบฮิวริสติกเพื่อให้ได้จำนวนผู้เข้าพักสูงสุด จากนั้นค่าที่จัดเตรียมโดยฟังก์ชันนั้นสามารถใช้เป็นจุดเริ่มต้นของการปรับพารามิเตอร์การเรียกใช้ให้เหมาะสมด้วยตนเอง ด้านล่างนี้เป็นตัวอย่างเล็กน้อย

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

}

แก้ไข

cudaOccupancyMaxPotentialBlockSizeถูกกำหนดไว้ในcuda_runtime.hแฟ้มและมีกำหนดดังนี้

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

ความหมายของพารามิเตอร์มีดังต่อไปนี้

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.

โปรดทราบว่าใน CUDA 6.5 เราต้องคำนวณขนาดบล็อก 2D / 3D ของตนเองจากขนาดบล็อก 1D ที่ API แนะนำ

โปรดทราบด้วยว่า API ไดรเวอร์ CUDA มี API ที่เทียบเท่ากับฟังก์ชันสำหรับการคำนวณการเข้าพักดังนั้นจึงเป็นไปได้ที่จะใช้cuOccupancyMaxPotentialBlockSizeในโค้ด API ไดรเวอร์ในลักษณะเดียวกับที่แสดงสำหรับรันไทม์ API ในตัวอย่างด้านบน


2
ฉันมีสองคำถาม ประการแรกเมื่อใดควรเลือกขนาดกริดเป็น minGridSize เหนือ gridSize ที่คำนวณด้วยตนเอง ประการที่สองคุณกล่าวว่า "ค่าที่ให้มาจากฟังก์ชันนั้นสามารถใช้เป็นจุดเริ่มต้นของการเพิ่มประสิทธิภาพพารามิเตอร์การเปิดตัวด้วยตนเองได้" - คุณหมายถึงพารามิเตอร์การเปิดตัวยังคงต้องได้รับการปรับให้เหมาะสมด้วยตนเองหรือไม่?
nurabha

มีคำแนะนำเกี่ยวกับวิธีคำนวณขนาดบล็อก 2D / 3D หรือไม่? ในกรณีของฉันฉันกำลังมองหาขนาดบล็อก 2D มันเป็นเพียงกรณีของการคำนวณตัวประกอบ x และ y เมื่อคูณกันแล้วจะได้ขนาดบล็อกเดิมหรือไม่?
Graham Dawes

1
@GrahamDawes นี่อาจเป็นที่สนใจ
Robert Crovella

9

โดยปกติแล้วขนาดบล็อกจะถูกเลือกเพื่อเพิ่ม "จำนวนผู้เข้าพัก" ให้มากที่สุด ค้นหา CUDA Occupancy สำหรับข้อมูลเพิ่มเติม โดยเฉพาะอย่างยิ่งโปรดดูสเปรดชีตเครื่องคำนวณการเข้าพัก CUDA

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.