Запуск операций асинхронного копирования памяти на нескольких GPU

Я хочу разделить и скопировать массив данных с хоста на память устройства нескольких gpu. Кроме того, я хочу выполнять все эти операции копирования одновременно.

Для этого я использую cudaMemcpyAsync, который запускаю в приватном потоке каждого GPU.

Вот что я делаю (Сомнения в коде отмечены комментариями, начинающимися с ?? )

#define SIZE 1000
#define GPUCOUNT 2

int* hostData = nullptr;
int *devData[GPUCOUNT];
cudaStream_t stream[GPUCOUNT];

// Create one stream per GPU
for ( int i=0; i != GPUCOUNT ; ++i )
{    
    // DO I need to call cudaSetDevice before creating stream for each GPU ??
    cudaStreamCreate(&stream[i]));
}

// Allocate pinned data on host
cudaMallocHost (&hostData, SIZE );

// Allocate data on each device and copy part of host data to it
for( int i=0; i != GPUCOUNT ; ++i )
{
   cudaSetDevice(i);
   cudaMalloc( (void**) &devData[i], sizeof(int) * SIZE/GPUCOUNT  ); // ?? Does blocking behavior of cudamalloc prevents asynch memcpy invoked in stream of other GPUs from running concurrently 
   cudaMemcpyAsync( (void*) devData[i], hostData + i*SIZE/GPUCOUNT, SIZE/GPUCOUNT, cudaMemcpyHostToDevice, stream[i] );
}

// Some CPU code while copy is happening
// ....

// Wait for copy on all streams to finish
cudaDeviceSynchronize();

// Do something else

Когда я читаю руководство по программированию на C, я вижу, что описанные выше операции копирования памяти не будут выполняться асинхронно, потому что между двумя последовательными запусками асинхронного копирования памяти я вызываю операцию хоста, которая выделяет память устройства (блокирующий вызов).

3.2.5.5.4. Неявная синхронизация

Две команды из разных потоков не могут выполняться одновременно, если какая-либо из следующих операций выполняется между ними хост-потоком:

‣ выделение памяти хоста с блокировкой страницы,

‣ выделение памяти устройства,

‣ набор памяти устройства,

‣ копирование памяти между двумя адресами в одну и ту же память устройства,

‣ любую команду CUDA в поток по умолчанию,

Если приведенная выше причина кажется правдой, мне нужно разделить выделение памяти и операцию копирования.

// Allocate data on each device 
for( int i=0; i != GPUCOUNT ; ++i )
{
   cudaSetDevice(i);
   cudaMalloc( (void**) &devData[i], sizeof(int) * SIZE/GPUCOUNT  );
}

// Copy part of host data to each device
for( int i=0; i != GPUCOUNT ; ++i )
{
   // ?? DO I need to call cudaSetDevice before memory copy ??
   // CUDA guide says:"A memory copy will succeed even if it is issued to a stream that is not associated to the current device."

   cudaMemcpyAsync( (void*) devData[i], hostData + i*SIZE/GPUCOUNT, SIZE/GPUCOUNT, cudaMemcpyHostToDevice, stream[i] ); 
}

Действителен ли мой приведенный выше анализ?

Кроме того, нельзя ли сделать это без создания явного потока для каждого графического процессора путем запуска операции cudaMemcpyAsync в потоке по умолчанию (идентификатор потока 0) каждого графического процессора?. Я основываю это на следующих утверждениях, сделанных в руководстве по программированию CUDA C:

Каждое устройство имеет свой собственный поток по умолчанию (см. Поток по умолчанию), поэтому команды, отправленные в поток по умолчанию устройства, могут выполняться не по порядку или одновременно с командами, отправленными в поток по умолчанию любого другого устройства.

Тогда код будет выглядеть так

#define SIZE 1000
#define GPUCOUNT 2

int* hostData = nullptr;
int *devData[GPUCOUNT];

// Allocate pinned data on host
cudaMallocHost (&hostData, SIZE );

// Allocate data on each device
for( int i=0; i != GPUCOUNT ; ++i )
{
   cudaSetDevice(i);
   cudaMalloc( (void**) &devData[i], sizeof(int) * SIZE/GPUCOUNT  );
}

// Copy part of host data to each device
for( int i=0; i != GPUCOUNT ; ++i )
{
   // ?? DO I need to call cudaSetDevice before memory copy ??
   // CUDA guide says:"A memory copy will succeed even if it is issued to a stream that is not associated to the current device."

   cudaMemcpyAsync( (void*) devData[i], hostData + i*SIZE/GPUCOUNT, SIZE/GPUCOUNT, cudaMemcpyHostToDevice, 0 ); 
}

// Some CPU code while copy is happening
// ....

// Wait for copy on all streams to finish
cudaDeviceSynchronize();

// Do something else

person nurabha    schedule 13.01.2015    source источник


Ответы (1)


http://developer.download.nvidia.com/compute/cuda/4_1/rel/toolkit/docs/online/group__CUDART__DEVICE_g418c299b069c4803bfb7cab4943da383.html

cudaError_t cudaSetDevice   (   int     device   )      

Устанавливает устройство в качестве текущего устройства для вызывающего потока хоста.

Любая память устройства, впоследствии выделенная из этого хост-потока с помощью cudaMalloc(), cudaMallocPitch() или cudaMallocArray(), будет физически находиться на устройстве. Время жизни любой памяти хоста, выделенной из этого потока хоста с помощью cudaMallocHost(), cudaHostAlloc() или cudaHostRegister(), будет связано с устройством. Любые потоки или события, созданные из этого потока хоста, будут связаны с устройством. Любые ядра, запущенные из этого хост-потока с помощью оператора ‹‹‹>>> или cudaLaunch(), будут выполняться на устройстве.

Этот вызов может быть сделан из любого хост-потока, на любое устройство и в любое время. Эта функция не будет выполнять синхронизацию с предыдущим или новым устройством, и ее следует рассматривать как вызов с очень низкими накладными расходами.

Похоже, установленное устройство будет делать все, что вам нужно, без потоков. Вы должны иметь возможность пролистывать каждое устройство, использовать их поток по умолчанию и совершать вызовы malloc и memcpy. Использование асинхронного memcpy и потоковых вызовов ядра поможет в одновременной передаче памяти и вызовах ядра на устройстве.

Вам нужно вызывать setdevice перед всеми вызовами для этого устройства. Потоки в этом не помогут.

person Christian Sarofeen    schedule 13.01.2015
comment
Спасибо, что поделились подробностями API. Я должен был посмотреть раньше. Похоже, поток по умолчанию с cudasetdevice должен работать. Но я по-прежнему считаю, что cudamalloc следует отделить от асинхронных вызовов memcpy, поскольку это блокирующий вызов. - person nurabha; 13.01.2015
comment
Да, cudaMalloc не вернётся, пока память не будет выделена, поэтому для максимальной производительности её следует разделить. Однако cudaMalloc должен возвращаться относительно быстро, так что это не должно быть серьезным зависанием, если оно не является отдельным. Чем больше объем памяти, тем меньше должны выглядеть накладные расходы malloc. - person Christian Sarofeen; 13.01.2015