Что нового

В прошлой статье я писал о запуске Alpaca на Эльбрусе. На момент написания той статьи оптимизации под Эльбрус не проводились. Однако теперь, благодаря стараниям @troosh можем протестировать Эльбрус уже с оптимизациями. ВНИМАНИЕ! Проект llama.cpp обновляется очень часто, и многое меняется. На данный момент это самая актуальная версия llama.cpp под Эльбрус. Эльбрус 16С поддерживает аппаратную виртуализацию, мне как раз выдали доступ к "виртуалке". Благодарность @shigorin

И сразу тесты

В прошлой статье я уже описал что делал. Поэтому тут я сразу начну с тестов.

Тест проводился следующей командой. Данная команда запрашивает рандомную шутку и подсчитывает время выполнения запроса.

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МГц) результаты вполне неплохие.

Комментарии (18)