Практический guide по IgdrasilCompute API
Данная документация описывает практические паттерны использования API. Для полного списка методов и параметров обратитесь к XML документации в коде.
Жизненный цикл GPU приложения
Initialize → CommandQueue → Program → Kernel → Run → Wait → Read → Dispose
- Initialize (один раз):
ComputeManager.Initialize() - CommandQueue (создать): новая очередь для серии операций
- Program (загрузить): исходник ядер из файла или строки
- Kernel (создать): извлечь конкретное ядро из программы
- SetArguments: заполнить аргументы (буферы, скаляры)
- Run: запустить ядро в GPU
- Wait: синхронизировать хост и GPU
- Read: прочитать результаты (опционально)
- Dispose: освободить ресурсы в обратном порядке
Обработка ошибок и отладка
Все операции с GPU выбрасывают ComputeException (наследует IgdrasilException). Исключения содержат:
ErrorCode: код ошибки OpenCLOperationName: какая операция завершилась с ошибкойBuildLog: для ошибок компиляции — полный лог сборки
try
{
kernel.Run1D(queue, (nuint)size);
queue.Wait();
}
catch (ComputeException ex)
{
Console.WriteLine($"Error Code: {ex.ErrorCode}");
Console.WriteLine($"Operation: {ex.OperationName}");
if (ex.BuildLog != null)
Console.WriteLine($"Build Log:\n{ex.BuildLog}");
}
Частые ошибки:
InvalidWorkGroupSize: local size не кратна требованиям GPUOutOfMemory: GPU память исчерпанаInvalidKernel: ядро не найдено в программеBuildProgramFailure: синтаксис ошибка (смотрите BuildLog)
Инициализация и контекст
Первый запуск:
// Единожды в начале приложения
ComputeManager.Initialize(DeviceType.Gpu, DeviceType.Cpu);
// fallback на CPU если GPU недоступна
var device = ComputeManager.Device;
Console.WriteLine($"Computing on: {device}");
Многопоточность:
// ✅ Правильно: Initialize один раз из главного потока
ComputeManager.Initialize();
// Каждый рабочий поток создаёт свою CommandQueue
Task.Run(() => {
using var queue = new CommandQueue();
kernel.Run1D(queue, n);
});
Загрузка и компиляция программ
С опциями сборки:
var buildOptions = new ProgramBuildOptions()
.DefineSymbol("PRECISION", "2") // -D PRECISION=2
.EnableFastMath(true) // оптимизация
.SetStandard("CL2.0") // версия стандарта
.AddOption("-w"); // suppress warnings
using var program = Program.FromFile("kernels.cl", buildOptions);
Кэширование программ:
// Хорошо: одна программа, несколько ядер
using var program = Program.FromFile("kernels.cl");
var kernel1 = program.CreateKernel("preprocess");
var kernel2 = program.CreateKernel("process");
var kernel3 = program.CreateKernel("postprocess");
kernel1.Run1D(queue, n);
kernel2.Run1D(queue, n);
kernel3.Run1D(queue, n);
Установка аргументов
Скалярные аргументы:
kernel.SetArgument(0u, 42); // int
kernel.SetArgument(1u, 3.14f); // float
kernel.SetArgument(2u, true); // bool
GPU-память:
using var buf = new MemoryObject<float>(data, MemFlags.ReadOnly);
kernel.SetArgument(3u, buf);
Пакетная установка (SetArgs):
// Порядок совпадает с параметрами ядра
kernel.SetArgs(
inputBuffer, // __global const float* input
outputBuffer, // __global float* output
1024, // int width
3.14f // float scale
);
kernel.Run1D(queue, (nuint)n);
Когда используется что:
SetArgs: быстрое заполнение простых ядер (меньше кода)SetArgument: явное управление, условная установка, документирование
Запуск ядер
1D запуск (array processing):
kernel.Run1D(queue, 1024); // default local = 64
kernel.Run1D(queue, 1024, 128); // кастомный размер
2D запуск (image processing):
kernel.Run2D(queue, 512, 512); // default local = 16x16
kernel.Run2D(queue, 2048, 2048, 32, 32);
3D запуск (volume processing):
kernel.Run3D(queue, 64, 64, 64); // default local = 8x8x1
kernel.Run3D(queue, 256, 256, 256, 16, 16, 4);
Выбор work-group size:
| Hardware | Оптимально |
|---|---|
| NVIDIA (Warp=32) | 64, 128, 256 |
| AMD (Wave=64) | 64, 128, 256 |
| Intel iGPU | 8, 16, 32, 64 |
| CPU | 1-4 |
Безопасные defaults:
kernel.Run1D(queue, n, 64);
kernel.Run2D(queue, w, h, 16, 16);
kernel.Run3D(queue, w, h, d, 8, 8, 1);
Управление памятью
Создание буферов с правильными флагами:
// Только чтение
using var input = MemoryObject<float>.CreateReadOnly(hostData);
// Чтение и запись
using var working = MemoryObject<float>.CreateReadWrite((uint)size);
// Только запись (результаты)
using var output = MemoryObject<float>.CreateWriteOnly((uint)size);
Флаги влияют на производительность:
ReadOnly: ядро может кэшировать данныеWriteOnly: GPU оптимизирует записьReadWrite: универсальный, медленнее
Эффективное чтение результатов:
// ❌ Плохо: выделение каждый раз
float[] results = new float[10000];
gpuBuffer.Read(queue, 0, results);
// ✅ Хорошо: стеком для порций
Span<float> batch = stackalloc float[256];
for (int i = 0; i < 10000; i += 256)
{
gpuBuffer.Read(queue, (uint)i, batch);
ProcessBatch(batch);
}
// ✅ Хорошо: один большой read
float[] results = new float[10000];
gpuBuffer.Read(queue, 0, new Span<float>(results));
Свойства буфера:
using var buf = MemoryObject<float>.CreateReadWrite(1000);
uint count = buf.Count; // элементов
nuint sizeBytes = buf.SizeInBytes; // в байтах
MemFlags flags = buf.Flags; // доступа
Динамическая память
Синхронизация с ScalableArray:
using var hostArray = new ScalableArray<float>(1024);
using var queue = new CommandQueue();
using var gpuBuffer = new ScalableMemoryObject<float>(queue, hostArray, MemFlags.ReadWrite);
// GPU буфер автоматически растёт при расширении
hostArray.AddRange(newData);
kernel.SetArgument(0u, gpuBuffer);
kernel.Run1D(queue, (nuint)hostArray.Count);
Когда полезно:
- Динамические пулы объектов
- Streaming данных неизвестного размера
- Real-time рендеринг с переменным количеством примитивов
Когда избегать:
- Частые расширения (пересоздание буфера дорого)
- Critical latency сценарии
Работа с изображениями
Создание с helper-классами:
// ❌ Опасно
var fmt = new ImageFormat(ChannelOrder.RGBA, ChannelType.Float);
var desc = new ImageDesc(ImageType.Image2D, (nuint)w, (nuint)h, 0, 0, 0, 0, 0, 0);
// ✅ Ясно
var fmt = Image2D.CreateRGBAFloat();
var desc = Image2D.CreateDesc((nuint)w, (nuint)h);
using var img = new ComputeImage<float>(MemFlags.ReadWrite, fmt, desc);
Доступ к регионам:
var origin = new ImageOrigin(100, 50, 0);
var region = new ImageRegion(256, 256, 0);
using var buffer = new MemoryObject<float>(imageData, MemFlags.WriteOnly);
img.Read2D(queue, buffer, origin, region);
Копирование между изображениями:
src.CopyTo2D(queue, dst,
new ImageOrigin(0, 0, 0), // src
new ImageOrigin(100, 100, 0), // dst
new ImageRegion(512, 512, 0)); // размер
queue.Wait();
Синхронизация
Wait (ждём завершения):
// ❌ Опасно: читаем до завершения
kernel.Run1D(queue, n);
buf.Read(queue, 0, results);
// ✅ Правильно
kernel.Run1D(queue, n);
queue.Wait(); // синхронизация
buf.Read(queue, 0, results);
Flush (отправляем команды):
kernel.Run1D(queue, n);
queue.Flush(); // отправили на GPU
// Хост может подготовить следующие данные
var nextInput = PrepareNextBatch();
queue.Wait(); // убеждаемся, что первая операция закончилась
ReadResults(output1);
Утечки ресурсов
Гарантированное освобождение:
// ✅ Правильно
using (var queue = new CommandQueue())
using (var program = Program.FromFile("kernels.cl"))
{
var kernel = program.CreateKernel("process");
kernel.Run1D(queue, n);
}
// ✅ Или современный синтаксис
using var queue = new CommandQueue();
using var program = Program.FromFile("kernels.cl");
Выявление утечек:
// ❌ Утечка
var program = Program.FromFile("kernels.cl");
var kernel = program.CreateKernel("process");
kernel.Run1D(queue, n);
// program.Dispose() никогда не будет вызван
// ✅ Правильно
using var program = Program.FromFile("kernels.cl");
var kernel = program.CreateKernel("process");
kernel.Run1D(queue, n);
// автоматический Dispose
Performance Tips
1. Минимизируйте трансферы:
// ❌ Плохо: много маленьких read
for (int i = 0; i < 1000; i++)
{
buf.Read(queue, (uint)i, new float[1]);
}
// ✅ Хорошо: один большой read
float[] results = new float[1000];
buf.Read(queue, 0, results);
2. Используйте правильные флаги:
using var input = MemoryObject<float>.CreateReadOnly(data); // кэшируется
using var output = MemoryObject<float>.CreateWriteOnly(n); // быстро пишется
3. Трансформируйте на GPU:
// ❌ Плохо
var transformed = input.Select(x => x * 2 + 5).ToArray();
using var buf = new MemoryObject<float>(transformed, MemFlags.ReadOnly);
// ✅ Хорошо
const string kernel = @"
__kernel void transform(__global const float* in, __global float* out) {
int i = get_global_id(0);
out[i] = in[i] * 2.0f + 5.0f;
}";
4. Батчинг операций:
// ❌ Плохо: много маленьких запусков
for (int i = 0; i < 1000; i++)
{
kernel.SetArgument(0u, i);
kernel.Run1D(queue, 1000);
}
// ✅ Хорошо: один запуск с большей сеткой
kernel.Run1D(queue, 1000000);
Интеграция с логированием
Все ComputeException автоматически логируются:
[2026-01-24 14:30:45] ERROR [IgdrasilCompute] GPU | OpenCL | EnqueueNdrangeKernel
InvalidWorkGroupSize: Work group size 999 is not valid
Используйте для мониторинга GPU в production.
Автоматический debug-лог из kernel
Если нужен аналог printf, который библиотека сама подключает к ILogger, используйте встроенный debug buffer:
using var queue = new CommandQueue(device);
queue.EnableKernelDebug(logger, maxMessages: 512, messageBytes: 128);
using var program = Program.FromFile(device, "kernels.cl");
using var kernel = program.CreateKernel("simulate");
kernel.SetArgs(bodyBuffer, accelerationBuffer, deltaTime);
kernel.SetDebugArguments(queue, 3u); // args 3 и 4: counter + message buffer
kernel.Run1D(queue, (nuint)bodyCount);
queue.Wait(); // после Wait() сообщения автоматически попадут в logger.Debug(...)
Сигнатура kernel должна принять два дополнительных аргумента:
__kernel void simulate(
__global Body* bodies,
__global Acceleration* accelerations,
double delta_time,
__global int* debug_count,
__global uchar* debug_messages)
{
// ...
}
Простейший helper для OpenCL:
#define IG_DEBUG_MESSAGE_BYTES 128
inline void ig_debug_log(__global int* debug_count,
__global uchar* debug_messages,
const char* text)
{
int slot = atomic_inc(debug_count);
int base_index = slot * IG_DEBUG_MESSAGE_BYTES;
for (int i = 0; i < IG_DEBUG_MESSAGE_BYTES - 1 && text[i] != '\0'; ++i)
debug_messages[base_index + i] = (uchar)text[i];
debug_messages[base_index + IG_DEBUG_MESSAGE_BYTES - 1] = 0;
}
Это не настоящий OpenCL printf, но зато работает предсказуемо, не зависит от stdout драйвера и автоматически подключается на уровне IgdrasilCompute после queue.EnableKernelDebug(...).