Практический guide по IgdrasilCompute API

Данная документация описывает практические паттерны использования API. Для полного списка методов и параметров обратитесь к XML документации в коде.

Жизненный цикл GPU приложения

Initialize → CommandQueue → Program → Kernel → Run → Wait → Read → Dispose
  1. Initialize (один раз): ComputeManager.Initialize()
  2. CommandQueue (создать): новая очередь для серии операций
  3. Program (загрузить): исходник ядер из файла или строки
  4. Kernel (создать): извлечь конкретное ядро из программы
  5. SetArguments: заполнить аргументы (буферы, скаляры)
  6. Run: запустить ядро в GPU
  7. Wait: синхронизировать хост и GPU
  8. Read: прочитать результаты (опционально)
  9. Dispose: освободить ресурсы в обратном порядке

Обработка ошибок и отладка

Все операции с GPU выбрасывают ComputeException (наследует IgdrasilException). Исключения содержат:

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}");
}

Частые ошибки:

Инициализация и контекст

Первый запуск:

// Единожды в начале приложения
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);

Когда используется что:

Запуск ядер

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);

Флаги влияют на производительность:

Эффективное чтение результатов:

// ❌ Плохо: выделение каждый раз
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);

Когда полезно:

Когда избегать:

Работа с изображениями

Создание с 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(...).