Ма ти не знаеш ли? Вече е срамно да се пише на асемблер. Гледат с лошо око, колегите.
Хм, моите колеги, като че ли не ме гледат с лошо око. Добре, че не са програмисти.
И като видиш как С пише по-оптимално от тебе...
хубаво е все пак да имаш job security, затова колкото повече асемблер, толкова по-добре
Кой ви учи на тез глупости ве, банкирането ми е мацано на .нет, кажи неко асемблерска банка, че по-сигурна 🤪 🤓 🤑 🙃 ...
Оп, сори, после разбрах ко искаше да кажеш. Дремя на едни лекции по многозадачност. Съгласен съм.
Абе BIGBUGEX, какво ще стане ако края на вектора е в невалидна памет? Ще гръмне ли програмата или AVX2 знае как да го оправи? Защото не го виждам в кода ти.
Ето моята SVE имплементация, базирана на оптимизирана версия на strchr. Тества за нула.
setffr /* initialize FFR */
ptrue p1.b /* all ones; loop invariant */
mov x1, xzr
dup z1.b, 'p' /* replicate byte across vector */
dup z2.b, 's' /* replicate byte across vector */
.p2align 4
0: ldff1b z0.b, p1/z, [x0, xzr]
rdffrs p0.b, p1/z
b.nlast 1f
/* First fault did not fail: the whole vector is valid.
Avoid depending on the contents of FFR beyond the branch. */
incb x0 /* speculate increment */
cmpeq p3.b, p1/z, z0.b, 0 /* search for 0 */
b.ne 1f
/* no end string */
brka p4.b, p1/z, p3.b /* find first 0 */
cmpeq p2.b, p4/z, z0.b, z1.b /* search for 'p' */
decp x1, p2.b /* count the predicates and subtract them from x1 */
cmpeq p3.b, p4/z, z0.b, z2.b /* search for 's' */
incp x1, p3.b /* count the predicates and add them to x1 */
b 0b
1: /* end string or first fault */
brka p4.b, p0/z, p3.b /* find first 0 */
cmpeq p2.b, p4/z, z0.b, z1.b /* search for 'p' */
decp x1, p2.b /* count the predicates and subtract them from x1 */
cmpeq p3.b, p4/z, z0.b, z2.b /* search for 's' */
incp x1, p3.b /* count the predicates and add them to x1 */
mov x0, x1
ret
Скоростта на Graviton4 e 1.86 пъти по-бавна от eqsub.
Ето и версията ми за SME. Това е реално Streaming SVE, но имплементацията е различна защото SSVE няма FFR и incp/decp са много бавни (мързи ме да обяснявам, но ако някой е любопитен може да го направя).
smstart
dup z1.b, 'p' /* replicate byte across vector */
dup z2.b, 's' /* replicate byte across vector */
dup z3.b, 0 /* all 0s for sel */
dup z4.b, 1 /* all 1s for sel */
dup z5.b, -1 /* all -1s for sel */
dup z8.s, 0 /* return value(s) */
ptrue p1.b /* all ones; loop invariant */
.p2align 4
0: ld1b z0.b, p1/z, [x0] /* read bytes in vector */
incb x0 /* speculate increment */
dup z9.h, 0 /* 32-bit count for the iteration */
cmpeq p3.b, p1/z, z0.b, 0 /* search for 0 */
b.ne 1f
/* no end of string */
cmpeq p2.b, p1/z, z0.b, z1.b /* search for 'p' */
sel z10.b, p2, z5.b, z3.b /* for each match, select -1s from z5 */
cmpeq p3.b, p1/z, z0.b, z2.b /* search for 's' */
sel z10.b, p3, z4.b, z10.b /* for each match, select 1s from z4 */
sadalp z9.h, p1/m, z10.b /* widen to 16 bit by adding pairwise elements*/
sadalp z8.s, p1/m, z9.h /* add to the total count, widening to 32 bit */
b 0b
/* end of string */
1: brka p4.b, p1/z, p3.b /* find first 0 */
cmpeq p2.b, p4/z, z0.b, z1.b /* search for 'p' */
sel z10.b, p2, z5.b, z3.b /* for each match, select -1s from z5 */
cmpeq p3.b, p4/z, z0.b, z2.b /* search for 's' */
sel z10.b, p3, z4.b, z10.b /* for each match, select 1s from z4 */
sadalp z9.h, p1/m, z10.b /* widen to 16 bit by adding pairwise elements*/
sadalp z8.s, p1/m, z9.h /* add to the total count, widening to 32 bit */
saddv d0, p1, z8.s /* reduce counts to scalar register */
fmov x0, d0
smstop
ret
Скоростта е десетки пъти по-бавна от eqsub на Apple M4. Явно SME не е много подходящ за такива дребни алгоритми.
Няма да гръмне. Подравнявам старта на 32 байта и със съответните битове за валидност инвалидирам операциите които са пред масива. От там на сетне обработвам адреси кратни на 32 байта. Ако изскочи '\0' в текущия пакет от данни се излиза от цикъла като се инвалидират бройките след него.
Много добре е това. Ще го разгледам кода въпреки че не вдявам много.
Може да няма лесен начин за тестване за 0 на SME. Също така е трудно без участието на генерални регистри да се инвалидират 'p' и 's' бройките идващи непосредствено след нулевия знак.
А, това с подравняването в началото е хитро, не се бях сетил. Ще помисля дали/как мога да го направя за SVE/SME. SME е 512 битови вектори, така че ще чете направо цяла cache line наведнъж.
Друго нещо, което трябва да пробвам е четенето наведнъж на 4 вектора (LD4x). Ще чете 256 байта за една инструкция. Не че е задължително да е по-бързо, но ще видим.
Да не отварям нова тема. Интересна дискусия и терминология за пакетиране на троична бройна система (-1,0,1) - бит, трит, тет, tits, и т.н. Пакетирането и разпакетирането стават и с неон/симд т.е. има поле за забавление.
https://compilade.net/blog/ternary-packing
Коментарите в хакер нюз също си струват - иде реч за цици даже
Моето наивно решение би било използване на таблица (по мои сметки около 512 байта дълга и лесно събираща се дори в L1), но може би SIMD ще е по-бързо, не съм го мислил в детайли.
BIGBUGEX сигурно може да каже повече.
Аз вече не бързам да се изказвам неподготвен и да тропвам с наполовина по-къса чурка по масата. Бих ползвал vpmulhuw за екстрактване от 16 битови думи. Но трябва да помисля допълнително.
Както казва @|, доста по-добре ще стане с таблица за всички 243 варианта от байта. То ще е бързо със SIMD но няма да е подредено. За подреждането се иска доста код и няма да е елегантно.
Вариант с подредено разопаковане. Но ме съмнява да е по-бързо от просто копиране от таблица. За 5 стойности се използват 10 инструкции.
#include <immintrin.h>
#include <cstdint>
#include <cstdio>
void tr_unpack( char s[8], uint8_t val ) {
uint16_t lval = ( ( (uint16_t)val << 8 ) + 242 ) / 243;
__m128i xval = _mm_set1_epi16( lval << 8 ),
xmul = _mm_setr_epi16( 1, 3, 3*3, 3*3*3, 3*3*3*3, 0,0,0 ),
x3 = _mm_set1_epi16( 3 )
;
xval = _mm_mullo_epi16( xval, xmul );
xval = _mm_mulhi_epu16( xval, x3 );
xval = _mm_add_epi16( xval, _mm_set1_epi16( '0' ) );
xval = _mm_packus_epi16( xval, _mm_setzero_si128() );
_mm_storeu_si64( s, xval );
s[5] = 0;
}
uint8_t tr_pack( char s[8] ) {
uint8_t res = 0;
for( int i = 0; i < 5; i++ ) {
res = res * 3 + ( s[i] - '0' );
}
return res;
}
int main() {
char s[8];
for( unsigned i = 0; i < 243; i++ ) {
tr_unpack( s, i );
if( tr_pack( s ) != i ) printf( "%3u fail: %s\n", i, s );
}
return 0;
}
Не съм гледал подробно кода в ggml (който е споменат в блог поста), но според мен идеята не е да се unpack-не само един байт със SIMD, а колкото може стойности от матрицата едновременно. И резултата вероятно не е цели числа, а fp8 и не се записват някъде в паметта, а се прави dot product с тях и се записва резултата (fp16 или fp32, не знам какво точно използват).
Интуицията ми казва, че с таблица би трябвало да е по-бързо за да може FP юнитите да се използват за умножаването на векторите паралелно със зареждането на следващите стойности, но може и да греша. Не претендирам да разбирам микроархитектурата на процесорите много добре.
Аз го погледнах. По точно кода от комита който добавя поддръжка за това тритово пакетиране. Изглежда читаво написано но не съм в час с векторните инструкции за да преценя доколко е ефективно.
Според мен най ефективно би било да се заредят примерно 4 пакетирани байта в 16 битови фиксед поинт фрагменти 8:8 и след това да се вадят наведнъж с по едно умножение в паралел по един трит.
Проблемът е какво ще ги правиш тези 20 стойности след като ги разпакетираш. Основната причина да се прави куантизация на моделите е да се намали трафика на данни от/до паметта. Ако разпакетираш и записваш в паметта, правиш повече трафик. Ако разпакетираш и използваш за сметки, 20 стойности не влизат точно в никакви векторни регистри. Най-вероятно 4 от стойностите ще трябва да се изхвърлят и следващия път да се разпакетират пак, (с малко по-различен алгоритъм за да се окажат на правилното място във векторните регистри).
Затова микробенчмарките са доста малоумно занятие. Отделят някаква важна, но дребна логика и оптимизираш като улав неща, които няма да се случат по същия начин в реалния живот.
Т.е. това пакетиране на 5 стойности в 1 байт не решава кой знае какви проблеми и е по добре да се кодират 4 стойности по 2 бита за да не се минава през излишно междинно декомпресиране?
То тогава единствената разумна причина да компресираш по 5 стойности в байт е да спестиш едни няма и 20% при зареждането на моделите от диска когато имаш бол памет и бавна външен носител. Т.е. може и да има някакъв смисъл ако се налага непрекъснато да зареждаш модела от диск.
Това е моето разбиране, но може и да греша. Не знам детайли за ggml, нито съм чел статиите на хората, които са пакетирали в 1.5 бита. Един младеж от групата ми се забавлява доста време с ggml, ще го питам как точно работи куантизацията там.