Понимание ограничений памяти кучи cuda на поток

Этот вопрос касается ограничения размера кучи в cuda. Посетив некоторые вопросы, касающиеся этой темы, в том числе этот: новый оператор в ядре .. странное поведение Я сделал несколько тестов. Учитывая ядро ​​следующим образом:

#include <cuda.h>
#include <cuda_runtime.h>
#define CUDA_CHECK( err ) __cudaSafeCall( err, __FILE__, __LINE__ )
#define CUDA_CHECK_ERROR()    __cudaCheckError( __FILE__, __LINE__ )
inline void __cudaSafeCall( cudaError err, const char *file, const int line )
{
    if ( cudaSuccess != err )
    {
        fprintf( stderr, "cudaSafeCall() failed at %s:%i : %s\n",
                file, line, cudaGetErrorString( err ) );
        exit( -1 );
    }
    return;
}

inline void __cudaCheckError( const char *file, const int line )
{
    cudaError err = cudaGetLastError();
    if ( cudaSuccess != err )
    {
        fprintf( stderr, "cudaCheckError() failed at %s:%i : %s\n",
                file, line, cudaGetErrorString( err ) );
        exit( -1 );
    }
    return;
}
#include <stdio>
#define NP 900000
__device__ double *temp;
__device__ double *temp2;
__global__
void test(){
int i = blockDim.x*blockIdx.x + threadIdx.x;
if(i==0){
    temp = new double[NP];
    //temp2 = new double[NP];
}

if(i==0){
    for(int k=0;k<NP;k++){
        temp[i] = 1.;
        if(k%1000 == 0){
            printf("%d : %g\n", k, temp[i]);
        }
    }
}
if(i==0){
    delete(temp);
    //delete(temp2);
}
}
int main(){
    //cudaDeviceSetLimit(cudaLimitMallocHeapSize, 32*1024*1024);
    //for(int k=0;k<2;k++){
        test<<<ceil((float)NP/512), 512>>>();
        CUDA_CHECK_ERROR();
    //}
    return 0;
}

Я хочу проверить ограничение размера кучи.

  1. Динамическое выделение одного массива (temp) с одним потоком, размер которого примерно превышает 960 000 * sizeof (double) (около 8 МБ, что является пределом размера кучи по умолчанию), дает ошибку: ok. 900 000 работ. (кто-то знает, как рассчитать истинный предел?)
  2. Увеличение предела размера кучи позволяет выделить больше памяти: нормально, хорошо.
  3. Вернуться к размеру кучи 8 МБ, выделяя один массив на поток с ДВУМЯ потоками (таким образом, заменив if (i==0) на if(i==0 || i==1), каждый из 900 000 * sizeof(double) терпит неудачу , Но 450 000*sizeof(double) каждый работает.
  4. Вот моя проблема: выделение ДВУХ массивов с ОДНИМ потоком (таким образом, temp и temp2 для потока 0), каждое 900 000 * sizeof(double) тоже работает, но не должно? Действительно, когда я пытаюсь записать в оба массива, это не удается. Но у кого-нибудь есть идея, почему это различное поведение при распределении при использовании двух массивов с одним потоком вместо двух массивов с двумя потоками?

РЕДАКТИРОВАТЬ: еще один тест, который я нахожу интересным для тех, кто, как и я, будет изучать использование кучи: 5. Выполнение ядра два раза с одним массивом размером 900 000 * sizeof(double), выделенным одним потоком 0, работает, если есть удаление. Если удаление не указано, во второй раз произойдет сбой, но будет выполнен первый вызов.

РЕДАКТИРОВАТЬ 2: как выделить переменную для всего устройства один раз, но доступную для записи всеми потоками (не с хоста, используя динамическое распределение в коде устройства)?


person François Laenen    schedule 29.07.2013    source источник
comment
Можете ли вы просто сфокусировать свой код и свое описание на проблеме, которую, по вашему мнению, вы наблюдаете? Не могли бы вы предоставить полный компилируемый код? Оно не должно быть очень длинным. Как вы проверяете успех/неудача операции new? Обычный тест - это проверка, равен ли возвращенный указатель нулю, но я нигде не вижу этого в вашем коде.   -  person Robert Crovella    schedule 29.07.2013
comment
Прекратите редактировать вопрос с новыми вопросами. Это делает невозможным ответ. Это не чат-форум.   -  person Robert Crovella    schedule 29.07.2013
comment
Я тестирую возвратом printf, и сбой вызван ошибкой cudaerror: неуказанный сбой запуска. Я отредактировал код, который компилируется.   -  person François Laenen    schedule 29.07.2013


Ответы (1)


Вероятно, вы не тестируете возвращенный нулевой указатель в операции new, которая является допустимый метод в C++ для сообщения оператором об ошибке.

Когда я изменяю ваш код следующим образом, я получаю сообщение "Второй новый не удалось":

#include <stdio.h>

 #define NP 900000
__device__ double *temp;
__device__ double *temp2;
__global__
void test(){
int i = blockDim.x*blockIdx.x + threadIdx.x;
if(i==0){
    temp = new double[NP];
    if (temp == 0) {printf("first new failed\n"); return;}
    temp2 = new double[NP];
    if (temp2 == 0) {printf("second new failed\n"); return;}
}

if(i==0){
    for(int k=0;k<NP;k++){
        temp[i] = 1.;
        if(k%1000 == 0){
            printf("%d : %g\n", k, temp[i]);
        }
    }
}
if(i==0){
    delete(temp);
    delete(temp2);
}
}

int main() {

  test<<<1,1>>>();
  cudaDeviceSynchronize();
  return 0;
}

Будет удобно, если вы предоставите полный, компилируемый код, с которым могут работать другие, как это сделал я.

Для вашего первого вопроса EDIT неудивительно, что второй новый будет работать, если первый будет удален. Первый выделяет почти все доступные 8 МБ. Если вы удалите это распределение, то второе будет успешным. Ссылаясь на документации, мы видим, что память, выделенная таким образом динамически, существует в течение всего времени жизни контекста cuda или до тех пор, пока не будет выполнена соответствующая операция удаления (т.е. не только один вызов ядра. Завершение ядра не обязательно освободить выделение.)

Для вашего второго вопроса EDIT вы уже демонстрируете метод, используя указатель __device__ double *temp;, с помощью которого один поток может выделить хранилище, к которому могут получить доступ все потоки. Однако у вас возникнет проблема между блоками, потому что нет гарантии порядка синхронизации между блоками или порядка выполнения между блоками, поэтому, если вы выделяете из потока 0 в блоке 0, это полезно только в том случае, если блок 0 выполняется раньше других блоков. Вы можете придумать сложную схему для проверки того, было ли уже выполнено выделение переменной (возможно, путем проверки указателя на NULL, а также, возможно, с помощью атомарных вычислений), но это создает хрупкий код. Лучше заранее спланировать свои глобальные распределения и соответственно выделить их с хоста.

person Robert Crovella    schedule 29.07.2013
comment
Хорошо, я не привык к этому тесту, я посмотрю. - person François Laenen; 29.07.2013
comment
На самом деле это своего рода функция сокращения. Я хочу, чтобы временный массив хранил частичные суммы из общих блоков, поэтому действительно могут быть проблемы с синхронизацией между блоками, но метод, представленный в B.5 руководства по программированию, достаточно надежен. Но да, выделение с хоста предпочтительнее. Я подумаю об этом. - person François Laenen; 29.07.2013