Как умножить байты без знака на 32-битные элементы без переполнения с помощью векторов SIMD расширения RISC-V «V»?

Я пишу векторный код со встроенными функциями RISC-V для векторов расширения V, но этот вопрос, вероятно, относится к векторизации в целом.

Мне нужно умножить и накопить много значений uint8. Для этого я хочу заполнить векторные регистры uint8s, умножить и суммировать (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 байт?

этот вопрос, вероятно, относится к векторизации в целом. - на x86 вы должны использовать psadbw (сумма абсолютных разностей) против обнуленного вектора, чтобы накапливать суммы 8 байтов без переполнения. В AArch64 есть инструкция горизонтального суммирования, IIRC, которая может иметь некоторую аппаратную поддержку, а также микрокодирование нескольких операций в конвейере для перетасовки и суммирования. Я ничего не искал для расширения RISC-V V, но, возможно, у него есть умножение-накопление байтов в 16-битные элементы или что-то в этом роде? Например, x86 pmaddwd или pmaddubsw, которые полезны для такого рода вещей.

Peter Cordes 04.04.2023 01:39

Кстати, я добавил ссылку на github.com/riscv-non-isa/rvv-intrinsic-doc/blob/master/… для документации по внутренним компонентам. Я понятия не имею, является ли это авторитетным или современным источником материалов расширения RISC-V "V".

Peter Cordes 04.04.2023 03:35

Расширение 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. Поэтому я думаю, что вы бы искали для них внутренности.

Peter Cordes 04.04.2023 04:47

clang может автоматически векторизовать скалярное произведение байтов без знака: godbolt.org/z/Eezxz6YE4 показывает clang -O3 -march=rv64gv1p0 (расширение V 1.0 = v1p0), используя vzext.vf4 v12, v10 на обоих входах отдельно для подачи vmacc.vv внутри цикла с vredsum.vs только снаружи цикла , что, возможно, не оптимально, если бы можно было использовать более узкие элементы дольше без переполнения.

Peter Cordes 04.04.2023 05:23

Я думаю, что вы получили это с 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.

confusedandsad 04.04.2023 17:11

Возможно, я смогу буквально просто закинуть векторный тип с i8 на u8, буду экспериментировать и выложу результаты. Обновлено: вы не можете их разыграть, но есть переинтерпретировать встроенные функции приведения

confusedandsad 04.04.2023 17:12

Имеют ли векторные регистры знаковое значение? Существуют подписанные и неподписанные инструкции для каждого случая, когда это имеет значение, например, при расширении (vwmaccu против wvmacc, точно так же, как скаляр mul против mulu или vwredsum[u]). Операции с целыми числами одинаковой ширины, кроме деления или арифметического сдвига вправо, не заботятся о значении старшего бита, unsigned — это та же бинарная операция, что и дополнение до 2 для add/sub/mul. Или подожди, ты сказал внутренний. Я не смотрел так много документов по внутренним характеристикам, так как не был так уверен, что нашел текущий/официальный; векторные типы имеют знаковое значение?

Peter Cordes 04.04.2023 20:08

Да, поскольку я не могу использовать автоматическую векторизацию в моем случае, я должен использовать встроенные функции для создания правильной сборки, они немного придирчивы к типам и не позволяют мне предоставить, например, тип вектора u8 встроенной функции, ожидающей тип вектора i8. Я также генерировал vsext инструкции вместо vzext, которые, я думаю, изменили бы поведение. Я все еще тестирую. Вот где я сейчас: godbolt.org/z/dr5Ere78e

confusedandsad 04.04.2023 21:24

Типы не должны иметь большого значения, как вы говорите, но они имеют значение в API, если я правильно понимаю. Я полагаю, что встроенная сборка также является вариантом, поскольку встроенные функции переосмысления здесь кажутся немного неоптимальными.

confusedandsad 04.04.2023 21:27

Вы забыли включить оптимизацию! Неудивительно, что ваш внутренний код скомпилирован в полный мусор, включая безумные вещи, такие как li a0, 6 / mul reg, reg, a0 вместо сдвига. godbolt.org/z/e5dEfjqPG показывает гораздо более разумный ассемблер внутри цикла. Неудачный vsetvli, которого избегает автовекторизованная версия, но в остальном похожий. IDK, почему -fno-vectorize не останавливает clang trunk и 16.0 от автоматической векторизации скалярного кода, но на самом деле полезно иметь это для сравнения.

Peter Cordes 04.04.2023 22:29

Вы правы, теперь намного яснее. Теперь мне просто нужно отладить код, потому что он не совсем работает! Как я уже сказал, я новичок в этом. Большое спасибо за вашу помощь

confusedandsad 04.04.2023 22:34

Ваш код работает для длин, кратных 16 или 32? Автоматическая векторизация Clang просто использует скаляр для последних нескольких элементов, которые не кратны ширине вектора, не используя преимущества маскировки RVV. (Это проще, но потенциально неэффективно, если есть много оставшихся элементов, особенно для AVX-512 на x86, который также поддерживает маскирование). Это то, что делает vsetvli внутри вашего основного цикла, обрабатывая случай, когда эта итерация может быть окончательным частичным вектором? Интересно, эффективно ли это или лучше сделать это отдельной финальной итерацией.

Peter Cordes 04.04.2023 22:49

В любом случае, удачи, и я бы посоветовал вам опубликовать ответ, как только вы что-то выясните; RVV очень новый, и, вероятно, вопросов и ответов по нему нет. Возможно, даже не тег, я подумаю, будет ли [rvv] хорошей идеей; короткие имена тегов часто могут конфликтовать с другими технологиями, например, [sse] часто неправильно помечают в вопросах [server-sent-events]. Но [riscv-v] или [riscv-extension-v] неуклюжи. На данный момент у нас есть [riscv][simd] в качестве пары тегов; хм, вероятно, следует добавить [intrinsics], так как это то, о чем вы спрашиваете.

Peter Cordes 04.04.2023 22:51

Работает до 16 элементов. Это то, что должен делать setvl, я скопировал это из одного из примеров rvv. Не уверен, что он работает, и не уверен, как лучше всего отлаживать на данный момент.

confusedandsad 04.04.2023 22:54

Я опубликую полный ответ и пример, когда он заработает! :)

