Skip to content

Commit 3bb88e8

Browse files
authored
feat(qwen3): 跑通 W1 两层 RISC-V 仿真,补齐精度、计时与独立复现验收 (#91)
* feat(probe): validate a real two-layer Qwen3 through ONNX and IR * feat(qwen3): execute tensor IR on RV64 and close W1 validation gaps * test(runtime): handle Linux process reaping races and record CI evidence * ci: verify CNN tensor IR through the dedicated IR backend * feat: report QEMU timing and close W1 reproduction gaps * fix: make PR91 reproduction tests deterministic and explicit
1 parent d1c2b5f commit 3bb88e8

54 files changed

Lines changed: 8181 additions & 1130 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,12 @@ jobs:
242242
run: |
243243
mkdir -p benchmark_reports/ir_verifier_smoke
244244
for level in none basic all; do
245+
# Verify tensor IR without invoking the scalar-only legacy backend.
245246
$PY -m scratchv models/graph/cnn.onnx \
246-
--verify-ir --optimize "$level" --dump-ir \
247-
-o "benchmark_reports/ir_verifier_smoke/cnn_${level}.s" \
247+
--backend ir --verify-ir --optimize "$level" --dump-ir \
248+
-o "benchmark_reports/ir_verifier_smoke/cnn_${level}.ir" \
248249
2>&1 | tee "benchmark_reports/ir_verifier_smoke/cnn_${level}.log"
249-
test -s "benchmark_reports/ir_verifier_smoke/cnn_${level}.s"
250+
test -s "benchmark_reports/ir_verifier_smoke/cnn_${level}.ir"
250251
done
251252
shell: bash
252253

‎.github/workflows/llm-deploy.yml‎

Lines changed: 252 additions & 58 deletions
Large diffs are not rendered by default.

‎benchmarks/bench_cnn_schedule.py‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929

3030
ROOT = Path(__file__).resolve().parents[1]
3131
DEFAULT_MODEL = ROOT / "models/graph/cnn.onnx"
32+
LEGACY_TENSOR_REJECTION = (
33+
"Codegen error: Legacy riscv/llvm backends cannot lower tensor buffers; "
34+
"use --backend tensor-c and the RISC-V tensor runtime"
35+
)
3236

3337

3438
def sha256(data: bytes) -> str:
@@ -199,7 +203,7 @@ def run_benchmark(model_path: Path = DEFAULT_MODEL, *, standalone_asm: Path | No
199203
model_bytes = model_path.read_bytes()
200204
model = onnx.load_model_from_string(model_bytes)
201205
cpu_model = model_metadata(llvm_mca)
202-
cases = []
206+
cases, unsupported_cases = [], []
203207
with tempfile.TemporaryDirectory(prefix="cnn-schedule-") as directory:
204208
work = Path(directory)
205209
out = artifacts.resolve() if artifacts else work
@@ -230,7 +234,7 @@ def run_benchmark(model_path: Path = DEFAULT_MODEL, *, standalone_asm: Path | No
230234
if standalone_asm is not None and standalone_asm.read_bytes() != default_asm.read_bytes():
231235
raise ValueError("Standalone assembly does not match this ONNX model and --const-merge")
232236
primary = analyze_assembly(generated, "standalone/const-merge", llvm_mca)
233-
if primary["assembly"]["after"] != scheduled.read_text():
237+
if primary["assembly"]["after"] != scheduled.read_bytes().decode("utf-8"):
234238
raise ValueError("Standalone scheduled binary differs from the public assembly pass")
235239
if baseline_bin.read_bytes()[metadata["code_bytes"]:] != scheduled_bin.read_bytes()[metadata["code_bytes"]:]:
236240
raise ValueError("Scheduling changed weight bytes")
@@ -250,6 +254,13 @@ def run_benchmark(model_path: Path = DEFAULT_MODEL, *, standalone_asm: Path | No
250254
config = CompilerConfig(reg_alloc=allocator, optimize_level="none")
251255
compiled = CompilerDriver(config).compile(str(model_path), str(path))
252256
if not compiled.success:
257+
if compiled.errors == [LEGACY_TENSOR_REJECTION]:
258+
unsupported_cases.append({
259+
"case": f"CompilerDriver/{allocator}", "role": "supplemental",
260+
"status": "unsupported", "reason": LEGACY_TENSOR_REJECTION,
261+
"compiler_errors": list(compiled.errors),
262+
})
263+
continue
253264
raise RuntimeError("; ".join(compiled.errors))
254265
case = analyze_assembly(path, f"CompilerDriver/{allocator}", llvm_mca)
255266
(out / f"compiler-{allocator}-after.s").write_text(case["assembly"]["after"])
@@ -273,8 +284,11 @@ def run_benchmark(model_path: Path = DEFAULT_MODEL, *, standalone_asm: Path | No
273284
if any(value is False for value in primary["acceptance"].values()):
274285
primary["status"] = "failed"
275286
return {
276-
"schema_version": 4, "benchmark_type": "cnn-scheduling-ab",
287+
"schema_version": 5, "benchmark_type": "cnn-scheduling-ab",
277288
"status": "passed" if all(c["status"] == "passed" for c in cases) else "failed",
289+
"acceptance_scope": "standalone CNN scheduling and execution",
290+
"supplemental_status": "unsupported" if unsupported_cases else "passed",
291+
"unsupported_cases": unsupported_cases,
278292
"validation_scope": "static_and_llvm" + ("_and_execution" if execute else ""),
279293
"whole_cnn_execution": primary["execution"]["status"],
280294
"input": {"path": model_path.relative_to(ROOT).as_posix() if model_path.is_relative_to(ROOT) else str(model_path),
@@ -299,6 +313,11 @@ def markdown(report: dict) -> str:
299313
lines = ["# Topic 18:CNN 指令调度验证", "", f"验证状态:**{report['status']}**。"]
300314
if "error" in report:
301315
return "\n".join(lines + ["", report["error"], ""])
316+
lines += ["", "验收范围:**standalone CNN 调度与执行**;是否实际执行见下方完整执行状态。"]
317+
if report["unsupported_cases"]:
318+
lines += ["", "**CompilerDriver CNN 张量未支持;以下补充路径为 unsupported,未生成汇编、未测量周期,也未通过执行验证。**"]
319+
lines += [f"- `{case['case']}`:{case['status']};{case['reason']}"
320+
for case in report["unsupported_cases"]]
302321
lines += [
303322
f"输入:`{report['input']['path']}`,{report['input']['nodes']} 个节点;SHA-256:`{report['input']['sha256']}`。",
304323
"主路径为 standalone CNN 编译器,固定启用常量合并;CompilerDriver 两种分配器仅作补充回归。",
@@ -392,7 +411,7 @@ def markdown(report: dict) -> str:
392411
"未建模指令作为调度边界:自身固定,前后指令分别在各自区域内分析,禁止跨越该边界移动。其他未建模原因及所在行见 JSON 的 `scheduling.diagnostics`;CFG 活跃性遇到未知寄存器语义时标为 N/A,不能解释为零活跃寄存器。",
393412
"",
394413
"测试对象与结论范围:", "",
395-
"- **CompilerDriver 是补充编译回归。** 它将 Conv/Gemm 等算子简化为少量乘加,没有展开完整张量计算;与 standalone 的静态指令数差异不能归因于调度。完整执行列的 `not_run` 表示未执行机器码做结果验证;静态结构检查通过不等于完整 CNN 推理正确。",
414+
"- **CompilerDriver 是补充编译回归。** 旧标量后端拒绝 CNN 张量输入,拒绝原因单列于 `unsupported_cases`,不提供虚构的汇编或周期数据。完整执行列的 `not_run` 表示未执行机器码做结果验证;静态结构检查通过不等于完整 CNN 推理正确。",
396415
"- **静态区域只计一次。** 完整 standalone 代码也包含循环;例如 4 条循环体指令执行 1000 次,静态仍是 4 条,动态执行量为 4000 次。报告不按循环次数或分支频率加权,各区域从周期 0、入口操作数就绪开始独立估算,没有贯通跨区域的动态等待。",
397416
"- **性能结论限于模型。** llvm-mca 使用 SiFive7 的资源约束;报告未测量真实硬件周期、缓存未命中或分支预测开销,因此局部周期合计下降不能直接换算为整网运行时间下降。",
398417
"- **执行验证覆盖已测输入。** QEMU 用于比较调度前后执行状态,不用于测量硬件加速;固定输入上的一致性不等于所有输入的证明,也不验证定点结果与 ONNX 浮点参考的精度。", ""]

‎benchmarks/bench_ir_verifier.py‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Compare shared-pipeline compilation with IR verification disabled/enabled.
22
33
Run with ``python -m benchmarks.bench_ir_verifier``. Timings include parsing,
4-
optimizations, code generation and writing output; they are not model runtime.
4+
optimizations, IR serialization and writing output; they are not model runtime.
55
"""
66

77
from __future__ import annotations
@@ -89,7 +89,7 @@ def benchmark(model: Path, *, repeats: int = 5, warmup: int = 1,
8989
model_hash = hashlib.sha256(model.read_bytes()).hexdigest()
9090
rows = []
9191
with tempfile.TemporaryDirectory(prefix="scratchv-ir-bench-") as directory:
92-
output = str(Path(directory) / "output.s")
92+
output = str(Path(directory) / "output.ir")
9393
for level in levels:
9494
samples = {False: [], True: []}
9595
reference = None
@@ -98,7 +98,7 @@ def benchmark(model: Path, *, repeats: int = 5, warmup: int = 1,
9898
order = (False, True) if iteration % 2 == 0 else (True, False)
9999
for enabled in order:
100100
driver = CompilerDriver(CompilerConfig(
101-
backend="riscv", optimize_level=level, verify_ir=enabled,
101+
backend="ir", optimize_level=level, verify_ir=enabled,
102102
))
103103
start = time.perf_counter()
104104
result = driver.compile(str(model), output)
@@ -115,7 +115,7 @@ def benchmark(model: Path, *, repeats: int = 5, warmup: int = 1,
115115
reference = digest
116116
elif digest != reference:
117117
raise RuntimeError(
118-
f"optimize={level}: generated assembly differs between runs"
118+
f"optimize={level}: generated IR differs between runs"
119119
)
120120
if iteration >= warmup:
121121
samples[enabled].append(elapsed)
@@ -135,20 +135,20 @@ def benchmark(model: Path, *, repeats: int = 5, warmup: int = 1,
135135
return {
136136
"model": str(model), "model_sha256": model_hash,
137137
"python": platform.python_version(), "platform": platform.platform(),
138-
"backend": "riscv", "repeats": repeats, "warmup": warmup,
139-
"timing_scope": "parse + optimize + codegen + output write",
138+
"backend": "ir", "repeats": repeats, "warmup": warmup,
139+
"timing_scope": "parse + optimize + IR serialization + output write",
140140
"results": rows,
141141
}
142142

143143

144144
def render_markdown(report: dict) -> str:
145145
lines = [
146146
"# Topic 21 IR 验证器:编译耗时与规则示例", "",
147-
f"模型:`{Path(report['model']).name}`;后端:RISC-V;"
147+
f"模型:`{Path(report['model']).name}`;输出:优化后 IR;"
148148
f"预热 {report['warmup']} 次,采样 {report['repeats']} 次,取中位数。", "",
149149
"通过 CompilerConfig.verify_ir 开关,在正式编译流程的解析后、每个 IR pass "
150150
"前后和代码生成前验证。两组均重新解析模型。", "",
151-
"| 优化级别 | 关闭验证 (ms) | 开启验证 (ms) | 差值 (ms) | 差值 (%) | 汇编一致 |",
151+
"| 优化级别 | 关闭验证 (ms) | 开启验证 (ms) | 差值 (ms) | 差值 (%) | IR 一致 |",
152152
"| --- | ---: | ---: | ---: | ---: | --- |",
153153
]
154154
for row in report["results"]:
@@ -160,10 +160,11 @@ def render_markdown(report: dict) -> str:
160160
f"| {row['overhead_s'] * 1000:.3f} | {percent_text} | 是 |"
161161
)
162162
lines.extend([
163-
"", "计时包含解析、优化、代码生成和写文件,不是模型运行耗时,也不是验证器函数的独立耗时。",
163+
"", "计时包含解析、优化、IR 序列化和写文件,不是模型运行耗时,也不是验证器函数的独立耗时。",
164164
"差值可能受测量噪声影响而为负;不以耗时差值作为 CI 通过门槛。"
165-
"编译或验证失败、汇编内容不一致会使 benchmark 失败。",
166-
"汇编一致仅检查开关未改变生成结果,不代表与 ONNX Runtime 数值等价。", "",
165+
"编译或验证失败、IR 内容不一致会使 benchmark 失败。",
166+
"IR 一致仅检查开关未改变生成结果,不代表与 ONNX Runtime 数值等价。"
167+
"张量图不再交给仅支持标量的旧后端;本报告不能与旧 RISC-V 代码生成计时直接比较。", "",
167168
])
168169
if "rule_examples" in report:
169170
lines.extend(["## 七条规则的诊断示例", ""])

‎benchmarks/cnn_schedule_execution.py‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
"""Run complete standalone CNN binaries under QEMU user mode and compare state."""
22

33
import hashlib
4+
import os
45
from pathlib import Path
5-
import resource
66
import shutil
77
import struct
88
import subprocess
99

1010

11+
def qemu_process_options() -> dict:
12+
"""Suppress Unix core dumps without requiring Unix-only APIs on Windows."""
13+
if os.name != "posix":
14+
return {}
15+
import resource
16+
17+
return {"preexec_fn": lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0))}
18+
19+
1120
def assemble_listing(source: Path, directory: Path) -> bytes:
21+
tools = {name: shutil.which(name) for name in ("clang", "ld.lld")}
22+
if not all(tools.values()):
23+
raise RuntimeError(f"CNN assembly requires clang and ld.lld: {tools}")
1224
obj, binary = directory / "roundtrip.o", directory / "roundtrip.bin"
13-
subprocess.run(["clang", "--target=riscv32-unknown-elf", "-march=rv32im", "-mabi=ilp32",
25+
subprocess.run([tools["clang"], "--target=riscv32-unknown-elf", "-march=rv32im", "-mabi=ilp32",
1426
"-mno-relax", "-c", str(source), "-o", str(obj)],
1527
capture_output=True, check=True, timeout=30)
16-
subprocess.run(["ld.lld", "-m", "elf32lriscv", "--no-relax", "--image-base=0", "-Ttext=0", "-e", "0",
28+
subprocess.run([tools["ld.lld"], "-m", "elf32lriscv", "--no-relax", "--image-base=0", "-Ttext=0", "-e", "0",
1729
"--oformat=binary", str(obj), "-o", str(binary)],
1830
capture_output=True, check=True, timeout=30)
1931
return binary.read_bytes()
@@ -83,7 +95,7 @@ def execute_pair(before: Path, after: Path, metadata: dict, directory: Path) ->
8395
"-nostdlib", "-static", "-fuse-ld=lld", "-Wl,--no-relax", str(asm), "-o", str(elf)],
8496
check=True, capture_output=True, timeout=30)
8597
result = subprocess.run([tools["qemu-riscv32"], str(elf)], capture_output=True, timeout=60,
86-
preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0)))
98+
**qemu_process_options())
8799
if result.returncode or len(result.stdout) != total:
88100
reason = (f"Full CNN {phase} failed: return={result.returncode}, "
89101
f"bytes={len(result.stdout)}/{total}, stderr={result.stderr.decode(errors='replace')}")

0 commit comments

Comments
 (0)