IgdrasilCompute
Модуль вычислений на GPU поверх OpenCL (через Silk.NET.OpenCL). Предоставляет удобные обёртки для типичных GPU-операций с фокусом на безопасность ресурсов и удобство использования.
Архитектура
- ComputeManager: управление инициализацией, выбор устройства, создание контекста OpenCL
- CommandQueue: очередь команд для выполнения ядер и синхронизации
- Program: загрузка и компиляция OpenCL C кода
- Kernel: создание вычислительного ядра, установка аргументов, запуск
- MemoryObject
: GPU-буфер для скалярных данных с операциями чтения/записи - ComputeImage
: GPU-изображение с поддержкой различных форматов - ScalableMemoryObject
: буфер, синхронизированный с динамическим ScalableArray<T>
Все ресурсы реализуют IDisposable и могут использоваться в using блоках. Попытка доступа к выгруженным ресурсам выбросит ObjectDisposedException.
Быстрый старт: векторное сложение
using IgdrasilEngine.Engine.Runtime.GPU;
using Silk.NET.OpenCL;
// 1) Инициализация (единожды)
ComputeManager.Initialize(DeviceType.Gpu, DeviceType.Cpu);
using var queue = new CommandQueue();
// 2) Исходник ядра (обычно из файла)
const string kernelSrc = @"
__kernel void add(__global const float* a,
__global const float* b,
__global float* c)
{
int i = get_global_id(0);
c[i] = a[i] + b[i];
}";
// 3) Сборка программы
using var program = new Program(kernelSrc);
var kernel = program.CreateKernel("add");
// 4) Подготовка данных
int n = 1024;
float[] a = Enumerable.Range(0, n).Select(i => (float)i).ToArray();
float[] b = Enumerable.Range(0, n).Select(i => (float)(2 * i)).ToArray();
// 5) Выделение GPU-памяти
using var aBuf = MemoryObject<float>.CreateReadOnly(a);
using var bBuf = MemoryObject<float>.CreateReadOnly(b);
using var cBuf = MemoryObject<float>.CreateWriteOnly((uint)n);
// 6) Запуск ядра (удобный способ через Run1D)
kernel.SetArgs(aBuf, bBuf, cBuf);
kernel.Run1D(queue, (nuint)n); // автоматические разумные defaults
queue.Wait();
// 7) Чтение результатов в стеке (без лишних выделений)
Span<float> result = stackalloc float[Math.Min(n, 256)];
cBuf.Read(queue, 0, result); // чтение первых 256 элементов
Когда что использовать
ComputeManager
Используйте: Один раз в начале приложения для инициализации.
// Инициализация с предпочтением GPU, резервный вариант — CPU
ComputeManager.Initialize(DeviceType.Gpu, DeviceType.Cpu);
// После инициализации доступны глобальные объекты
var device = ComputeManager.Device;
var context = ComputeManager.Context;
Не делайте: Не вызывайте Initialize несколько раз — это переинициализирует контекст.
CommandQueue
Используйте: Для каждого потока или логической последовательности операций.
// Хорошо: одна очередь для batch-операций
using var queue = new CommandQueue();
for (int i = 0; i < 100; i++)
{
kernel.Run1D(queue, (nuint)size);
}
queue.Wait();
// Хорошо: разные очереди для параллельных операций (если драйвер поддерживает)
using var queue1 = new CommandQueue();
using var queue2 = new CommandQueue();
kernel1.Run1D(queue1, (nuint)n1);
kernel2.Run1D(queue2, (nuint)n2);
queue1.Wait();
queue2.Wait();
Не делайте: Не используйте старую очередь после Dispose().
Program и ProgramBuildOptions
Используйте: Загружайте программы один раз и переиспользуйте несколько ядер.
// Оптимально: загрузить один раз
using var program = Program.FromFile("kernels.cl",
new ProgramBuildOptions()
.EnableFastMath(true)
.SetStandard("CL2.0"));
var kernel1 = program.CreateKernel("process");
var kernel2 = program.CreateKernel("filter");
kernel1.Run1D(queue, n);
kernel2.Run1D(queue, n);
При ошибке компиляции:
try
{
using var program = new Program(badCode);
}
catch (ComputeException ex)
{
Console.WriteLine($"Compile error in {ex.OperationName}:");
if (ex.BuildLog != null)
Console.WriteLine(ex.BuildLog);
// ex.ErrorCode покажет конкретный код ошибки
}
Kernel: установка аргументов
Хорошо: SetArgs для быстрого заполнения
// Просто и читаемо
kernel.SetArgs(inputBuf, outputBuf, 1024, 3.14f);
kernel.Run2D(queue, (nuint)width, (nuint)height);
Хорошо: SetArgument для явности
kernel.SetArgument(0u, inputBuf);
kernel.SetArgument(1u, outputBuf);
kernel.SetArgument(2u, 1024);
kernel.SetArgument(3u, 3.14f);
kernel.Run2D(queue, (nuint)width, (nuint)height);
Не делайте: не смешивайте SetArgs и SetArgument
// ❌ Путано и опасно
kernel.SetArgument(0u, inputBuf);
kernel.SetArgs(outputBuf, 1024); // перезапишет аргумент 0!
Kernel: запуск
Хорошо: используйте Run1D/Run2D/Run3D
// 1D вычисления (array processing)
kernel.Run1D(queue, (nuint)arraySize); // local size = 64 по умолчанию
// 2D вычисления (image processing)
kernel.Run2D(queue, (nuint)width, (nuint)height); // local = 16x16
// 3D вычисления (volume processing)
kernel.Run3D(queue, (nuint)width, (nuint)height, (nuint)depth); // local = 8x8x1
Когда нужна кастомизация:
// Настройка работ-групп для оптимизации на конкретном железе
kernel.Run1D(queue, (nuint)n, 128); // 128 threads в группе
kernel.Run2D(queue, w, h, 8, 8); // 8x8 groups (64 threads)
kernel.Run3D(queue, w, h, d, 4, 4, 4); // 4x4x4 groups (64 threads)
Не делайте: не забывайте про queue.Wait()
// ❌ Опасно: ядро может ещё выполняться
kernel.Run1D(queue, (nuint)n);
var data = new float[n];
buf.Read(queue, 0, data);
kernel.Dispose(); // Поток может быть не завершён!
// ✅ Правильно
kernel.Run1D(queue, (nuint)n);
buf.Read(queue, 0, data);
queue.Wait(); // дождались завершения
kernel.Dispose(); // безопасно
Работа с памятью
MemoryObject
Создание с нужными флагами:
// Для данных, которые только читает ядро
using var input = MemoryObject<float>.CreateReadOnly(hostData);
// Для данных, которые ядро читает и пишет
using var buffer = MemoryObject<float>.CreateReadWrite((uint)size);
// Для результатов (только пишет ядро)
using var output = MemoryObject<float>.CreateWriteOnly((uint)size);
Эффективное чтение результатов с Span:
// ❌ Плохо: лишнее выделение памяти
var result = new float[1000];
buf.Read(queue, 0, result);
// ✅ Хорошо: стековая память для маленьких batch-операций
Span<float> result = stackalloc float[256];
buf.Read(queue, 0, result);
// ✅ Хорошо: большие блоки через yield/streaming
const int batchSize = 1024;
var batch = new float[batchSize];
for (int offset = 0; offset < total; offset += batchSize)
{
buf.Read(queue, (uint)offset, batch);
ProcessBatch(batch);
}
Доступ к свойствам буфера:
using var buf = MemoryObject<float>.CreateReadWrite(1000);
uint count = buf.Count; // 1000
nuint sizeBytes = buf.SizeInBytes; // 4000 (sizeof(float) * 1000)
var flags = buf.Flags; // MemFlags.ReadWrite
ScalableMemoryObject
Для динамических структур данных:
using var arr = new ScalableArray<float>(initialSize: 1024);
using var queue = new CommandQueue();
using var gpuBuffer = new ScalableMemoryObject<float>(queue, arr, MemFlags.ReadWrite);
// GPU буфер автоматически растёт вместе с arr
arr.AddRange(newData); // буфер пересоздан, данные перекопированы
kernel.SetArgument(0u, gpuBuffer);
kernel.Run1D(queue, (nuint)arr.Count);
queue.Wait();
Не делайте: не ожидайте синхронизации вне Run/Wait
// ❌ Данные могут не совпадать
arr.Clear();
kernel.Run1D(queue, (nuint)arr.Count); // размер может отличаться от памяти GPU
// ✅ Явно синхронизируйте если нужно
arr.Clear();
queue.Wait(); // убедитесь, что GPU закончил предыдущие операции
Работа с изображениями
Создание изображений с helper-классами:
// 2D RGBA float изображение
var fmt = Image2D.CreateRGBAFloat();
var desc = Image2D.CreateDesc((nuint)width, (nuint)height);
using var img = new ComputeImage<float>(MemFlags.ReadWrite, fmt, desc);
// 3D RGBA изображение
var fmt3d = Image3D.CreateRGBAFloat();
var desc3d = Image3D.CreateDesc((nuint)w, (nuint)h, (nuint)d);
using var img3d = new ComputeImage<float>(MemFlags.ReadWrite, fmt3d, desc3d);
Удобная работа с регионами (вместо сырых nuint[]):
// ❌ Старый способ: легко ошибиться
nuint[] origin = [10, 20, 0];
nuint[] region = [256, 256, 0];
img.Read(queue, buf, origin, region, 0, 0);
// ✅ Новый способ: типизированный и ясный
var origin = new ImageOrigin(10, 20, 0);
var region = new ImageRegion(256, 256, 0);
img.Read2D(queue, buf, origin, region);
Копирование между изображениями:
var srcOrigin = new ImageOrigin(0, 0, 0);
var dstOrigin = new ImageOrigin(10, 10, 0);
var region = new ImageRegion(100, 100, 0);
img1.CopyTo2D(queue, img2, srcOrigin, dstOrigin, region);
queue.Wait();
Обработка ошибок
Все операции с GPU выбрасывают ComputeException (наследует IgdrasilException). Исключения содержат:
ErrorCode: код ошибки OpenCLOperationName: какая операция завершилась с ошибкойBuildLog: для ошибок компиляции — полный лог сборки
try
{
kernel.Run1D(queue, (nuint)size);
}
catch (ComputeException ex)
{
// ex.ErrorCode — код ошибки (например, InvalidWorkGroupSize)
// ex.OperationName — "EnqueueNdrangeKernel"
Console.WriteLine($"[{ex.OperationName}] {ex.ErrorCode}: {ex.Message}");
}
Практические советы
Производительность
Выбирайте правильный размер работ-группы (local size):
- NVIDIA: 32, 64, 128, 256 (warp size = 32)
- AMD: 64, 128, 256 (wave size = 64)
- Intel: 8, 16, 32, 64
Минимизируйте host↔GPU трансферы:
- Читайте результаты батчами, а не поэлементно
- Используйте
Span<T>вместо выделения новых массивов - Держите промежуточные данные на GPU
Кэшируйте программы:
// Хорошо: загрузить один раз using var program = Program.FromFile("kernels.cl"); for (int i = 0; i < iterations; i++) { var kernel = program.CreateKernel("process"); kernel.SetArgs(...); kernel.Run1D(queue, n); }
Отладка
- Проверьте
BuildLogпри ошибке компиляции - Убедитесь в правильных флагах памяти (
ReadOnly,ReadWrite,WriteOnly) - Используйте
Flush()для гарантированного выполнения команд - Логируйте исключения — они интегрированы с
IgdrasilLogger
Безопасность ресурсов
- Всегда используйте
usingдляProgram,Kernel,MemoryObject<T>,ComputeImage<T>,ScalableMemoryObject<T> - Очередь (
CommandQueue) можно переиспользовать - Используйте
queue.Wait()перед чтением результатов - Используйте
queue.Flush()если нужна гарантированная отправка команд