Я реализовал виртуальный массив использование OpenCL и графических карт в качестве резервного хранилища, с кэшированием некоторых данных в ОЗУ с использованием алгоритма LRU и доступом к данным через несколько простых установщиков / получателей. Доступ к элементу массива происходит через блокировку страницы (с использованием массива мьютексов для всех независимых страниц), затем кеш LRU, затем передачу данных pcie в случае вытеснения и возврат с данными (или редактирование данных страницы, если это запись).
#include "GraphicsCardSupplyDepot.h"
#include "VirtualMultiArray.h"
#include "PcieBandwidthBenchmarker.h"
#include "CpuBenchmarker.h"
// testing
#include <random>
#include <iostream>
#include "omp.h"
constexpr bool TEST_BANDWIDTH=true;
constexpr bool TEST_LATENCY=false;
constexpr bool testType = TEST_BANDWIDTH;
// a test object for virtual array
// testing bandwidth: 512kB size
// testing latency: 8byte size
class Object
{
public:
Object():id(-1){}
Object(int p):id(p){}
const int getId() const {return id;}
private:
char data[testType?(1024*512 - 4):(4)];
int id;
};
int main()
{
// number of elements per cache page
const long long pageSize = 1;
// number of elements of array
const long long n = pageSize*(testType?1000:100000);
// number of benchmark runs
const int numTestsPerThread = 25;
// virtual array of objects in video memory
VirtualMultiArray<Object> test(n,GraphicsCardSupplyDepot().requestGpus(),pageSize,3,PcieBandwidthBenchmarker().bestBandwidth(10));
// heating cpu to get precise benchmark results
#pragma omp parallel for
for(long long j=0;j<n;j++)
{
test.set(j,Object(j));
}
// test for single thread, 2 threads, .. 64 threads
for(int i=1;i<=64;i++)
{
// benchmark set method
{
CpuBenchmarker bench(i*numTestsPerThread*sizeof(Object),std::string("scalar set, ")+std::to_string(i)+std::string("threads"),i*numTestsPerThread);
#pragma omp parallel for num_threads(i)
for(long long j=0;j<i;j++)
{
// random-access to data
std::random_device rd;
std::mt19937 rng(rd());
std::uniform_real_distribution<float> rnd(0,n-1);
for(int k=0;k<numTestsPerThread;k++)
{
int rndv = rnd(rng);
test.set(rndv,Object(rndv));
}
}
}
// benchmark get method
{
CpuBenchmarker bench(i*numTestsPerThread*sizeof(Object),std::string("scalar get, ")+std::to_string(i)+std::string("threads"),i*numTestsPerThread);
#pragma omp parallel for num_threads(i)
for(long long j=0;j<i;j++)
{
// random-access to data
std::random_device rd;
std::mt19937 rng(rd());
std::uniform_real_distribution<float> rnd(0,n-1);
for(int k=0;k<numTestsPerThread;k++)
{
int rndv = rnd(rng);
const auto obj = test.get(rndv);
if(obj.getId()!=rndv)
{
throw std::invalid_argument("Error: set/get");
}
}
}
}
std::cout<<"==================================================================="<<std::endl;
}
return 0;
}
Когда я тестирую его на Ubuntu 18.04, он использует до 3/4 совокупной пропускной способности системы pcie, но в Windows 10 он использует в основном 1/4 (что составляет всего 2 ГБ / с) пикового значения.
Моя система имеет следующие характеристики:
- fx8150
- 4 ГБ одноканальной оперативной памяти ddr3 (аппаратная пиковая скорость 10,6 ГБ / с)
- 3 видеокарты начального уровня (gt1030, 2xk420), поддерживающие OpenCL (пиковая скорость аппаратного обеспечения 8 ГБ / с)
- Режим драйвера TCC включен для 2 карт, но первая карта не может работать в режиме TCC
- g ++ — 10 для Ubuntu C ++ 17
- MSVC 2019 для Windows C ++ 17
Я попытался сделать его настолько независимым от платформы, насколько это возможно, но в одной части мне пришлось разветвлять некоторый код для выравнивания распределений. Что я делаю не так на стороне Windows? (помимо выровненных выделений)
Например, в Ubuntu я получаю такой вывод:
scalar set, 1threads: 12182103 nanoseconds (bandwidth = 1075.94 MB/s) (throughput = 487284.12 nanoseconds per iteration)
scalar get, 1threads: 10874521 nanoseconds (bandwidth = 1205.31 MB/s) (throughput = 434980.84 nanoseconds per iteration)
===================================================================
scalar set, 2threads: 15642385 nanoseconds (bandwidth = 1675.86 MB/s) (throughput = 312847.70 nanoseconds per iteration)
scalar get, 2threads: 16902450 nanoseconds (bandwidth = 1550.92 MB/s) (throughput = 338049.00 nanoseconds per iteration)
===================================================================
scalar set, 3threads: 15004388 nanoseconds (bandwidth = 2620.67 MB/s) (throughput = 200058.51 nanoseconds per iteration)
scalar get, 3threads: 16687201 nanoseconds (bandwidth = 2356.39 MB/s) (throughput = 222496.01 nanoseconds per iteration)
...
...
===================================================================
scalar set, 63threads: 212283324 nanoseconds (bandwidth = 3889.87 MB/s) (throughput = 134783.06 nanoseconds per iteration)
scalar get, 63threads: 136367146 nanoseconds (bandwidth = 6055.37 MB/s) (throughput = 86582.31 nanoseconds per iteration)
===================================================================
scalar set, 64threads: 229655008 nanoseconds (bandwidth = 3652.70 MB/s) (throughput = 143534.38 nanoseconds per iteration)
scalar get, 64threads: 149184573 nanoseconds (bandwidth = 5622.97 MB/s) (throughput = 93240.36 nanoseconds per iteration)
===================================================================
но в Windows он достигает максимальной скорости 2 ГБ / с с гораздо меньшим количеством потоков. Тестировал его и без кеширования LRU (просто прямое отображение страниц во vram), но была такая же разница в производительности.
Редактировать:
Согласно профилировщику производительности Visual Studio, генератор случайных чисел не является узким местом:
Когда я иду по горячему пути, он заканчивается командой OpenCL, которая запрашивает событие:
clGetEventInfo(evt, evtInf,sizeof(cl_int), &evtStatus0, nullptr)
в заголовке PageCache, строка 248.
Это вне проекта, может быть, это связано с тем, что моя система устарела? Или, может быть, дело в неправильном использовании API в другом месте, например, в каких-то неправильных флагах в построении буфера opencl?
Изменить 2:
Для Windows я удалил clGetEventInfo () и использовал блокирующую версию команд opencl для чтения / записи:
clEnqueueReadBuffer(q->getQueue(), gpu->getMem(), CL_TRUE
это дало + 25% производительности (2,5 ГБ / с вместо 2,0 ГБ / с) для теста в Windows, но теперь есть еще одно узкое место:
накладные расходы, связанные с ядром, почти равны накладным расходам pcie-i / o! Когда я иду по горячему пути, опять же, часть ввода-вывода является наиболее узким местом:
Означает ли это, что эти операции чтения / записи буфера OpenCL также проходят через ядро Windows? Я думаю, у них внутри занято-ожидание с какой-то блокировкой мьютекса, которая работает медленно? Когда я нажимаю «включить внешний код», он добавляет «nvopencl64.dll» в список горячих путей с тегом «ядро» в своей строке. Означает ли это, что драйвер nvidia использует некоторое время ожидания при синхронизации OpenCL, что снижает масштабируемость потокового ввода-вывода из-за конфликтов блокировки, связанных с Windows? В Ubuntu он в 2 раза быстрее с тем же графическим процессором nvidia и на 25% быстрее с явным циклом ожидания-ожидания с использованием событий. Итак, (я думаю) это больше связано с проблемой Windows, а не с Nvidia. Но не уверен.
Когда я не использую все 3 видеокарты одновременно, я получаю следующие результаты:
- GT1030: 1 ГБ / с
- K420 на 8-кратном слоте: 2 ГБ / с
- K420 с 4 разъемами: 1 ГБ / с
но когда я использую все 3 со многими потоками, я получаю всего 2,5 ГБ / с. Это не то же самое с Ubuntu, которая достигает 6 ГБ / с. Если бы существовал способ указать OpenCL использовать цикл ожидания-ожидания вместо цикла «занято-ожидание», тогда потоки фактически эффективно перекрывали бы ввод-вывод. Но в Windows они не только перестают масштабироваться при 8 потоках, но и уменьшаются после дополнительных потоков. В Ubuntu он постоянно увеличивается до 64 потоков. Может быть, проблема масштабируемости вызвана конфликтом из-за блокировки страниц внутри драйвера OpenCL?
Редактировать-3:
Когда я тестирую простой пример мьютекса:
int main()
{
std::mutex m;
for (int i = 0; i < 100; i++)
{
CpuBenchmarker bench;
for (int j = 0; j < 1000; j++)
{
std::unique_lock<std::mutex> l(m);
}
}
return 0;
}
профилировщик говорит, что 97% накладных расходов приходится на пространство ядра, а тестер выдает ~ 70 наносекунд на длительность блокировки. Процессор — FX8150 на 2,1 ГГц.
Наконец, выглядит ли структура кода нормально?




