探索 PIE 在 ESP32-P4 上的应用
近年来,人工智能技术 (AI) 在图像识别、语音识别以及自然语言处理等领域取得了突破性进展,为嵌入式系统带来了更多的可能性。在将 AI 模型推理部署到 ESP32-P4 等嵌入式设备时,开发者总是希望能够持续优化模型的推理速度,以满足实时处理的需求。
为了应对这一挑战,处理器指令拓展 (PIE, Processor Instruction Extensions) 应运而生。 PIE
是一组在 ESP32-S3/ESP32-P4 上新增的扩展指令,旨在提升特定 AI 与信号处理 (DSP, Digital Signal Processing) 算法的执行效率。基于单指令多数据 (SIMD, Single Instruction Multiple Data) 的设计理念,PIE 支持 8-bit、16-bit 以及 32-bit 的向量运算,能够显著提升数据运算效率。此外,对于乘法、位移、累加等运算指令,PIE 支持在数据计算的同时完成数据搬运操作,进一步提升单条指令的执行效率。
ESP32-S3 与 ESP32-P4 的 PIE 差异:
您可以在 ESP32-S3 和 ESP32-P4 上使用 PIE。虽然在这两种芯片上应用 PIE 的差别很小,但了解它们之间的细微差别对于优化您的应用性能至关重要:
- 指令架构差异:ESP32-S3 的 PIE 是基于 `TIE (Tensilica Instruction Extension)` 语言设计的,而 ESP32-P4 是支持标准的 `RV32IMAFCZc` 扩展,并包含一个自定义的 `Xhwlp` 硬件循环扩展,能够提高在硬件中执行 `for-loop` 的效率;
- 内部结构差异:以乘法累加器 (MAC, Multiplication-accumulation) 为例,ESP32-S3 具有两个 160-bit 的累加器,而 ESP32-P4 具有两个 256-bit 的累加器;
- 指令格式差异:ESP32-S3 和 ESP32-P4 的 PIE 指令大体相似。但需要注意的是,ESP32-S3 的指令以 `ee` 开头,而 ESP32-P4 的指令以 `esp` 开头。此外,某些指令的功能或用法可能有所不同,详细信息请参阅相应的技术参考手册。
上图显示了 PIE 中乘法累加器的数据路径,其中的虚线框突出显示了 ESP32-S3 和 ESP32-P4 之间的差异,蓝色代表 ESP32-S3,绿色代表 ESP32-P4。图中从左到右各块的详细说明如下:
- 内存 (Memory): 用于存储与快速访问数据;
- 地址单元 (Address Unit): PIE 中的大部分指令都允许在一个周期内从 128-bit 的 Q 寄存器中并行加载或存储数据。此外,地址单元还提供了并行操作地址寄存器的功能,从而节省了更新地址寄存器的时间;
- 向量寄存器组 (Vector Registers): 包含 8 个 128-bit 的向量寄存器 (QR)。每个寄存器可以表示为 16 x 8-bit、8 x 16-bit 或 4 x 32-bit 的数据向量;
- QACC 累加寄存器: QACC 累加寄存器可用于 8-bit 或 16-bit 数据的乘累加运算。对于 ESP32-S3 而言,QACC 由 160-bit 的 QACC_H 与 160-bit 的 QACC_L 构成;对于 ESP32-P4 而言,QACC_H 与 QACC_L 的大小各为 256-bit。此外,可以将内存中的数据加载到 QACC 中,也可以将初始值重置为 0;
- ACCX 累加寄存器: ACCX 是一个 40-bit 寄存器,其存储的数据既可以是 8-bit 数据的乘累加结果,也可以是 16-bit 数据的乘累加结果,取决于使用的指令。
如果您对上述 PIE 架构还不熟悉,无需担心。本文将从实用角度出发,以直观的方式展示 PIE 的应用场景和性能表现。
基本的指令格式与应用
如果您的工作经常涉及 AI 模型推理或信号处理,您可能会经常遇到内存拷贝与向量计算等应用场景。以内存拷贝和向量加法为例,您可以通过内联汇编或 `.s` 文件来使用 PIE 指令。需要注意的是,PIE 中的大多数指令都可以在一个周期内从 128-bit Q 寄存器中加载或存储数据,因此,数据应 128-bit 对齐。
内存拷贝加速
在内存拷贝场景中,PIE 中的 8 个 128 位 Q 寄存器可用于读取和存储数据,从而实现内存拷贝加速。ESP32-P4 的 PIE 格式如下:
esp.vld.128.ip qu,rs1,imm # 从内存中加载 128-bit 数据
; 该指令将 128-bit 的数据从内存加载到寄存器 qu 中,同时指针按立即数 (imm) 进行递增。
; imm 的取值范围为 -2048 到 2032,步长为 16。
esp.vst.128.ip qu,rs1,imm # 将 128-bit 数据写入到内存中
; 该指令将寄存器 qu 中的 128 位数据存储到内存中,同时指针按立即数 (imm) 进行递增。
; imm 的取值范围为 -2048 到 2032,步长为 16。
接下来,根据上述指令,以 `.s` 文件 的形式编写用于内存复制的代码。
.data
.align 16
.text
.align 4
.global memcpy_pie
.type memcpy_pie, @function
memcpy_pie:
# a0: store_ptr
# a1: load_ptr
# a2: length(bytes)
li x28,0
Loop:
esp.vld.128.ip q0, a1, 16
esp.vld.128.ip q1, a1, 16
esp.vld.128.ip q2, a1, 16
esp.vld.128.ip q3, a1, 16
esp.vld.128.ip q4, a1, 16
esp.vld.128.ip q5, a1, 16
esp.vld.128.ip q6, a1, 16
esp.vld.128.ip q7, a1, 16
esp.vst.128.ip q0, a0, 16
esp.vst.128.ip q1, a0, 16
esp.vst.128.ip q2, a0, 16
esp.vst.128.ip q3, a0, 16
esp.vst.128.ip q4, a0, 16
esp.vst.128.ip q5, a0, 16
esp.vst.128.ip q6, a0, 16
esp.vst.128.ip q7, a0, 16
addi x28, x28, 128
bge x28, a2, exit
j Loop
exit:
ret
在上述汇编文件中,原始数据分别存储在寄存器 q0 至 q7 中,并回写到指定的内存块。
本文在 ESP32-P4 上进行了 `memcpy` 对比实验。ESP32-P4 的运行频率为 360 MHz,二级缓存大小为 128 KB,二级缓存行大小为 64 Bytes。测试结果显示,在重复 100 轮复制 2040 个字节的过程中,PIE 相较于标准库快 74.3%,相较于采用单字节拷贝的 ANSI C 版本快 97.2%。
此外,深入探索内存拷贝速度差异的原因或许能给您带来更多启发。
Newlib-esp32 包含了各个平台的 `memcpy` 实现方法。以基于 RISC-V 的 ESP32-P4 为例,与单字节拷贝相比,优化后的 `memcpy` 通过使用内存对齐和块级传输提高了数据的拷贝效率。同时,PIE 通过使用 8 个 128-bit 的寄存器来加载和存储数据,进一步加快了内存拷贝的速度。
void *
__inhibit_loop_to_libcall
memcpy(void *__restrict aa, const void *__restrict bb, size_t n)
{
#define BODY(a, b, t) { \
t tt = *b; \
a++, b++; \
*(a - 1) = tt; \
}
char *a = (char *)aa;
const char *b = (const char *)bb;
char *end = a + n;
uintptr_t msk = sizeof (long) - 1;
if (unlikely ((((uintptr_t)a & msk) != ((uintptr_t)b & msk))
|| n < sizeof (long)))
{
small:
if (__builtin_expect (a < end, 1))
while (a < end)
BODY (a, b, char);
return aa;
}
if (unlikely (((uintptr_t)a & msk) != 0))
while ((uintptr_t)a & msk)
BODY (a, b, char);
long *la = (long *)a;
const long *lb = (const long *)b;
long *lend = (long *)((uintptr_t)end & ~msk);
if (unlikely (lend - la > 8))
{
while (lend - la > 8)
{
long b0 = *lb++;
long b1 = *lb++;
long b2 = *lb++;
long b3 = *lb++;
long b4 = *lb++;
long b5 = *lb++;
long b6 = *lb++;
long b7 = *lb++;
long b8 = *lb++;
*la++ = b0;
*la++ = b1;
*la++ = b2;
*la++ = b3;
*la++ = b4;
*la++ = b5;
*la++ = b6;
*la++ = b7;
*la++ = b8;
}
}
while (la < lend)
BODY (la, lb, long);
a = (char *)la;
b = (const char *)lb;
if (unlikely (a < end))
goto small;
return aa;
}
数学运算加速
以 `int16_t` 类型的向量加法为例,相较于内存拷贝而言,其增加了一条加法指令。ESP32-P4 上的指令格式如下:
esp.vadd.s16 qv, qx, qy
; 该指令对寄存器 qx 和 qy 中的 16 位数据执行向量有符号加法。
; 然后,对计算得到的 8 个结果进行饱和处理,并将饱和后的结果写入寄存器 qv。
接下来,本文将以内联汇编方式编写 PIE 代码:
void add_ansic(int16_t *x, int16_t *y, int16_t *z, int n)
{
for (int i = 0; i < n; i++)
{
z[i] = x[i] + y[i];
}
}
void add_pie(int16_t *x, int16_t *y, int16_t *z, int n)
{
asm volatile(
" addi sp, sp, -32 \n"
" sw x31, 28(sp) \n"
" sw x30, 24(sp) \n"
" sw x29, 20(sp) \n"
" sw x28, 16(sp) \n"
" sw x27, 12(sp) \n"
" mv x31, %0 \n"
" mv x30, %1 \n"
" mv x29, %2 \n"
" mv x28, %3 \n"
"li x27, 0 \n"
"loop:\n"
" beq x27, x28, exit \n"
" esp.vld.128.ip q0, x31, 16 \n"
" esp.vld.128.ip q1, x31, 16 \n"
" esp.vld.128.ip q2, x31, 16 \n"
" esp.vld.128.ip q3, x31, 16 \n"
" esp.vld.128.ip q4, x30, 16 \n"
" esp.vld.128.ip q5, x30, 16 \n"
" esp.vld.128.ip q6, x30, 16 \n"
" esp.vld.128.ip q7, x30, 16 \n"
" esp.vadd.s16 q0, q0, q4 \n"
" esp.vadd.s16 q1, q1, q5 \n"
" esp.vadd.s16 q2, q2, q6 \n"
" esp.vadd.s16 q3, q3, q7 \n"
" esp.vst.128.ip q0, x29, 16 \n"
" esp.vst.128.ip q1, x29, 16 \n"
" esp.vst.128.ip q2, x29, 16 \n"
" esp.vst.128.ip q3, x29, 16 \n"
" addi x27, x27, 32 \n"
" j loop \n"
"exit:\n"
" lw x31, 28(sp) \n"
" lw x30, 24(sp) \n"
" lw x29, 20(sp) \n"
" lw x28, 16(sp) \n"
" lw x27, 12(sp) \n"
" addi sp, sp, 32 \n"
:: "r" (x), "r" (y) , "r" (z), "r" (n)
);
}
随后,在与内存拷贝实验相同的条件下,进行了 100 轮 `int16_t` 的向量加法实验,每个向量的长度为 2048。测试结果显示,PIE 版本相比 ANSI C 版本的速度提升了 93.8%。
综上所述,本文介绍了 PIE 的两个应用场景及其实现方法。此外,PIE 还包含其他指令,您可以参考上述流程进行测试。
添加 PIE 相关组件到工程中
如果您不想编写 PIE 指令,但仍想加速 AI 推理或信号处理,您可以尝试使用集成了 PIE 指令的组件来简化开发过程。以下是一些可以轻松集成到您的现有项目中,以加速 AI 和 DSP 性能的乐鑫官方组件:
- esp-tflite-micro: 在 `esp-tflite-micro` 中的 esp-nn 组件使用 PIE 优化了一些 AI 算子,从而加速了模型的推理时间;
- esp-dl: esp-dl 提供了一种专为 ESP 系列芯片设计的轻量级、高效的神经网络推理框架,能够高效实现诸如 Conv2D、Pool2D、Gemm、Add 和 Mul 等常见的 AI 算子;
- esp-dsp: esp-dsp 通过汇编优化实现矩阵乘法、点积以及 FFT 等数学运算,同时也提供了 ANSI-C 实现版本。
总结
总而言之,PIE 包含如下特性:
- 支持 128-bit 位宽的向量数据操作,包括:乘法、加法、减法、累加、移位、比较等;
- 合并数据搬运指令与运算指令;
- 支持非对齐 128-bit 带宽的向量数据;
- 支持取饱和操作。
借助 PIE 指令或相关组件,您可以尝试加速现有项目的计算过程。
欢迎您按照本文步骤进行尝试并分享您的经验!