RISC-V与嵌入式系统¶
RISC-V是正在重塑芯片行业的开源指令集架构。本文涵盖RISC-V哲学、V向量扩展、嵌入式ML推理、微控制器上的TinyML、AI加速器中的RISC-V以及边缘部署约束
- 我们之前讨论的每一种芯片架构(x86、ARM)都需要许可。Intel和AMD为x86付费。Apple、Qualcomm以及每一家智能手机厂商每年向ARM支付数十亿美元。RISC-V则不同:它是一个开放标准。任何人都可以设计、制造和销售RISC-V芯片,无需向任何人支付版税。这正在改变芯片设计的经济性,特别是对于AI。
RISC-V哲学¶
-
RISC-V(发音为"risk five")于2010年在加州大学伯克利分校创建,作为一个简洁、现代的RISC指令集。关键原则:
-
开放标准:ISA规范免费提供。你可以在没有许可费、NDA或法律协议的情况下构建RISC-V CPU。这就像Linux之于操作系统——任何人都可以使用、修改和在此基础上构建。
-
模块化设计:基础ISA(RV32I或RV64I)是最小的——仅47条指令。其他一切都是可选的扩展:M(乘法/除法)、A(原子操作)、F/D(浮点)、C(压缩指令)、V(向量处理)。你只选择需要的,保持芯片小巧高效。
-
无遗留包袱:x86背负着45年的向后兼容性。ARM背负着35年。RISC-V从零开始,融入了从两者中吸取的经验教训。没有仅为与1980年代软件兼容而存在的晦涩指令。
-
-
谁在使用RISC-V:SiFive(通用核心)、阿里巴巴(玄铁服务器核心)、西部数据(存储控制器,已出货数十亿)、乐鑫(ESP32-C3,流行IoT芯片),以及数十家使用RISC-V作为管理其自定义计算单元的控制处理器的AI加速器初创公司。
RISC-V基础架构¶
- 基础整数ISA(RV64I用于64位)具有:
- 32个通用寄存器(x0-x31,每个64位)。x0硬连接为零(用于在没有特殊指令的情况下实现常见模式)。
- 固定32位指令宽度(C扩展为代码密度添加了16位压缩指令)。
- 加载-存储架构:与ARM一样,算术仅操作寄存器。内存访问通过显式加载/存储指令进行。
# RISC-V汇编(感受风格——你将使用C/C++)
add x3, x1, x2 # x3 = x1 + x2
lw x4, 0(x5) # 从x5中的地址加载字
sw x4, 8(x5) # 存储字到地址 x5 + 8
beq x1, x2, label # 如果x1 == x2则分支
- ISA的简洁性使RISC-V核心小巧且能效高。最小的RV32I核心可以用约10,000个门实现(ARM Cortex-M0约为12,000)。这对于每一毫瓦和每一平方毫米硅片都至关重要的嵌入式系统很重要。
V扩展:RISC-V向量处理¶
- V扩展(RVV)为RISC-V添加了可伸缩向量处理,类似于ARM SVE。向量寄存器具有可配置长度(VLEN),由硬件指定(128到65,536位)。代码编写为向量长度无关:无需重新编译可在任何VLEN上工作。
#include <riscv_vector.h>
// 使用RVV内联函数进行向量加法
void vadd_rvv(const float* a, const float* b, float* c, int n) {
while (n > 0) {
// vsetvl:设置向量长度——处理 min(n, 硬件最大值) 个元素
size_t vl = __riscv_vsetvl_e32m1(n);
// 加载vl个元素
vfloat32m1_t va = __riscv_vle32_v_f32m1(a, vl);
vfloat32m1_t vb = __riscv_vle32_v_f32m1(b, vl);
// 相加
vfloat32m1_t vc = __riscv_vfadd_vv_f32m1(va, vb, vl);
// 存储
__riscv_vse32_v_f32m1(c, vc, vl);
// 前进指针
a += vl; b += vl; c += vl; n -= vl;
}
}
-
vsetvl是关键指令。它告诉硬件"我想处理这么多元素",硬件回应"我可以处理这么多"(受VLEN限制)。循环自动适应任何向量宽度,无需标量清理(最后一次迭代只处理较少的元素)。 -
LMUL(长度乘数):RVV可以将多个向量寄存器分组在一起(m1、m2、m4、m8),以每条指令处理更多元素,代价是可用的寄存器更少。
m1每个向量操作数使用一个寄存器;m8使用八个,处理8倍元素,但只留下4个寄存器组可用。 -
与x86 AVX(固定256/512位)和ARM NEON(固定128位)相比,RVV的可伸缩性对于多样化硬件是一个重要优势:相同代码在小型嵌入式核心(VLEN=128)和高性能服务器核心(VLEN=1024+)上运行。
嵌入式ML:TinyML¶
-
TinyML是微控制器上的机器学习——具有千字节RAM、兆赫级CPU和毫瓦功率预算的设备。想想:检测关键词的传感器("Hey Siri")、分类手势的加速度计、或计数人数的小型摄像头,所有这些都在一个售价0.50美元、无需互联网连接的芯片上运行。
-
约束条件极其严苛:
| 资源 | 服务器GPU | 智能手机 | 微控制器 |
|---|---|---|---|
| RAM | 80 GB | 6 GB | 256 KB |
| 存储 | TB | 128 GB | 1 MB |
| 计算能力 | 1000 TFLOPS | 10 TFLOPS | 0.001 TFLOPS |
| 功耗 | 700 W | 5 W | 0.001 W |
| 成本 | $30,000 | $500 | $1 |
- 适合服务器GPU的模型(\(O(10^{10})\) 参数)无法放入微控制器。TinyML模型有 \(O(10^4)\)–\(O(10^6)\) 参数,并使用INT8甚至INT4量化。
TensorFlow Lite Micro(TFLM)¶
- TFLM是Google的微控制器推理框架。它运行量化的TensorFlow Lite模型,无需动态内存分配、无需操作系统,二进制占用约20 KB。
// 微控制器上的TinyML推理(简化版)
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
// 模型编译为C数组(const unsigned char model_data[])
const tflite::Model* model = tflite::GetModel(model_data);
// 分配固定内存缓冲区(无malloc!)
constexpr int kArenaSize = 10 * 1024; // 10 KB
uint8_t tensor_arena[kArenaSize];
// 设置解释器
tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, kArenaSize);
interpreter.AllocateTensors();
// 设置输入
float* input = interpreter.input(0)->data.f;
input[0] = sensor_reading;
// 运行推理
interpreter.Invoke();
// 读取输出
float* output = interpreter.output(0)->data.f;
if (output[0] > 0.8f) {
trigger_alert();
}
- 此代码中的关键约束:
tensor_arena是静态分配的——没有malloc,没有堆。嵌入式系统通常没有动态内存分配器。- 模型是一个
const字节数组,存储在闪存(ROM)中,而非从文件系统加载。 - 整个框架+模型+运行时适合几十KB。
边缘模型优化¶
-
让模型在微控制器上运行需要激进优化:
-
量化(第18章):将float32权重转换为INT8(小4倍,在纯整数硬件上快2-4倍)。训练后量化简单;量化感知训练保留更多精度。
-
剪枝:移除接近零的权重。结构化剪枝(移除整个通道/头)比非结构化剪枝(随机零)对硬件更友好,因为它减少实际计算,而不仅是存储。
-
知识蒸馏(第6章):训练一个小型"学生"模型来模仿大型"教师"模型。学生模型比从头训练获得更高精度,因为它从教师模型的软预测中学习。
-
神经架构搜索(NAS):自动搜索适合硬件预算(延迟、内存、功耗)的高效架构。MicroNets和MCUNet为特定微控制器寻找优化架构。
-
算子融合:将卷积+批归一化+ReLU组合成单个融合操作,消除中间内存写入(与GPU核函数融合同一原则,但在只有256 KB RAM时更加关键)。
-
RISC-V在AI加速器中的应用¶
- 许多AI加速器初创公司使用RISC-V并非直接运行ML模型,而是作为管理自定义计算单元的控制处理器:
┌─────────────────────────────────────────┐
│ AI加速器 │
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ RISC-V │───→│ 自定义矩阵 │ │
│ │ 控制 │ │ 乘法单元 │ │
│ │ 核心 │ │ (脉动阵列、 │ │
│ │ │ │ 自定义数据流) │ │
│ └──────────┘ └──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ 内存 │ │ 片上SRAM │ │
│ │ 控制 │ │ (激活缓冲) │ │
│ │ │ │ │ │
│ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
-
RISC-V核心处理:从外部内存加载模型权重、调度层执行、管理计算单元之间的数据流以及与主机通信(通过PCIe、USB或SPI)。繁重计算(矩阵乘法、卷积)由自定义硬件完成,而非RISC-V核心。
-
为什么用RISC-V做控制:无需许可费用(对初创公司至关重要)、可定制(添加领域特定指令)、小占用空间(控制核心不需要x86的复杂性),以及开放生态系统支持快速原型开发。
-
示例:Esperanto Technologies(1000+个RISC-V核心用于ML)、Tenstorrent(RISC-V控制+自定义tensix核心)、SiFive(带向量扩展的RISC-V核心用于边缘ML)。
边缘部署约束¶
-
在边缘部署ML(设备端,而非云端)引入了云端部署不需要的约束:
-
功耗:电池供电的设备总功耗预算可能为100 mW。运行消耗50 mW的模型只给系统其余部分(传感器、无线电、显示器)留下50 mW。功耗感知推理调度计算以避免热降频并延长电池寿命。
-
延迟:边缘推理通常必须是实时的。唤醒词检测器("Hey Siri")必须在约200 ms内响应。自动驾驶感知系统(第11章)必须在约30 ms内处理帧。到云端的网络往返(50-200 ms)对这些用例来说太慢了。
-
隐私:在设备上处理数据意味着敏感数据(医学图像、语音记录、个人照片)永远不会离开设备。这在某些司法管辖区是法律要求(GDPR),在所有地方都是用户信任的要求。
-
连接性:边缘设备可能间歇性或完全没有互联网连接。在火星车(第11章)、潜艇或农村农田传感器上运行的模型必须完全离线工作。
-
规模成本:将ML部署到十亿部智能手机每台成本为$0(硬件已经存在)。将ML部署到十亿个IoT传感器意味着每个传感器的ML硬件预算只有几分钱。RISC-V的零许可成本在这个规模下意义重大。
编程任务(用g++或riscv64-gcc交叉编译器编译)¶
-
编写一个C程序,模拟TinyML推理流水线:静态分配模型缓冲区,运行模拟前向传播,并测量资源使用。这教授嵌入式约束(无malloc、固定内存缓冲区)。
// task1_tinyml_sim.cpp // 编译:g++ -O2 -o task1 task1_tinyml_sim.cpp #include <iostream> #include <chrono> #include <cmath> #include <cstring> // 模拟微控制器:固定内存缓冲区,无动态分配 static constexpr int ARENA_SIZE = 32 * 1024; // 32 KB总RAM预算 static uint8_t arena[ARENA_SIZE]; // 简单的2层MLP:784 -> 64 -> 10(类似MNIST,INT8权重) struct TinyModel { int8_t w1[784 * 64]; // 层1权重:50,176字节 int8_t b1[64]; // 层1偏置 int8_t w2[64 * 10]; // 层2权重:640字节 int8_t b2[10]; // 层2偏置 // 总计:约51 KB → 必须放在闪存(ROM),而非RAM }; // 检查模型是否适合闪存 void check_model_fit(int flash_kb) { int model_bytes = sizeof(TinyModel); std::cout << "模型大小: " << model_bytes << " 字节(" << model_bytes / 1024 << " KB)\n"; std::cout << "闪存: " << flash_kb << " KB → " << (model_bytes <= flash_kb * 1024 ? "适合" : "太大") << "\n"; } // 使用固定缓冲区进行激活的模拟推理 void mock_inference(const int8_t* input, int8_t* output) { // 激活值放在缓冲区(RAM)中,而非动态分配 int8_t* act1 = (int8_t*)arena; // 层1输出64字节 int8_t* act2 = (int8_t*)(arena + 64); // 层2输出10字节 // 层1:简化版矩阵乘法(不是真正的量化矩阵乘法,仅结构演示) for (int j = 0; j < 64; j++) { int32_t sum = 0; // 用int32累加避免溢出 for (int i = 0; i < 784; i++) { sum += (int32_t)input[i] * 1; // 模拟:权重=1 } act1[j] = (int8_t)std::max(-128, std::min(127, sum / 784)); // 量化回 act1[j] = act1[j] > 0 ? act1[j] : 0; // ReLU } // 层2 for (int j = 0; j < 10; j++) { int32_t sum = 0; for (int i = 0; i < 64; i++) { sum += (int32_t)act1[i] * 1; } act2[j] = (int8_t)std::max(-128, std::min(127, sum / 64)); } std::memcpy(output, act2, 10); } int main() { std::cout << "=== TinyML资源预算 ===\n"; std::cout << "缓冲区(RAM): " << ARENA_SIZE << " 字节(" << ARENA_SIZE / 1024 << " KB)\n"; check_model_fit(256); // 典型MCU闪存 // 激活内存使用 int activation_bytes = 64 + 10; // 层1 + 层2输出 std::cout << "激活内存: " << activation_bytes << " 字节 / " << ARENA_SIZE << " 可用\n\n"; // 基准测试推理 int8_t input[784]; int8_t output[10]; std::memset(input, 1, 784); auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 10000; i++) { mock_inference(input, output); } auto end = std::chrono::high_resolution_clock::now(); double us = std::chrono::duration<double, std::micro>(end - start).count() / 10000; std::cout << "推理延迟: " << us << " us\n"; std::cout << "在160 MHz MCU(约6.25 ns/周期)下:约" << (int)(us * 160) << " 周期\n"; std::cout << "输出logits: "; for (int i = 0; i < 10; i++) std::cout << (int)output[i] << " "; std::cout << "\n"; return 0; } -
编写一个C++程序,将float32权重量化为INT8,并测量压缩比和量化误差。
// task2_quantise.cpp // 编译:g++ -O3 -o task2 task2_quantise.cpp #include <iostream> #include <vector> #include <cmath> #include <algorithm> #include <numeric> // 对称量化:将浮点范围 [-max, +max] 映射到 [-127, +127] void quantise_symmetric(const float* input, int8_t* output, int n, float& scale) { float max_val = 0.0f; for (int i = 0; i < n; i++) { max_val = std::max(max_val, std::abs(input[i])); } scale = max_val / 127.0f; for (int i = 0; i < n; i++) { float scaled = input[i] / scale; output[i] = (int8_t)std::max(-127.0f, std::min(127.0f, std::round(scaled))); } } // 反量化:INT8转回float void dequantise(const int8_t* input, float* output, int n, float scale) { for (int i = 0; i < n; i++) { output[i] = (float)input[i] * scale; } } int main() { const int N = 100000; // 模拟随机权重(大致正态分布) std::vector<float> weights(N); for (int i = 0; i < N; i++) { // 简单的伪随机正态值 float u1 = (float)(i * 7 % 997 + 1) / 998.0f; float u2 = (float)(i * 13 % 991 + 1) / 992.0f; weights[i] = std::sqrt(-2.0f * std::log(u1)) * std::cos(6.2832f * u2) * 0.1f; } // 量化 std::vector<int8_t> quantised(N); float scale; quantise_symmetric(weights.data(), quantised.data(), N, scale); // 反量化并测量误差 std::vector<float> reconstructed(N); dequantise(quantised.data(), reconstructed.data(), N, scale); float max_error = 0.0f, total_error = 0.0f; for (int i = 0; i < N; i++) { float err = std::abs(weights[i] - reconstructed[i]); max_error = std::max(max_error, err); total_error += err; } std::cout << "=== 量化结果 ===\n"; std::cout << "原始: " << N * 4 << " 字节(float32)\n"; std::cout << "量化: " << N * 1 << " 字节(int8)+ 4 字节(缩放因子)\n"; std::cout << "压缩比: " << 4.0f << "x\n"; std::cout << "缩放因子: " << scale << "\n"; std::cout << "平均绝对误差: " << total_error / N << "\n"; std::cout << "最大绝对误差: " << max_error << "\n"; std::cout << "最大绝对误差/缩放因子: " << max_error / scale << "(应 <= 0.5 量化级别)\n"; return 0; } -
编写一个C++程序,执行INT8矩阵乘法(INT32累加)——这是在嵌入式ML加速器上运行的实际计算。
// task3_int8_matmul.cpp // 编译:g++ -O3 -o task3 task3_int8_matmul.cpp #include <iostream> #include <chrono> #include <vector> #include <cstdint> // INT8矩阵乘法(INT32累加)——张量核心和MCU加速器的实际工作方式 void matmul_int8(const int8_t* A, const int8_t* B, int32_t* C, int M, int N, int K) { for (int i = 0; i < M; i++) { for (int j = 0; j < N; j++) { int32_t sum = 0; for (int k = 0; k < K; k++) { sum += (int32_t)A[i * K + k] * (int32_t)B[k * N + j]; } C[i * N + j] = sum; } } } // 用于比较的Float32矩阵乘法 void matmul_f32(const float* A, const float* B, float* C, int M, int N, int K) { for (int i = 0; i < M; i++) { for (int j = 0; j < N; j++) { float sum = 0.0f; for (int k = 0; k < K; k++) { sum += A[i * K + k] * B[k * N + j]; } C[i * N + j] = sum; } } } int main() { const int M = 128, N = 128, K = 128; std::vector<int8_t> A_i8(M * K, 1), B_i8(K * N, 1); std::vector<int32_t> C_i32(M * N); std::vector<float> A_f32(M * K, 1.0f), B_f32(K * N, 1.0f); std::vector<float> C_f32(M * N); // 基准测试INT8 auto start = std::chrono::high_resolution_clock::now(); for (int t = 0; t < 100; t++) { matmul_int8(A_i8.data(), B_i8.data(), C_i32.data(), M, N, K); } auto end = std::chrono::high_resolution_clock::now(); double i8_ms = std::chrono::duration<double, std::milli>(end - start).count() / 100; // 基准测试FP32 start = std::chrono::high_resolution_clock::now(); for (int t = 0; t < 100; t++) { matmul_f32(A_f32.data(), B_f32.data(), C_f32.data(), M, N, K); } end = std::chrono::high_resolution_clock::now(); double f32_ms = std::chrono::duration<double, std::milli>(end - start).count() / 100; double gops_i8 = 2.0 * M * N * K / i8_ms / 1e6; double gflops_f32 = 2.0 * M * N * K / f32_ms / 1e6; std::cout << "INT8矩阵乘法: " << i8_ms << " ms(" << gops_i8 << " GOPS)\n"; std::cout << "FP32矩阵乘法: " << f32_ms << " ms(" << gflops_f32 << " GFLOPS)\n"; std::cout << "INT8加速比: " << f32_ms / i8_ms << "x\n"; std::cout << "内存: INT8 = " << M*K + K*N << " 字节 vs FP32 = " << (M*K + K*N) * 4 << " 字节(小4倍)\n"; return 0; }