Загоняем Альпаку на Эльбрус (Часть 2. Оптимизации)
Что нового
В прошлой статье я писал о запуске Alpaca на Эльбрусе. На момент написания той статьи оптимизации под Эльбрус не проводились. Однако теперь, благодаря стараниям @troosh можем протестировать Эльбрус уже с оптимизациями. ВНИМАНИЕ! Проект llama.cpp обновляется очень часто, и многое меняется. На данный момент это самая актуальная версия llama.cpp под Эльбрус.
И сразу тесты
В прошлой статье я уже описал что делал. Поэтому тут я сразу начну с тестов.
Тест проводился следующей командой. Данная команда запрашивет рандомную шутку и подсичтывает время выполнения запроса.
for a in {1..8};do printf "%s;" $a;./main -t $a -m ./models/ggml-alpaca-7b-q4.bin -s 42 -p "Random joke:" -n 32 2>&1 |grep "llama_print_timings: eval time" | cut -d "(" -f 2 | grep -o -e "[0-9\.]*" ;done
Ryzen 7 5800H | Эльбрус-16С | Эльбрус-8СВ |
707,81 | 903,02 | 1094,07 |
370,47 | 472,6 | 571,45 |
258,1 | 330,39 | 398,84 |
199,1 | 256,79 | 310,96 |
163,97 | 213,01 | 260,76 |
140,87 | 184,04 | 226,59 |
127,37 | 163,37 | 207,54 |
126,05 | 148,54 | 193,7 |
График скорости (меньше — лучше)
Тесты с оптимизацией и без.
Потоки | Ryzen 7 5800H | Эльбрус-16С (Оптимизировано) | Эльбрус-8СВ (Оптимизировано) | Эльбрус-8СВ 1550MHz | Эльбрус-16С 2000MHz |
1 | 707,81 | 903,02 | 1094,07 | 2542,64 | 2389,05 |
2 | 370,47 | 472,6 | 571,45 | 1279,05 | 1225,16 |
3 | 258,1 | 330,39 | 398,84 | 915,73 | 823,2 |
4 | 199,1 | 256,79 | 310,96 | 710,14 | 638,5 |
5 | 163,97 | 213,01 | 260,76 | 575,53 | 513,72 |
6 | 140,87 | 184,04 | 226,59 | 487,12 | 438,66 |
7 | 127,37 | 163,37 | 207,54 | 419,23 | 380,11 |
8 | 126,05 | 148,54 | 193,7 | 375,21 | 342,84 |
График скорости (Меньше — лучше)
Бенчмарк
тесты были сделаны при помощи benchmark-q4_0-matmult. Собиралось через команду: make benchmark
1 поток | FLOPS_per_u_Second | |
Ryzen 7 5800H | 3200МГц | 40205.95 |
Эльбрус-16С | 2000МГц | 22183.21 |
Эльбрус-8СВ | 1550МГц | 17452.88 |
И многопоточный тест
8 потоков | FLOPS_per_u_Second | |
Ryzen 7 5800H | 3200МГц | 255353.06 |
Эльбрус-16С | 2000МГц | 161953.14 |
Эльбрус-8СВ | 1550МГц | 129111.80 |
До оптимизации результаты были следующими:
1 поток | FLOPS_per_u_Second | |
Ryzen 7 5800H | 3200МГц | 40205.95 |
Эльбрус 16С | 2000МГц | 10761.69 |
Эльбрус 8СВ | 1550МГц | 7202.17 |
8 потоков | FLOPS_per_u_Second | |
Ryzen 7 5800H | 3200МГц | 255353.06 |
Эльбрус 16С | 2000МГц | 82397.41 |
Эльбрус 8СВ | 1550МГц | 55424.05 |
Что было сделано
Был оптимизирован код ggml.c под Эльбрус и конкретно под модель Q4_0. Немного пояснений от @troosh. В данной статье тесты проводились на модели Q4_0
Попытался оптимизировать работу в формате Q4_0 для e2k процессоров с 5-й и выше версией системы команд (для тех которые с 128-ми битными регистрами), выкладываю сюда: https://github.com/E2Kports/llama.cpp
Именно в этом формате проверят умножение матриц бенчмарк. А вот используемая в статье модель сконвертирована под Q4_1, на ней ускорения ждать не стоит. Нужно брать модели в Q4_0, либо подождать пока доработаю и этот формат.
А вообще, проект llama.cpp ну очень уж быстро меняется — пришлось пару раз под новые правки подстраиваться…
Ну и небольшой пример кода
#if defined(__e2k__) && __iset__ >= 5
static inline __v2di __attribute__((__always_inline__))
e2k_dot_4_0_8_0_quants(__v2di bx, __v2di by0, __v2di by1)
{
const __v2di lowMask = __builtin_e2k_qppackdl(0x0f0f0f0f0f0f0f0fLL,
0x0f0f0f0f0f0f0f0fLL);
const __v2di bais = __builtin_e2k_qppackdl(0x0808080808080808LL,
0x0808080808080808LL);
const __v2di ones = __builtin_e2k_qppackdl(0x0001000100010001LL,
0x0001000100010001LL);
// Unpack nibbles into individual bytes
__v2di bx0 = __builtin_e2k_qpand( bx, lowMask ); // {HLhl} -> {oLol}
__v2di bx1 = __builtin_e2k_qpsrlh( bx, 4 ); // {HLhl} -> {oHLh}
bx1 = __builtin_e2k_qpand( bx1, lowMask ); // -> {oHoh}
// The output vectors contains 32 bytes, each one in [ 0 .. 15 ] interval
// Reorder bytes in "y" block to order in bx0,bx1
__v2di lo = __builtin_e2k_qppermb(by1, by0,
__builtin_e2k_qppackdl(0x1e1c1a1816141210LL,
0x0e0c0a0806040200LL));
__v2di hi = __builtin_e2k_qppermb(by1, by0,
__builtin_e2k_qppackdl(0x1f1d1b1917151311LL,
0x0f0d0b0907050301LL));
#if __iset__ >= 7
// Move each one in [ -8 .. +7 ] interval:
bx0 = __builtin_e2k_qpsubb(bx0, bais);
bx1 = __builtin_e2k_qpsubb(bx1, bais);
__v2di xy_int32 = __builtin_e2k_qpidotsbwss(bx0, lo, __builtin_e2k_qppackdl(0, 0));
xy_int32 = __builtin_e2k_qpidotsbwss(bx1, hi, xy_int32);
#else
// Get absolute values of "x" vectors:
__v2di ax0 = __builtin_e2k_qppermb(bx0 /* not used */,
__builtin_e2k_qppackdl(0x0706050403020100LL,
0x0102030405060708LL), bx0);
__v2di ax1 = __builtin_e2k_qppermb(bx1 /* not used */,
__builtin_e2k_qppackdl(0x0706050403020100LL,
0x0102030405060708LL), bx1);
// Move each one in [ -8 .. +7 ] interval:
bx0 = __builtin_e2k_qpsubb(bx0, bais);
bx1 = __builtin_e2k_qpsubb(bx1, bais);
// Sign the values of the y vectors
__v2di sy0 = __builtin_e2k_qpsignb(lo, bx0);
__v2di sy1 = __builtin_e2k_qpsignb(hi, bx1);
// Perform multiplication and create 16-bit values
__v2di dot0 = __builtin_e2k_qpmaddubsh(sy0, ax0);
__v2di dot1 = __builtin_e2k_qpmaddubsh(sy1, ax1);
// Reduce to 8 int16_t (overflow not possible: 8 bit * 4 bit => 12 bit)
__v2di dot = __builtin_e2k_qpaddh(dot0, dot1);
// Reduce to 4 int32_t by integer horizontal sums
__v2di xy_int32 = __builtin_e2k_qpmaddh(ones, dot);
#endif
// Convert vector of 4 int32_t to 4 floats
return __builtin_e2k_qpistofs(xy_int32);
}
Из интересного, в данном коде предусматривается работа с Эльбрус V7.
Заключение
На данный момент это лучшие из возможных результаты, в дальнейшем можно сделать оптимизации для модели Q4_1.
Благодаря оптимизациям под архитектуру VLIW можно добиться довольно хороших результатов. С учетом того что Ryzen 7 5800H произведен по техпроцессу 7нм и имеет частоту 3200МГц с ускорением до 4400МГц. А Эльбрус 16с произведен по 16нм техпроцессу и имеет 2000МГц (У 8СВ вообще 1550МГц) результаты вполне неплохие.