RISC-V 与嵌入式系统 RISC-V 是一种正在重塑芯片产业的开源指令集架构。本节讲 RISC-V 的设计哲学、V 向量扩展、嵌入式 ML 推理、微控制器上的 TinyML、AI 加速器中的 RISC-V,以及边缘部署的约束。 我们到目前为止讲过的每一种芯片架构(x86、ARM)都需要授权。Intel 和 AMD 为 x86 付费;苹果、高通和每一家手机厂商每年向 ARM 付几十亿。RISC-V 不同:它是一个开放标准。任何人都可以设计、制造、销售 RISC-V 芯片,不必向任何人付版税。这正在改变芯片设计的经济学,尤其是对 AI 而言。
RISC-V 是一种正在重塑芯片产业的开源指令集架构。本节讲 RISC-V 的设计哲学、V 向量扩展、嵌入式 ML 推理、微控制器上的 TinyML、AI 加速器中的 RISC-V,以及边缘部署的约束。
RISC-V(读作 "risk five")于 2010 年在 UC Berkeley 诞生,是一套干净、现代的 RISC 指令集。核心原则:
开放标准: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(存储控制器,出货量数十亿)、Espressif(ESP32-C3,流行的 IoT 芯片),以及数十家 AI 加速器初创公司——它们用 RISC-V 做控制处理器,去管理自家的定制计算单元。
# RISC-V 汇编(感受一下风格——你之后用的是 C/C++) add x3, x1, x2 # x3 = x1 + x2 lw x4, 0(x5) # 从 x5 中的地址加载一个 word sw x4, 8(x5) # 把 word 存到地址 x5 + 8 beq x1, x2, label # 如果 x1 == x2 则分支跳转
#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(长度乘子):RVV 可以把多个向量寄存器编组(m1、m2、m4、m8),每条指令处理更多元素,但可用寄存器变少。m1 每个向量操作数用一个寄存器;m8 用八个,处理 8 倍元素,但只剩 4 组寄存器可用。
相比 x86 AVX(固定 256/512 位)和 ARM NEON(固定 128 位),RVV 的可扩展性对多样化的硬件是一大优势:同一份代码既能跑在微型嵌入式核心(VLEN=128)上,也能跑在高性能服务器核心(VLEN=1024+)上。
TinyML 是在微控制器上做机器学习——这些设备只有几 KB 的 RAM、主频在 MHz 级别、功耗预算在毫瓦级。想象一下:一个能检测唤醒词("Hey Siri")的传感器、一个能分类手势的加速度计、一个能数人的摄像头,全部跑在一颗 0.5 美元的芯片上,且无需联网。
约束极其严苛:
| 资源 | 服务器 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 美元 |
// 在微控制器上做 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)里,而不是从文件系统加载。让模型在微控制器上跑起来,需要激进的优化:
量化(quantisation)(第 18 章):把 float32 权重转成 INT8(小 4 倍,在纯整数硬件上快 2-4 倍)。训练后量化很简单;量化感知训练能保住更多精度。
剪枝(pruning):移除接近零的权重。结构化剪枝(整通道/头地移除)比非结构化剪枝(随机的零)对硬件更友好,因为它减少的是实际计算量,而不只是存储。
知识蒸馏(knowledge distillation)(第 6 章):训练一个小的「学生」模型去模仿大的「教师」模型。学生比从零训练的精度更高,因为它从教师的软预测中学习。
神经架构搜索(Neural Architecture Search,NAS):自动搜索能在硬件预算(延迟、内存、功耗)内放得下的高效架构。MicroNets 和 MCUNet 找到了针对特定微控制器优化的架构。
算子融合:把卷积 + batch norm + ReLU 合成一个融合操作,消除中间的内存写入(与 GPU 内核融合是同一原理,但当你只有 256 KB RAM 时更要命)。
┌─────────────────────────────────────────┐ │ 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 美元(硬件已经存在)。部署到十亿个 IoT 传感器上,意味着每个传感器的 ML 硬件预算只有几分钱。在这种规模下,RISC-V 的零授权成本意义重大。
// 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 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) { // 激活值放在竞技场(RAM)里,不动态分配 int8_t* act1 = (int8_t*)arena; // 64 字节,第 1 层输出 int8_t* act2 = (int8_t*)(arena + 64); // 10 字节,第 2 层输出 // 第 1 层:简化的 matmul(不是真正的量化 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)); // 量化回去 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 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; // 第 1 层 + 第 2 层输出 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; }
// task2_quantise.cpp // 编译:g++ -O3 -o task2 task2_quantise.cpp #include <iostream> #include <vector> #include <cmath> #include <algorithm> #include <numeric> // 对称量化:把 float 范围 [-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; }
// task3_int8_matmul.cpp // 编译:g++ -O3 -o task3 task3_int8_matmul.cpp #include <iostream> #include <chrono> #include <vector> #include <cstdint> // INT8 matmul,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 matmul 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; }