Я пишу векторный код со встроенными функциями RISC-V для векторов расширения V, но этот вопрос, вероятно, относится к векторизации в целом.
Мне нужно умножить и накопить много значений uint8
. Для этого я хочу заполнить векторные регистры uint8
s, умножить и суммировать (MAC) в цикле, готово. Однако во избежание переполнения результат накопления обычно должен храниться в более крупном типе, например uint32
. Как это распространяется на векторы?
Я предполагаю, что мне нужно разбить векторные регистры на 32-битные дорожки и накапливать в них, но написание векторизованного кода для меня в новинку. Есть ли способ разделить векторные регистры на 8-битные дорожки для лучшего параллелизма и при этом избежать переполнения?
Проблема возникает, потому что я заполняю векторный регистр, предоставляя указатель на массив uint8
vuint8m1_t vec_u8s = __riscv_vle64_v_u8m1(ptr_a, vl);
но если бы я заменил это на...
vuint32m1_t vec_u8s_in_32bit_lanes = __riscv_vle64_v_u32m1(ptr_a, vl);
Он может читать из моего массива как 32-битные значения, читая 4 (uint8) элемента в одну (uint32) полосу. Правильно ли я понимаю? Как мне этого избежать?
Это нормально, потому что ptr_a определяется как uint8_t * ptr_a ...
?
Редактировать:
Возможно, то, что я ищу, это
vint32m1_t __riscv_vlse32_v_i32m1_m (vbool32_t mask, const int32_t *base, ptrdiff_t bstride, size_t vl);
где я могу установить маску на 0xFF и шаг на 1 для чтения данных с шагом 1 байт?
Кстати, я добавил ссылку на github.com/riscv-non-isa/rvv-intrinsic-doc/blob/master/… для документации по внутренним компонентам. Я понятия не имею, является ли это авторитетным или современным источником материалов расширения RISC-V "V".
Расширение RISC-V V (согласно официальной спецификации 1.0: github.com/riscv/riscv-v-spec/releases/download/v1.0/…) имеет vwredsumu.vs
возможность расширения +
сокращения неподписанных элементов. Или, если вам нужно избежать переполнения даже в самом умножении, vwmaccu.vx vd, rs1, vs2, vm
— это расширяющееся целочисленное умножение без знака. Это привело бы к созданию 16-битных элементов, поэтому вы не могли бы добавлять дальше без риска переполнения (или 2 * 0xff^2
уже может переполняться?), поэтому, я думаю, вам придется снова расширить с помощью vwredsumu
. Поэтому я думаю, что вы бы искали для них внутренности.
clang может автоматически векторизовать скалярное произведение байтов без знака: godbolt.org/z/Eezxz6YE4 показывает clang -O3 -march=rv64gv1p0
(расширение V 1.0 = v1p0), используя vzext.vf4 v12, v10
на обоих входах отдельно для подачи vmacc.vv
внутри цикла с vredsum.vs
только снаружи цикла , что, возможно, не оптимально, если бы можно было использовать более узкие элементы дольше без переполнения.
Я думаю, что вы получили это с vzext
. В разделе 11.3 спецификации "V" есть vzext.vf4 vd, vs2, vm # Zero-extend SEW/4 source to SEW destination
. Вывод godbolt подразумевает чтение uint8 в векторные регистры, расширение этих векторов, а затем MAC. Трудно найти правильные встроенные функции для использования из-за задействованного «оболочки» ... Кажется, нет встроенной функции для расширения и перехода от беззнакового к подписанному типу. Godbolt, кажется, читает uint8 как int8, а затем использует vzext
, который расширяет их до нуля, сводя на нет эффективное приведение к int8.
Возможно, я смогу буквально просто закинуть векторный тип с i8 на u8, буду экспериментировать и выложу результаты. Обновлено: вы не можете их разыграть, но есть переинтерпретировать встроенные функции приведения
Имеют ли векторные регистры знаковое значение? Существуют подписанные и неподписанные инструкции для каждого случая, когда это имеет значение, например, при расширении (vwmaccu
против wvmacc
, точно так же, как скаляр mul
против mulu
или vwredsum[u]
). Операции с целыми числами одинаковой ширины, кроме деления или арифметического сдвига вправо, не заботятся о значении старшего бита, unsigned — это та же бинарная операция, что и дополнение до 2 для add/sub/mul. Или подожди, ты сказал внутренний. Я не смотрел так много документов по внутренним характеристикам, так как не был так уверен, что нашел текущий/официальный; векторные типы имеют знаковое значение?
Да, поскольку я не могу использовать автоматическую векторизацию в моем случае, я должен использовать встроенные функции для создания правильной сборки, они немного придирчивы к типам и не позволяют мне предоставить, например, тип вектора u8 встроенной функции, ожидающей тип вектора i8. Я также генерировал vsext
инструкции вместо vzext
, которые, я думаю, изменили бы поведение. Я все еще тестирую. Вот где я сейчас: godbolt.org/z/dr5Ere78e
Типы не должны иметь большого значения, как вы говорите, но они имеют значение в API, если я правильно понимаю. Я полагаю, что встроенная сборка также является вариантом, поскольку встроенные функции переосмысления здесь кажутся немного неоптимальными.
Вы забыли включить оптимизацию! Неудивительно, что ваш внутренний код скомпилирован в полный мусор, включая безумные вещи, такие как li a0, 6
/ mul reg, reg, a0
вместо сдвига. godbolt.org/z/e5dEfjqPG показывает гораздо более разумный ассемблер внутри цикла. Неудачный vsetvli
, которого избегает автовекторизованная версия, но в остальном похожий. IDK, почему -fno-vectorize
не останавливает clang trunk и 16.0 от автоматической векторизации скалярного кода, но на самом деле полезно иметь это для сравнения.
Вы правы, теперь намного яснее. Теперь мне просто нужно отладить код, потому что он не совсем работает! Как я уже сказал, я новичок в этом. Большое спасибо за вашу помощь
Ваш код работает для длин, кратных 16 или 32? Автоматическая векторизация Clang просто использует скаляр для последних нескольких элементов, которые не кратны ширине вектора, не используя преимущества маскировки RVV. (Это проще, но потенциально неэффективно, если есть много оставшихся элементов, особенно для AVX-512 на x86, который также поддерживает маскирование). Это то, что делает vsetvli
внутри вашего основного цикла, обрабатывая случай, когда эта итерация может быть окончательным частичным вектором? Интересно, эффективно ли это или лучше сделать это отдельной финальной итерацией.
В любом случае, удачи, и я бы посоветовал вам опубликовать ответ, как только вы что-то выясните; RVV очень новый, и, вероятно, вопросов и ответов по нему нет. Возможно, даже не тег, я подумаю, будет ли [rvv] хорошей идеей; короткие имена тегов часто могут конфликтовать с другими технологиями, например, [sse] часто неправильно помечают в вопросах [server-sent-events]. Но [riscv-v] или [riscv-extension-v] неуклюжи. На данный момент у нас есть [riscv][simd] в качестве пары тегов; хм, вероятно, следует добавить [intrinsics], так как это то, о чем вы спрашиваете.
Работает до 16 элементов. Это то, что должен делать setvl, я скопировал это из одного из примеров rvv. Не уверен, что он работает, и не уверен, как лучше всего отлаживать на данный момент.
Я опубликую полный ответ и пример, когда он заработает! :)
Я бы предложил выполнить asm пошагово и посмотреть, какое целочисленное значение передается в vsetvli
, и проверить, что указатели продвигаются так, как вы ожидаете для 2-го 16-байтового фрагмента. И если вы можете заставить отладчик показать вам содержимое векторного регистра после загрузки во 2-й итерации, это может подтвердить, что они получают ожидаемые значения. (Посмотрев на ассемблерный код, вы сможете избежать любой потенциальной путаницы на уровне исходного кода C в отношении того, как устроен API встроенных функций. И/или просто показать вам логику программы, которую вы на самом деле написали, в других терминах, потенциально разоблачив мозговой пердеж.)
Ответ заключался в том, чтобы увеличить ширину векторных элементов, используя соответствующую встроенную функцию v{s;z}ext
, а затем использовать внутреннюю интерпретацию результата, чтобы «преобразовать» его значения.
Ниже приведен пример функции и ее векторизованного эквивалента с учетом изменений ширины/типа.
Большое спасибо Питеру Кордесу за то, что помог мне разобраться!
int byte_mac(unsigned char a[], unsigned char b[], int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += a[i] * b[i];
}
return sum;
}
int byte_mac_vec(unsigned char *a, unsigned char *b, int len) {
size_t vlmax = __riscv_vsetvlmax_e8m1();
vint32m4_t vec_s = __riscv_vmv_v_x_i32m4(0, vlmax);
vint32m1_t vec_zero = __riscv_vmv_v_x_i32m1(0, vlmax);
int k = len;
for (size_t vl; k > 0; k -= vl, a += vl, b += vl) {
vl = __riscv_vsetvl_e8m1(k);
vuint8m1_t a8s = __riscv_vle8_v_u8m1(a, vl);
vuint8m1_t b8s = __riscv_vle8_v_u8m1(b, vl);
vuint32m4_t a8s_extended = __riscv_vzext_vf4_u32m4(a8s, vl);
vuint32m4_t b8s_extended = __riscv_vzext_vf4_u32m4(b8s, vl);
vint32m4_t a8s_as_i32 = __riscv_vreinterpret_v_u32m4_i32m4(a8s_extended);
vint32m4_t b8s_as_i32 = __riscv_vreinterpret_v_u32m4_i32m4(b8s_extended);
vec_s = __riscv_vmacc_vv_i32m4_tu(vec_s, a8s_as_i32, b8s_as_i32, vl);
}
vint32m1_t vec_sum = __riscv_vredsum_vs_i32m4_i32m1(vec_s, vec_zero, __riscv_vsetvl_e32m4(len));
int sum = __riscv_vmv_x_s_i32m1_i32(vec_sum);
return sum;
}
Итак, единственное изменение по сравнению с вашей предыдущей попыткой в комментариях было __riscv_vmacc_vv_i32m4_tu
внутри цикла вместо __riscv_vmacc_vv_i32m4
? Что означает _tu
? Хвост что-нибудь, из беглого просмотра github.com/riscv-non-isa/rvv-intrinsic-doc/blob/master/…
Это означает, что хвост не потревожен. Кто-то из SiFive сказал мне, что это «для того, чтобы верхние элементы на последней итерации были сохранены из предыдущих итераций», я нашел в презентации pdf от Barcalona Supercomuting Centre: «Когда vl < vlmax, тогда у нас есть элементы, которые не работают • Эти элементы называются хвостовыми элементами. RVV предлагает здесь две политики: • tail unturbed — хвостовые элементы в регистре назначения остаются неизмененными • tail agnostic — могут вести себя как хвостовые элементы без изменений или, в качестве альтернативы, все биты хвостовых элементов регистра назначения установлены на 1 дюйм
Я все еще не понимаю, как именно это работает. Попытка с/без _tu кажется хорошей. Другие изменения включают установку vl
на __riscv_vsetvl_e32m4(len)
в строке redsum
. Это нужно для случая, когда len < vlmax. Опять же, это сказал мне парень SiFive, но я думаю, что это справится, если я попытаюсь запустить функцию с len
, например, 1.
Хорошо, Tail Undisturbed имеет смысл здесь для конечного короткого вектора в конце длинного массива. Затем вам нужно redsum
добавить все элементы по всему вектору, а не только vl
, который вы использовали для последнего частичного вектора. Поэтому, если вы уже устанавливаете vl
внутри цикла, повторная установка снаружи будет обрабатывать случай, когда len > vlmax
и вам нужно выполнить несколько итераций.
Я только что нашел в rvv-intrinsic-api.md: «Примечание: встроенные функции сокращения будут генерировать код с использованием политики хвоста без помех, если vundefined() не будет передан аргументу dest». Может быть, поэтому для меня не было разницы с ту или без ту. Мое понимание ненарушенного хвоста (трудно найти четкое определение) заключается в том, что любые элементы вектора за пределами vl
не будут затронуты данной операцией, другие политики могут перезаписываться 1 с. Итак, здесь в коротком векторе vl
байты считываются и zext
читаются, но если их меньше vl
, то из-за _tu
0, а не 1, будут распространяться на vacc
?
Я не знаю, как работает загрузка, но это не имеет большого значения (кроме ложных зависимостей и необходимости слияния), если vl
считается в элементах, а не в байтах. vmacc
будет умножать + добавлять к младшим vl
элементам vec_s
, оставляя более высокие элементы vec_s
без изменений, по-прежнему удерживая суммы из более ранних итераций, когда vl
было выше. Это «хвост», и он остается неизменным. Неважно, что произошло в высоких частях нагрузки и временных результатов zext, потому что мы не сохраняем их, маскируя операцию MAC, они не влияют на нее.
этот вопрос, вероятно, относится к векторизации в целом. - на x86 вы должны использовать
psadbw
(сумма абсолютных разностей) против обнуленного вектора, чтобы накапливать суммы 8 байтов без переполнения. В AArch64 есть инструкция горизонтального суммирования, IIRC, которая может иметь некоторую аппаратную поддержку, а также микрокодирование нескольких операций в конвейере для перетасовки и суммирования. Я ничего не искал для расширения RISC-V V, но, возможно, у него есть умножение-накопление байтов в 16-битные элементы или что-то в этом роде? Например, x86pmaddwd
илиpmaddubsw
, которые полезны для такого рода вещей.