Введение
Не всегда необходимо писать собственные CUDA-ядра вручную — для многих стандартных операций (сортировка, поиск, свёртка, преобразование коллекций) есть библиотека Thrust, которая предоставляет интерфейс, похожий на STL, но с реализациями, оптимизированными для GPU. Thrust позволяет писать параллельный код на уровне абстракции алгоритмов, а не низкоуровневых ядер, существенно сокращая объём кода и снижая риск ошибок. В статье рассмотрим основные возможности Thrust на практических примерах.
Концепция
Thrust оперирует контейнерами thrust::host_vector (память CPU) и thrust::device_vector (память GPU), автоматически управляющими выделением и освобождением памяти (аналог std::vector, но для GPU). Алгоритмы Thrust (thrust::sort, thrust::reduce, thrust::transform, thrust::copy_if и многие другие) принимают итераторы этих контейнеров и автоматически параллелизуют выполнение на GPU, либо могут выполняться на CPU при передаче host-итераторов — один и тот же вызов алгоритма работает на обоих «бэкендах» в зависимости от типа переданных итераторов.
Пример кода
#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/sort.h>
#include <thrust/reduce.h>
#include <thrust/transform.h>
#include <thrust/functional.h>
#include <thrust/copy.h>
#include <iostream>
struct SquareFunctor
{
__host__ __device__ float operator()(float x) const { return x * x; }
};
int main()
{
const int n = 1'000'000;
thrust::host_vector<float> hostData(n);
for (int i = 0; i < n; ++i) {
hostData[i] = static_cast<float>(n - i); // обратный порядок для демонстрации сортировки
}
// Копирование данных на GPU происходит автоматически при присваивании
thrust::device_vector<float> deviceData = hostData;
// Параллельная сортировка на GPU
thrust::sort(deviceData.begin(), deviceData.end());
// Параллельное преобразование: возведение каждого элемента в квадрат
thrust::device_vector<float> squared(n);
thrust::transform(deviceData.begin(), deviceData.end(), squared.begin(), SquareFunctor());
// Параллельная свёртка (сумма всех элементов)
float sum = thrust::reduce(squared.begin(), squared.end(), 0.0f, thrust::plus<float>());
std::cout << "Минимальный элемент после сортировки: " << deviceData[0] << std::endl;
std::cout << "Сумма квадратов: " << sum << std::endl;
return 0;
}
Пояснения к коду
thrust::device_vector<float> deviceData = hostData автоматически выполняет копирование данных с host на device одной строкой — без явных вызовов cudaMalloc/cudaMemcpy, как это требовалось бы при работе с «сырым» CUDA API. thrust::sort параллельно сортирует данные прямо на GPU, используя оптимизированную реализацию (как правило, основанную на radix sort для примитивных типов).
SquareFunctor — функтор с квалификаторами __host__ __device__, позволяющими ему выполняться как на CPU, так и на GPU; thrust::transform применяет его к каждому элементу параллельно, аналогично std::transform, но на GPU. thrust::reduce выполняет параллельную свёртку (в данном случае — суммирование) — типичная задача, ручная реализация которой на CUDA требует кода для редукции внутри блока и между блоками, тогда как Thrust скрывает все эти детали.
Подводные камни
- Накладные расходы на копирование между host_vector и device_vector при частых небольших операциях могут превышать выигрыш от параллелизации — Thrust эффективен при больших объёмах данных и минимизации числа пересечений границы CPU/GPU.
- Меньший контроль над деталями реализации алгоритмов. В отличие от ручного ядра, разработчик не может точно настроить tile-размер, использование shared memory или паттерн доступа к памяти внутри алгоритмов Thrust — для специфичных задач, где нужна максимальная производительность под конкретную архитектуру GPU, ручное ядро может оказаться быстрее.
- Время компиляции. Thrust интенсивно использует шаблоны C++, и интенсивное использование сложных комбинаций алгоритмов и функторов может заметно увеличить время компиляции CUDA-кода по сравнению с написанием ядер вручную.
- Совместимость версий Thrust с версией CUDA Toolkit. Thrust поставляется как часть CUDA Toolkit, и использование отдельно установленной версии Thrust, не совпадающей с версией Toolkit, иногда приводит к трудно диагностируемым ошибкам компиляции или линковки.