Nano Hash - криптовалюты, майнинг, программирование

Реализация Max Reduce в Cuda

Я изучаю Cuda и все еще справляюсь с параллелизмом. Проблема, с которой я сталкиваюсь на данный момент, заключается в реализации максимального уменьшения массива значений. это мое ядро

__global__ void max_reduce(const float* const d_array,
                     float* d_max,
                     const size_t elements)
{
    extern __shared__ float shared[];

    int tid = threadIdx.x;
    int gid = (blockDim.x * blockIdx.x) + tid;

    if (gid < elements)
        shared[tid] = d_array[gid];
    __syncthreads();

    for (unsigned int s=blockDim.x/2; s>0; s>>=1) 
    {
        if (tid < s && gid < elements)
            shared[tid] = max(shared[tid], shared[tid + s]);
        __syncthreads();
    }

    if (gid == 0)
        *d_max = shared[tid];
}

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

Чтобы протестировать ядро, я нашел минимальное и максимальное значения, используя последовательный цикл for. Минимальное и максимальное значения всегда получаются одинаковыми в ядре, но совпадает только минимальное уменьшение.

Есть ли что-то очевидное, что я упускаю/делаю неправильно?


  • Возможно, вам следует инициализировать вашу общую память на -FLOAT_MAX для максимума и FLOAT_MAX для минимума. 28.06.2013
  • @PavanYalamanchili Общая память заполняется глобальным массивом, есть ли необходимость помещать туда -FLOAT_MAX? Кроме того, максимальное значение, которое я получаю от параллельной функции, по какой-то причине меньше серийного максимума. 29.06.2013
  • В последнем блоке будет несколько элементов разделяемой памяти, которые не установлены (когда gid ›= elements). Это вызовет проблемы. 29.06.2013
  • @PavanYalamanchili (со ссылкой на мой удаленный ответ) - Значит, мне нужно синхронизировать устройство после ядра, а затем запустить другое ядро, чтобы объединить результаты? 29.06.2013
  • Вам не нужно синхронизировать устройство, ядра стоят в очереди по порядку. 29.06.2013

Ответы:


1

Ваш основной вывод в вашем удаленном ответе был правильным: опубликованное вами ядро ​​​​не понимает того факта, что в конце выполнения этого ядра вы выполнили значительную часть общего сокращения, но результаты не совсем полные. Результаты каждого блока должны быть объединены (каким-то образом). Как указано в комментариях, есть еще несколько проблем с вашим кодом. Давайте посмотрим на его модифицированную версию:

__device__ float atomicMaxf(float* address, float val)
{
    int *address_as_int =(int*)address;
    int old = *address_as_int, assumed;
    while (val > __int_as_float(old)) {
        assumed = old;
        old = atomicCAS(address_as_int, assumed,
                        __float_as_int(val));
        }
    return __int_as_float(old);
}


__global__ void max_reduce(const float* const d_array, float* d_max, 
                                              const size_t elements)
{
    extern __shared__ float shared[];

    int tid = threadIdx.x;
    int gid = (blockDim.x * blockIdx.x) + tid;
    shared[tid] = -FLOAT_MAX;  // 1

    if (gid < elements)
        shared[tid] = d_array[gid];
    __syncthreads();

    for (unsigned int s=blockDim.x/2; s>0; s>>=1) 
    {
        if (tid < s && gid < elements)
            shared[tid] = max(shared[tid], shared[tid + s]);  // 2
        __syncthreads();
    }
    // what to do now?
    // option 1: save block result and launch another kernel
    if (tid == 0)        
        d_max[blockIdx.x] = shared[tid]; // 3
    // option 2: use atomics
    if (tid == 0)
      atomicMaxf(d_max, shared[0]);
}
  1. Как указал Паван, вам необходимо инициализировать массив общей памяти. Последний запущенный блок может не быть «полным», если gridDim.x*blockDim.x больше elements.
  2. Обратите внимание, что в этой строке, даже несмотря на то, что мы проверяем, что рабочий поток (gid) меньше elements, когда мы добавляем s к gid для индексации в разделяемую память, мы все еще можем индексировать за пределами легитимного значения, скопированные в разделяемую память, в последнем блоке. Поэтому нам нужна инициализация разделяемой памяти, указанная в примечании 1.
  3. Как вы уже обнаружили, ваша последняя строка была неверной. Каждый блок выдает свой результат, и мы должны их как-то комбинировать. Один из методов, который вы можете рассмотреть, если количество запущенных блоков невелико (подробнее об этом позже), заключается в использовании атомарность. Обычно мы отговариваем людей от использования атомарных вычислений, поскольку они «затратны» с точки зрения времени выполнения. Однако другой вариант, с которым мы сталкиваемся, — это сохранение результата блока в глобальной памяти, завершение работы ядра, а затем, возможно, запуск другого ядра для объединения результатов отдельных блоков. Если я изначально запустил большое количество блоков (скажем, более 1024), то, если я буду следовать этой методологии, я могу в конечном итоге запустить два дополнительных ядра. Таким образом, рассмотрение атомарности. Как указано, встроенной функции atomicMax для чисел с плавающей запятой нет, но, как указано в документации, вы можете использовать atomicCAS для создания любой произвольной атомарной функции, и я привел пример в atomicMaxf, который обеспечивает атомарный максимум для float.

