From 92bf72229c00bb70de07d7ce91cd25a9fc03d7a5 Mon Sep 17 00:00:00 2001 From: 2813183274-cloud <2813183274@qq.com> Date: Tue, 23 Jun 2026 18:48:14 +0800 Subject: [PATCH] Add files via upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 课题6的课程版提交 --- ScratchV-topic06-deliverable/LICENSE | 21 + .../docs/topic06_bench_suite_usage.md | 166 ++++ ScratchV-topic06-deliverable/pyproject.toml | 22 + .../reports/benchmark_baseline.json | 117 +++ .../reports/course_report_instructions.png | Bin 0 -> 69532 bytes .../reports/report.html | 286 +++++++ .../reports/report.md | 495 +++++++++++ ScratchV-topic06-deliverable/run_tests.py | 787 ++++++++++++++++++ ScratchV-topic06-deliverable/setup.py | 3 + .../tests_main/activation/add_relu_relu.dsl | 5 + .../activation/add_relu_relu.meta.json | 9 + .../tests_main/activation/relu_add.dsl | 4 + .../tests_main/activation/relu_add.meta.json | 9 + .../tests_main/activation/relu_only.dsl | 3 + .../tests_main/activation/relu_only.meta.json | 8 + .../tests_main/activation/relu_twice.dsl | 4 + .../activation/relu_twice.meta.json | 8 + .../tests_main/branch/if_else.dsl | 8 + .../tests_main/branch/if_else.meta.json | 10 + .../tests_main/branch/if_relu.dsl | 8 + .../tests_main/branch/if_relu.meta.json | 10 + .../tests_main/branch/if_then.dsl | 8 + .../tests_main/branch/if_then.meta.json | 10 + .../tests_main/elementwise/add_chain.dsl | 4 + .../elementwise/add_chain.meta.json | 10 + .../tests_main/elementwise/add_chain_3.dsl | 5 + .../elementwise/add_chain_3.meta.json | 11 + .../tests_main/elementwise/add_fan_in_4.dsl | 5 + .../elementwise/add_fan_in_4.meta.json | 11 + .../tests_main/elementwise/add_reuse.dsl | 4 + .../elementwise/add_reuse.meta.json | 9 + .../tests_main/elementwise/vector_add.dsl | 3 + .../elementwise/vector_add.meta.json | 9 + .../tests_main/loop/loop_add_4.dsl | 5 + .../tests_main/loop/loop_add_4.meta.json | 9 + .../tests_main/loop/loop_add_chain_4.dsl | 6 + .../loop/loop_add_chain_4.meta.json | 10 + .../tests_main/loop/loop_relu_add_4.dsl | 6 + .../tests_main/loop/loop_relu_add_4.meta.json | 9 + .../tests_main/reduction/dot_4.dsl | 3 + .../tests_main/reduction/dot_4.meta.json | 9 + .../tests_main/reduction/dot_8.dsl | 3 + .../tests_main/reduction/dot_8.meta.json | 9 + .../tests_main/reduction/dot_relu_4.dsl | 4 + .../tests_main/reduction/dot_relu_4.meta.json | 9 + .../tests_main/reduction/dot_relu_8.dsl | 4 + .../tests_main/reduction/dot_relu_8.meta.json | 9 + .../tests_main/tensor/matmul_2x2.dsl | 3 + .../tests_main/tensor/matmul_2x2.meta.json | 9 + .../tests_main/tensor/matmul_4x4.dsl | 3 + .../tests_main/tensor/matmul_4x4.meta.json | 9 + .../tests_main/tensor/matmul_add_2x2.dsl | 4 + .../tensor/matmul_add_2x2.meta.json | 10 + .../tests_main/tensor/matmul_relu_2x2.dsl | 4 + .../tensor/matmul_relu_2x2.meta.json | 9 + 55 files changed, 2218 insertions(+) create mode 100644 ScratchV-topic06-deliverable/LICENSE create mode 100644 ScratchV-topic06-deliverable/docs/topic06_bench_suite_usage.md create mode 100644 ScratchV-topic06-deliverable/pyproject.toml create mode 100644 ScratchV-topic06-deliverable/reports/benchmark_baseline.json create mode 100644 ScratchV-topic06-deliverable/reports/course_report_instructions.png create mode 100644 ScratchV-topic06-deliverable/reports/report.html create mode 100644 ScratchV-topic06-deliverable/reports/report.md create mode 100644 ScratchV-topic06-deliverable/run_tests.py create mode 100644 ScratchV-topic06-deliverable/setup.py create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/relu_add.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/relu_add.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/relu_only.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/relu_only.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/relu_twice.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/activation/relu_twice.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/branch/if_else.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/branch/if_else.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/branch/if_relu.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/branch/if_relu.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/branch/if_then.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/branch/if_then.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_4.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_4.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_8.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_8.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.meta.json create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.dsl create mode 100644 ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.meta.json diff --git a/ScratchV-topic06-deliverable/LICENSE b/ScratchV-topic06-deliverable/LICENSE new file mode 100644 index 0000000..87324d7 --- /dev/null +++ b/ScratchV-topic06-deliverable/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ScratchV + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ScratchV-topic06-deliverable/docs/topic06_bench_suite_usage.md b/ScratchV-topic06-deliverable/docs/topic06_bench_suite_usage.md new file mode 100644 index 0000000..ea752b3 --- /dev/null +++ b/ScratchV-topic06-deliverable/docs/topic06_bench_suite_usage.md @@ -0,0 +1,166 @@ +# 课题 6:ScratchV 课程版性能测试套件使用说明 + +本文档只说明当前作业交付使用的课程版测试套件:`run_tests.py` 和 `tests_main/`。 + +## 1. 目录结构 + +```text +run_tests.py # 自动化测试脚本 + +tests_main/ + activation/ # relu 等激活函数用例 + branch/ # if/else 分支用例 + elementwise/ # add 和链式 add 用例 + loop/ # for/endfor 循环用例 + reduction/ # dot/reduction 用例 + tensor/ # matmul/tensor 用例 + +build/ # 编译后生成的 RISC-V 汇编 + +reports/ + report.md # Markdown 测试报告 + report.html # HTML 测试报告 + course_report_instructions.png + benchmark_baseline.json + +.github/workflows/ + benchmark.yml # CI 示例,运行课程版测试套件 +``` + +当前 `tests_main/` 下有 23 个 DSL 用例,覆盖算术、神经网络算子、循环、if/else 分支、矩阵计算和组合场景。 + +## 2. 运行测试 + +在项目根目录运行: + +```powershell +python run_tests.py +``` + +运行后会自动: + +- 遍历 `tests_main/` 下的 `.dsl` 文件。 +- 调用 ScratchV 编译器生成汇编。 +- 调用 TinyFive 适配器模拟执行。 +- 使用参考执行器计算实际返回值。 +- 对比实际返回值和 `.meta.json` 中的预期返回值。 +- 统计 PASS/FAIL 和指令数。 +- 生成报告。 + +## 3. Benchmark 模式 + +重复运行 3 次并取平均: + +```powershell +python run_tests.py --benchmark 3 +``` + +报告会记录: + +- 平均指令数 +- 最小指令数 +- 最大指令数 +- 95% 置信区间 +- 基线指令数 +- 性能变化率 +- 是否性能退化 + +## 4. 性能基线和退化判断 + +第一次生成基线: + +```powershell +python run_tests.py --benchmark 3 --update-baseline +``` + +之后正常运行: + +```powershell +python run_tests.py --benchmark 3 +``` + +判断规则: + +- 当前平均指令数比基线高出 5% 以上,判定为性能退化。 +- 低于或等于 5% 的波动不算退化。 +- 基线文件保存在 `reports/benchmark_baseline.json`。 + +## 5. 报告文件 + +运行后生成: + +```text +reports/report.md +reports/report.html +reports/course_report_instructions.png +``` + +`report.md` 适合提交作业或放进文档。`report.html` 适合演示,包含表格和 `matplotlib` 生成的性能图表。 + +## 6. 添加新测试用例 + +每个课程版用例由两个文件组成: + +```text +tests_main/{category}/{name}.dsl +tests_main/{category}/{name}.meta.json +``` + +`.dsl` 示例: + +```text +# Simple add +result = add(a, b) +return result +``` + +`.meta.json` 示例: + +```json +{ + "description": "Simple scalar add.", + "expected_output_type": "return_value", + "inputs": { + "a": 2, + "b": 3 + }, + "expected_return": 5 +} +``` + +添加后运行: + +```powershell +python run_tests.py --benchmark 3 +``` + +如果失败,优先检查 DSL 语法、输入变量名、预期输出和当前编译器是否支持该算子。 + +## 7. if/else 分支语法 + +当前支持简单分支: + +```text +if flag +result = add(a, b) +return result +else +result = sub(a, b) +return result +endif +``` + +规则: + +- `flag` 非 0 时走 `if` 分支。 +- `flag` 为 0 时走 `else` 分支。 +- 当前不支持复杂比较表达式,比如 `if a > b`。 + +## 8. CI + +`.github/workflows/benchmark.yml` 会在 push 和 pull request 时运行: + +- `python -m pytest -q` +- `python run_tests.py --benchmark 3` + +CI 会上传课程版报告文件,方便查看每次修改后的正确性和性能变化。 diff --git a/ScratchV-topic06-deliverable/pyproject.toml b/ScratchV-topic06-deliverable/pyproject.toml new file mode 100644 index 0000000..6d3be8a --- /dev/null +++ b/ScratchV-topic06-deliverable/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "scratchv" +version = "0.1.0" +description = "A compiler from ONNX models to RISC-V assembly" +requires-python = ">=3.10" +dependencies = [ + "onnx>=1.14", + "numpy>=1.24", + "protobuf>=4.21", + "jinja2>=3.1", + "matplotlib>=3.7", +] + +[project.scripts] +scratchv = "scratchv.main:main" + +[tool.setuptools.packages.find] +include = ["scratchv*"] diff --git a/ScratchV-topic06-deliverable/reports/benchmark_baseline.json b/ScratchV-topic06-deliverable/reports/benchmark_baseline.json new file mode 100644 index 0000000..2f42a38 --- /dev/null +++ b/ScratchV-topic06-deliverable/reports/benchmark_baseline.json @@ -0,0 +1,117 @@ +{ + "add_relu_relu": { + "category": "activation", + "avg_instr_count": 9.0, + "runs": 3 + }, + "relu_add": { + "category": "activation", + "avg_instr_count": 8.0, + "runs": 3 + }, + "relu_only": { + "category": "activation", + "avg_instr_count": 7.0, + "runs": 3 + }, + "relu_twice": { + "category": "activation", + "avg_instr_count": 8.0, + "runs": 3 + }, + "if_else": { + "category": "branch", + "avg_instr_count": 26.0, + "runs": 3 + }, + "if_relu": { + "category": "branch", + "avg_instr_count": 22.0, + "runs": 3 + }, + "if_then": { + "category": "branch", + "avg_instr_count": 26.0, + "runs": 3 + }, + "add_chain": { + "category": "elementwise", + "avg_instr_count": 8.0, + "runs": 3 + }, + "add_chain_3": { + "category": "elementwise", + "avg_instr_count": 9.0, + "runs": 3 + }, + "add_fan_in_4": { + "category": "elementwise", + "avg_instr_count": 9.0, + "runs": 3 + }, + "add_reuse": { + "category": "elementwise", + "avg_instr_count": 8.0, + "runs": 3 + }, + "vector_add": { + "category": "elementwise", + "avg_instr_count": 7.0, + "runs": 3 + }, + "loop_add_4": { + "category": "loop", + "avg_instr_count": 27.0, + "runs": 3 + }, + "loop_add_chain_4": { + "category": "loop", + "avg_instr_count": 31.0, + "runs": 3 + }, + "loop_relu_add_4": { + "category": "loop", + "avg_instr_count": 30.0, + "runs": 3 + }, + "dot_4": { + "category": "reduction", + "avg_instr_count": 8.0, + "runs": 3 + }, + "dot_8": { + "category": "reduction", + "avg_instr_count": 8.0, + "runs": 3 + }, + "dot_relu_4": { + "category": "reduction", + "avg_instr_count": 9.0, + "runs": 3 + }, + "dot_relu_8": { + "category": "reduction", + "avg_instr_count": 9.0, + "runs": 3 + }, + "matmul_2x2": { + "category": "tensor", + "avg_instr_count": 9.0, + "runs": 3 + }, + "matmul_4x4": { + "category": "tensor", + "avg_instr_count": 9.0, + "runs": 3 + }, + "matmul_add_2x2": { + "category": "tensor", + "avg_instr_count": 10.0, + "runs": 3 + }, + "matmul_relu_2x2": { + "category": "tensor", + "avg_instr_count": 10.0, + "runs": 3 + } +} \ No newline at end of file diff --git a/ScratchV-topic06-deliverable/reports/course_report_instructions.png b/ScratchV-topic06-deliverable/reports/course_report_instructions.png new file mode 100644 index 0000000000000000000000000000000000000000..d3414420e8ea5faeb986d24640ae6c8402885fae GIT binary patch literal 69532 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ÒYscZW10FWUu8gf#mW*(~1|2YC;L2UeY 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 literal 0 HcmV?d00001 diff --git a/ScratchV-topic06-deliverable/reports/report.html b/ScratchV-topic06-deliverable/reports/report.html new file mode 100644 index 0000000..2ea5619 --- /dev/null +++ b/ScratchV-topic06-deliverable/reports/report.html @@ -0,0 +1,286 @@ + + + + + ScratchV 课程版测试报告 + + + +

