CUDA गुठली के लिए मैं ग्रिड और ब्लॉक आयाम कैसे चुनूं?


112

यह एक सवाल है कि 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

उस उत्तर के दो भाग हैं (मैंने इसे लिखा था)। एक भाग को निर्धारित करना आसान है, दूसरा अधिक अनुभवजन्य है।

हार्डवेयर की कमी:

यह हिस्सा निर्धारित करना आसान है। वर्तमान सीयूडीए प्रोग्रामिंग गाइड के परिशिष्ट एफ कई कठिन सीमाओं को सूचीबद्ध करता है जो एक कर्नेल प्रक्षेपण के प्रति ब्लॉक कितने धागे हो सकते हैं। यदि आप इनमें से किसी एक से अधिक हैं, तो आपका कर्नेल कभी नहीं चलेगा। उन्हें मोटे तौर पर संक्षेप में प्रस्तुत किया जा सकता है:

  1. प्रत्येक ब्लॉक में कुल मिलाकर 512/1024 से अधिक धागे नहीं हो सकते ( गणना क्षमता 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 रजिस्टर का कुल उपभोग नहीं कर सकता (गणना 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 से अधिक की साझा मेमोरी का उपभोग नहीं कर सकता (गणना 1.x / 2.x-6.2 / 7.0)

यदि आप उन सीमाओं के भीतर रहते हैं, तो कोई भी कर्नेल जिसे आप सफलतापूर्वक संकलित कर सकते हैं, बिना त्रुटि के लॉन्च करेगा।

प्रदर्शन सुधारना:

यह आनुभविक भाग है। प्रति ब्लॉक थ्रेड्स की संख्या जो आप ऊपर बताए गए हार्डवेयर अवरोधों के भीतर चुनते हैं और हार्डवेयर पर चल रहे कोड के प्रदर्शन को प्रभावित करते हैं। प्रत्येक कोड कैसे व्यवहार करता है यह अलग होगा और इसे निर्धारित करने का एकमात्र वास्तविक तरीका सावधानीपूर्वक बेंचमार्किंग और प्रोफाइलिंग है। लेकिन फिर, बहुत मोटे तौर पर संक्षेप में:

  1. प्रति ब्लॉक थ्रेड्स की संख्या राउंड मल्टीप्ल के कई आकार होनी चाहिए, जो सभी वर्तमान हार्डवेयर पर 32 है।
  2. GPU पर प्रत्येक स्ट्रीमिंग मल्टीप्रोसेसर यूनिट में आर्किटेक्चर की विभिन्न मेमोरी और इंस्ट्रक्शन पाइपलाइन लेटेंसी को पर्याप्त रूप से छिपाने और अधिकतम थ्रूपुट प्राप्त करने के लिए पर्याप्त सक्रिय वॉर होने चाहिए। यहां रूढ़िवादी दृष्टिकोण इष्टतम हार्डवेयर अधिभोग (जो रोजर डाहल का उत्तर संदर्भित कर रहा है) को प्राप्त करने की कोशिश करना है।

दूसरा बिंदु एक बहुत बड़ा विषय है जिस पर मुझे संदेह है कि कोई भी एक एकल स्टैकऑवरफ्लो उत्तर में कोशिश करने और इसे कवर करने जा रहा है। वहाँ समस्या के पहलुओं के मात्रात्मक विश्लेषण के आसपास पीएचडी शोध करे लिखने के लोग हैं (देखें इस प्रस्तुति यूसी बर्कले से वसीली Volkov द्वारा और इस पत्र कैसे जटिल सवाल वास्तव में है के उदाहरण के लिए टोरंटो विश्वविद्यालय से हेनरी वाँग द्वारा)।

प्रवेश स्तर पर, आपको ज्यादातर पता होना चाहिए कि आपके द्वारा चुने गए ब्लॉक आकार (ऊपर की बाधाओं से परिभाषित कानूनी ब्लॉक आकार की सीमा के भीतर) का प्रभाव आपके कोड पर कितनी तेजी से चल सकता है, लेकिन यह हार्डवेयर पर निर्भर करता है आपके पास और जो कोड आप चला रहे हैं। बेंचमार्किंग करके, आप शायद पाएंगे कि अधिकांश गैर-तुच्छ कोड में 128-512 थ्रेड्स प्रति ब्लॉक रेंज में "स्वीट स्पॉट" है, लेकिन यह आपके लिए कुछ विश्लेषण की आवश्यकता होगी कि वह कहां है। अच्छी खबर यह है कि क्योंकि आप ताना आकार के गुणकों में काम कर रहे हैं, खोज स्थान बहुत परिमित है और किसी दिए गए कोड के लिए सबसे अच्छा कॉन्फ़िगरेशन अपेक्षाकृत आसान है।


2
"प्रति ब्लॉक थ्रेड्स की संख्या एक राउंड मल्टीपल ऑफ़ द वॉरप साइज़ होनी चाहिए" यह एक ज़रूरी नहीं है, लेकिन अगर यह नहीं है तो आप संसाधनों को बर्बाद करते हैं। मैंने देखा कि cudaErrorInvalidValue बहुत सारे ब्लॉकों के साथ कर्नेल लॉन्च के बाद cudaGetLastError द्वारा लौटाया गया है (ऐसा लगता है कि कंप्यूट 2.0, 1 बिलियन ब्लॉक को नहीं संभाल सकता है, 5.0 गणना कर सकता है) - इसलिए यहां भी सीमाएं हैं।
मास्टरएक्सिलो

4
आपका वासिली वोल्कोव लिंक मर चुका है। मुझे लगता है कि आप उनकी सितंबर 2010 को पसंद करते हैं: लोअर ऑक्युपेंसी लेख में बेहतर प्रदर्शन (वर्तमान में nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf पर पाया गया ), यहाँ कोड के साथ बिटबकेट है: bitbucket.org/rvuduc/volkov -gtc10
ofer.sheffer

37

ऊपर दिए गए उत्तर बताते हैं कि ब्लॉक का आकार प्रदर्शन को कैसे प्रभावित कर सकता है और अधिभोग अधिकतमकरण के आधार पर अपनी पसंद के लिए एक सामान्य अनुमानी सुझाव दे सकता है। ब्लॉक आकार का चयन करने के लिए मानदंड प्रदान करने के लिए बिना , यह ध्यान देने योग्य होगा कि CUDA 6.5 (अब रिलीज़ कैंडिडेट संस्करण में) अधिभोग गणना और लॉन्च कॉन्फ़िगरेशन में सहायता के लिए कई नए रनटाइम फ़ंक्शन शामिल हैं, देखें

CUDA प्रो टिप: ऑक्यूपेंसी एपीआई लॉन्च कॉन्फ़िगरेशन को सरल करता है

उपयोगी कार्यों में से 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 के रूप में, किसी को एपीआई द्वारा सुझाए गए 1 डी ब्लॉक आकार से अपने 2 डी / 3 डी ब्लॉक आयामों की गणना करने की आवश्यकता है।

यह भी ध्यान दें कि CUDA ड्राइवर API में ऑक्यूपेंसी कैलकुलेशन के लिए कार्यात्मक रूप से समतुल्य एपीआई होते हैं, इसलिए cuOccupancyMaxPotentialBlockSizeऊपर दिए गए उदाहरण में रनटाइम एपीआई के लिए दिखाए गए तरीके से ड्राइवर एपीआई कोड का उपयोग करना संभव है ।


2
मेरे दो सवाल हैं। सबसे पहले कब ग्रिड ग्रिड का चयन करना चाहिए क्योंकि मैन्युअल रूप से गणना ग्रिड पर मिनीग्रिडाइज करें। दूसरे आपने उल्लेख किया है कि "उस फ़ंक्शन द्वारा प्रदान किए गए मानों को तब लॉन्च मापदंडों के मैनुअल ऑप्टिमाइज़ेशन के शुरुआती बिंदु के रूप में इस्तेमाल किया जा सकता है।" - क्या आपको लगता है कि लॉन्च मापदंडों को अभी भी मैन्युअल रूप से अनुकूलित करने की आवश्यकता है?
प्रातः

क्या कोई मार्गदर्शन है कि 2D / 3D ब्लॉक आयामों की गणना कैसे की जाए? मेरे मामले में मैं 2D ब्लॉक आयामों की तलाश कर रहा हूं। क्या एक्स और वाई कारकों की गणना करना सिर्फ एक मामला है जब एक साथ गुणा करने से मूल ब्लॉक आकार मिलता है?
ग्राहम डावेस

1
@GrahamDawes यह ब्याज की हो सकती है।
राबर्ट क्रॉवेल्ला डे

9

"ऑक्यूपेंसी" को अधिकतम करने के लिए आमतौर पर ब्लॉकेज का चयन किया जाता है। अधिक जानकारी के लिए CUDA व्यवसाय पर खोजें। विशेष रूप से, CUDA ऑक्युपेंसी कैलकुलेटर स्प्रेडशीट देखें।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.