RISC-V 与嵌入式系统¶
一句话总结: RISC-V 是开源、模块化、无需授权的指令集架构,正在重塑芯片产业,也是 TinyML 与 AI 加速器控制核心的首选平台。
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 指令集。 核心原则:
-
开放标准:指令集架构 (Instruction Set Architecture, ISA) 规范免费公开。 你可以在不支付授权费、不签保密协议、不订立法律协议的前提下造出 RISC-V CPU. 这就像 Linux 之于操作系统 —— 任何人都可以使用、修改、并在其之上构建。
-
模块化设计:基础 ISA (RV32I 或 RV64I) 极简 —— 只有 47 条指令。 其他一切都是可选的扩展: M (乘除法)、A (原子操作)、F/D (浮点)、C (压缩指令)、V (向量处理). 你只挑自己需要的,芯片保持小巧高效。
-
无历史包袱: x86 背着 45 年的向后兼容包袱。 ARM 背着 35 年。 RISC-V 是从零开始,吸取了两者的经验教训。 没有那些只为兼容 1980 年代软件而存在的冷僻指令。
-
-
谁在用 RISC-V: SiFive (通用核)、阿里巴巴 (玄铁服务器核)、Western Digital (存储控制器,出货以十亿计)、乐鑫 (ESP32-C3,流行的物联网芯片),以及数十家 AI 加速器初创公司 —— 它们用 RISC-V 作为管理自家定制计算单元的控制处理器。
RISC-V 基础架构¶
- 基础整数 ISA (64 位版本 RV64I) 具备:
- 32 个通用寄存器 (x0-x31,每个 64 位). x0 硬连线为 0 (方便实现常见模式而无需专门指令).
- 固定 32 位指令宽度 (C 扩展额外提供 16 位压缩指令以提高代码密度).
- 加载-存储架构:与 ARM 一样,算术只作用于寄存器。 内存访问必须通过显式的 load/store 指令。
# 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, hardware_max) 个元素
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 (length multiplier,长度倍数): RVV 可以把多个向量寄存器组合起来 (m1, m2, m4, m8),每条指令处理更多元素,代价是可用寄存器数量减少。
m1每个向量操作数占一个寄存器;m8占八个,一次多处理 8 倍元素,但只剩 4 组寄存器可用。 -
相比 x86 AVX (固定 256/512 位) 与 ARM NEON (固定 128 位), RVV 的可扩展性是应对多样硬件的重大优势:同一份代码既能跑在极小的嵌入式核 (VLEN=128) 上,也能跑在高性能服务器核 (VLEN=1024+) 上。
嵌入式 ML: TinyML¶
-
TinyML 是跑在微控制器上的机器学习 —— 那些只有几十 KB RAM、MHz 级 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 模型,不做动态内存分配,不依赖操作系统 (Operating System, OS),二进制体积只有约 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 章):训练一个小"学生"模型去模仿大"老师"模型。 学生的精度比从头训练要高,因为它能从老师的软预测中学到更多信息。
-
神经架构搜索 (Neural Architecture Search, NAS):自动搜索能塞进硬件预算 (延迟、内存、功耗) 的高效架构。 MicroNets 和 MCUNet 就是为特定微控制器搜索出的最优架构。
-
算子融合:把卷积 + 批归一化 + ReLU 融合成一个操作,省掉中间结果的内存写回 (原理和 GPU 上的核函数融合一样,但在只有 256 KB RAM 时更为关键).
-
AI 加速器中的 RISC-V¶
- 许多 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) 对这类场景来说太慢了。
-
隐私:数据在设备端处理意味着敏感数据 (医疗影像、语音录音、私人照片) 从不离开设备。 这在某些司法辖区 (通用数据保护条例,General Data Protection Regulation, GDPR) 是法律要求,到处都是用户信任的要求。
-
连接性:边缘设备的网络连接可能时断时续或干脆没有。 跑在火星车上 (第 11 章)、潜艇里、乡村农场传感器上的模型必须完全离线工作。
-
规模化成本:把 ML 部署到十亿部智能手机,每台设备成本为 0 (硬件本来就存在). 部署到十亿个 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; // 总 RAM 预算 32 KB static uint8_t arena[ARENA_SIZE]; // 简单的 2 层 MLP: 784 -> 64 -> 10 (类似 MNIST, INT8 权重) struct TinyModel { int8_t w1[784 * 64]; // 第一层权重: 50,176 字节 int8_t b1[64]; // 第一层偏置 int8_t w2[64 * 10]; // 第二层权重: 640 字节 int8_t b2[10]; // 第二层偏置 // 合计: 约 51 KB → 必须放到闪存 (ROM), 不放到 RAM }; // 检查模型能否装进闪存 void check_model_fit(int flash_kb) { int model_bytes = sizeof(TinyModel); std::cout << "Model size: " << model_bytes << " bytes (" << model_bytes / 1024 << " KB)\n"; std::cout << "Flash: " << flash_kb << " KB → " << (model_bytes <= flash_kb * 1024 ? "FITS" : "TOO LARGE") << "\n"; } // 模拟推理, 使用固定内存区存放激活 void mock_inference(const int8_t* input, int8_t* output) { // 激活放在 arena (RAM) 里, 不做动态分配 int8_t* act1 = (int8_t*)arena; // 第一层输出占 64 字节 int8_t* act2 = (int8_t*)(arena + 64); // 第二层输出占 10 字节 // 第一层: 简化的矩阵乘 (不是真正的量化 matmul, 只演示结构) 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)); // 量化回 int8 act1[j] = act1[j] > 0 ? act1[j] : 0; // ReLU } // 第二层 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 Resource Budget ===\n"; std::cout << "Arena (RAM): " << ARENA_SIZE << " bytes (" << ARENA_SIZE / 1024 << " KB)\n"; check_model_fit(256); // 典型 MCU 的闪存 // 用掉的激活内存 int activation_bytes = 64 + 10; // 第一层 + 第二层输出 std::cout << "Activation memory: " << activation_bytes << " bytes / " << ARENA_SIZE << " available\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 << "Inference latency: " << us << " us\n"; std::cout << "At 160 MHz MCU (~6.25 ns/cycle): ~" << (int)(us * 160) << " cycles\n"; std::cout << "Output 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 << "=== Quantisation Results ===\n"; std::cout << "Original: " << N * 4 << " bytes (float32)\n"; std::cout << "Quantised: " << N * 1 << " bytes (int8) + 4 bytes (scale)\n"; std::cout << "Compression: " << 4.0f << "x\n"; std::cout << "Scale factor: " << scale << "\n"; std::cout << "Mean abs error: " << total_error / N << "\n"; std::cout << "Max abs error: " << max_error << "\n"; std::cout << "Max abs error / scale: " << max_error / scale << " (should be <= 0.5 quantisation levels)\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 累加 (Tensor Core 和 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 gflops_i8 = 2.0 * M * N * K / i8_ms / 1e6; double gflops_f32 = 2.0 * M * N * K / f32_ms / 1e6; std::cout << "INT8 matmul: " << i8_ms << " ms (" << gflops_i8 << " GOPS)\n"; std::cout << "FP32 matmul: " << f32_ms << " ms (" << gflops_f32 << " GFLOPS)\n"; std::cout << "INT8 speedup: " << f32_ms / i8_ms << "x\n"; std::cout << "Memory: INT8 = " << M*K + K*N << " bytes vs FP32 = " << (M*K + K*N) * 4 << " bytes (4x less)\n"; return 0; }
📌 本节要点¶
- RISC-V 开放标准:免授权费的开源 ISA,改变了芯片经济模型,尤其利好 AI 创业与嵌入式。
- 模块化基础 ISA: RV32I/RV64I 只有 47 条基础指令,M/A/F/D/C/V 全为可选扩展,按需取用。
- V 扩展与 VLEN: RVV 向量长度硬件可配,代码写成向量长度无关形式,同一份代码通吃嵌入式核到服务器核。
- LMUL 灵活度:用 m1/m2/m4/m8 合并寄存器,换取每指令更多数据吞吐但可用寄存器减少。
- TinyML 三重极限:微控制器只有 KB 级 RAM、MHz 级 CPU、mW 级功耗,强迫使用 INT8/INT4 与激进优化。
- TFLM 关键约束:静态内存区 + 模型驻留闪存 + 无堆分配,二进制体积可压到约 20 KB.
- 边缘优化组合拳:量化、剪枝、知识蒸馏、神经架构搜索 (NAS)、算子融合协同压缩模型。
- RISC-V 做控制核: AI 加速器用 RISC-V 调度定制矩阵单元,重计算交给脉动阵列而非 CPU 完成。
- 边缘部署四大约束:功耗预算、实时延迟、数据隐私、离线可用性缺一不可。
- 规模化的经济账:十亿级 IoT 传感器上,RISC-V 零授权费的成本优势极其显著。