ScratchV DSL 编译器性能测试报告

+
+

用例总数:23,通过:23,失败:0,通过率:100.0%

+

测试目录:tests_main,性能退化阈值:5.0%

+
+ 课程版指令数图表 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
用例类别状态平均指令数95%置信区间变化率(%)是否退化描述
add_relu_reluactivationPASS9.00±0.000.00FalseAdd input and bias, then apply ReLU twice.
relu_addactivationPASS8.00±0.000.00FalseAdd input and bias, then apply one ReLU.
relu_onlyactivationPASS7.00±0.000.00FalseApply ReLU directly to a single input value.
relu_twiceactivationPASS8.00±0.000.00FalseApply ReLU twice to the same activation path.
if_elsebranchPASS26.00±0.000.00Falseif/else branch returns subtraction result when flag is zero.
if_relubranchPASS22.00±0.000.00Falseif/else branch combined with add and relu.
if_thenbranchPASS26.00±0.000.00Falseif/else branch returns add result when flag is non-zero.
add_chainelementwisePASS8.00±0.000.00FalseAdd a and b, then add c to the intermediate result.
add_chain_3elementwisePASS9.00±0.000.00FalseChain three add operations across four symbolic inputs.
add_fan_in_4elementwisePASS9.00±0.000.00FalseCompute two independent adds and then merge them with a final add.
add_reuseelementwisePASS8.00±0.000.00FalseReuse the same intermediate add result on both operands of a second add.
vector_addelementwisePASS7.00±0.000.00FalseSingle add over two symbolic vector inputs.
loop_add_4loopPASS27.00±0.000.00FalseRun a four-iteration loop whose body computes one add; final returned value is the last loop-body result.
loop_add_chain_4loopPASS31.00±0.000.00FalseRun a four-iteration loop whose body computes two chained adds; final returned value is the last loop-body result.
loop_relu_add_4loopPASS30.00±0.000.00FalseRun a four-iteration loop whose body computes add followed by ReLU; final returned value is the last loop-body result.
dot_4reductionPASS8.00±0.000.00FalseCompute the dot product of two symbolic vectors of length 4.
dot_8reductionPASS8.00±0.000.00FalseCompute the dot product of two symbolic vectors of length 8.
dot_relu_4reductionPASS9.00±0.000.00FalseCompute a length-4 dot product and pass it through ReLU.
dot_relu_8reductionPASS9.00±0.000.00FalseCompute a length-8 dot product and pass it through ReLU.
matmul_2x2tensorPASS9.00±0.000.00FalseCompute a symbolic 2x2 by 2x2 matrix multiplication.
matmul_4x4tensorPASS9.00±0.000.00FalseCompute a symbolic 4x4 by 4x4 matrix multiplication.
matmul_add_2x2tensorPASS10.00±0.000.00FalseCompute a 2x2 matmul and then add a symbolic bias term.
matmul_relu_2x2tensorPASS10.00±0.000.00FalseCompute a 2x2 matmul and then apply ReLU to its result.
+ + \ No newline at end of file diff --git a/ScratchV-topic06-deliverable/reports/report.md b/ScratchV-topic06-deliverable/reports/report.md new file mode 100644 index 0000000..c2112a8 --- /dev/null +++ b/ScratchV-topic06-deliverable/reports/report.md @@ -0,0 +1,495 @@ +# ScratchV DSL 编译器性能测试报告 + +## 测试概览 + +- 生成时间: 2026-06-23 18:20:50 +- 用例总数: 23 +- 通过数量: 23 +- 失败数量: 0 +- 通过率: 100.0% +- 测试目录: `tests_main` +- 汇编输出目录: `build` +- 性能基线文件: `reports\benchmark_baseline.json` +- 性能退化阈值: 5.0% + +## 测试结果 + +| 用例 | 类别 | 状态 | 模拟后端 | 平均指令数 | 95%置信区间 | 最小 | 最大 | 基线 | 变化率(%) | 是否退化 | 预期输出 | 实际输出 | 输出匹配 | 汇编文件 | +|---|---|---|---|---:|---:|---:|---:|---:|---:|---|---|---|---|---| +| add_relu_relu | activation | PASS | stub | 9.00 | ±0.00 | 9 | 9 | 9.00 | 0.00 | False | 7 | 7 | True | build\add_relu_relu.s | +| relu_add | activation | PASS | stub | 8.00 | ±0.00 | 8 | 8 | 8.00 | 0.00 | False | 3 | 3 | True | build\relu_add.s | +| relu_only | activation | PASS | stub | 7.00 | ±0.00 | 7 | 7 | 7.00 | 0.00 | False | 0 | 0 | True | build\relu_only.s | +| relu_twice | activation | PASS | stub | 8.00 | ±0.00 | 8 | 8 | 8.00 | 0.00 | False | 4 | 4 | True | build\relu_twice.s | +| if_else | branch | PASS | stub | 26.00 | ±0.00 | 26 | 26 | 26.00 | 0.00 | False | 5 | 5 | True | build\if_else.s | +| if_relu | branch | PASS | stub | 22.00 | ±0.00 | 22 | 22 | 22.00 | 0.00 | False | 0 | 0 | True | build\if_relu.s | +| if_then | branch | PASS | stub | 26.00 | ±0.00 | 26 | 26 | 26.00 | 0.00 | False | 13 | 13 | True | build\if_then.s | +| add_chain | elementwise | PASS | stub | 8.00 | ±0.00 | 8 | 8 | 8.00 | 0.00 | False | 9 | 9 | True | build\add_chain.s | +| add_chain_3 | elementwise | PASS | stub | 9.00 | ±0.00 | 9 | 9 | 9.00 | 0.00 | False | 14 | 14 | True | build\add_chain_3.s | +| add_fan_in_4 | elementwise | PASS | stub | 9.00 | ±0.00 | 9 | 9 | 9.00 | 0.00 | False | 10 | 10 | True | build\add_fan_in_4.s | +| add_reuse | elementwise | PASS | stub | 8.00 | ±0.00 | 8 | 8 | 8.00 | 0.00 | False | 10 | 10 | True | build\add_reuse.s | +| vector_add | elementwise | PASS | stub | 7.00 | ±0.00 | 7 | 7 | 7.00 | 0.00 | False | 5 | 5 | True | build\vector_add.s | +| loop_add_4 | loop | PASS | stub | 27.00 | ±0.00 | 27 | 27 | 27.00 | 0.00 | False | 5 | 5 | True | build\loop_add_4.s | +| loop_add_chain_4 | loop | PASS | stub | 31.00 | ±0.00 | 31 | 31 | 31.00 | 0.00 | False | 9 | 9 | True | build\loop_add_chain_4.s | +| loop_relu_add_4 | loop | PASS | stub | 30.00 | ±0.00 | 30 | 30 | 30.00 | 0.00 | False | 2 | 2 | True | build\loop_relu_add_4.s | +| dot_4 | reduction | PASS | stub | 8.00 | ±0.00 | 8 | 8 | 8.00 | 0.00 | False | 70 | 70 | True | build\dot_4.s | +| dot_8 | reduction | PASS | stub | 8.00 | ±0.00 | 8 | 8 | 8.00 | 0.00 | False | 36 | 36 | True | build\dot_8.s | +| dot_relu_4 | reduction | PASS | stub | 9.00 | ±0.00 | 9 | 9 | 9.00 | 0.00 | False | 0 | 0 | True | build\dot_relu_4.s | +| dot_relu_8 | reduction | PASS | stub | 9.00 | ±0.00 | 9 | 9 | 9.00 | 0.00 | False | 8 | 8 | True | build\dot_relu_8.s | +| matmul_2x2 | tensor | PASS | stub | 9.00 | ±0.00 | 9 | 9 | 9.00 | 0.00 | False | [[19, 22], [43, 50]] | [[19, 22], [43, 50]] | True | build\matmul_2x2.s | +| matmul_4x4 | tensor | PASS | stub | 9.00 | ±0.00 | 9 | 9 | 9.00 | 0.00 | False | [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] | [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] | True | build\matmul_4x4.s | +| matmul_add_2x2 | tensor | PASS | stub | 10.00 | ±0.00 | 10 | 10 | 10.00 | 0.00 | False | [[20, 23], [44, 51]] | [[20, 23], [44, 51]] | True | build\matmul_add_2x2.s | +| matmul_relu_2x2 | tensor | PASS | stub | 10.00 | ±0.00 | 10 | 10 | 10.00 | 0.00 | False | [[0, 2], [0, 4]] | [[0, 2], [0, 4]] | True | build\matmul_relu_2x2.s | + +## 性能图表 + +![课程版指令数图表](course_report_instructions.png) + +### Mermaid 图表 + +```mermaid +xychart-beta + title "各测试用例指令数" + x-axis ["add_relu_relu", "relu_add", "relu_only", "relu_twice", "if_else", "if_relu", "if_then", "add_chain", "add_chain_3", "add_fan_in_4", "add_reuse", "vector_add", "loop_add_4", "loop_add_chain_4", "loop_relu_add_4", "dot_4", "dot_8", "dot_relu_4", "dot_relu_8", "matmul_2x2", "matmul_4x4", "matmul_add_2x2", "matmul_relu_2x2"] + y-axis "指令数" 0 --> 33.0 + bar [9.0, 8.0, 7.0, 8.0, 26.0, 22.0, 26.0, 8.0, 9.0, 9.0, 8.0, 7.0, 27.0, 31.0, 30.0, 8.0, 8.0, 9.0, 9.0, 9.0, 9.0, 10.0, 10.0] +``` + +## 用例详情 + +### add_relu_relu + +- 类别: activation +- 描述: Add input and bias, then apply ReLU twice. +- 预期输出 (return_value): 7 +- 实际输出: 7 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\add_relu_relu.s +- Benchmark 重复次数: 3 +- 平均指令数: 9.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 9 +- 最大指令数: 9 +- 基线指令数: 9.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### relu_add + +- 类别: activation +- 描述: Add input and bias, then apply one ReLU. +- 预期输出 (return_value): 3 +- 实际输出: 3 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\relu_add.s +- Benchmark 重复次数: 3 +- 平均指令数: 8.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 8 +- 最大指令数: 8 +- 基线指令数: 8.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### relu_only + +- 类别: activation +- 描述: Apply ReLU directly to a single input value. +- 预期输出 (return_value): 0 +- 实际输出: 0 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\relu_only.s +- Benchmark 重复次数: 3 +- 平均指令数: 7.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 7 +- 最大指令数: 7 +- 基线指令数: 7.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### relu_twice + +- 类别: activation +- 描述: Apply ReLU twice to the same activation path. +- 预期输出 (return_value): 4 +- 实际输出: 4 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\relu_twice.s +- Benchmark 重复次数: 3 +- 平均指令数: 8.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 8 +- 最大指令数: 8 +- 基线指令数: 8.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### if_else + +- 类别: branch +- 描述: if/else branch returns subtraction result when flag is zero. +- 预期输出 (return_value): 5 +- 实际输出: 5 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\if_else.s +- Benchmark 重复次数: 3 +- 平均指令数: 26.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 26 +- 最大指令数: 26 +- 基线指令数: 26.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### if_relu + +- 类别: branch +- 描述: if/else branch combined with add and relu. +- 预期输出 (return_value): 0 +- 实际输出: 0 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\if_relu.s +- Benchmark 重复次数: 3 +- 平均指令数: 22.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 22 +- 最大指令数: 22 +- 基线指令数: 22.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### if_then + +- 类别: branch +- 描述: if/else branch returns add result when flag is non-zero. +- 预期输出 (return_value): 13 +- 实际输出: 13 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\if_then.s +- Benchmark 重复次数: 3 +- 平均指令数: 26.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 26 +- 最大指令数: 26 +- 基线指令数: 26.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### add_chain + +- 类别: elementwise +- 描述: Add a and b, then add c to the intermediate result. +- 预期输出 (return_value): 9 +- 实际输出: 9 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\add_chain.s +- Benchmark 重复次数: 3 +- 平均指令数: 8.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 8 +- 最大指令数: 8 +- 基线指令数: 8.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### add_chain_3 + +- 类别: elementwise +- 描述: Chain three add operations across four symbolic inputs. +- 预期输出 (return_value): 14 +- 实际输出: 14 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\add_chain_3.s +- Benchmark 重复次数: 3 +- 平均指令数: 9.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 9 +- 最大指令数: 9 +- 基线指令数: 9.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### add_fan_in_4 + +- 类别: elementwise +- 描述: Compute two independent adds and then merge them with a final add. +- 预期输出 (return_value): 10 +- 实际输出: 10 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\add_fan_in_4.s +- Benchmark 重复次数: 3 +- 平均指令数: 9.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 9 +- 最大指令数: 9 +- 基线指令数: 9.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### add_reuse + +- 类别: elementwise +- 描述: Reuse the same intermediate add result on both operands of a second add. +- 预期输出 (return_value): 10 +- 实际输出: 10 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\add_reuse.s +- Benchmark 重复次数: 3 +- 平均指令数: 8.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 8 +- 最大指令数: 8 +- 基线指令数: 8.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### vector_add + +- 类别: elementwise +- 描述: Single add over two symbolic vector inputs. +- 预期输出 (return_value): 5 +- 实际输出: 5 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\vector_add.s +- Benchmark 重复次数: 3 +- 平均指令数: 7.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 7 +- 最大指令数: 7 +- 基线指令数: 7.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### loop_add_4 + +- 类别: loop +- 描述: Run a four-iteration loop whose body computes one add; final returned value is the last loop-body result. +- 预期输出 (return_value): 5 +- 实际输出: 5 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\loop_add_4.s +- Benchmark 重复次数: 3 +- 平均指令数: 27.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 27 +- 最大指令数: 27 +- 基线指令数: 27.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### loop_add_chain_4 + +- 类别: loop +- 描述: Run a four-iteration loop whose body computes two chained adds; final returned value is the last loop-body result. +- 预期输出 (return_value): 9 +- 实际输出: 9 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\loop_add_chain_4.s +- Benchmark 重复次数: 3 +- 平均指令数: 31.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 31 +- 最大指令数: 31 +- 基线指令数: 31.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### loop_relu_add_4 + +- 类别: loop +- 描述: Run a four-iteration loop whose body computes add followed by ReLU; final returned value is the last loop-body result. +- 预期输出 (return_value): 2 +- 实际输出: 2 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\loop_relu_add_4.s +- Benchmark 重复次数: 3 +- 平均指令数: 30.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 30 +- 最大指令数: 30 +- 基线指令数: 30.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### dot_4 + +- 类别: reduction +- 描述: Compute the dot product of two symbolic vectors of length 4. +- 预期输出 (return_value): 70 +- 实际输出: 70 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\dot_4.s +- Benchmark 重复次数: 3 +- 平均指令数: 8.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 8 +- 最大指令数: 8 +- 基线指令数: 8.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### dot_8 + +- 类别: reduction +- 描述: Compute the dot product of two symbolic vectors of length 8. +- 预期输出 (return_value): 36 +- 实际输出: 36 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\dot_8.s +- Benchmark 重复次数: 3 +- 平均指令数: 8.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 8 +- 最大指令数: 8 +- 基线指令数: 8.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### dot_relu_4 + +- 类别: reduction +- 描述: Compute a length-4 dot product and pass it through ReLU. +- 预期输出 (return_value): 0 +- 实际输出: 0 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\dot_relu_4.s +- Benchmark 重复次数: 3 +- 平均指令数: 9.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 9 +- 最大指令数: 9 +- 基线指令数: 9.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### dot_relu_8 + +- 类别: reduction +- 描述: Compute a length-8 dot product and pass it through ReLU. +- 预期输出 (return_value): 8 +- 实际输出: 8 +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\dot_relu_8.s +- Benchmark 重复次数: 3 +- 平均指令数: 9.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 9 +- 最大指令数: 9 +- 基线指令数: 9.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### matmul_2x2 + +- 类别: tensor +- 描述: Compute a symbolic 2x2 by 2x2 matrix multiplication. +- 预期输出 (return_value): [[19, 22], [43, 50]] +- 实际输出: [[19, 22], [43, 50]] +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\matmul_2x2.s +- Benchmark 重复次数: 3 +- 平均指令数: 9.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 9 +- 最大指令数: 9 +- 基线指令数: 9.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### matmul_4x4 + +- 类别: tensor +- 描述: Compute a symbolic 4x4 by 4x4 matrix multiplication. +- 预期输出 (return_value): [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] +- 实际输出: [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\matmul_4x4.s +- Benchmark 重复次数: 3 +- 平均指令数: 9.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 9 +- 最大指令数: 9 +- 基线指令数: 9.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### matmul_add_2x2 + +- 类别: tensor +- 描述: Compute a 2x2 matmul and then add a symbolic bias term. +- 预期输出 (return_value): [[20, 23], [44, 51]] +- 实际输出: [[20, 23], [44, 51]] +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\matmul_add_2x2.s +- Benchmark 重复次数: 3 +- 平均指令数: 10.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 10 +- 最大指令数: 10 +- 基线指令数: 10.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + +### matmul_relu_2x2 + +- 类别: tensor +- 描述: Compute a 2x2 matmul and then apply ReLU to its result. +- 预期输出 (return_value): [[0, 2], [0, 4]] +- 实际输出: [[0, 2], [0, 4]] +- 输出是否匹配: True +- 模拟后端: stub +- 汇编文件: build\matmul_relu_2x2.s +- Benchmark 重复次数: 3 +- 平均指令数: 10.00 +- 95% 置信区间: ±0.00 +- 最小指令数: 10 +- 最大指令数: 10 +- 基线指令数: 10.00 +- 性能变化率: 0.00% +- 性能退化阈值: 5.00% +- 是否性能退化: False + diff --git a/ScratchV-topic06-deliverable/run_tests.py b/ScratchV-topic06-deliverable/run_tests.py new file mode 100644 index 0000000..e23d330 --- /dev/null +++ b/ScratchV-topic06-deliverable/run_tests.py @@ -0,0 +1,787 @@ +import argparse +import json +import math +import os +import subprocess +import sys +import tempfile +from datetime import datetime +from pathlib import Path + +from scratchv.simulator.tinyfive import verify_assembly + +TEST_DIR = Path("tests_main") +BUILD_DIR = Path("build") +REPORT_DIR = Path("reports") +REPORT_FILE = REPORT_DIR / "report.md" +HTML_REPORT_FILE = REPORT_DIR / "report.html" +CHART_FILE = REPORT_DIR / "course_report_instructions.png" +BASELINE_FILE = REPORT_DIR / "benchmark_baseline.json" +REGRESSION_THRESHOLD_PCT = 5.0 + + +def run_compile(dsl_file: Path): + output_file = BUILD_DIR / (dsl_file.stem + ".s") + + cmd = [ + sys.executable, + "-m", + "scratchv.main", + str(dsl_file), + "-o", + str(output_file), + "--optimize", + "all", + "--dump-ir", + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="ignore", + ) + + return result, output_file + + +def load_metadata(dsl_file: Path): + meta_file = dsl_file.with_suffix(".meta.json") + if not meta_file.exists(): + return { + "description": "", + "expected_output_type": "return_value", + "expected_return": "", + } + return json.loads(meta_file.read_text(encoding="utf-8")) + + +def run_simulation(asm_file: Path): + if not asm_file.exists(): + return { + "success": False, + "instr_count": 0, + "return_value": None, + "backend": "none", + "error": "assembly file not found", + } + + asm_code = asm_file.read_text(encoding="utf-8") + return verify_assembly(asm_code) + + +def apply_add(lhs, rhs): + if isinstance(lhs, list) and isinstance(rhs, list): + return [apply_add(a, b) for a, b in zip(lhs, rhs)] + if isinstance(lhs, list): + return [apply_add(a, rhs) for a in lhs] + if isinstance(rhs, list): + return [apply_add(lhs, b) for b in rhs] + return lhs + rhs + + +def apply_binary(lhs, rhs, op): + if isinstance(lhs, list) and isinstance(rhs, list): + return [apply_binary(a, b, op) for a, b in zip(lhs, rhs)] + if isinstance(lhs, list): + return [apply_binary(a, rhs, op) for a in lhs] + if isinstance(rhs, list): + return [apply_binary(lhs, b, op) for b in rhs] + return op(lhs, rhs) + + +def apply_relu(value): + if isinstance(value, list): + return [apply_relu(v) for v in value] + return value if value > 0 else 0 + + +def apply_gelu(value): + if isinstance(value, list): + return [apply_gelu(v) for v in value] + return 0.5 * value * (1.0 + math.erf(value / math.sqrt(2.0))) + + +def apply_softmax(value): + if not isinstance(value, list): + return 1.0 + max_value = max(value) + exp_values = [math.exp(v - max_value) for v in value] + total = sum(exp_values) + return [v / total for v in exp_values] + + +def apply_maxpool(value, kernel, stride): + if not isinstance(value, list): + return value + return [max(value[i:i + kernel]) for i in range(0, len(value) - kernel + 1, stride)] + + +def apply_dot(lhs, rhs, length): + return sum(lhs[i] * rhs[i] for i in range(length)) + + +def apply_matmul(lhs, rhs, m, n, k): + result = [] + for i in range(m): + row = [] + for j in range(n): + cell = 0 + for kk in range(k): + cell += lhs[i][kk] * rhs[kk][j] + row.append(cell) + result.append(row) + return result + + +def resolve_value(token, env): + try: + return int(token) + except ValueError: + pass + try: + return float(token) + except ValueError: + pass + return env[token] + + +def execute_block(lines, env, start_idx=0, end_idx=None): + if end_idx is None: + end_idx = len(lines) + + idx = start_idx + while idx < end_idx: + line = lines[idx] + + if line.startswith("for "): + loop_var_text = line.replace("for ", "", 1) + loop_var, bounds = [p.strip() for p in loop_var_text.split("=", 1)] + loop_start_text, loop_end_text = [p.strip() for p in bounds.split(",", 1)] + loop_start = int(loop_start_text) + loop_end = int(loop_end_text) + + depth = 1 + body_start = idx + 1 + body_end = body_start + while body_end < end_idx and depth > 0: + current = lines[body_end] + if current.startswith("for "): + depth += 1 + elif current == "endfor": + depth -= 1 + if depth == 0: + break + body_end += 1 + + for i in range(loop_start, loop_end): + env[loop_var] = i + returned, value = execute_block(lines, env, body_start, body_end) + if returned: + return True, value + + idx = body_end + 1 + continue + + if line.startswith("if "): + cond_text = line.replace("if ", "", 1).strip() + cond_value = resolve_value(cond_text, env) + + depth = 1 + body_start = idx + 1 + scan_idx = body_start + else_idx = None + endif_idx = None + while scan_idx < end_idx: + current = lines[scan_idx] + if current.startswith("if "): + depth += 1 + elif current == "endif": + depth -= 1 + if depth == 0: + endif_idx = scan_idx + break + elif current == "else" and depth == 1: + else_idx = scan_idx + scan_idx += 1 + + if endif_idx is None: + raise ValueError("if without matching endif") + + if cond_value: + branch_start = body_start + branch_end = else_idx if else_idx is not None else endif_idx + else: + branch_start = else_idx + 1 if else_idx is not None else endif_idx + branch_end = endif_idx + + returned, value = execute_block(lines, env, branch_start, branch_end) + if returned: + return True, value + + idx = endif_idx + 1 + continue + + if line in {"else", "endif"}: + return False, None + + if line == "endfor": + return False, None + + if line.startswith("return "): + return True, resolve_value(line.replace("return ", "", 1).strip(), env) + + dest_name, expr = [p.strip() for p in line.split("=", 1)] + op_name = expr[:expr.index("(")] + arg_text = expr[expr.index("(") + 1: expr.rindex(")")] + args = [a.strip() for a in arg_text.split(",") if a.strip()] + + plain_args = [] + kwargs = {} + for arg in args: + if ":" in arg: + key, value = arg.split(":", 1) + kwargs[key.strip()] = int(value.strip()) + else: + plain_args.append(resolve_value(arg, env)) + + if op_name == "add": + env[dest_name] = apply_add(plain_args[0], plain_args[1]) + elif op_name == "sub": + env[dest_name] = apply_binary(plain_args[0], plain_args[1], lambda a, b: a - b) + elif op_name == "mul": + env[dest_name] = apply_binary(plain_args[0], plain_args[1], lambda a, b: a * b) + elif op_name == "div": + env[dest_name] = apply_binary(plain_args[0], plain_args[1], lambda a, b: a / b) + elif op_name == "relu": + env[dest_name] = apply_relu(plain_args[0]) + elif op_name == "gelu": + env[dest_name] = apply_gelu(plain_args[0]) + elif op_name == "softmax": + env[dest_name] = apply_softmax(plain_args[0]) + elif op_name == "maxpool": + env[dest_name] = apply_maxpool( + plain_args[0], + kwargs.get("kernel", 2), + kwargs.get("stride", 2), + ) + elif op_name == "dot": + env[dest_name] = apply_dot(plain_args[0], plain_args[1], kwargs["len"]) + elif op_name == "matmul": + env[dest_name] = apply_matmul( + plain_args[0], + plain_args[1], + kwargs["m"], + kwargs["n"], + kwargs["k"], + ) + else: + raise ValueError(f"Unsupported op in reference executor: {op_name}") + + idx += 1 + + return False, None + + +def execute_dsl_reference(dsl_file: Path, inputs): + raw_lines = dsl_file.read_text(encoding="utf-8").splitlines() + lines = [] + for raw_line in raw_lines: + line = raw_line.strip() + if not line or line.startswith("#"): + continue + lines.append(line) + + env = dict(inputs) + returned, value = execute_block(lines, env) + return value if returned else None + + +def values_equal(lhs, rhs): + if isinstance(lhs, list) and isinstance(rhs, list): + if len(lhs) != len(rhs): + return False + return all(values_equal(a, b) for a, b in zip(lhs, rhs)) + if isinstance(lhs, float) or isinstance(rhs, float): + return math.isclose(lhs, rhs, rel_tol=1e-7, abs_tol=1e-7) + return lhs == rhs + + +def summarize_benchmark_runs(instr_counts): + if not instr_counts: + return { + "runs": 0, + "avg_instr_count": 0.0, + "min_instr_count": 0, + "max_instr_count": 0, + "ci95_instr_count": 0.0, + } + avg = sum(instr_counts) / len(instr_counts) + if len(instr_counts) > 1: + variance = sum((value - avg) ** 2 for value in instr_counts) / (len(instr_counts) - 1) + ci95 = 1.96 * math.sqrt(variance) / math.sqrt(len(instr_counts)) + else: + ci95 = 0.0 + return { + "runs": len(instr_counts), + "avg_instr_count": avg, + "min_instr_count": min(instr_counts), + "max_instr_count": max(instr_counts), + "ci95_instr_count": ci95, + } + + +def detect_regression(avg_instr_count, baseline_instr_count): + delta = avg_instr_count - baseline_instr_count + delta_pct = 0.0 if baseline_instr_count == 0 else (delta / baseline_instr_count) * 100.0 + return { + "baseline_instr_count": baseline_instr_count, + "delta": round(delta, 4), + "delta_pct": round(delta_pct, 4), + "threshold_pct": REGRESSION_THRESHOLD_PCT, + "regressed": delta_pct > REGRESSION_THRESHOLD_PCT, + } + + +def load_baseline(): + if not BASELINE_FILE.exists(): + return {} + return json.loads(BASELINE_FILE.read_text(encoding="utf-8")) + + +def save_baseline(results): + REPORT_DIR.mkdir(exist_ok=True) + payload = {} + for r in results: + payload[r["name"]] = { + "category": r["category"], + "avg_instr_count": r["avg_instr_count"], + "runs": r["benchmark_runs"], + } + BASELINE_FILE.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + +def generate_report_text(results, passed, failed): + lines = [] + lines.append("# ScratchV DSL 编译器性能测试报告\n\n") + + lines.append("## 测试概览\n\n") + lines.append(f"- 用例总数: {len(results)}\n") + lines.append(f"- 通过数量: {passed}\n") + lines.append(f"- 失败数量: {failed}\n\n") + + benchmark_mode = any(r.get("benchmark_runs", 1) > 1 for r in results) + if benchmark_mode: + lines.append("## 性能基准概览\n\n") + lines.append("- 运行模式: benchmark\n") + lines.append(f"- 性能基线文件: `{BASELINE_FILE}`\n\n") + + lines.append("## 测试结果\n\n") + if benchmark_mode: + lines.append("| 测试用例 | 类别 | 状态 | 模拟后端 | 平均指令数 | 最小值 | 最大值 | 基线 | 变化率 | 是否退化 | 预期输出 | 实际输出 | 是否匹配 | 汇编文件 |\n") + lines.append("|---|---|---|---|---:|---:|---:|---:|---:|---|---|---|---|---|\n") + else: + lines.append("| 测试用例 | 类别 | 状态 | 模拟后端 | 指令数 | 预期输出 | 实际输出 | 是否匹配 | 汇编文件 |\n") + lines.append("|---|---|---|---|---:|---|---|---|---|\n") + + for r in results: + expected = str(r["expected"]).replace("\n", " ").replace("|", "\\|") + actual = str(r["actual"]).replace("\n", " ").replace("|", "\\|") + if benchmark_mode: + lines.append( + f"| {r['name']} | {r['category']} | {r['status']} | {r['backend']} | " + f"{r['avg_instr_count']:.2f} | {r['min_instr_count']} | {r['max_instr_count']} | " + f"{r['baseline_instr_count']:.2f} | {r['delta_pct']:.2f} | {r['regressed']} | " + f"{expected} | {actual} | {r['matched']} | {r['asm']} |\n" + ) + else: + lines.append( + f"| {r['name']} | {r['category']} | {r['status']} | {r['backend']} | " + f"{r['instr_count']} | {expected} | {actual} | {r['matched']} | {r['asm']} |\n" + ) + + lines.append("\n## 性能图表\n\n") + chart_cases = [r["name"] for r in results] + chart_instr = [str(round(r.get("avg_instr_count", r["instr_count"]), 2)) for r in results] + lines.append("### 各测试用例指令数\n\n") + lines.append("```mermaid\n") + lines.append("xychart-beta\n") + lines.append(' title "各测试用例指令数"\n') + lines.append(" x-axis [" + ", ".join(f'"{name}"' for name in chart_cases) + "]\n") + max_instr = max((r.get("avg_instr_count", r["instr_count"]) for r in results), default=0) + lines.append(f' y-axis "指令数" 0 --> {max_instr + 2}\n') + lines.append(" bar [" + ", ".join(chart_instr) + "]\n") + lines.append("```\n\n") + + category_totals = {} + for r in results: + category_totals[r["category"]] = category_totals.get(r["category"], 0) + r.get("avg_instr_count", r["instr_count"]) + lines.append("### 各类别指令数占比\n\n") + lines.append("```mermaid\n") + lines.append("pie showData\n") + lines.append(' title 各类别指令数占比\n') + for category, total in sorted(category_totals.items()): + lines.append(f' "{category}" : {total}\n') + lines.append("```\n") + + lines.append("\n## 用例详情\n\n") + for r in results: + lines.append(f"### {r['name']}\n\n") + lines.append(f"- 类别: {r['category']}\n") + lines.append(f"- 描述: {r['description']}\n") + lines.append(f"- 预期输出 ({r['expected_type']}): {r['expected']}\n") + lines.append(f"- 实际输出: {r['actual']}\n") + lines.append(f"- 是否匹配: {r['matched']}\n") + lines.append(f"- 模拟后端: {r['backend']}\n") + if benchmark_mode: + lines.append(f"- Benchmark 重复次数: {r['benchmark_runs']}\n") + lines.append(f"- 平均指令数: {r['avg_instr_count']:.2f}\n") + lines.append(f"- 最小指令数: {r['min_instr_count']}\n") + lines.append(f"- 最大指令数: {r['max_instr_count']}\n") + lines.append(f"- 基线指令数: {r['baseline_instr_count']:.2f}\n") + lines.append(f"- 性能变化率 (%): {r['delta_pct']:.2f}\n") + lines.append(f"- 是否性能退化: {r['regressed']}\n") + else: + lines.append(f"- 指令数: {r['instr_count']}\n") + lines.append(f"- 汇编文件: {r['asm']}\n\n") + + return "".join(lines) + + +def write_report(results, passed, failed): + REPORT_DIR.mkdir(exist_ok=True) + + REPORT_FILE.write_text(generate_report_text(results, passed, failed), encoding="utf-8") + print(f"\nReport written to {REPORT_FILE}") + + +def _markdown_cell(value): + return str(value).replace("\n", " ").replace("|", "\\|") + + +def generate_report_text(results, passed, failed): + benchmark_mode = any(r.get("benchmark_runs", 1) > 1 for r in results) + pass_rate = 0.0 if not results else passed / len(results) * 100.0 + lines = [ + "# ScratchV DSL 编译器性能测试报告\n\n", + "## 测试概览\n\n", + f"- 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n", + f"- 用例总数: {len(results)}\n", + f"- 通过数量: {passed}\n", + f"- 失败数量: {failed}\n", + f"- 通过率: {pass_rate:.1f}%\n", + f"- 测试目录: `{TEST_DIR}`\n", + f"- 汇编输出目录: `{BUILD_DIR}`\n", + f"- 性能基线文件: `{BASELINE_FILE}`\n", + f"- 性能退化阈值: {REGRESSION_THRESHOLD_PCT:.1f}%\n\n", + "## 测试结果\n\n", + ] + + if benchmark_mode: + lines.extend([ + "| 用例 | 类别 | 状态 | 模拟后端 | 平均指令数 | 95%置信区间 | 最小 | 最大 | 基线 | 变化率(%) | 是否退化 | 预期输出 | 实际输出 | 输出匹配 | 汇编文件 |\n", + "|---|---|---|---|---:|---:|---:|---:|---:|---:|---|---|---|---|---|\n", + ]) + else: + lines.extend([ + "| 用例 | 类别 | 状态 | 模拟后端 | 指令数 | 预期输出 | 实际输出 | 输出匹配 | 汇编文件 |\n", + "|---|---|---|---|---:|---|---|---|---|\n", + ]) + + for r in results: + expected = _markdown_cell(r["expected"]) + actual = _markdown_cell(r["actual"]) + if benchmark_mode: + lines.append( + f"| {r['name']} | {r['category']} | {r['status']} | {r['backend']} | " + f"{r['avg_instr_count']:.2f} | ±{r['ci95_instr_count']:.2f} | " + f"{r['min_instr_count']} | {r['max_instr_count']} | " + f"{r['baseline_instr_count']:.2f} | {r['delta_pct']:.2f} | " + f"{r['regressed']} | {expected} | {actual} | {r['matched']} | {r['asm']} |\n" + ) + else: + lines.append( + f"| {r['name']} | {r['category']} | {r['status']} | {r['backend']} | " + f"{r['instr_count']} | {expected} | {actual} | {r['matched']} | {r['asm']} |\n" + ) + + lines.extend([ + "\n## 性能图表\n\n", + f"![课程版指令数图表]({CHART_FILE.name})\n\n", + "### Mermaid 图表\n\n", + "```mermaid\n", + "xychart-beta\n", + ' title "各测试用例指令数"\n', + " x-axis [" + ", ".join(f'"{r["name"]}"' for r in results) + "]\n", + ]) + max_instr = max((r.get("avg_instr_count", r["instr_count"]) for r in results), default=0) + chart_values = [str(round(r.get("avg_instr_count", r["instr_count"]), 2)) for r in results] + lines.extend([ + f' y-axis "指令数" 0 --> {max_instr + 2}\n', + " bar [" + ", ".join(chart_values) + "]\n", + "```\n\n", + "## 用例详情\n\n", + ]) + + for r in results: + lines.extend([ + f"### {r['name']}\n\n", + f"- 类别: {r['category']}\n", + f"- 描述: {r['description']}\n", + f"- 预期输出 ({r['expected_type']}): {r['expected']}\n", + f"- 实际输出: {r['actual']}\n", + f"- 输出是否匹配: {r['matched']}\n", + f"- 模拟后端: {r['backend']}\n", + f"- 汇编文件: {r['asm']}\n", + ]) + if benchmark_mode: + lines.extend([ + f"- Benchmark 重复次数: {r['benchmark_runs']}\n", + f"- 平均指令数: {r['avg_instr_count']:.2f}\n", + f"- 95% 置信区间: ±{r['ci95_instr_count']:.2f}\n", + f"- 最小指令数: {r['min_instr_count']}\n", + f"- 最大指令数: {r['max_instr_count']}\n", + f"- 基线指令数: {r['baseline_instr_count']:.2f}\n", + f"- 性能变化率: {r['delta_pct']:.2f}%\n", + f"- 性能退化阈值: {r['threshold_pct']:.2f}%\n", + f"- 是否性能退化: {r['regressed']}\n", + ]) + else: + lines.append(f"- 指令数: {r['instr_count']}\n") + lines.append("\n") + + return "".join(lines) + + +def write_chart(results): + try: + mpl_config_dir = Path(tempfile.gettempdir()) / "scratchv-matplotlib" + mpl_config_dir.mkdir(parents=True, exist_ok=True) + os.environ.setdefault("MPLCONFIGDIR", str(mpl_config_dir)) + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except ImportError: + return None + + names = [r["name"] for r in results] + values = [r.get("avg_instr_count", r["instr_count"]) for r in results] + width = max(10, len(names) * 0.45) + fig, ax = plt.subplots(figsize=(width, 5)) + ax.bar(range(len(names)), values, color="#2563eb") + ax.set_title("ScratchV Course Benchmark Instruction Counts") + ax.set_ylabel("Instructions") + ax.set_xticks(range(len(names))) + ax.set_xticklabels(names, rotation=60, ha="right", fontsize=8) + ax.grid(axis="y", linestyle="--", alpha=0.35) + fig.tight_layout() + fig.savefig(CHART_FILE, dpi=160) + plt.close(fig) + return CHART_FILE + + +def write_html_report(results, passed, failed): + try: + from jinja2 import Template + except ImportError: + return None + + pass_rate = 0.0 if not results else passed / len(results) * 100.0 + template = Template(""" + + + + ScratchV 课程版测试报告 + + + +

