Skip to content

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年在UC Berkeley创建,作为一个干净现代的RISC指令集。关键原则:

    • 开放标准:ISA规范免费可用。你可以构建RISC-V CPU而无需许可费、NDA或法律协议。这就像Linux之于操作系统——任何人都可以使用、修改和在其上构建。

    • 模块化设计:基础ISA(RV32I或RV64I)极小——仅47条指令。其他一切是可选的扩展:M(乘/除)、A(原子操作)、F/D(浮点)、C(压缩指令)、V(向量处理)。你只选择需要的,保持芯片小巧高效。

    • 没有遗留包袱:x86背负45年的向后兼容。ARM背负35年。RISC-V从头开始,吸收了从两者中学到的经验。没有仅为兼容1980年代软件而存在的晦涩指令。

  • 谁在使用RISC-V:SiFive(通用核心)、Alibaba(Xuantie服务器核心)、Western Digital(存储控制器,已出货数十亿颗)、Espressif(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中的地址加载word
sw   x4, 8(x5)       # 存储word到地址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(长度乘数):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模型,无需动态内存分配,无需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);

// 分配固定内存arena(没有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):自动搜索在硬件预算内(延迟、内存、功耗)的高效架构。MicroNetsMCUNet找到针对特定微控制器优化的架构。

    • 算子融合:将conv + batch norm + 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)对于这些用例来说太慢。

  • 隐私:设备端处理数据意味着敏感数据(医学图像、语音录音、个人照片)永不离开设备。这在某些司法管辖区是法律要求(GDPR),在各地都是用户信任要求。

  • 连接性:边缘设备可能具有间歇或无互联网连接。在火星车(第11章)、潜艇或农村农场传感器上运行的模型必须完全离线工作。

  • 规模成本:将ML部署到十亿部智能手机上每部设备成本为$0(硬件已存在)。部署到十亿个IoT传感器意味着每个传感器的ML硬件预算是几分钱。RISC-V的零许可成本在此规模上极其重要。

编程任务(用g++或riscv64-gcc交叉编译器编译)

  1. 编写一个模拟TinyML推理流水线的C程序:静态分配模型缓冲区,运行模拟前向传播,测量资源使用。这教授嵌入式约束(无malloc、固定内存arena)。

    // task1_tinyml_sim.cpp
    // 编译:g++ -O2 -o task1 task1_tinyml_sim.cpp
    
    #include <iostream>
    #include <chrono>
    #include <cmath>
    #include <cstring>
    
    // 模拟微控制器:固定内存arena,无动态分配
    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";
    }
    
    // 使用固定arena进行模拟推理
    void mock_inference(const int8_t* input, int8_t* output) {
        // 激活值放入arena(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 << "Arena (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;
    }
    

  2. 编写一个将float32权重量化为INT8并测量压缩比和量化误差的C++程序。

    // 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 << "=== 量化结果 ===\n";
        std::cout << "原始:    " << N * 4 << " 字节 (float32)\n";
        std::cout << "量化:   " << N * 1 << " 字节 (int8) + 4 字节 (scale)\n";
        std::cout << "压缩: " << 4.0f << "x\n";
        std::cout << "缩放因子: " << scale << "\n";
        std::cout << "平均绝对误差: " << total_error / N << "\n";
        std::cout << "最大绝对误差:  " << max_error << "\n";
        std::cout << "最大绝对误差 / scale: " << max_error / scale
                  << " (应该 <= 0.5 量化等级)\n";
    
        return 0;
    }
    

  3. 编写执行INT8矩阵乘法和INT32累积的C++程序——在嵌入式ML加速器上运行的实际计算。

    // task3_int8_matmul.cpp
    // 编译:g++ -O3 -o task3 task3_int8_matmul.cpp
    
    #include <iostream>
    #include <chrono>
    #include <vector>
    #include <cstdint>
    
    // INT8矩阵乘法,INT32累积(Tensor Cores和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 矩阵乘法:  " << i8_ms << " ms (" << gflops_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;
    }