02 — 快速上手教程

目标: 30 分钟内,亲自动手编译一个 AI 模型,看懂每一步发生了什么。 前置要求: 环境已搭建(00-环境搭建指南),虚拟环境已激活。


目录
  1. Hello World: 你的第一个 DSL 程序
  2. 理解编译产物
  3. 编译 CNN 模型
  4. 用 LLVM 路径编译
  5. 运行基准测试
  6. 查看性能仪表盘
  7. 下一步学什么

1. Hello World: 你的第一个 DSL 程序

ScratchV 有自己的迷你编程语言(DSL),用来描述张量计算。我们先写一个最简单的:两个数相加。

1.1 创建 DSL 文件
mkdir -p my_examples

cat > my_examples/hello.dsl << 'EOF'
# 我的第一个 DSL 程序: 两个数相加
a = add(3, 5)
return a
EOF

💡 DSL 语法要点: 数字可以直接内联使用(如 3, 5),不需要 const() 包装。支持的算子:add, mul, relu, matmul, dot, gelu, softmax, maxpool 等。

1.2 编译为 IR(中间表示)

DSL 程序第一步是翻译成 ScratchV 的中间表示(IR):

python3 -c "
from scratchv.frontend.dsl_parser import DSLParser

parser = DSLParser()
program = parser.parse(open('my_examples/hello.dsl').read())
print(program.dump())
"

实际输出:

fun $main(
  .entry:
    $v_1 = load_const [value=3.0]
    $v_2 = load_const [value=5.0]
    $v_3 = add $v_1 $v_2
    return $v_3

这就是 三地址码 IR。每一行是一个操作,$v_1, $v_2, $v_3 是虚拟寄存器。

1.3 编译为 RISC-V 汇编

接下来把 IR 一步步变成 RISC-V 汇编:

python3 -c "
from scratchv.frontend.dsl_parser import DSLParser
from scratchv.backend.instruction_select import InstructionSelector
from scratchv.backend.register_alloc import RegisterAllocator
from scratchv.backend.asm_emit import AsmEmitter

# 1. 解析 DSL → IR
code = open('my_examples/hello.dsl').read()
parser = DSLParser()
program = parser.parse(code)

# 2. 指令选择: IR → RISC-V 伪指令
selector = InstructionSelector(program)
instrs = selector.run()

# 3. 寄存器分配: 虚拟寄存器 → 物理寄存器
allocator = RegisterAllocator(instrs, mode='greedy')
allocated = allocator.run()

# 4. 汇编发射: 输出文本
emitter = AsmEmitter(allocated)
print(emitter.emit())
"

实际输出:

.text
.align 2
  .globl main
  .type main, @function
main:
.entry:
    li t0, 3
    li t1, 5
    add t2, t0, t1
    mv a0, t2
    jalr zero, ra

💡 这就是 a = add(3, 5) 变出来的 RISC-V 汇编。li = 加载常数,add = 加法,mv = 移动数据,jalr zero, ra = 返回。


2. 理解编译产物

2.1 指令长什么样?

RISC-V 汇编的格式是:操作码 目标寄存器, 源寄存器1, 源寄存器2

add t2, t0, t1        t2 = t0 + t1
mul a3, a0, a1        a3 = a0 * a1
lw  a4, 0(a0)         a4 = 从内存地址 a0 加载 32 位数据
sw  a4, 0(a0)          a4 存到内存地址 a0
li  t0, 3             t0 = 3(加载立即数)
mv  a0, t2            a0 = t2(寄存器间复制)

寄存器命名约定: - a0-a7 — 函数参数 / 返回值 - t0-t6 — 临时变量(函数可以随意改写) - s0-s11 — 保留变量(函数必须恢复原值) - zero — 永远为 0 - ra — 返回地址 - sp — 栈指针 - gp — 全局数据指针

2.2 编译流程全景
my_examples/hello.dsl      你写的 DSL 代码
        
   DSLParser.parse()        解析成 IR Program
        
   IR Program (三地址码)     中间表示
        
   [Optimizer]              (可选) 优化 IR
        
   InstructionSelector      IR  RISC-V 伪指令 (MachineInstr 列表)
        
   RegisterAllocator        虚拟寄存器  物理寄存器
        
   AsmEmitter               输出汇编文本
        
   RISC-V 汇编 (.s)          人类可读

3. 编译 CNN 模型

ScratchV 最核心的功能是把 ONNX 格式的 CNN 模型编译成 RISC-V 机器码。

3.1 一条命令编译
python3 scratchv/standalone/onnx_to_riscv_standalone.py models/graph/cnn.onnx \
    -o /tmp/cnn_riscv.bin --estimate --report

也可以使用 make bench-cnn(效果一样)。

3.2 实际输出
ScratchV: Library-free ONNX  RISC-V RV32IM Pipeline
  Input:  models/graph/cnn.onnx

[1/5] Parsing ONNX protobuf (manual wire-format parser)...
  Graph: main_graph
  Nodes: 15
  Initializers: 12
    [0] Conv:  input1, layer1.0.weight  ...
    [1] Relu:  ...
    [2] MaxPool: ...
    [3] Conv:  ...
    [4] Relu:  ...
    [5] MaxPool: ...
    [6] Conv:  ...
    [7] Relu:  ...
    [8] MaxPool: ...
    [9] Reshape: ...
    [10] Gemm:  ...
    [11] Relu:  ...
    [12] Gemm:  ...
    [13] Sigmoid: ...
    [14] Reshape: ...

[2/5] Converting weights to Q16.16 fixed-point and planning memory...
  Weight data: 27,673,524 bytes (26.4 MB)
  Input 'input1': shape=(1, 3, 250, 250), 187,500 elements

[3/5] Generating inline RISC-V RV32IM machine code...
  Code size: 3,548 bytes (887 instructions)
  Workspace: 24,963,644 bytes (23.8 MB)

[4/5] Assembling flat binary...
  Total binary: 27,677,072 bytes (26.4 MB)

[5/5] Writing output files...
  Binary: /tmp/cnn_riscv.bin (27,677,072 bytes)
  Assembly: /tmp/cnn_riscv.s

Pipeline complete!
  Code:   3,548 bytes (887 instructions)
  Data:   27,673,524 bytes (26.4 MB)
  Total:  27,677,072 bytes (26.4 MB)

[estimate] Analytical instruction count estimation...
  Estimated total instructions: 2,203,799,060
  Compute:  62.8%  Memory: 24.8%  Branch: 3.8%

  Layer                             Instructions        %
  ────────────────────────────── ─────────────── ────────
  Conv1                              425,115,648    19.3%
  Conv2                            1,097,367,552    49.8%
  Conv3                              513,294,336    23.3%
  MaxPool1                            23,617,536     1.1%
  FC1                                103,342,080     4.7%
  ... (其他层)                        ...            ...
  ────────────────────────────── ─────────────── ────────
  TOTAL                            2,203,799,060   100.0%

3.3 参数说明
参数 作用
models/graph/cnn.onnx 输入:ONNX 模型文件
-o /tmp/cnn_riscv.bin 输出:RISC-V flat binary
--estimate 开启性能估算(动态指令数、CPI 分析)
--report 生成 HTML/JSON/MD 三种格式报告到 benchmark_reports/
--asm output.s (可选) 同时输出汇编文件到指定路径

3.4 产出文件
# 编译产出的文件
ls /tmp/cnn_riscv.bin    # 二进制机器码 (26.4 MB)
ls /tmp/cnn_riscv.s      # 汇编源码
ls benchmark_reports/    # 性能报告目录
#   benchmark.html        - HTML 报告
#   benchmark.json        - JSON 数据
#   github_summary.md     - Markdown 摘要

💡 静态指令 = 编译器生成了多少条指令(887 条,不管循环执行多少次)。 动态指令 = CPU 实际执行了多少条(~22 亿条,循环里的指令每次迭代都算),这个数才是真正影响运行速度的。


4. 用 LLVM 路径编译

ScratchV 还有一条 LLVM 路径,生成 LLVM IR 而不是直接生成 RISC-V 机器码。

# 需要先安装 llvmlite: pip install llvmlite
python3 scratchv/standalone/onnx_to_llvm_standalone.py models/graph/cnn.onnx \
    -o /tmp/output.ll --compare

实际输出:

ScratchV: Library-free ONNX  LLVM IR Pipeline
  Input:  models/graph/cnn.onnx
  Graph: main_graph
  Nodes: 15
  Initializers: 12

[1/3] Generating LLVM IR with real nested-loop compute...
  LLVM IR written to: /tmp/output.ll
  Lines: 866172
  Static LLVM IR instructions: 1102
  Functions: 17
  - fadd: 35, fmul: 13, fdiv: 10
  - load: 94, store: 129, alloca: 57
  - br: 99, br_cond: 51, icmp: 60

对比两条路径:

指标 ScratchV 原生路径 LLVM 路径
静态指令数 887 1102
数值格式 Q16.16 定点(整数指令) float32(浮点指令)
动态指令估算 ~22.0 亿 取决于 LLVM 优化级别

💡 ScratchV 用 Q16.16 定点mul + srai 两条整数指令模拟浮点乘法),比 LLVM 的 fmul 单条浮点指令多一条。但 ScratchV 的紧耦合代码生成在循环展开和内存访问模式上更激进,综合静态指令数反而更少(887 vs 1102)。


