Вычислить шейдер с numthreads (1,1,1) работает очень медленно
Я только начинаю изучать программирование DirectX, используя F# и SharpDX в качестве оболочки.NET. В качестве тестового примера я отображаю множество Мандельброта. Вычисление выполняется с использованием 2 вычислительных шейдеров.
Первый шейдер вычисляет глубину для каждого пикселя (функция "CalcMandel"), результаты сохраняются в RWStructuredBuffer. Это вычисление требует большого количества одинарных или двойных умножений, но на моем графическом процессоре это происходит с невероятной скоростью (AMD 7790). "CalcMandel" имеет атрибут
[numthreads(16, 16, 1)]
и отправляется через
context.Dispatch (imageWidth / 16, imageHeight / 16, 1)
Никаких проблем здесь нет - изображение 1000 x 800 пикселей "основного" набора Мандельброта работает с частотой более 1000 кадров в секунду (с использованием одинарной точности на графическом процессоре).
Второй шейдер почти ничего не делает: он вычисляет минимальное, максимальное и среднее значения предыдущих вычислений (функция "CalcMinMax"). "CalcMinMax" имеет атрибут
[numthreads(1, 1, 1)]
и вызывается через
context.Dispatch (1,1,1)
Для данного заданного размера изображения один поток GPU должен пройти через буфер более 800 000 целых чисел, чтобы вычислить минимальное, максимальное и среднее значение. Я использую один поток, потому что я не знаю, как реализовать этот расчет параллельно.
ПРОБЛЕМА: "CalcMinMax" ужасно медленный: частота кадров падает с более чем 1000 до 5 кадров в секунду!
МОИ ВОПРОСЫ: Что здесь не так? Я использую неправильные настройки / параметры (numthreads)? Как я могу ускорить расчет min-max?
МОИ МЫСЛИ: Мое первое предположение было, что доступ к RWBuffer может быть медленным - это не тот случай. Когда я заменил доступ к буферу константой, частота кадров не увеличилась.
Мой GPU имеет ок. 900 шейдерных ядер и используют тысячи потоков для вычисления набора Мандельброта, в то время как CalcMinMax использует только один поток. Тем не менее, я до сих пор не понимаю, почему все так медленно.
Буду признателен за любой совет!
================================================
// HLSL CONTENT (расчет множества Мандельброта опущен):
cbuffer cbCSMandel : register( b0 )
{
double a0, b0, da, db;
double ja0, jb0;
int max_iterations;
bool julia; int cycle;
int width; int height;
double colorFactor;
int algoIndex;
int step;
};
struct statistics
{
int minDepth;
int axDepth;
float avgDepth;
int loops;
};
RWStructuredBuffer<float4> colorOutputTable : register (u0);
StructuredBuffer<float4> output2 : register (t0);
RWStructuredBuffer<int> counterTable : register (u1);
RWStructuredBuffer<float4> colorTable : register (u2);
RWStructuredBuffer<statistics>statsTable : register (u3);
// Mandelbrot calculations….
// Results are written to counterTable and colorOutputTable
// I limit the samples to 10000 pixels because calcMinMax is too slow
#define NUM_SAMPLES 10000;
void calcMinMax()
{
int minDepth = 64000;
int maxDepth = 0;
int len = width * height;
int crit = len / NUM_SAMPLES;
int steps = max (crit, 1);
int index = 0;
int sumCount = 0;
float sum = 0.0;
while (index < len)
{
int cnt = counterTable[index];
minDepth = cnt < minDepth & cnt > 0 ? cnt : minDepth;
maxDepth = cnt > maxDepth ? cnt : maxDepth;
sum += cnt > 0 ? cnt : 0.0f;
sumCount += cnt > 0 ? 1 : 0;
index += steps;
}
statsTable[0].minDepth = minDepth;
statsTable[0].maxDepth = maxDepth;
statsTable[0].avgDepth = sum / sumCount;
statsTable[0].loops += 1;
}
[numthreads(1, 1, 1)]
void CalcMinMax ( uint3 Gid : SV_GroupID, uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint GI : SV_GroupIndex )
{
switch (GI) // this switch is used to verify GI number (always 0)
{
case 0: calcMinMax();
break;
default: ;
break;
}
}
// ******************* F# программа (мин-макс партия) *************
настройка шейдера:
use minMaxShaderCode = ShaderBytecode.CompileFromFile(shaderPath, "CalcMinMax", "cs_5_0")
minMaxShader <- new ComputeShader(device, minMaxShaderCode.Bytecode.Data )
использование шейдеров:
// ---------- CONNECT MinMap Shader
context.ComputeShader.Set(minMaxShader)
context.ComputeShader.SetUnorderedAccessView(STATS_SLOT, statsBuffer.BufferView)
context.ComputeShader.SetConstantBuffer(CONSTANT_SLOT, constantBuffer)
context.ComputeShader.SetUnorderedAccessView (COUNTER_SLOT, dataBuffer.BufferView)
context.Dispatch (1,1,1)
// ---------- DISCONNECT MinMap Shader
context.ComputeShader.SetConstantBuffer(CONSTANT_SLOT, null)
context.ComputeShader.SetUnorderedAccessView (STATS_SLOT, null)
context.ComputeShader.SetUnorderedAccessView (COUNTER_SLOT, null)
context.ComputeShader.Set (null)
чтение статистики:
context.CopyResource(statsBuffer.DataBuffer, statsBuffer.StagingBuffer)
let boxer, stream = context.MapSubresource(statsBuffer.StagingBuffer, MapMode.Read, MapFlags.None)
calcStatistics <- stream.Read<CalcStatistics>()
context.UnmapSubresource(statsBuffer.DataBuffer, 0)
2 ответа
Если вы отправляете только 1 поток, каждый шейдерный блок, кроме одного, на вашем графическом процессоре будет простаивать в ожидании завершения этого потока. Вам нужно распараллелить ваш алгоритм minmax и, учитывая, что вам нужно вычислить массив значений, чтобы получить одно единственное значение, это типичная проблема сокращения. Более эффективный подход заключается в рекурсивном вычислении локальных минимальных и максимальных значений. Подробное объяснение с примером суммирования значений массива можно увидеть здесь (начиная со слайда 19).
Большое спасибо за ваш отзыв. На мой вопрос ответили.
В своем ответе Аханубис поделился ссылкой на документ в формате PDF, описывающий проблему уменьшения карты на графических процессорах. Прежде чем опубликовать свой вопрос в stackru, я провел обширный поиск в Интернете - и я уже нашел эту статью и прочитал ее дважды!
Почему я все еще упускаю суть? Время восстановления 8 мс на массиве 4M в худшем случае показалось мне приемлемым (имея всего 800 000 точек). Но я не осознавал, что даже наихудший случай в демоверсии был, по крайней мере, в 100 раз быстрее, чем мой однопоточный подход, потому что он использует 128 групп потоков.
Я буду использовать концепции, изложенные в статье, для реализации многопоточной версии моего расчета min-max.