Автор: Хун Чао | Архитектор MegEngine, Megvii Technology
предисловие
ЦСП Cadence Vision серии P6/Q6/Q7 развернуты во многих чипах ISP («процессор сигналов изображения»), которые могут дополнять или даже подавлять вычислительную мощность ЦП в сценариях обработки изображений. Более того, Cadence официально предоставляет относительно полную базовую библиотеку операторов libxi, а многие стандартные операторы имеют эталонные реализации при определенных комбинациях параметров в libxi. Однако, учитывая, что группа разработчиков Cadence DSP относительно невелика, а китайских ресурсов, которые можно найти в Интернете, почти нет, порог входа в состояние разработки с нуля не низкий. В этой статье рассматриваются некоторые ключевые моменты разработки операторов Cadence DSP, в надежде помочь студентам, заинтересованным в разработке Cadence DSP.
Особенности архитектуры DSP
Во-первых, возьмем Cadence Q7 в качестве примера, чтобы представить характеристики архитектуры DSP. На рисунке ниже представлена упрощенная аппаратная архитектура Q7.
Из рисунка можно интуитивно получить вычислительную мощность, регистры и другую информацию процессора DSP.Обратите внимание, что на DSP есть два блока данных (называемых драмами), и каждый драм разделен на два банка шириной 512 бит. В то же время на DSP есть два блока загрузки/хранения.Пропускная способность модуля загрузки/хранения для доступа к dram составляет 512 бит, поэтому теоретическая пропускная способность доступа к памяти составляет 1024 бит/цикл, а модуль SuperGather не зависит от загрузки/хранения. заключается в поддержке операций сбора/разброса DSP Efficient. Кроме того, видно, что DSP также имеет модуль dma, который используется для передачи данных между внешним пространством и драмом.
Чтобы в полной мере использовать вычислительную мощность и возможности доступа к памяти, Cadence DSP поддерживает функции SIMD (одна инструкция, несколько данных) и VLIW (очень длинное слово инструкции). Первая поддерживает векторный доступ к памяти и векторные вычисления с общей битовой разрядностью 512 бит, например 64 дорожки * 8 бит или 32 дорожки * 16 бит, а вторая представляет собой технологию поиска параллелизма на уровне инструкций (ILP, параллелизм на уровне инструкций). VLIW может объединять несколько инструкций и выполнять их одновременно для обеспечения параллелизма на уровне инструкций. В отличие от других технологий ILP, таких как суперскалярное выполнение и выполнение не по порядку, расположение параллельных инструкций VLIW определяется во время компиляции и не требует сложного планирования времени выполнения со стороны ЦП. VLIW позволяет процессорам DSP получить преимущества ускорения ILP без существенного усложнения оборудования.
Следует добавить, что Cadence DSP представляет собой гарвардскую архитектуру, и его инструкции и данные адресуются независимо. Конкретная спецификация адресации определяется LSP (пакетом поддержки компоновщика), и пользователи могут определять и изменять ее с помощью файла конфигурации памяти с именем memmap. xmm LSP. Содержимое раздела файла xmm перехватывается, а простые комментарии выглядят следующим образом:
// 存指令的地址段
BEGIN iram0
0xe000000: instRam : iram0 : 0x8000 : executable,writable ;
iram0_0 : F : 0xe000000 - 0xe007fff : .iram0.literal .iram0.text ...
END iram0
// 256k 的 dram0
BEGIN dram0
0xe080000: dataRam : dram0 : 0x40000 : writable ;
dram0_0 : C : 0xe080000 - 0xe0bffff : .dram0.rodata .dram0.data .dram0.bss;
END dram0
// 240k 的 dram1
BEGIN dram1
0xe0c0000: dataRam : dram1 : 0x3c000 : writable ;
dram1_0 : C : 0xe0c0000 - 0xe0fbfff : .dram1.rodata .dram1.data .dram1.bss;
END dram1
// 16k 的栈空间,创建在 dram1 的尾巴后面
BEGIN dram1_stack
0xe0fc000: dataRam : dram1_stack : 0x4000 : writable ;
dram1_stack : C : 0xe0fc000 - 0xe0fffff : STACK : ;
END dram1_stack
// 存 os 相关的地址段
BEGIN sram0
0x10000000: instRam : sram0 : 0x2000000 : executable,writable ;
sram0 : F : 0x10000000 - 0x11ffffff: HEAP : .sram.rodata .rtos.data
END sram0
Из комментариев видно, что файл xmm указывает диапазон адресов данных, инструкций, стека, ОС и других частей среды выполнения.
Поток звонков оператора
Используя базовые знания из предыдущего раздела, давайте получим перцептивное понимание того, как настраивается следующий оператор DSP.
Мы инициируем вызов со стороны процессора и вызываем сервисы, предоставляемые стороной DSP, через протокол rpc, программа на стороне процессора называется rpc_host, а программа на стороне DSP называется rpc_dsp. rpc_dsp отвечает за запуск потока для отслеживания сообщения от rpc_host, анализирует действие, которое необходимо выполнить из сообщения, и отвечает rpc_host сообщением после выполнения действия. Нам нужно заранее скомпилировать rpc_dsp в исполняемую программу, а затем выполнить дамп исполняемой программы в bin-файл, который здесь называется dsp_bin (включая iram.bin и sram.bin). Сторона ЦП отвечает за подготовку всех входных данных вызова оператора и загрузку скомпилированного dsp_bin в память DSP (в предыдущей части введения LSP есть инструкции о том, как выполнять сопоставление памяти), и в то же время , запускается поток мониторинга на стороне rpc_dsp и, наконец, rpc_host Выполняет вызов rpc и ждет возврата rpc.
Следует отметить, что IPCM (модуль межъядерной связи) обычно используется между ЦП и DSP для совместного использования сегмента адресного пространства ddr. Однако задержка прямого доступа DSP к этому ddr намного больше, чем задержка доступа к dram, поэтому для данных ddr, к которым необходимо часто обращаться во время выполнения оператора, обычно используется dma для передачи их в dram. После выполнения оператора вычисление вывода затем перемещается обратно в ddr через dma.
Выше приведен обзор процесса вызова оператора с временной диаграммой, на которой несколько шагов с временными отношениями отмечены пунктирными прямоугольниками, как показано ниже:
Введение в цепочку инструментов
Cadence предоставляет комплект средств разработки Xtensa для разработчиков DSP, который содержит полный набор инструментов командной строки, связанных с компиляцией, компоновкой, выполнением и отладкой. Использование этих команд очень похоже на стандартные инструменты GNU, и Cadence в основном усиливает часть компиляции, потому что Cadence DSP использует VLIW для ускорения, а технология VLIW требует от компилятора дополнительных действий для получения более оптимальной инструкции времени компиляции. размещение.
Процесс вызова, описанный в предыдущем разделе, представляет собой процесс запуска операторов на оборудовании DSP, что выглядит не очень дружелюбно. К счастью, набор инструментов Xtensa также предоставляет симулятор Cadence DSP, а команду xt-run можно использовать для выполнения операторов в симуляторе, так что проверку разработки и отладку производительности можно отделить от реального оборудования.
Давайте возьмем «hello world» в качестве примера, чтобы представить использование инструментов командной строки:
// file: hello_world.c
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
Скомпилировать:
xt-xcc hello_world.c -o hello_world.bin
Выполняется без модели памяти, используемой для первой версии оператора, без имитации латентности доступа к памяти:
xt-run ./hello_world.bin
При выполнении модели памяти производительность симуляции очень близка к скорости на оборудовании DSP:
xt-run --mem_model ./hello_world.bin
Выполненный с параметром --summary, вы можете получить статистический результат распределения циклов, такой как коэффициент цикла сохраненной инструкции, задержка ветвления, cache_miss и другие части:
xt-run --summary ./hello_world.bin
Если вам нужна отладка gdb, вы можете использовать xt-gdb:
xt-gdb ./hello_world.bin
Если вам нужно профилирование, вам нужно добавить параметр --client_cmds="profile --all gmon.out в период выполнения для создания различных файлов профилирования в текущем каталоге, включая gmon.out.cyc, gmon.out.bdelay, gmon.out.interlock и т. д., а затем используйте инструмент xt-gprof для просмотра файла профилирования, созданного на предыдущем шаге. Например, выполните следующие две строки команд, чтобы просмотреть распределение циклов на уровне функций:
xt-run --client_cmds="profile --all gmon.out" ./hello_world.bin
xt-gprof ./hello_world.bin ./gmon.out.cyc > hello_world_cyc.txt
блочные вычисления
Основной сценарий обработки изображений Cadence DSP реалистичен. Размер изображения для бизнеса часто составляет даже разрешение 1080P 4K, в то время как емкость DSP DSP хотя и настраивается, но обычно составляет около 200 КБ (траншея с десятком триллионов драм является исключением), просто не подходит для большого map, и это ведет к тому, что наш оператор должен вычислить блок. Разбивая большое изображение на маленькие блоки (тайлы), каждый проход dma ddr передается от src_tile к dram, оператор выполняет get dst_tile, затем переходит через dma к dst_tile ddr.
знать плитку
Сделайте снимок, чтобы проиллюстрировать конкретные параметры плитки:
Видно, что тайл разделен на два слоя, красная область внутреннего слоя — это исходная область данных, размер — ширина_тайла*высота_тайла, а внешний слой — это круг ребер, потому что некоторые операции оператора, такие как filter2d, требуют заполнения при вычислении, а размер края является размером заполнения. Именно из-за существования края существует различие между pData и pBuffer.
управление оперативной памятью
Тайл размещается на dram, а это сегменты dram0 и dram1 в файле xmm, dram используется нами свободно, поэтому требуется логика управления памятью.
Сначала определите структуру данных DramCtrl:
struct DramCtrl {
char* dram_start; // xmm 文件中 dram0/1 的起始地址
char* dram_end; // xmm 文件中 dram0/1 的终止地址
char* dram_cst_use; // 算子开发中可以自由使用的起始地址
char* dram_free; // 当前尚未分配区域的起始地址
char* dram_idx; // 区分不同 dram 段的索引
};
Параметр DRAM_CST_USE существует, потому что некоторые переменные должны быть выделены на DRAM, но не нужно обновляться при вызове разных операторов, показывающих определенную устойчивость. Такие переменные включают сам Damctrl и дескрипторы DMA, используемые для определения задач передачи, поэтому пространство, занятое этими переменными, удаляется, а DRAM, начиная с позиции DRAM_CST_USE, является пространством, свободно используемым оператором.
После того, как у вас есть структура данных, вам нужно определить некоторые функции интерфейса, чтобы удовлетворить основные требования управления:
void dram_init(): 在 DSP 开机后,调用第一个算子前,执行 dram_init,初始化 DramCtrl 结构体,dram_cst_use=dram_free=dram_start+sizeof(DramCtrl)
void dram_static_alloc(): 在 dma_init 调用之后,分配 dma 的 descriptors,dram_cst_use+=sizeof(descriptors), dram_free=dram_cst_use
void dram_free_size(): 查询当前还有多少空闲内存,返回的是 dram_end-dram_free
void dram_alloc(sz): 分配 tile 等变量的空间,先 check 空闲空间大小,分配成功后修改 dram_free+=sz
void dram_reset(): 在一次算子执行结束后调用,重置 dram_free=dram_cst_use
пинг-понг дма транспорт
Задержка dma для завершения передачи плитки значительна.Если передача dma и вызов оператора выполняются последовательно, передача dma серьезно повлияет на производительность. Таким образом, правильный способ — позаимствовать концепцию буфера пинг-понга и предварительно выбрать следующий тайл при вычислении текущего тайла, чтобы время обработки DMA могло быть скрыто временем вычисления. Логика выполнения оператора на основе pingpong dma следующая:
step 0. dram_alloc src_tile[2], dst_tile[2] and set pingpong = 0
step 1. dma pull src_tile[pingpong]
step 2. dma sync, make src_tile[pingpong] be ready on dram
// loop begin ->
loop_for (h = 0; h < image_height; h += tile_height)
loop_for (w = 0; w < image_width; w += tile_width)
step 3. prefetch, using dma pull src_tile[pingpong^1]
step 4. exec on src_tile[pingpong] to get dst_tile[pingong]
step 5. dma sync, sync for last iter dma push and this iter prefetch
step 6. dma push dst_tile[pingong]
step 7. pingpong = pingpong^1
// loop end <-
step 8. dma sync & dram_reset
блочная логика
Теперь мы поняли концепцию плиток, простое управление динамической памятью и логику обработки пинг-понг dma и параллелизма вычислений, но все еще остается одна недостающая часть: блочная логика. Блокировка предназначена для определения размера плитки в соответствии с соотношением размеров между src_tile и dst_tile при ограничении емкости драм. На самом деле универсальной блочной логики не существует, во многих случаях детально разбираются конкретные проблемы, здесь автор выделяет три категории, исходя из опыта разработки:
- Первая категория: src_tile и dst_tile имеют одинаковый размер
Например, в классе elelwise и классе фильтра размеры входных и выходных данных операторов класса elemwise точно такие же, а в классе фильтра только на один tile_edge больше, чем в классе elemwise. Размер тайла этого типа оператора хорошо определен: если предположить, что сумма входного и выходного изображений оператора равна inout_cnt, а tile_width равна tile_height, то
tile_w=tile_h=srqt(min_dram_sz / inout_cnt)
Среди них min_dram_sz является наименьшим значением двух емкостей драм, потому что потребность в пинг-понге dma, фактическое общее количество выделенных плиток составляет inout_cnt * 2.
- Вторая категория: размеры src_tile и dst_tile не равны, но есть четкая относительная связь
Например, в операторе изменения размера размеры src_tile и dst_tile больше не совпадают, но коэффициенты масштабирования scale_x и scale_y определяют соотношение размеров тайлов:
dst_tile_w=dst_tile_h=srqt(min_dram_sz / (1.0 + scale_x * scale_y))
src_tile_w=dst_tile_w * scale_x
src_tile_h=dst_tile_h * scale_y
- Третья категория: src_tile и dst_tile не имеют четкого отношения размера
Например, для оператора warp_perspective, поскольку прямоугольный dst_tile сопоставляется с src_image через warp_perspective, получается выпуклый четырехугольник, и ограничивающий_бокс этого выпуклого четырехугольника должен быть оформлен как src_tile. Кроме того, очень важно, чтобы размер src_tile, полученный при отображении dst_tile одного размера и разных позиций координат, также был разным. Чтобы гарантировать, что все src_tiles, отображенные dst_tiles в dst_image, могут поместиться в dram, необходима стратегия поиска для определения размера тайлов:
int guess_tile_size(min_dram_sz, frame) {
int l = 0, r = sqrt(min_dram_sz);
while (l <= r) {
int mid = (l + r) / 2;
int ret = 0;
ret = iter_warp_perspective(mid, frame);
if (ret < 0)
r = mid - 1; // ret < 0 表示当前尝试的 dst_tile 的尺寸会使得 src_tile 在 dram 上放不下,所以可行域直接减半
else
l = mid + 1; // ret = 0 表示当前尝试的 dst_tile 的尺寸是 ok 的,但是继续尝试更优解
}
if (r < 0) {
LOG(ERROR, "get tile size failed %d\n");
return -1;
}
LOG(DEBUG, "get the best guess tile width %d\n", r);
return r;
}
Среди них весь размер изображения dst_image, хранящийся в кадре, логика iter_warp_perspective состоит в том, чтобы пройти dst_tile каждой координатной позиции dst_image, вычислить размер bounding_box src_tile через матрицу сопоставления warp_perspective и проверить, может подойти. Iter_warp_perspective возвращает 0, если все проверки на всех позициях пройдены, и -1 в противном случае.
Введение в ИСА
Сначала вставьте введение в синтаксис, инструкция SIMD в Cadence DSP обычно состоит из четырех частей: prefix_op_size_suffix. Префиксы инструкций первой части — все IVP (обработка вектора изображения); вторая часть — это аббревиатура имени конкретной инструкции операции, например ADD, MUL, SEL и т. д.; третья часть — отношение каналов в указанный вектор, такой как 64lanes * 8bit или 32lanes * 16it, но первое на самом деле записывается как 2NX8, а второе записывается как NX16, потому что здесь N означает 32; четвертая часть - это некоторые суффиксные модификаторы, такие как U означает без знака данные для унарных операций, US означает бинарные операции. Данные беззнаковые и знаковые соответственно, T означает, что операция будет иметь маску, PACK означает, что операция побитово сожмет результат промежуточного вычисления и вернет более узкий тип данных и т. д. .
Теперь поместите несколько простых SIMD-инструкций, пусть все сядут и пересмотрят прошлое:
IVP_ADDNX16: 32lanes * 16bit 有符号整数的加法运算
IVP_MUL2NX8U: 64lanes * 8bit 无符号整数的乘法运算
IVP_LV2NX8U_I: LV 表示 vector load, _I 后缀在这里是表示有一个立即数(immediate)的 offset,该命令是在一个 64byte 对齐的地址(base_ptr + offset)上 load 64lanes * 8bit 的数据
Учитывая, что введение ISA является относительно скучным, и многие люди имеют некоторое представление об инструкциях SIMD на ЦП, поэтому здесь представлены только четыре набора инструкций, которые отличаются от общей реализации SIMD и используются очень часто.
Группа 1: инструкции VLOAD с автоматическим выравниванием указателя, автоматическим смещением и поддержкой переменной длины.
Cadence DSP требует, чтобы доступ к VLOAD не мог пересекать банки, а битовая ширина банка составляет 512 бит, что ограничивает адрес VLOAD выравниванием по 64 байтам. Если адрес соответствует требованиям выравнивания, вы можете использовать инструкции IVP_LVxxx для прямого доступа к памяти, в противном случае вам нужно использовать инструкции IVP_LAxxx для автоматического выравнивания доступа к памяти указателя:
void IVP_LAVNX16_XP(xb_vecNx16 v_out, valign a_load /*inout*/, const xb_vecNx16 * src_ptr /*inout*/, int bytes_cnt);
Его необходимо вызвать один раз перед одной или последовательной группой вызовов IVP_LAVNX16_XP:
valign a_load = IVP_LANX16_PP(src_ptr);
Среди них a_load хранит 64 байта данных с начальным адресом [src_ptr & 0x40].64 байта a_load и 64 байта непрерывной загрузки по адресу [src_ptr & 0x40 + 64] образуют 128-байтовый массив, с [src_ptr|0x40] as Смещение перехватывает bytes_cnt байты из массива 128 байт и выводит их на v_out. Обратите внимание, что значение bytes_cnt будет усечено до допустимого диапазона от 0 до 64, что также означает, что эта инструкция может охватывать операции загрузки размером менее 64 байт, что является так называемой tail_load. Еще одна особенность, которую следует объяснить, заключается в том, что эта инструкция будет обновлять src_ptr и a_load после завершения операции загрузки.Смещение src_ptr представляет собой усеченное значение bytes_cnt, а a_load обновляется до содержимого v_out.Эти два обновления делают инструкцию непрерывной. call без повторного вызова IVP_LANX16_PP и ручного перемещения указателя src_ptr.
Вторая группа: умножить, упаковать
Типичный поток вычислений в Cadence DSP заключается в загрузке данных в вектор и применении инструкций расчета.Если числовой диапазон полученного промежуточного результата необходимо увеличить, для его хранения необходимо использовать более широкий вектор с большей разрядностью, и затем передайте класс PACK.Инструкция безопасно сжимает данные в широком векторе в представление битовой ширины вектора:
xb_vec2Nx24 IVP_MULUSP2N8XR16(xb_vec2Nx8U b, xb_vec2Nx8U c, xb_int32 d);
Приведенная выше команда представляет собой векторное умножение двух векторов типа xb_vec2Nx8U и двух пар int16.Результатом двух векторных умножений является сложение векторов, а на выходе получается широкий вектор типа xb_vec2Nx24. Два набора умножений выполняются между старшими 16 битами b и d и между младшими 16 битами c и d.
xb_vec2Nx8U IVP_PACKVRU2NX24(xb_vec2Nx24 b, int c);
И эта инструкция упаковки может сдвигать вправо 24-битные данные каждого канала в широком векторе типа xb_vec2Nx24 на c бит, а затем насыщать их до диапазона выражений u8, а на выходе получается вектор типа xb_vec2Nx8U.
Третья группа: выберите
Иногда логике нашего алгоритма необходимо чередовать или обратно чередовать два вектора.Следующая инструкция может быть реализована:
void IVP_DSELNX16I(xb_vecNx16 a, xb_vecNx16 b, xb_vecNx16 c, xb_vecNx16 d, immediate e);
Эта команда будет обрабатывать 64-байтовые данные в d как 64 дорожки * u8, а 64-байтовые данные в c как 64 дорожки * u8, затем сначала d, а затем c, от младшего к старшему, объединяя данные в d и c в массив из 128 дорожек * u8. . И e — это непосредственное значение, каждое другое значение e соответствует предустановленному index_list, а каждый index_list — это перестановка целочисленной последовательности от 0 до 127. Предустановленный index_list имеет 8-битную/16-битную гранулярность для операций чередования/удаления чередования, а также поддерживает различные операции rotate_left/rotate_right.
Однако может случиться так, что несколько предустановленных списков index_list в IVP_DSEL2NX8I не могут удовлетворить наши потребности, поэтому требуется следующая команда:
void IVP_DSELNX16(xb_vecNx16 a, xb_vecNx16 b, xb_vecNx16 c, xb_vecNx16 d, xb_vec2Nx8 e);
Эта команда упорядочивает данные в d и c в массив из 64 дорожек * 16 бит в порядке d, а затем c, от младшего к старшему, а данные в e представляют собой пользовательскую последовательность из 0–63 целых чисел.
Группа 4: собрать/разбросать
Последний набор инструкций, который нужно ввести, — эффективный сбор/разброс. Сбор — это загрузка данных из набора прерывистых адресов драм в вектор, а разброс — обратная операция, которая сохраняет данные в векторе по дискретным адресам драм. В воображении накладные расходы на сбор/разброс должны быть очень большими, но на практике оказывается, что сбор/разброс занимает больше циклов, чем общие инструкции, но накладные расходы неочевидны, и в некоторых сценариях, если сбор /scatter не используется, SIMD работать не будет, его можно вычислить только с помощью скаляров.
Кратко объясните, почему эффективность сбора/рассеивания Cadence DSP высока. Команда «сбор/разброс» отличается от обычной команды, сбор/разброс полностью берет на себя аппаратный модуль SuperGather после возникновения проблемы. Последний дополнительно разделит драмовый банк шириной 512 бит на 8 подбанков шириной 64 бита и поддержит одновременную загрузку данных, распределенных в разных подбанках с аппаратного уровня (конечно, существует более серьезный риск возникновения конфликта подбанков, который будет объяснен подробно позже. ). Кроме того, команда «сбор» разделена на две подкоманды: «собраться» и «собраться». collecta — это инструкция, которую фактически берет на себя SuperGather, отвечающая за сбор данных по дискретным адресам в регистр сбора. Причина разделения инструкций заключается в том, что сборка может выполняться асинхронно, не блокируя процессор DSP для продолжения выполнения других инструкций. Сбор — это инструкция, выполняемая процессором DSP, который отвечает за копирование собранных данных из регистра gr в обычный векторный регистр, поэтому только команды, которые полагаются на возвращаемое значение, должны ждать завершения операции сбора. Что касается разброса, в дополнение к кредиту параллельного хранилища sub_bank, наиболее важной причиной является то, что разброс будет выполнять буфер аппаратного уровня при обнаружении sub_bank_conflict, а затем планировать операции сохранения, когда есть свободные слоты.
Инструкции по сбору/разбросу следующие:
xb_gsr IVP_GATHERANX8U(const unsigned char * base_ptr, xb_vecNx16U offset_vec);
xb_vecNx16U IVP_GATHERDNX16(xb_gsr b);
void IVP_SCATTERNX16U(xb_vecNx16U out, const unsigned short * base_ptr, xb_vecNx16U offset_vec);
Объясните здесь, IVP_GATHERANX8U — это данные сбора 32 дорожек * u8, а затем данные u8 каждого канала расширяются до u16 путем добавления 0 к старшему разряду, поэтому в регистре gr хранятся данные 32 дорожек * u16.
оптимизация производительности
В предыдущей статье были представлены некоторые часто используемые инструкции SIMD, читатели могут попытаться разработать свои собственные операторы DSP, но производительность первой версии может быть неудовлетворительной, поэтому в этом разделе будут добавлены некоторые знания для оптимизации производительности операторов.
Понять СРП
Прежде всего, мы представим основную концепцию оптимизации планирования компилятором Cadence DSP-SWP (программный конвейер).Цель конвейера на программном уровне - сделать плотность эффективных инструкций в VLIW, компилируемом внутренним цикл выше и минимизировать долю nop.
Чтобы понять SWP более интуитивно, давайте возьмем alphablend в качестве примера, чтобы подробно объяснить SWP, фактически отправленный компилятором:
#define _LOCAL_DRAM0_ __attribute__((section(".dram0.data"))) // 变量是分配在 dram0 上
#define _LOCAL_DRAM1_ __attribute__((section(".dram1.data"))) // 变量是分配在 dram1 上
#define ALIGN64 __attribute__((aligned(64))) // 变量在 dram 上的起始地址是 64byte 对齐的
#define WIDTH 256
#define HEIGHT 32
#define DATA_SIZE 8192 // 256 * 32 = 8192
uint8_t _LOCAL_DRAM0_ ALIGN64 src0[DATA_SIZE];
uint8_t _LOCAL_DRAM1_ ALIGN64 src1[DATA_SIZE];
uint8_t _LOCAL_DRAM1_ ALIGN64 dst[DATA_SIZE];
void alpha_blend(uint8_t* psrc0, uint8_t* psrc1, uint8_t* pdst, int16_t alpha) {
int32_t i, j, alpha_beta;
xb_vec2Nx8U* __restrict vpsrc0 = (xb_vec2Nx8U*) psrc0;
xb_vec2Nx8U* __restrict vpsrc1 = (xb_vec2Nx8U*) psrc1;
xb_vec2Nx8U* __restrict vpdst = (xb_vec2Nx8U*) pdst;
xb_vec2Nx8U vsrc0, vsrc1, vdst;
xb_vec2Nx24 wvec0;
alpha_beta = ((0x3fff - alpha) << 16) + alpha;
// DATA_SIZE = 256 * 32
// XCHAL_IVPN_SIMD_WIDTH = 32
for (i = 0; i < DATA_SIZE / 2 / XCHAL_IVPN_SIMD_WIDTH; ++i) {
vsrc0 = *vpsrc0++; // 因为这里 psrc0/psrc1 的地址是 64byte 对齐的,
// 所以汇编指令为 ivp_lv2nx8_ip vsrc0,vpsrc0,64
vsrc1 = *vpsrc1++;
wvec0 = IVP_MULUSP2N8XR16(vsrc1, vsrc0, alpha_beta);
vdst = IVP_PACKVRU2NX24(wvec0, 14);
*vpdst++ = vdst;
}
}
// call alpha_blend in main function
alpha_blend(src0, src1, dst, 8192);
Приведенный выше код представляет собой оператор alpha_blend, написанный на SIMD, а инструмент командной строки используется для получения файла сборки по расписанию компилятора:
xt-xcc -S alphablend.c -o alphablend.s -O2
Перехваченная часть SWP в файле сборки выглядит следующим образом:
#<loop> Loop body line 139, nesting depth: 1, kernel iterations: 62
#<loop> unrolled 2 times
#<swps>
#<swps> 4 cycles per pipeline stage in steady state with unroll=2
#<swps> 3 pipeline stages
#<swps> 10 real ops (excluding nop)
#<swps>
#<swps> 4 cycles lower bound required by resources
#<swps> min 3 cycles required by recurrences
#<swps> min 4 cycles required by resources/recurrence
#<swps> min 9 cycles required for critical path
#<swps> 12 cycles non-loop schedule length
#<swps> register file usage:
#<swps> 'a' total 4 out of 16 [2-4,10]
#<swps> 'v' total 6 out of 32 [0-5]
#<swps> 'wv' total 2 out of 4 [0-1]
#<swps> 'pr' total 1 out of 16 [0]
#<swps>
#<freq> BB:72 => BB:72 probability = 0.98438
#<freq> BB:72 => BB:79 probability = 0.01562
.frequency 1.000 63.492
// steady 阶段
{ # format N2
ivp_lv2nx8_ip v0,a2,128 # [0*II+0] id:45
ivp_lv2nx8_i v3,a2,64 # [0*II+0] id:45
}
{ # format N2
ivp_lv2nx8_ip v1,a3,128 # [0*II+1] id:46
ivp_lv2nx8_i v4,a3,64 # [0*II+1] id:46
}
{ # format F0
ivp_sv2nx8_ip v2,a4,128 # [2*II+2] id:47
ivp_packvru2nx24 v2,wv0,a10 # [1*II+2]
ivp_mulusp2n8xr16 wv0,v1,v0,pr0 # [0*II+2]
nop #
}
{ # format F0
ivp_sv2nx8_i v5,a4,-64 # [2*II+3] id:47
ivp_packvru2nx24 v5,wv1,a10 # [1*II+3]
ivp_mulusp2n8xr16 wv1,v4,v3,pr0 # [0*II+3]
nop #
}
Как видно из раздела комментариев, компилятор разворачивает тело цикла с unroll=2, количество циклов после развертывания равно 62, получается 4-тактный 3-этапный SWP, и он сообщает вам, что в SWP испускается 10 ненулевых единиц. , инструкции (вы можете рассчитать CPI (цикл на инструкцию) как 0,4), и есть некоторые дополнительные данные анализа коэффициента заполнения регистров.
Каждая фигурная скобка в кодовой части является VLIW. Обратите внимание, что количество инструкций, закодированных каждым VLIW, может быть разным. Это связано с тем, что Cadence DSP поддерживает более десяти различных форматов VLIW (над каждой фигурной скобкой в коде есть комментарий чтобы указать этот тип формата). Затем в правой части каждой инструкции есть комментарий, только обратите внимание на часть квадратных скобок, число справа от плюсика - это количество циклов, которые текущая инструкция отсылает в SWP, а число слева от знака плюс, умноженное на II, представляет собой стадию. Поскольку alphablend отправляет 3-этапный SWP, вы можете видеть, что числа, умноженные на II, равны 0, 1 и 2, которые называются стадиями 0/1/2 соответственно. Здесь 3 этапа означает, что в SWP появится VLIW для упаковки трех итераций (трех итераций после развертывания) инструкций одновременно, стадия 0 — это текущая итерация, стадия 1 — предыдущая итерация, стадия 2 — предыдущая итерация.
По аналогии с конвейером аппаратного обеспечения процессора приведенный выше код представляет собой устойчивую стадию заполнения конвейера SWP.SWP также имеет этапы заполнения и выхода из конвейера, называемые прологом и эпилогом соответственно. Это также объясняет, что количество циклов исходного цикла на самом деле 256 * 32/32/2 равно 128, но количество циклов после развертывания SWP составляет не 64, а 62. Чтобы понять SWP более интуитивно, вставьте ассемблерный код трех этапов пролога, устойчивого и эпилога в картинку следующим образом:
Как видно из рисунка, так называемый SWP должен рассматривать различные инструкции исходного итератора как разные этапы и применять концепцию конвейера для распределения времени запуска нескольких инструкций со строгой логикой синхронизации в исходном итераторе к SWP. В другом случае цель состоит в том, чтобы добиться более низкого ИПЦ.
На самом деле, мы также можем оценить время выполнения оператора по ассемблерному коду: устойчивый этап 4цикл * 62 + этап пролога 7цикл + этап эпилога 5цикл = 260цикл, время выполнения тела цикла, измеренное xt-run, составляет 276цикл, а другие накладные расходы составляют 276-260=16 циклов, поэтому время расчета, рассчитанное по ассемблерному коду, очень точное (но есть исключения, о которых будет сказано позже).
Поняв концепцию SWP, мы внесем некоторые изменения в реализацию alphablend и посмотрим, как это повлияет на SWP. Первый тест — удалить __restrict, который изменяет переменную-указатель, и снова получить файл сборки.Теперь SWP выглядит так:
#<loop> Loop body line 143, nesting depth: 1, iterations: 128
#<swps>
#<swps> 8 cycles per pipeline stage in steady state with unroll=1
#<swps> 1 pipeline stages
#<swps> 5 real ops (excluding nop)
#<swps>
#<swps> 2 cycles lower bound required by resources
#<swps> min 8 cycles required by recurrences
#<swps> min 8 cycles required by resources/recurrence
#<swps> min 8 cycles required for critical path
#<swps> 8 cycles non-loop schedule length
#<swps> register file usage:
#<swps> 'a' total 4 out of 16 [2-4,11]
#<swps> 'v' total 2 out of 32 [0-1]
#<swps> 'wv' total 1 out of 4 [0]
#<swps> 'pr' total 1 out of 16 [0]
#<swps>
#<freq> BB:30 => BB:30 probability = 0.99219
#<freq> BB:30 => BB:32 probability = 0.00781
.frequency 1.000 127.992
{ # format N2
ivp_lv2nx8_ip v0,a2,64 # [0*II+0] id:49
ivp_lv2nx8_ip v1,a3,64 # [0*II+0] id:50
}
{ # format N1
nop #
ivp_mulusp2n8xr16 wv0,v1,v0,pr0 # [0*II+1]
}
{ # format N2
nop #
ivp_packvru2nx24 v0,wv0,a11 # [0*II+4]
}
{ # format N1
ivp_sv2nx8_ip v0,a4,64 # [0*II+7] id:51
nop #
}
Видно, что в новом SWP нет развёртки, а этап равен 1, значит, компилятор не помог нам сделать программный пайплайн.Хотя всё ещё есть закомментированный код этого SWP, но он не сделал ничего осмысленного планирование. Текущий CPI=8/5=1,6, предыдущая версия CPI была 0,4, поэтому производительность упала более чем в 3 раза. Конечно, здесь такая большая разница в скорости.Еще одна причина в том, что логика внутреннего цикла слишком проста.Без раскрутки компилятору действительно не остается места для планирования и он не может воспользоваться преимуществами VLIW. Если логика внутреннего цикла сложная, даже если она не развернута, компилятор может улучшить параллелизм инструкции через VLIW, и разрыв в производительности после эффективного планирования с SWP не будет столь очевидным.
Объясните здесь, некоторые читатели могут увидеть, что в SWP всего 4 VLIW, и непонятно, почему используется 8 тактов. Пожалуйста, обратите внимание на комментарии в квадратных скобках справа от каждой инструкции, количество циклов действительно составляет от 0 до 7, а прерывистые числа в середине будут заменены соответствующим количеством пузырьков. Почему возникает пузырь?Это потому, что четыре VLIW в текущем внутреннем цикле планируют разные инструкции одного и того же итера, а данные соседних VLIW все зависят от чтения после записи, поэтому после непрерывной передачи будет предыдущий .Результат выполнения инструкции не был записан обратно, последняя инструкция прочитала число и дошла до стадии выполнения, которая является так называемой блокировкой конвейера. Чтобы устранить блокировку, необходимо добавить определенное количество пузырьков между двумя VLIW до и после. Внимательные читатели все еще могут задаваться вопросом, почему между vload и vmul нет пузырька, потому что код, который я запускаю на Q7, и Q7 специально оптимизировал задержку vload для dram. Если вы запустите этот код на P6, вы увидите, что между vload и vmul также есть пузырь.
Затем мы проводим второй эксперимент с SWP: обращаемся к невыровненным обращениям. Как упоминалось ранее, все src0/src1 выровнены по 64 байтам, поэтому, взглянув на ассемблерный код, вы обнаружите, что vload на самом деле использует инструкцию ivp_lv2nx8_ip. Но предполагая, что прямо сейчас нельзя гарантировать, что src0/src1 будут выровнены по 64 байтам, необходимо реализовать следующую более общую версию alphablend:
void alpha_blend(uint8_t* psrc0, uint8_t* psrc1, uint8_t* pdst, int16_t alpha) {
// 注意,直接粗暴的把 64byte 对齐的地址都加了 1,构造非对齐地址
psrc0++;
psrc1++;
pdst++;
int32_t i, j, alpha_beta;
xb_vec2Nx8U* __restrict vpsrc0 = (xb_vec2Nx8U*) psrc0;
xb_vec2Nx8U* __restrict vpsrc1 = (xb_vec2Nx8U*) psrc1;
xb_vec2Nx8U* __restrict vpdst = (xb_vec2Nx8U*) pdst;
xb_vec2Nx8U vsrc0, vsrc1, vdst;
xb_vec2Nx24 wvec0;
alpha_beta = ((0x3fff - alpha) << 16) + alpha;
// DATA_SIZE = 256 * 32
// XCHAL_IVPN_SIMD_WIDTH = 32
valign va_dst = IVP_ZALIGN();
valign a_load1 = IVP_LA2NX8U_PP(vpsrc0);
valign a_load2 = IVP_LA2NX8U_PP(vpsrc1);
for (i = 0; i < DATA_SIZE / 2 / XCHAL_IVPN_SIMD_WIDTH; ++i) {
IVP_LAV2NX8U_XP(vsrc0, a_load1, vpsrc0, DATA_SIZE - 1 - i * 64);
IVP_LAV2NX8U_XP(vsrc1, a_load2, vpsrc1, DATA_SIZE - 1 - i * 64);
wvec0 = IVP_MULUSP2N8XR16(vsrc1, vsrc0, alpha_beta);
vdst = IVP_PACKVRU2NX24(wvec0, 14);
IVP_SAV2NX8U_XP(vdst, va_dst, vpdst, DATA_SIZE - 1 - i * 64);
}
IVP_SAV2NX8UPOS_FP(va_dst, vpdst);
}
Использование xt-run для измерения времени, потребляемого внутренним циклом, составляет 298 циклов, что немного больше, чем 276 циклов версии с выровненным адресом. Заголовок SWP перехватывается из соображений экономии места):
#<loop> Loop body line 112, nesting depth: 1, kernel iterations: 15
#<loop> unrolled 8 times
#<swps>
#<swps> 16 cycles per pipeline stage in steady state with unroll=8
#<swps> 2 pipeline stages
#<swps> 48 real ops (excluding nop)
#<swps>
#<swps> 14 cycles lower bound required by resources
#<swps> min 8 cycles required by recurrences
#<swps> min 14 cycles required by resources/recurrence
#<swps> min 15 cycles required for critical path
#<swps> 23 cycles non-loop schedule length
#<swps> register file usage:
#<swps> 'a' total 12 out of 16 [2-5,8-15]
#<swps> 'v' total 4 out of 32 [0-3]
#<swps> 'u' total 3 out of 4 [0-2]
#<swps> 'wv' total 2 out of 4 [0-1]
#<swps> 'pr' total 1 out of 16 [0]
#<swps>
#<freq> BB:83 => BB:83 probability = 0.93750
#<freq> BB:83 => BB:88 probability = 0.06250
Было обнаружено, что компилятор нашел SWP с развёрткой=8, CPI=16/48=0,33 (немного ниже версии выравнивания адресов 0,4), но поскольку развёртка слишком велика, CPI пролога/эпилога относительно большое, что приводит к тому, что общее количество циклов немного больше, чем у версии с выровненным адресом. Но если loop_count немного больше, разница в скорости между двумя версиями будет еще меньше. Не знаю, разочаруются ли читатели, результаты этого эксперимента не говорят нам, как это сделать быстрее, а лишь позволяют сделать вывод: скорость оператора, не выровненного по адресу, не обязательно медленнее, чем адресного. -выровненная версия, но оператор без выравнивания по адресу не обязательно медленнее, чем версия с выравниванием по адресу Версия оператора немного более общая.
Понимание bank_conflict
Возвращаясь назад и заполняя яму, почему оценка времени DSP на основе ассемблерного кода иногда неточна? На самом деле причина также упоминалась ранее, то есть призраки bank_conflict и sub_bank_confilct.
Давайте сначала поговорим о влиянии bank_conflict или возьмем в качестве примера предыдущую alphablend. У оператора есть два входа src0/src1.Если две инструкции vload отправляются на один и тот же VLIW, а доступ к двум адресам относится к разным адресам одного и того же банка в одной и той же DRAM, срабатывает bank_confilct, и процессор должен остановить цикл. Интуиция подсказывает нам, что размещение src0 и src1 на разных драмах должно снизить вероятность возникновения bank_confilct.
Проведите тест для проверки, поместите src0/src1 предыдущей начальной версии alphablend на dram0, и измеренное время внутреннего цикла изменилось с исходных 276 циклов на 280 циклов. Замедление не кажется заметным, и просмотр SWP ничего не меняет, последнее ожидаемо, потому что время компиляции не проверяет каждый доступ к адресу для bank_conflict. Внимательно посмотрев на ассемблерный код, вы можете обнаружить, что код на устойчивом этапе представляет собой две последовательные 64-байтовые vload одного и того же dram, помещенные в VLIW, поэтому эта тестовая модификация не повлияет на него. Добавленные 4 цикла на самом деле связаны с тем, что четыре VLIW связаны с инструкциями vload на разных драмах на этапе пролога, и бывает, что все эти четыре VLIW запускают bank_confilct. Учитывая, что планирование разных операторов отличается, чтобы уменьшить влияние bank_conflict на производительность, мы должны создать несколько входных тайлов на разных DRAM.
Далее давайте проанализируем влияние sub_bank_conflcit на производительность. sub_bank_conflict будет возникать только во время выполнения команды collecta.Когда collecta собирает несколько данных по непоследовательным адресам, если адреса нескольких данных встречаются по разным адресам в одном и том же sub_bank одного и того же банка одного и того же драма, будет несколько sub_bank_conflicts В самом крайнем случае собирается вектор 32, но конфликтов 32. На сбор сбора уходит 32 такта.
Поэтому, если в разработанном нами операторе используется команда collecta, добавьте параметр -mem_model для запуска ее на симуляторе.После выполнения будут напечатаны некоторые статистические параметры, одним из которых является номер цикла сбора. Если, к сожалению, ларек сбора относительно большой, вам нужно проверить логику доступа к памяти в операторе, чтобы увидеть, нужно ли вам настроить распределение данных на тайле, например, вы можете вставить несколько столбцов бесполезных данных в горизонтальном направлении. тайла, чтобы одновременно сократить сбор целевых данных.Возможность sub_bank.
Сводка по оптимизации производительности
Подводя итог, с точки зрения реализации оператора скорость операторов Cadence DSP ограничена только CPI, запланированным SWP, и конфликтом доступа к памяти банка.
Наконец, разбросанные по разным частям этой статьи пункты, которые повлияют на производительность оператора, собираем воедино (есть также некоторые приемы, способные снизить CPI, о которых не упоминалось выше), и делаем чек-лист для проверки всем:
-
Проверить, находятся ли данные, к которым часто обращается оператор, в DRAM? вместо ддр.
-
Убедитесь, что вы используете pingpog dma, и действительно ли он скрывает накладные расходы на портирование dma после его использования? Вы можете проверить долю времени, затрачиваемого ядром, на общее время.
-
Подтвердите, имеют ли указатели dram, к которым обращаются функции загрузки/сохранения и сбора/разброса, __restrict?
-
Если основная логика вычисления оператора представляет собой двойной цикл for, подтвердите, используется ли tile_height в качестве внутреннего цикла? И нужно ли разворачивать внешний цикл?
-
Если используется команда сбора, проверьте, не слишком ли много циклов сбора? Тогда назначьте правильное лекарство.
-
Проверить, не слишком ли много локальных переменных используется в цикле внутри оператора? Например, когда размер ядра filter2d больше или равен 5, во избежание переполнения регистров необходимо разделить один внутренний цикл на несколько небольших независимых внутренних циклов.
-
Если это операция elemwise, логика вычисления более сложна, но окончательный результат обработки каждого значения пикселя находится в относительно ограниченном диапазоне, например, некоторые операторы обработки цвета, вход представляет собой 2-кортеж u8 или u8, после серия обработки По логике вещей, конечным результатом будет по-прежнему 2-кортеж u8 или u8. В этом случае рекомендуется поменяться идеями, чтобы убедиться, что операция поиска в таблице в порядке (потому что у нас есть SuperGather на DSP).
-
Если оператор имеет более одного входа, есть ли какие-либо меры для снижения bank_confilct?
9. При пересадке операторов из более ранней модели DSP в более новую модель DSP, например, при пересадке кода на P6 на Q7, нужно обратить внимание на применение новых инструкций, например, у Q7 больше Dual-Quad 8x8 и Quad 32x16, чем P6.умножить Два дополнительных модуля расширения.
Разное
В этом разделе организованы некоторые детали, которые неудобно раскрывать в предыдущей статье, но на самом деле они очень важны:
-
Есть много вариантов команды Gathera, некоторые варианты командыgagger offset_vec имеют значение 16lanes * u32, но следует отметить, что данные в offset_vec должны находиться в диапазоне от 0 до 65535. В противном случае SuperGather не сможет прочитать данные соответствующего адреса и напрямую присвоить дорожкам 0.
-
В процессе разработки и тестирования может возникнуть необходимость изменить файл memmap.xmm, например настроить положение и размер пространства стека.После редактирования файла xmm используйте инструмент командной строки xtensa xt-genldscripts для выполнения в каталоге, где находится файл xmm.
"xt-genldscripts -b ."
команда доступна. Новый файл сценариев компоновщика находится в каталоге /ldscripts, и модификация xmm вступит в силу. -
Приведите два примера высокочастотного конфликта sub_bank_conflict: например, когда оператор заполнения выполняет left_padding и right_padding, он соберет несколько последовательных данных в определенном столбце плитки, если все данные в столбце находятся в одном и том же sub_bank. Производительность будет очень низкой; другим примером является транспонирование, строка dst_tile на самом деле является столбцом src_tile, поэтому во время сбора также может возникнуть экстремальный конфликт sub_bank_conflict.
-
Объясните, почему компилятор применяет оптимизацию планирования SWP только к внутреннему циклу.Прикладная ориентация Cadence DSP — обработка графики, а плотность вычислений внутреннего цикла общих алгоритмов обработки графики очень высока, что почти определяет производительность всего алгоритма.
-
Внутренний цикл оптимизированного планирования SWP фактически известен как цикл с нулевыми накладными расходами, что означает, что для обычных циклов нет накладных расходов на проверку условий цикла, обновление итерации цикла и т. д., но в приведенном выше примере alphablend кажется, что внутренний цикл по-прежнему имеет некоторые накладные расходы, да Потому что для получения внутреннего цикла с нулевыми накладными расходами требуются два дополнительных условия: количество инструкций во внутреннем цикле не может быть слишком маленьким, а количество циклов относительно велико.
-
Как упоминалось выше, хранение разных входных тайлов на разных драмах уменьшает bank_confilct, но окончательная стратегия уменьшения bank_conflict такова: сначала хранить разные тайлы на разных драмах, а затем использовать их в коде.
#pragma ymemory (tile_on_dram0)
Сообщите компилятору, какая плитка находится на dram1, компилятор пометит тип памяти плитки как ymemory, а тип памяти других плиток как xmemory, и, наконец, добавит параметр компиляции -mcbox, чтобы указать компилятору доступ только к другой памяти. две инструкции vload типа могут быть упакованы в один и тот же VLIW.
Суммировать
В этом документе описываются архитектурные особенности Cadence DSP, поток вызовов подсчета, подсчет для выполнения логического подблока, а также разработка, отладка и оптимизация операторской практики, в надежде на участие в отсталых разработках студентов. игра служит стимулом.
Источник на гитхабе:GitHub.com/Мег двигатель/М…Официальный сайт:megengine.org.cn/Группа технического обмена QQ: 1029741705