ScratchV DSL 编译器性能测试报告

+
+

用例总数:{{ total }},通过:{{ passed }},失败:{{ failed }},通过率:{{ "%.1f"|format(pass_rate) }}%

+

测试目录:{{ test_dir }},性能退化阈值:{{ threshold }}%

+
+ 课程版指令数图表 + + + + + + {% for r in results %} + + + + + + + + + + + {% endfor %} + +
用例类别状态平均指令数95%置信区间变化率(%)是否退化描述
{{ r.name }}{{ r.category }}{{ r.status }}{{ "%.2f"|format(r.avg_instr_count) }}±{{ "%.2f"|format(r.ci95_instr_count) }}{{ "%.2f"|format(r.delta_pct) }}{{ r.regressed }}{{ r.description }}
+ + +""") + HTML_REPORT_FILE.write_text( + template.render( + total=len(results), + passed=passed, + failed=failed, + pass_rate=pass_rate, + test_dir=str(TEST_DIR), + threshold=REGRESSION_THRESHOLD_PCT, + chart_name=CHART_FILE.name, + results=results, + ), + encoding="utf-8", + ) + return HTML_REPORT_FILE + + +def write_report(results, passed, failed): + REPORT_DIR.mkdir(exist_ok=True) + write_chart(results) + REPORT_FILE.write_text(generate_report_text(results, passed, failed), encoding="utf-8") + html_path = write_html_report(results, passed, failed) + print(f"\nMarkdown report written to {REPORT_FILE}") + if html_path: + print(f"HTML report written to {html_path}") + + +def parse_args(argv=None): + parser = argparse.ArgumentParser(description="Run ScratchV DSL benchmark suite.") + parser.add_argument("--benchmark", type=int, default=0, metavar="N", + help="Run each case N times and report average instruction count.") + parser.add_argument("--update-baseline", action="store_true", + help="Write current benchmark averages to the baseline file.") + return parser.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + BUILD_DIR.mkdir(exist_ok=True) + baseline = load_baseline() if args.benchmark else {} + + dsl_files = list(TEST_DIR.rglob("*.dsl")) + + if not dsl_files: + print("No DSL test files found.") + return + + passed = 0 + failed = 0 + results = [] + + print("Running DSL compiler tests...") + print("=" * 50) + + for dsl_file in dsl_files: + print(f"\n[TEST] {dsl_file}") + + meta = load_metadata(dsl_file) + result, output_file = run_compile(dsl_file) + sim_result = run_simulation(output_file) if result.returncode == 0 else { + "success": False, + "instr_count": 0, + "return_value": None, + "backend": "none", + "error": "compile failed", + } + expected_value = meta.get("expected_return") + actual_value = execute_dsl_reference(dsl_file, meta.get("inputs", {})) + matched = values_equal(actual_value, expected_value) + benchmark_counts = [] + benchmark_summary = { + "runs": 1, + "avg_instr_count": sim_result.get("instr_count", 0), + "min_instr_count": sim_result.get("instr_count", 0), + "max_instr_count": sim_result.get("instr_count", 0), + "ci95_instr_count": 0.0, + } + regression = { + "baseline_instr_count": 0.0, + "delta": 0.0, + "delta_pct": 0.0, + "threshold_pct": REGRESSION_THRESHOLD_PCT, + "regressed": False, + } + + if args.benchmark > 0 and result.returncode == 0 and output_file.exists(): + for _ in range(args.benchmark): + benchmark_counts.append(run_simulation(output_file).get("instr_count", 0)) + benchmark_summary = summarize_benchmark_runs(benchmark_counts) + baseline_entry = baseline.get(dsl_file.stem) + if baseline_entry: + regression = detect_regression( + avg_instr_count=benchmark_summary["avg_instr_count"], + baseline_instr_count=baseline_entry.get("avg_instr_count", 0.0), + ) + + ok = ( + result.returncode == 0 + and output_file.exists() + and sim_result["success"] + and matched + and not regression["regressed"] + ) + + if ok: + print("PASS") + passed += 1 + status = "PASS" + else: + print("FAIL") + failed += 1 + status = "FAIL" + print(sim_result.get("error") or result.stderr or result.stdout) + + results.append({ + "name": dsl_file.stem, + "category": dsl_file.parent.name, + "path": str(dsl_file), + "status": status, + "description": meta.get("description", ""), + "expected_type": meta.get("expected_output_type", "return_value"), + "expected": expected_value, + "actual": actual_value, + "matched": matched, + "backend": sim_result.get("backend", "none"), + "instr_count": sim_result.get("instr_count", 0), + "benchmark_runs": benchmark_summary["runs"], + "avg_instr_count": benchmark_summary["avg_instr_count"], + "min_instr_count": benchmark_summary["min_instr_count"], + "max_instr_count": benchmark_summary["max_instr_count"], + "ci95_instr_count": benchmark_summary["ci95_instr_count"], + "baseline_instr_count": regression["baseline_instr_count"], + "delta": regression["delta"], + "delta_pct": regression["delta_pct"], + "threshold_pct": regression["threshold_pct"], + "regressed": regression["regressed"], + "asm": str(output_file), + }) + + print("\n" + "=" * 50) + print(f"Total: {len(dsl_files)}") + print(f"Passed: {passed}") + print(f"Failed: {failed}") + + if args.benchmark and args.update_baseline: + save_baseline(results) + print(f"Baseline written to {BASELINE_FILE}") + + write_report(results, passed, failed) + + +if __name__ == "__main__": + main() diff --git a/ScratchV-topic06-deliverable/setup.py b/ScratchV-topic06-deliverable/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/ScratchV-topic06-deliverable/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.dsl b/ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.dsl new file mode 100644 index 0000000..52c1325 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.dsl @@ -0,0 +1,5 @@ +# Add followed by two ReLU stages +x = add(input, bias) +y = relu(x) +result = relu(y) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.meta.json b/ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.meta.json new file mode 100644 index 0000000..e587d8a --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/add_relu_relu.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Add input and bias, then apply ReLU twice.", + "expected_output_type": "return_value", + "inputs": { + "input": -3, + "bias": 10 + }, + "expected_return": 7 +} diff --git a/ScratchV-topic06-deliverable/tests_main/activation/relu_add.dsl b/ScratchV-topic06-deliverable/tests_main/activation/relu_add.dsl new file mode 100644 index 0000000..7e323b8 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/relu_add.dsl @@ -0,0 +1,4 @@ +# Add + ReLU activation +x = add(input, bias) +y = relu(x) +return y \ No newline at end of file diff --git a/ScratchV-topic06-deliverable/tests_main/activation/relu_add.meta.json b/ScratchV-topic06-deliverable/tests_main/activation/relu_add.meta.json new file mode 100644 index 0000000..f1b7f84 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/relu_add.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Add input and bias, then apply one ReLU.", + "expected_output_type": "return_value", + "inputs": { + "input": -2, + "bias": 5 + }, + "expected_return": 3 +} diff --git a/ScratchV-topic06-deliverable/tests_main/activation/relu_only.dsl b/ScratchV-topic06-deliverable/tests_main/activation/relu_only.dsl new file mode 100644 index 0000000..404a911 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/relu_only.dsl @@ -0,0 +1,3 @@ +# Single ReLU activation +result = relu(x) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/activation/relu_only.meta.json b/ScratchV-topic06-deliverable/tests_main/activation/relu_only.meta.json new file mode 100644 index 0000000..b08af80 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/relu_only.meta.json @@ -0,0 +1,8 @@ +{ + "description": "Apply ReLU directly to a single input value.", + "expected_output_type": "return_value", + "inputs": { + "x": -5 + }, + "expected_return": 0 +} diff --git a/ScratchV-topic06-deliverable/tests_main/activation/relu_twice.dsl b/ScratchV-topic06-deliverable/tests_main/activation/relu_twice.dsl new file mode 100644 index 0000000..7d2defe --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/relu_twice.dsl @@ -0,0 +1,4 @@ +# Two-stage ReLU activation +x = relu(input) +result = relu(x) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/activation/relu_twice.meta.json b/ScratchV-topic06-deliverable/tests_main/activation/relu_twice.meta.json new file mode 100644 index 0000000..fabbb38 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/activation/relu_twice.meta.json @@ -0,0 +1,8 @@ +{ + "description": "Apply ReLU twice to the same activation path.", + "expected_output_type": "return_value", + "inputs": { + "input": 4 + }, + "expected_return": 4 +} diff --git a/ScratchV-topic06-deliverable/tests_main/branch/if_else.dsl b/ScratchV-topic06-deliverable/tests_main/branch/if_else.dsl new file mode 100644 index 0000000..16f3c0d --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/branch/if_else.dsl @@ -0,0 +1,8 @@ +# Branch takes the else path when flag is zero. +if flag +result = add(a, b) +return result +else +result = sub(a, b) +return result +endif diff --git a/ScratchV-topic06-deliverable/tests_main/branch/if_else.meta.json b/ScratchV-topic06-deliverable/tests_main/branch/if_else.meta.json new file mode 100644 index 0000000..fdacf6a --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/branch/if_else.meta.json @@ -0,0 +1,10 @@ +{ + "description": "if/else branch returns subtraction result when flag is zero.", + "expected_output_type": "return_value", + "inputs": { + "flag": 0, + "a": 9, + "b": 4 + }, + "expected_return": 5 +} diff --git a/ScratchV-topic06-deliverable/tests_main/branch/if_relu.dsl b/ScratchV-topic06-deliverable/tests_main/branch/if_relu.dsl new file mode 100644 index 0000000..f748a83 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/branch/if_relu.dsl @@ -0,0 +1,8 @@ +# Branch selects whether to apply ReLU after an add. +sum = add(a, b) +if use_relu +result = relu(sum) +return result +else +return sum +endif diff --git a/ScratchV-topic06-deliverable/tests_main/branch/if_relu.meta.json b/ScratchV-topic06-deliverable/tests_main/branch/if_relu.meta.json new file mode 100644 index 0000000..1925fb5 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/branch/if_relu.meta.json @@ -0,0 +1,10 @@ +{ + "description": "if/else branch combined with add and relu.", + "expected_output_type": "return_value", + "inputs": { + "use_relu": 1, + "a": -8, + "b": 3 + }, + "expected_return": 0 +} diff --git a/ScratchV-topic06-deliverable/tests_main/branch/if_then.dsl b/ScratchV-topic06-deliverable/tests_main/branch/if_then.dsl new file mode 100644 index 0000000..b4f72d4 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/branch/if_then.dsl @@ -0,0 +1,8 @@ +# Branch takes the then path when flag is non-zero. +if flag +result = add(a, b) +return result +else +result = sub(a, b) +return result +endif diff --git a/ScratchV-topic06-deliverable/tests_main/branch/if_then.meta.json b/ScratchV-topic06-deliverable/tests_main/branch/if_then.meta.json new file mode 100644 index 0000000..04748d1 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/branch/if_then.meta.json @@ -0,0 +1,10 @@ +{ + "description": "if/else branch returns add result when flag is non-zero.", + "expected_output_type": "return_value", + "inputs": { + "flag": 1, + "a": 9, + "b": 4 + }, + "expected_return": 13 +} diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.dsl b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.dsl new file mode 100644 index 0000000..e39db66 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.dsl @@ -0,0 +1,4 @@ +# Chain of two vector adds +x = add(a, b) +y = add(x, c) +return y \ No newline at end of file diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.meta.json b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.meta.json new file mode 100644 index 0000000..b07adaf --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain.meta.json @@ -0,0 +1,10 @@ +{ + "description": "Add a and b, then add c to the intermediate result.", + "expected_output_type": "return_value", + "inputs": { + "a": 2, + "b": 3, + "c": 4 + }, + "expected_return": 9 +} diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.dsl b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.dsl new file mode 100644 index 0000000..aeca4df --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.dsl @@ -0,0 +1,5 @@ +# Chain of three add operations +x = add(a, b) +y = add(x, c) +z = add(y, d) +return z diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.meta.json b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.meta.json new file mode 100644 index 0000000..368b3aa --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_chain_3.meta.json @@ -0,0 +1,11 @@ +{ + "description": "Chain three add operations across four symbolic inputs.", + "expected_output_type": "return_value", + "inputs": { + "a": 2, + "b": 3, + "c": 4, + "d": 5 + }, + "expected_return": 14 +} diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.dsl b/ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.dsl new file mode 100644 index 0000000..0e4449e --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.dsl @@ -0,0 +1,5 @@ +# Fan-in add over four inputs +x = add(a, b) +y = add(c, d) +result = add(x, y) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.meta.json b/ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.meta.json new file mode 100644 index 0000000..3eb1f4a --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_fan_in_4.meta.json @@ -0,0 +1,11 @@ +{ + "description": "Compute two independent adds and then merge them with a final add.", + "expected_output_type": "return_value", + "inputs": { + "a": 1, + "b": 2, + "c": 3, + "d": 4 + }, + "expected_return": 10 +} diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.dsl b/ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.dsl new file mode 100644 index 0000000..4618948 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.dsl @@ -0,0 +1,4 @@ +# Reuse intermediate add result +x = add(a, b) +result = add(x, x) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.meta.json b/ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.meta.json new file mode 100644 index 0000000..2d2c396 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/add_reuse.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Reuse the same intermediate add result on both operands of a second add.", + "expected_output_type": "return_value", + "inputs": { + "a": 2, + "b": 3 + }, + "expected_return": 10 +} diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.dsl b/ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.dsl new file mode 100644 index 0000000..129f923 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.dsl @@ -0,0 +1,3 @@ +# Vector add +result = add(a, b) +return result \ No newline at end of file diff --git a/ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.meta.json b/ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.meta.json new file mode 100644 index 0000000..7347fff --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/elementwise/vector_add.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Single add over two symbolic vector inputs.", + "expected_output_type": "return_value", + "inputs": { + "a": 2, + "b": 3 + }, + "expected_return": 5 +} diff --git a/ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.dsl b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.dsl new file mode 100644 index 0000000..381f1c0 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.dsl @@ -0,0 +1,5 @@ +# Loop with repeated add body over 4 iterations +for i = 0, 4 +x = add(a, b) +endfor +return x diff --git a/ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.meta.json b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.meta.json new file mode 100644 index 0000000..fc31fae --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_4.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Run a four-iteration loop whose body computes one add; final returned value is the last loop-body result.", + "expected_output_type": "return_value", + "inputs": { + "a": 2, + "b": 3 + }, + "expected_return": 5 +} diff --git a/ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.dsl b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.dsl new file mode 100644 index 0000000..eca595b --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.dsl @@ -0,0 +1,6 @@ +# Loop with chained add inside the body +for i = 0, 4 +x = add(a, b) +y = add(x, c) +endfor +return y diff --git a/ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.meta.json b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.meta.json new file mode 100644 index 0000000..154d91d --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/loop/loop_add_chain_4.meta.json @@ -0,0 +1,10 @@ +{ + "description": "Run a four-iteration loop whose body computes two chained adds; final returned value is the last loop-body result.", + "expected_output_type": "return_value", + "inputs": { + "a": 2, + "b": 3, + "c": 4 + }, + "expected_return": 9 +} diff --git a/ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.dsl b/ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.dsl new file mode 100644 index 0000000..44f3080 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.dsl @@ -0,0 +1,6 @@ +# Loop with add followed by ReLU in the body +for i = 0, 4 +x = add(input, bias) +y = relu(x) +endfor +return y diff --git a/ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.meta.json b/ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.meta.json new file mode 100644 index 0000000..8baca28 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/loop/loop_relu_add_4.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Run a four-iteration loop whose body computes add followed by ReLU; final returned value is the last loop-body result.", + "expected_output_type": "return_value", + "inputs": { + "input": -4, + "bias": 6 + }, + "expected_return": 2 +} diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_4.dsl b/ScratchV-topic06-deliverable/tests_main/reduction/dot_4.dsl new file mode 100644 index 0000000..7c02794 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_4.dsl @@ -0,0 +1,3 @@ +# Dot product of two 4-element vectors +result = dot(a, b, len:4) +return result \ No newline at end of file diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_4.meta.json b/ScratchV-topic06-deliverable/tests_main/reduction/dot_4.meta.json new file mode 100644 index 0000000..748f260 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_4.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Compute the dot product of two symbolic vectors of length 4.", + "expected_output_type": "return_value", + "inputs": { + "a": [1, 2, 3, 4], + "b": [5, 6, 7, 8] + }, + "expected_return": 70 +} diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_8.dsl b/ScratchV-topic06-deliverable/tests_main/reduction/dot_8.dsl new file mode 100644 index 0000000..e1b71fa --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_8.dsl @@ -0,0 +1,3 @@ +# Dot product of two 8-element vectors +result = dot(a, b, len:8) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_8.meta.json b/ScratchV-topic06-deliverable/tests_main/reduction/dot_8.meta.json new file mode 100644 index 0000000..d3172a2 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_8.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Compute the dot product of two symbolic vectors of length 8.", + "expected_output_type": "return_value", + "inputs": { + "a": [1, 2, 3, 4, 5, 6, 7, 8], + "b": [1, 1, 1, 1, 1, 1, 1, 1] + }, + "expected_return": 36 +} diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.dsl b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.dsl new file mode 100644 index 0000000..279fc19 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.dsl @@ -0,0 +1,4 @@ +# Dot product followed by ReLU +x = dot(a, b, len:4) +result = relu(x) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.meta.json b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.meta.json new file mode 100644 index 0000000..24910bc --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_4.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Compute a length-4 dot product and pass it through ReLU.", + "expected_output_type": "return_value", + "inputs": { + "a": [1, -2, 3, -4], + "b": [2, 3, 4, 5] + }, + "expected_return": 0 +} diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.dsl b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.dsl new file mode 100644 index 0000000..d2b906d --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.dsl @@ -0,0 +1,4 @@ +# Dot product of length 8 followed by ReLU +x = dot(a, b, len:8) +result = relu(x) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.meta.json b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.meta.json new file mode 100644 index 0000000..776fab5 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/reduction/dot_relu_8.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Compute a length-8 dot product and pass it through ReLU.", + "expected_output_type": "return_value", + "inputs": { + "a": [1, 0, 1, 0, 1, 0, 1, 0], + "b": [2, 2, 2, 2, 2, 2, 2, 2] + }, + "expected_return": 8 +} diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.dsl b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.dsl new file mode 100644 index 0000000..30307f1 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.dsl @@ -0,0 +1,3 @@ +# Matrix multiplication: 2x2 * 2x2 +result = matmul(A, B, m:2, n:2, k:2) +return result \ No newline at end of file diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.meta.json b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.meta.json new file mode 100644 index 0000000..f970a61 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_2x2.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Compute a symbolic 2x2 by 2x2 matrix multiplication.", + "expected_output_type": "return_value", + "inputs": { + "A": [[1, 2], [3, 4]], + "B": [[5, 6], [7, 8]] + }, + "expected_return": [[19, 22], [43, 50]] +} diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.dsl b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.dsl new file mode 100644 index 0000000..ab5a865 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.dsl @@ -0,0 +1,3 @@ +# Matrix multiplication: 4x4 * 4x4 +result = matmul(A, B, m:4, n:4, k:4) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.meta.json b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.meta.json new file mode 100644 index 0000000..ead52b9 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_4x4.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Compute a symbolic 4x4 by 4x4 matrix multiplication.", + "expected_output_type": "return_value", + "inputs": { + "A": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], + "B": [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] + }, + "expected_return": [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] +} diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.dsl b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.dsl new file mode 100644 index 0000000..d41a2cb --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.dsl @@ -0,0 +1,4 @@ +# Matrix multiplication followed by add +x = matmul(A, B, m:2, n:2, k:2) +result = add(x, bias) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.meta.json b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.meta.json new file mode 100644 index 0000000..9bca125 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_add_2x2.meta.json @@ -0,0 +1,10 @@ +{ + "description": "Compute a 2x2 matmul and then add a symbolic bias term.", + "expected_output_type": "return_value", + "inputs": { + "A": [[1, 2], [3, 4]], + "B": [[5, 6], [7, 8]], + "bias": [[1, 1], [1, 1]] + }, + "expected_return": [[20, 23], [44, 51]] +} diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.dsl b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.dsl new file mode 100644 index 0000000..28c7c61 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.dsl @@ -0,0 +1,4 @@ +# Matrix multiplication followed by ReLU +x = matmul(A, B, m:2, n:2, k:2) +result = relu(x) +return result diff --git a/ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.meta.json b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.meta.json new file mode 100644 index 0000000..352dfa6 --- /dev/null +++ b/ScratchV-topic06-deliverable/tests_main/tensor/matmul_relu_2x2.meta.json @@ -0,0 +1,9 @@ +{ + "description": "Compute a 2x2 matmul and then apply ReLU to its result.", + "expected_output_type": "return_value", + "inputs": { + "A": [[-1, 2], [-3, 4]], + "B": [[1, 0], [0, 1]] + }, + "expected_return": [[0, 2], [0, 4]] +}