Но лучше ли запускать 1024 или более атомарных функций (по одной на блок)? Возможно нет.

При запуске ядра блоков потоков нам действительно нужно запустить достаточное количество блоков потоков, чтобы компьютер был занят. Как правило, мы хотим, чтобы на один SM работало не менее 4-8 варпов, и, вероятно, неплохо было бы сделать несколько больше. Но с точки зрения использования машины нет особой выгоды от первоначального запуска тысяч блоков потоков. Если мы выберем число, например, 8 блоков потоков на SM, а у нас самое большее, скажем, 14-16 SM в нашем графическом процессоре, это даст нам относительно небольшое число 8*14 = 112 блоков потоков. Давайте выберем 128 (8*16) для красивого круглого числа. В этом нет ничего волшебного, этого достаточно, чтобы GPU был загружен. Если мы заставим каждый из этих 128 блоков выполнять дополнительную работу для решения всей проблемы, мы сможем использовать атомарность без (возможно) больших штрафов за это и избежать множественного ядра. запускает. Итак, как это будет выглядеть?:

__device__ float atomicMaxf(float* address, float val)
{
    int *address_as_int =(int*)address;
    int old = *address_as_int, assumed;
    while (val > __int_as_float(old)) {
        assumed = old;
        old = atomicCAS(address_as_int, assumed,
                        __float_as_int(val));
        }
    return __int_as_float(old);
}


__global__ void max_reduce(const float* const d_array, float* d_max, 
                                              const size_t elements)
{
    extern __shared__ float shared[];

    int tid = threadIdx.x;
    int gid = (blockDim.x * blockIdx.x) + tid;
    shared[tid] = -FLOAT_MAX; 

    while (gid < elements) {
        shared[tid] = max(shared[tid], d_array[gid]);
        gid += gridDim.x*blockDim.x;
        }
    __syncthreads();
    gid = (blockDim.x * blockIdx.x) + tid;  // 1
    for (unsigned int s=blockDim.x/2; s>0; s>>=1) 
    {
        if (tid < s && gid < elements)
            shared[tid] = max(shared[tid], shared[tid + s]);
        __syncthreads();
    }

    if (tid == 0)
      atomicMaxf(d_max, shared[0]);
}

С этим модифицированным ядром при создании запуска ядра мы не решаем, сколько блоков потоков запускать, исходя из общего размера данных (elements). Вместо этого мы запускаем фиксированное количество блоков (скажем, 128, вы можете изменить это число, чтобы узнать, что работает быстрее) и позволяем каждому блоку потока (и, следовательно, всей сетке) циклически проходить по памяти, вычисляя частичные максимальные операции над каждым элементом в Общая память. Затем в строке, отмеченной комментарием 1, мы должны переустановить переменную gid в исходное значение. Это на самом деле не нужно, и код цикла сокращения блоков можно еще больше упростить, если мы гарантируем, что размер сетки (gridDim.x*blockDim.x) меньше, чем elements, что несложно сделать при запуске ядра.

Обратите внимание, что при использовании этого атомарного метода необходимо инициализировать результат (в данном случае *d_max) соответствующим значением, например -FLOAT_MAX.

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

Для анализа на уровне ниндзя того, как выполнять быстрые параллельные сокращения, взгляните на отличный технический документ Марка Харриса, который доступен с соответствующим Пример CUDA.

