Руководство по началу работы с Cadence DSP Operator Development

искусственный интеллект глубокое обучение

Автор: Хун Чао | Архитектор 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, о которых не упоминалось выше), и делаем чек-лист для проверки всем:

  1. Проверить, находятся ли данные, к которым часто обращается оператор, в DRAM? вместо ддр.

  2. Убедитесь, что вы используете pingpog dma, и действительно ли он скрывает накладные расходы на портирование dma после его использования? Вы можете проверить долю времени, затрачиваемого ядром, на общее время.

  3. Подтвердите, имеют ли указатели dram, к которым обращаются функции загрузки/сохранения и сбора/разброса, __restrict?

  4. Если основная логика вычисления оператора представляет собой двойной цикл for, подтвердите, используется ли tile_height в качестве внутреннего цикла? И нужно ли разворачивать внешний цикл?

  5. Если используется команда сбора, проверьте, не слишком ли много циклов сбора? Тогда назначьте правильное лекарство.

  6. Проверить, не слишком ли много локальных переменных используется в цикле внутри оператора? Например, когда размер ядра filter2d больше или равен 5, во избежание переполнения регистров необходимо разделить один внутренний цикл на несколько небольших независимых внутренних циклов.

  7. Если это операция elemwise, логика вычисления более сложна, но окончательный результат обработки каждого значения пикселя находится в относительно ограниченном диапазоне, например, некоторые операторы обработки цвета, вход представляет собой 2-кортеж u8 или u8, после серия обработки По логике вещей, конечным результатом будет по-прежнему 2-кортеж u8 или u8. В этом случае рекомендуется поменяться идеями, чтобы убедиться, что операция поиска в таблице в порядке (потому что у нас есть SuperGather на DSP).

  8. Если оператор имеет более одного входа, есть ли какие-либо меры для снижения 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