5. 运行基准测试
# 基础测试(348 个用例,约 1-2 秒)
python3 -m pytest tests/ -v --tb=short

# CNN 模型编译 + 估算(约 2-3 秒)
make bench-cnn

# DSL 性能基准(约 3-5 秒)
make bench

# 完整 CI 基准(LLVM + Dashboard,约 10-15 秒)
make bench-ci

各命令耗时和产出参考:

命令 耗时 产出
python3 -m pytest tests/ -v ~2 秒 终端测试结果
make bench-cnn ~3 秒 性能报告 HTML + JSON + MD
make bench ~5 秒 DSL 基准 HTML + JSON
make bench-ci ~15 秒 仪表盘 HTML + JSON + MD 摘要

6. 查看性能仪表盘
# 生成静态 HTML 仪表盘(零外部依赖)
make bench-dashboard

# 用浏览器打开
xdg-open benchmark_reports/dashboard.html

仪表盘展示内容: - 静态/动态指令数对比(ScratchV vs LLVM) - 按算子拆分的代码量分析 - 缓存命中率估算


7. 下一步学什么

根据你的兴趣选择:

你想了解... 去看...
这些指标数字是什么意思 03-指标解读指南
遇到错误怎么解决 04-故障排除FAQ
整体架构是什么样 ARCHITECTURE
某个具体模块怎么实现 topics/INDEX — 30 个课题
想参与优化、贡献代码 developer_guide

🎉 恭喜!你已经完成了 ScratchV 的快速上手。 你刚才做的事情:手写 DSL → 看到 IR → 生成汇编 → 编译完整 CNN 模型 → 查看 LLVM 对比。这就是编译器工程师的日常工作。