Реализация критической секции в CUDA

Вопросы и ответы

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

Вот код


#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct MatchedPixelsStruct {
    int* matched_pixels;
    int size;
};

#define MAX_ERROR_LENGTH 256

#ifdef __cplusplus
extern "C" {
#endif

struct MatchedPixelsStruct run();

#ifdef __cplusplus
}
#endif

__device__ int idx_iter = 0;

__global__ void processPixelsKernel( struct MatchedPixelsStruct *d_best_match_pixels, int *d_pixels ) {
    int len = 5;

    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    // каждый поток обрабатывает одну уникальную комбинацию left_right и down_up. left_right и down_up варьируются от -20 до 19.
    int left_right = (idx / 40) - 20;
    int down_up = (idx % 40) - 20;

    if ( idx > 1600 ) {
        return;
    }

    int* thread_matched_pixels2 = (int*) malloc( sizeof(int) * len );
    for (int i = 0; i < len; i++ ) {
        thread_matched_pixels2[i] = -1;
    }
    int cur_matched_score = 0;
    for ( int i = 0; i < len; i++ ) {
        int cur_score = left_right + down_up + d_pixels[i];
        if ( cur_score >= 0 ) {
            cur_matched_score += cur_score;
            thread_matched_pixels2[i] = cur_score;
        }
    }

    bool goon = true;
    while (goon) {

        if ( idx == idx_iter ) {
            // критическая секция
            printf("idx %d вошел\n", idx);
            if ( d_best_match_pixels->size < cur_matched_score ) {
                for ( int i = 0; i < len; i++ ) {
                    d_best_match_pixels->matched_pixels[i] = thread_matched_pixels2[i];
                }
                d_best_match_pixels->size = cur_matched_score;
            }
            goon = false;
            atomicAdd(&idx_iter, 1 );
        }
    }

}

struct MatchedPixelsStruct run( ) {
    int len = 5;
    int *pixels = (int*) malloc( sizeof(int) * len );
    for ( int i = 0; i < len; i++ ) {
        pixels[i] = i;
    }
    int *d_pixels;
    cudaMalloc(&d_pixels, sizeof(int) * len);
    cudaMemcpy(d_pixels, pixels, sizeof(int) * len, cudaMemcpyHostToDevice );

    struct MatchedPixelsStruct best_match_pixels;
    best_match_pixels.size = 0;

    best_match_pixels.matched_pixels = (int*)malloc(sizeof(int) * len);

    struct MatchedPixelsStruct *d_best_match_pixels;
    cudaMalloc(&d_best_match_pixels, sizeof(struct MatchedPixelsStruct));

    int* d_matched_pixels;
    cudaMalloc(&d_matched_pixels, sizeof(int) * len);
    cudaMemcpy(d_matched_pixels, best_match_pixels.matched_pixels, sizeof(int) * len, cudaMemcpyHostToDevice );
    cudaMemcpy(&(d_best_match_pixels->matched_pixels), &d_matched_pixels, sizeof(int*), cudaMemcpyHostToDevice);
    cudaMemcpy(&(d_best_match_pixels->size), &(best_match_pixels.size), sizeof(int), cudaMemcpyHostToDevice);

    // Параметры запуска ядра
    int blockSize = 256;
    int total_iterations = ( 20 * 2 ) * ( 20 * 2 ); // от 0 до 20 для перемещения пикселей. есть положительное и отрицательное движение. есть движение left_right и up_down.
    int numBlocks = (total_iterations + blockSize - 1) / blockSize;

    // Запуск ядра
    processPixelsKernel<<<numBlocks, blockSize>>>( d_best_match_pixels, d_pixels );

    cudaError_t err = cudaGetLastError();
    if (err != cudaSuccess) {
        printf("Ошибка CUDA: %s\n", cudaGetErrorString(err));
        exit(1);
    }

    cudaDeviceSynchronize();

