02 — 快速上手教程
目标: 30 分钟内,亲自动手编译一个 AI 模型,看懂每一步发生了什么。 前置要求: 环境已搭建(00-环境搭建指南),虚拟环境已激活。
目录
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(中间表示)
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 等。
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 模型
my_examples/hello.dsl ← 你写的 DSL 代码
│
DSLParser.parse() ← 解析成 IR Program
│
IR Program (三地址码) ← 中间表示
│
[Optimizer] ← (可选) 优化 IR
│
InstructionSelector ← IR → RISC-V 伪指令 (MachineInstr 列表)
│
RegisterAllocator ← 虚拟寄存器 → 物理寄存器
│
AsmEmitter ← 输出汇编文本
│
RISC-V 汇编 (.s) ← 人类可读
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
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 参数说明
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%
| 参数 | 作用 |
|---|---|
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 路径编译
# 编译产出的文件
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 亿条,循环里的指令每次迭代都算),这个数才是真正影响运行速度的。
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
# 基础测试(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
# 生成静态 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 对比。这就是编译器工程师的日常工作。