confusedandsad 04.04.2023 22:55

Я бы предложил выполнить asm пошагово и посмотреть, какое целочисленное значение передается в vsetvli, и проверить, что указатели продвигаются так, как вы ожидаете для 2-го 16-байтового фрагмента. И если вы можете заставить отладчик показать вам содержимое векторного регистра после загрузки во 2-й итерации, это может подтвердить, что они получают ожидаемые значения. (Посмотрев на ассемблерный код, вы сможете избежать любой потенциальной путаницы на уровне исходного кода C в отношении того, как устроен API встроенных функций. И/или просто показать вам логику программы, которую вы на самом деле написали, в других терминах, потенциально разоблачив мозговой пердеж.)

Peter Cordes 04.04.2023 22:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
16
114
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ответ заключался в том, чтобы увеличить ширину векторных элементов, используя соответствующую встроенную функцию 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/…

Peter Cordes 05.04.2023 04:46

Это означает, что хвост не потревожен. Кто-то из SiFive сказал мне, что это «для того, чтобы верхние элементы на последней итерации были сохранены из предыдущих итераций», я нашел в презентации pdf от Barcalona Supercomuting Centre: «Когда vl < vlmax, тогда у нас есть элементы, которые не работают • Эти элементы называются хвостовыми элементами. RVV предлагает здесь две политики: • tail unturbed — хвостовые элементы в регистре назначения остаются неизмененными • tail agnostic — могут вести себя как хвостовые элементы без изменений или, в качестве альтернативы, все биты хвостовых элементов регистра назначения установлены на 1 дюйм

confusedandsad 05.04.2023 13:02

Я все еще не понимаю, как именно это работает. Попытка с/без _tu кажется хорошей. Другие изменения включают установку vl на __riscv_vsetvl_e32m4(len) в строке redsum. Это нужно для случая, когда len < vlmax. Опять же, это сказал мне парень SiFive, но я думаю, что это справится, если я попытаюсь запустить функцию с len, например, 1.

confusedandsad 05.04.2023 13:04

Хорошо, Tail Undisturbed имеет смысл здесь для конечного короткого вектора в конце длинного массива. Затем вам нужно redsum добавить все элементы по всему вектору, а не только vl, который вы использовали для последнего частичного вектора. Поэтому, если вы уже устанавливаете vl внутри цикла, повторная установка снаружи будет обрабатывать случай, когда len > vlmax и вам нужно выполнить несколько итераций.

Peter Cordes 05.04.2023 21:01

Я только что нашел в rvv-intrinsic-api.md: «Примечание: встроенные функции сокращения будут генерировать код с использованием политики хвоста без помех, если vundefined() не будет передан аргументу dest». Может быть, поэтому для меня не было разницы с ту или без ту. Мое понимание ненарушенного хвоста (трудно найти четкое определение) заключается в том, что любые элементы вектора за пределами vl не будут затронуты данной операцией, другие политики могут перезаписываться 1 с. Итак, здесь в коротком векторе vl байты считываются и zext читаются, но если их меньше vl, то из-за _tu 0, а не 1, будут распространяться на vacc?

confusedandsad 05.04.2023 23:01

Я не знаю, как работает загрузка, но это не имеет большого значения (кроме ложных зависимостей и необходимости слияния), если vl считается в элементах, а не в байтах. vmacc будет умножать + добавлять к младшим vl элементам vec_s, оставляя более высокие элементы vec_s без изменений, по-прежнему удерживая суммы из более ранних итераций, когда vl было выше. Это «хвост», и он остается неизменным. Неважно, что произошло в высоких частях нагрузки и временных результатов zext, потому что мы не сохраняем их, маскируя операцию MAC, они не влияют на нее.

Peter Cordes 05.04.2023 23:08

Другие вопросы по теме