    int size;
    cudaMemcpy(&size, &d_best_match_pixels->size, sizeof(int), cudaMemcpyDeviceToHost);
    int* matched_pixels = (int*) malloc( sizeof(int) * len );
    cudaMemcpy(matched_pixels, d_matched_pixels, sizeof(int) * len, cudaMemcpyDeviceToHost);

    best_match_pixels.size = size;
    best_match_pixels.matched_pixels = matched_pixels;

    cudaFree(d_best_match_pixels);
    cudaFree(d_matched_pixels);

    return best_match_pixels;
}

Часть критической секции гарантирует, что потоки входят в критическую секцию один за другим, начиная с потока 0 до потока 1600 последовательно. Но это работает только до 255, затем зависает.

Если я помещаю оператор printf внутрь цикла, чтобы увидеть, на каком значении idx_iter зависли ожидающие потоки, то зависание не происходит, но потоки с 0 по 30 отсутствуют, и от потока 31 происходит вход в критическую секцию, и это не зависает.

Вы не могли бы помочь мне понять, почему происходит такое странное поведение?

Ответ

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

Понимание проблемы

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

Вот несколько проблем:

  1. Тупиковая ситуация (Deadlock): Некоторые потоки могут застрять в бесконечном цикле while (goon) из-за того, что условие выхода может никогда не выполниться, если поток не получит доступ в критическую секцию до завершения других потоков.

  2. Отсутствие синхронизации: Ваша реализация не гарантирует, что все потоки видят обновленное значение idx_iter из-за отсутствия явного синхронизации между потоками.

  3. Неправильное управление памятью: Использование malloc в ядерной функции может привести к неоптимальному использованию памяти и даже потенциальным утечкам памяти.

Решение

Решением вашей проблемы будет применение более подходящих методов синхронизации, таких как использование атомарных операций и оррорических семафоров. Вот исправленная версия ядра, учитывающая эти аспекты:

__device__ int idx_iter = 0;

__global__ void processPixelsKernel(struct MatchedPixelsStruct *d_best_match_pixels, int *d_pixels) {
    int len = 5;
    int idx = blockIdx.x * blockDim.x + threadIdx.x;

    if (idx >= 1600) {
        return;
    }

    int left_right = (idx / 40) - 20;
    int down_up = (idx % 40) - 20;

    int thread_matched_pixels[5] = {-1, -1, -1, -1, -1};
    int cur_matched_score = 0;

    for (int i = 0; i < len; i++) {
        int cur_score = left_right + down_up + d_pixels[i];
        if (cur_score >= 0) {
            cur_matched_score += cur_score;
            thread_matched_pixels[i] = cur_score;
        }
    }

    // Вход в критическую секцию
    int iter_idx = atomicAdd(&idx_iter, 1);

    // Проверка, соответствует ли индекс текущему потоку
    if (iter_idx < 1600) {
        // Критическая секция
        printf("idx %d entered\n", idx);
        if (d_best_match_pixels->size < cur_matched_score) {
            for (int i = 0; i < len; i++) {
                d_best_match_pixels->matched_pixels[i] = thread_matched_pixels[i];
            }
            d_best_match_pixels->size = cur_matched_score;
        }
    }
}

Комментарии:

  1. Атомарная операция atomicAdd: Я использую atomicAdd для безопасного инкремента idx_iter, что позволяет избежать условий гонки и делает код более потокобезопасным.

  2. Локальное выделение памяти: Я использую массив постоянного размера thread_matched_pixels вместо malloc, что избавляет от необходимости управлять памятью на устройстве, что в свою очередь предотвращает утечки памяти.

  3. Упрощение логики критической секции: После инкремента idx_iter каждый поток проверяет, попадает ли он в диапазон индексов (меньше 1600) и только тогда выполняет условие в критической секции.

Заключение

Предложенные изменения должны помочь устранить случаи зависания вашего кода и улучшить его производительность. Постоянно следите за работой threads и избегайте использования конструкций, которые могут заблокировать выполнение в GPU.

Оцените материал
Добавить комментарий

Капча загружается...