Начало работы с JCuda
Авторы: jcuda.org
Автор перевода: В.В. Белый
Источник: jcuda.org
Содержание
- Введение — Общая информация о JCuda
- Установка — Установка и базовая настройка JCuda
- Первичное тестирование — Создание и проверка простейшего проекта
- Создание ядер — Как создавать, компилировать и запускать ядра CUDA с помощью JCuda
Введение
CUDA предоставляет два различных API: Runtime API и Driver API. Они оба очень похожи с точки зрения базовых задач, таких как работа с памятью. Однако, между ними есть некоторые важные различия. Самое главное из них - это способ управления ядрами и то, как они исполняются.
В оригинальном CUDA Runtime API ядра определяются и компилируется вместе с С-файлами. Исходный код компилируется с помощью NVCC - NVIDIA CUDA Compiler. Этот компилятор использует другой компилятор C (например, GCC или VS C), чтобы откомпилировать чистый C-код, после чего берется за компиляцию CUDA-составляющей - ядер и их вызовов (<<<...>>>). В результате получается исполнимый файл, представляющий целую программу.
Конечно же, NVCC не может использоваться для компиляции Java-программ. Синтаксис вызова ядер (<<<...>>>) неупотребим в Java, и после компиляции тут не создается единого исполнимого файла. Поэтому не существует возможности вызывать свои собственные ядра с помощью JCuda Runtime API. Вместо этого должен использоваться JCuda Driver API, как это будет показано ниже.
JCuda Runtime API в основном предназначен для взаимодействия с Java-привязками библиотек CUDA Runtime, такими как JCublas и JCufft. Java-программист, который хочет воспользоваться этими библиотеками и не хочет создавать собственные ядра может обойтись только JCuda Runtime API.
Установка
Перед тем, как использовать JCuda, необходимо установить драйвер и инструменты CUDA, которые можно загрузить на сайте NVIDIA CUDA. Стоит обратить внимание, что между выпусками новых версий CUDA и JCuda могут быть задержки. Для корректной инсталяции данного ПО, нужно следовать инструкциям на сайте.
SDK и примеры исходных кодов для работы JCuda не нужны, но последние могут быть полезны, чтобы разобраться в общих механизмах работы CUDA.
После корректной инсталяции CUDA следует скачать нужный архив JCuda в зависимости от операционной системы. Архив содержит JAR-файлы и соответствующие нативные библиотеки.
JAR-файлы нужно добавить в CLASSPATH, а нативные библиотеки нужно расположить в доступном Java месте. В большинстве случаев это либо java.library.path конкретной JVM, либо корневая директория проекта. Также можно воспользоваться средствами операционной системы - например, PATH или LD_LIBRARY_PATH.
Первичное тестирование
В этом разделе дается пояснение, как выполнить запуск проекта JCuda с помощью консоли команд. Тем, кто знаком с использованием JAR-файлов и нативных библиотек в Java, возможно, стоит пропустить этот раздел и перейти к созданию нового проекта из любимой IDE. Иначе нужно выполнить следующие шаги:
- Скопировать все файлы из скачанного архива JCuda в одну папку и добавить туда файл "JCudaRuntimeTest.java" со следующим содержимым:
import jcuda.*; import jcuda.runtime.*; public class JCudaRuntimeTest { public static void main(String args[]) { Pointer pointer = new Pointer(); JCuda.cudaMalloc(pointer, 4); System.out.println("Pointer: "+pointer); JCuda.cudaFree(pointer); } }
- Скомпилировать программу, выполнив следующую команду в директории проекта (нужно подставить актуальный номер вместо "0.3.2a"):
On Windows: javac -cp ".;jcuda-0.3.2a.jar" JCudaRuntimeTest.java On Linux: javac -cp ".:jcuda-0.3.2a.jar" JCudaRuntimeTest.java
После этого будет создан файл "JCudaRuntimeTest.class" в этой же директории. - Запустить программу с помощью следующей команды:
On Windows: java -cp ".;jcuda-0.3.2a.jar" JCudaRuntimeTest On Linux: java -cp ".:jcuda-0.3.2a.jar" JCudaRuntimeTest
После этого будет выведена на экран информация о созданном в программе указателе.
Создание ядер
Как было сказано во введении, собственные ядра могут быть запущены с использованием Driver API. В этом разделе описывается стандартный рабочий процесс создания ядра в рамках JCuda. Большая часть информации относится как к CUDA, так и JCuda. Так что будут полезными ресурсы, описывающие CUDA - например, CUDA Programming Guide.
Написание ядра
Код ядра пишется точно так же, как это делается для CUDA. Обычно код ядра располагется в отдельном файле. В CUDA Runtime API ядро часто является частью большего файла C. В таком случае весь дополнительный код будет проигнорирован JCuda.
Нужно уделить внимание следующему моменту: когда ядро будет вызвано с помощью Driver API, функция будет идентифицироваться по имени. Поэтому её нужно отметить как extern "C", чтобы компилятор C не поменял ее имя. Пример функции сложения векторов:
extern "C"
__global__ void add(int n, float *a, float *b, float *sum)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i<n)
{
sum[i] = a[i] + b[i];
}
}
Компиляция ядра
Исходный код ядра должен компилироваться с помощью NVCC. В результате будет создан файл, который может быть загружен и исполнен с помощью Driver API. Возможны два формата файлов для компиляции ядра:
- PTX - человеко-читаемый (но довольно сложно-понимаемый) файл, содержащий своеобразный "ассемблерный" код.
- CUBIN (CUDA binary) - откомпилированный под конкретный GPU код, который будет исполняться без дополнительных затрат.
Ранее повсеместно использовались CUBIN-файлы, но сейчас предпочтенее отдается PTX, которые являются переносимыми и используют компиляцию на лету.
Подробнее об этих форматах, и командах для их создания можно узнать в документации по NVCC.
Загрузка и исполнение ядра в JCuda
Процесс загрузки и исполнения ядра в JCuda Driver API из PTX- и CUBIN- файлов одинаков, так же, как и в CUDA Driver API. В первую очередь должен быть загружен модуль и получен указатель на функцию ядра:
// Load the ptx file.
CUmodule module = new CUmodule();
cuModuleLoad(module, "JCudaVectorAddKernel.ptx");
// Obtain a function pointer to the kernel function.
CUfunction function = new CUfunction();
cuModuleGetFunction(function, module, "add");
При вызове ядра проявляются некоторые языковые ограниченя Java. Функции установки параметров ядра довольно сложны в версиях вплоть до CUDA 3.2, а также в CUDA 4.0. Эти функции были заменены одной, которая принимает все параметры выполнения ядра. Дополнительно она принимает все входные параметры ядра в качестве одного void** указателя, который эмулируется с помощью класса Pointer. Таким образом, установка параметров ядра в JCuda может быть даже проще, чем в CUDA:
// Set up the kernel parameters: A pointer to an array
// of pointers which point to the actual values.
Pointer kernelParameters = Pointer.to(
Pointer.to(new int[]{numElements}),
Pointer.to(deviceInputA),
Pointer.to(deviceInputB),
Pointer.to(deviceOutput)
);
// Call the kernel function.
cuLaunchKernel(function,
gridSizeX, 1, 1, // Grid dimension
blockSizeX, 1, 1, // Block dimension
0, null, // Shared memory size and stream
kernelParameters, null // Kernel- and extra parameters
);
Стоит отметить, что тут проявляются проблемы, характерные для C — нужно быть крайне внимательными при работе с указателями.
В принципе, все программы JCuda имеют характерный паттерн. Это позволяет продолжить изучение JCuda на основе примеров.