29.06.2013
  • Очень исчерпывающий ответ, я вижу, вы приложили много усилий, чтобы переписать мой код! Теперь я начинаю немного лучше понимать параллельную обработку. 30.06.2013
  • На самом деле, я думал, что у тебя почти все правильно. Большая часть кода в моем ответе принадлежит вам. 30.06.2013
  • В первом фрагменте кода не было бы полезнее иметь проверку (gid ‹ elements) вне цикла for? Я просто хочу убедиться, что я правильно понял 09.02.2016
  • Возможно, вам следует задать новый вопрос. С этим изменением (и только с этим изменением) вы сталкиваетесь с возможностью незаконного использования __syncthreads(). 09.02.2016

  • 2

    Вот тот, который кажется наивным, но это не так. Это не будет распространяться на другие функции, такие как sum(), но отлично работает для min() и max().

    __device__ const float float_min = -3.402e+38;
    
    __global__ void maxKernel(float* d_data)
    { 
        // compute max over all threads, store max in d_data[0]
        int i = threadIdx.x;
        __shared__ float max_value;
    
        if (i == 0) max_value = float_min;
        float v = d_data[i];
        __syncthreads();
    
        while (max_value < v) max_value = v;
    
        __syncthreads();
        if (i == 0) d_data[0] = max_value;
    }
    

    Да, правильно, только один раз синхронизируется после инициализации и один раз перед записью результата. К черту условия гонки! Полный вперед!

    Прежде чем вы скажете мне, что это не сработает, пожалуйста, сначала попробуйте. Я тщательно тестировал, и он каждый раз работает на множестве произвольных размеров ядра. Оказывается, в данном случае условие гонки не имеет значения, потому что его разрешает цикл while.

    Он работает значительно быстрее, чем обычное сокращение. Еще одним сюрпризом является то, что среднее количество проходов для ядра размером 32 равно 4. Да, это (log(n)-1), что кажется нелогичным. Это потому, что состояние гонки дает шанс на удачу. Этот бонус предоставляется в дополнение к устранению накладных расходов, связанных с обычным сокращением.

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

    Для краткости я опустил размер и выходные аргументы. Размер — это просто количество потоков (которое может быть 137 или сколько угодно). Вывод возвращается в d_data[0].

    Я загрузил рабочий файл здесь: https://github.com/kenseehart/YAMR.

    28.02.2021
  • что произойдет, если max(d_data) ‹ 0? Хороший вопрос, не был в моем случае использования, поэтому упустил из виду. Я обновил YAMR (см. ссылку), чтобы использовать const float float_min = -3.402e+38; вместо 0.0f. 01.03.2021
  • что произойдет, если у вас есть более 1024 входных значений для уменьшения? Я бы повторил. С обычным шаблоном сокращения это было бы неоптимально, но поскольку max поддерживает сериализацию, я думаю, что итерация — это хорошо. Идея состоит в том, что если вы начинаете с максимальных значений 1024, ваша вторая итерация получит одно попадание, и оно быстро уменьшится. 01.03.2021
  • ... но мой вариант использования не связан с вводом-выводом, поскольку данные, для которых я вычисляю максимум, вычисляются ранее в том же ядре. Если ваш случай связан с вводом-выводом и у вас нет другой работы для других SM, вы можете пойти на какой-то компромисс сетки. Просто имейте в виду, что max поддерживает некоторую сериализацию, поэтому вы хотите, чтобы сравнение возвращало false как можно чаще. Вот почему традиционное сокращение сетки, вероятно, неоптимально. 01.03.2021
  • Отредактированный код согласно комментарию @talonmies 01.03.2021
  • Я должен упомянуть, что рассмотрение условий гонки немного более тонко, чем я указал. Подробнее см. ссылку на github в моем ответе. 01.03.2021
  • Не уверен, как вы получаете 404. Работает для меня: github.com/kenseehart/YAMR 01.03.2021
  • Давайте продолжим обсуждение в чате. 01.03.2021
  • Должно быть, в CDN Github была какая-то странность для моей части мира или что-то в этом роде. pastebin.com/zY6HykJW — так выглядит запуск в Google Colab. Я не могу не заметить, что он распечатывает FAIL 01.03.2021
  • Думаю, я немного поторопился с размещением этого кода для всеобщего обозрения. Он отлично работает для моего случая использования (96 значений, полученных на SM в трех деформациях) и хорошо протестирован для этого. Я получаю стабильные результаты до 24 деформаций. Но когда я превышаю 24 деформации, я получаю проблемное состояние гонки, поэтому я думаю, что мои рассуждения неверны в общем случае. Я обновил код, чтобы сделать его более безопасным. Так что можете попробовать. Теперь он повторяется только с одной деформацией. В этой форме это действительно полезно только в тех случаях, когда максимизируемые данные создаются ядром и уже находятся в общей памяти. 02.03.2021
  • Новые материалы

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

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

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..