Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ pytest tests/ -v # run all tests
## Adding a New Optimization Pass

1. Create `scratchv/optimizer/my_pass.py`.
2. Implement a class with a `run(program) → int` method (returns number of
transformations applied).
3. Register it in `scratchv/main.py` → `run_optimizer()`.
4. Add test cases (positive: should transform; negative: should not).
5. Run `pytest` to verify.
2. Subclass `OptimizationPass` and define a stable, lowercase, kebab-case
`name` such as `"constant-folding"`. The manager uses this identifier in
ordered reports and failure diagnostics.
3. Implement `optimize(program) → int`. It must return a non-negative count
of transformations made by that invocation, not a lifetime total.
4. Register it in `create_optimization_pass_manager()` in
`scratchv/compiler.py`.
5. Add test cases (positive: should transform; negative: should not).
6. Run `pytest` to verify.

## Documentation

Expand Down
32 changes: 8 additions & 24 deletions benchmarks/run_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,14 @@ def _parse_onnx(path: str) -> Program:


def _optimize(program: Program, level: str) -> float:
"""Run optimizations. Returns elapsed time in seconds."""
if level == "none":
return 0.0
"""Run the selected unified pipeline and return its reported elapsed time.

t0 = time.perf_counter()

from scratchv.optimizer.constant_folding import ConstantFolder
from scratchv.optimizer.dead_code import DeadCodeEliminator

ConstantFolder(program).run()
DeadCodeEliminator(program).run()
The ``none`` pipeline is empty, so its report is exactly ``0.0`` seconds.
"""
from scratchv.compiler import create_optimization_pass_manager

if level == "all":
from scratchv.optimizer.peephole import IRPeepholeOptimizer
from scratchv.optimizer.muladd_fusion import MulAddFusion
from scratchv.optimizer.licm import LICM
IRPeepholeOptimizer(program).run()
MulAddFusion(program).run()
LICM(program).run()

return time.perf_counter() - t0
manager = create_optimization_pass_manager(level)
return manager.run(program).elapsed_seconds


def _codegen_riscv(program: Program) -> tuple[str, float]:
Expand Down Expand Up @@ -162,11 +149,8 @@ def run_benchmark(model_name: str, model_path: str, *,
result.ir_inst_count, result.ir_bb_count = _count_ir(program)

# 2. Optimize
if optimize_level != "none":
result.optimize_time_s = _optimize(program, optimize_level)
result.ir_opt_inst_count, _ = _count_ir(program)
else:
result.ir_opt_inst_count = result.ir_inst_count
result.optimize_time_s = _optimize(program, optimize_level)
result.ir_opt_inst_count, _ = _count_ir(program)

# 3. Codegen
if backend == "llvm":
Expand Down
16 changes: 5 additions & 11 deletions benchmarks/test_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,18 @@ def test_parse_onnx(model_name: str, benchmark_models: dict[str, str]):
@pytest.mark.parametrize("model_name", MODEL_PARAMS, ids=_model_id)
def test_optimize(model_name: str, benchmark_models: dict[str, str]):
"""Parse + optimize, check IR is not empty."""
from scratchv.compiler import create_optimization_pass_manager
from scratchv.frontend.onnx_parser import ONNXParser
from scratchv.optimizer.constant_folding import ConstantFolder
from scratchv.optimizer.dead_code import DeadCodeEliminator
from scratchv.optimizer.peephole import IRPeepholeOptimizer

path = benchmark_models[model_name]
program = ONNXParser().parse(path)

inst_before = sum(1 for f in program.functions for bb in f.blocks for _ in bb.instructions)

ConstantFolder(program).run()
DeadCodeEliminator(program).run()
IRPeepholeOptimizer(program).run()
create_optimization_pass_manager("all").run(program)

inst_after = sum(1 for f in program.functions for bb in f.blocks for _ in bb.instructions)
assert inst_after >= 0, f"Optimization failed for {model_name}"
assert 0 < inst_after <= inst_before, f"Optimization failed for {model_name}"
print(f"\n {model_name}: {inst_before} → {inst_after} instructions")


Expand All @@ -92,18 +88,16 @@ def test_optimize(model_name: str, benchmark_models: dict[str, str]):
@pytest.mark.parametrize("backend", BACKEND_PARAMS)
def test_codegen_riscv(model_name: str, backend: str, benchmark_models: dict[str, str]):
"""Parse + codegen → RISC-V assembly, check output is non-empty."""
from scratchv.compiler import create_optimization_pass_manager
from scratchv.frontend.onnx_parser import ONNXParser
from scratchv.optimizer.constant_folding import ConstantFolder
from scratchv.optimizer.dead_code import DeadCodeEliminator
from scratchv.backend.instruction_select import InstructionSelector
from scratchv.backend.register_alloc import RegisterAllocator
from scratchv.backend.asm_emit import AsmEmitter

path = benchmark_models[model_name]
program = ONNXParser().parse(path)

ConstantFolder(program).run()
DeadCodeEliminator(program).run()
create_optimization_pass_manager("basic").run(program)

selector = InstructionSelector(program)
machine = selector.run()
Expand Down
44 changes: 38 additions & 6 deletions docs/optimization_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
Six beginner-friendly optimization passes for ScratchV, ordered by difficulty.
Each pass can be implemented as a standalone task.

All IR passes define a stable `name` and implement
`OptimizationPass.optimize(self, program: Program) -> int`. The return value is
the non-negative number of transformations made by the current invocation.
Use the canonical factory when selecting an optimization level:

```python
from scratchv.compiler import create_optimization_pass_manager

manager = create_optimization_pass_manager("all")
report = manager.run(program)
print(report.total_changes)
```

`manager.run()` returns an immutable `OptimizationReport`. Its ordered
`executions` contain each pass name, change count, and elapsed time;
`total_changes` and `elapsed_seconds` summarize the complete pipeline.

The main CLI accepts `--opt-level none|basic|all`; `--optimize` remains a
compatibility alias. The standalone LLVM tool has a separate numeric
`--opt-level 0|1|2|3` option.

---

## 1. Constant Folding (⭐)
Expand Down Expand Up @@ -66,21 +87,27 @@ Scans assembly for redundant patterns and removes them:
**Implementation** in `scratchv/optimizer/peephole.py`:
```python
class PeepholeOptimizer:
def run(self, program: Program) -> int:
def optimize(self, program: Program) -> int:
changes = 0
for func in program.functions:
for block in func.blocks:
self._optimize_block(block)
changes += self._optimize_block(block)
return changes

def _optimize_block(self, block):
def _optimize_block(self, block) -> int:
changes = 0
i = 0
while i < len(block.instructions):
if self._is_addi_zero(block.instructions[i]):
block.instructions.pop(i)
changes += 1
continue
elif self._is_jump_to_next(block, i):
block.instructions.pop(i)
changes += 1
continue
i += 1
return changes
```

---
Expand Down Expand Up @@ -109,14 +136,19 @@ for out_y in range(H_out):
**Implementation** in `scratchv/optimizer/licm.py`:
```python
class LICM:
def run(self, program: Program) -> int:
def optimize(self, program: Program) -> int:
changes = 0
for func in program.functions:
self._find_loops_and_hoist(func)
changes += self._find_loops_and_hoist(func)
return changes

def _find_loops_and_hoist(self, func):
def _find_loops_and_hoist(self, func) -> int:
hoisted_count = 0
# 1. Find FOR/ENDFOR pairs
# 2. Identify instructions whose operands don't change in loop
# 3. Move them before the FOR instruction
# 4. Increment hoisted_count for each moved instruction
return hoisted_count
```

---
Expand Down
33 changes: 32 additions & 1 deletion docs/topics/04-IR优化器框架.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,42 @@ IR 优化器框架包含 5 个 IR 层面的优化 pass,它们共享相同接
所有 pass 实现相同签名:
```python
class OptimizationPass:
@property
def name(self) -> str:
"""返回稳定的小写连字符名称,如 constant-folding。"""
...

def optimize(self, program: Program) -> int:
"""对 Program 原地优化,返回变更次数"""
"""对 Program 原地优化,返回本次调用的非负变更次数。"""
...
```

`name` 是报告和失败诊断中使用的稳定标识符;PassManager 按注册顺序
执行,不会根据名称排序或去重。具体 pass 可以用类属性实现:

```python
class ConstantFolder(OptimizationPass):
name = "constant-folding"
```

默认管线由唯一工厂创建,避免主编译器与 benchmark 各自维护 Pass 列表:

```python
from scratchv.compiler import create_optimization_pass_manager

manager = create_optimization_pass_manager("basic")
report = manager.run(program)
for execution in report.executions:
print(execution.name, execution.changes, execution.elapsed_seconds)
```

主命令行使用 `--opt-level none|basic|all`;旧参数名 `--optimize` 暂作兼容别名。

```bash
scratchv model.onnx --opt-level basic
scratchv model.onnx --optimize basic
```

### 死代码消除的回溯标记

```python
Expand Down
22 changes: 21 additions & 1 deletion docs/topics/archive/optimizer_framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,27 @@ if changes > 0:
print(f"Folded {changes} constants")
```

所有 pass 遵循相同接口,可在 `PassManager` 中链式调用。
所有 pass 遵循相同接口。默认三级管线通过唯一工厂创建:

```python
from scratchv.compiler import create_optimization_pass_manager

manager = create_optimization_pass_manager("all")
report = manager.run(program)
print(report.total_changes)
```

编译器入口和 benchmark 共用该工厂:`none` 创建空管线,`basic` 创建常量折叠与
死代码消除,`all` 按上表注册全部五个 pass。`manager.run()` 返回不可变的
`OptimizationReport`;其 `executions` 按执行顺序记录 pass 名称、变更数和耗时,
`total_changes` 与 `elapsed_seconds` 提供总计。

主 CLI 使用 `--opt-level none|basic|all`,并保留 `--optimize` 兼容别名。

```bash
scratchv model.onnx --opt-level all
scratchv model.onnx --optimize all
```

## 相关 Topic

Expand Down
7 changes: 5 additions & 2 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ Add verification to your workflow:

```bash
# 1. Compile with ScratchV (RISC-V backend)
scratchv model.onnx -o output.s --optimize
scratchv model.onnx -o output.s --opt-level all

# 2. Compile with LLVM backend
scratchv model.onnx --backend llvm -o model.ll --optimize
scratchv model.onnx --backend llvm -o model.ll --opt-level all

# 3. Verify against ONNX Runtime
scratchv model.onnx --verify
Expand All @@ -218,6 +218,9 @@ lli model.ll # LLVM JIT execution
# (before vs after optimization)
```

`--optimize all` 仍是主 CLI 的兼容别名;新文档优先使用规范写法
`--opt-level all`。

---

## LLVM IR Verification
Expand Down
28 changes: 13 additions & 15 deletions examples/end_to_end_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,13 @@ def demo_matmul(backend: str):
program = parser.parse(dsl_source)

# Optimize
from scratchv.optimizer.constant_folding import ConstantFolder
from scratchv.optimizer.dead_code import DeadCodeEliminator
folder = ConstantFolder(program)
folded = folder.run()
elim = DeadCodeEliminator(program)
eliminated = elim.run()
from scratchv.compiler import create_optimization_pass_manager
report = create_optimization_pass_manager("basic").run(program)
changes_by_name = {
execution.name: execution.changes for execution in report.executions
}
folded = changes_by_name["constant-folding"]
eliminated = changes_by_name["dead-code-elim"]
print(f" Optimizer: {folded} folded, {eliminated} eliminated")

from scratchv.backend.llvm_codegen import LLVMCodegen
Expand Down Expand Up @@ -163,15 +164,12 @@ def demo_optimized_pipeline():
# With optimization
parser2 = DSLParser()
program2 = parser2.parse(dsl_source)
from scratchv.optimizer.constant_folding import ConstantFolder
from scratchv.optimizer.dead_code import DeadCodeEliminator
from scratchv.optimizer.peephole import IRPeepholeOptimizer
folder = ConstantFolder(program2)
folder.run()
elim = DeadCodeEliminator(program2)
elim.run()
peep = IRPeepholeOptimizer(program2)
peep.run()
from scratchv.compiler import create_optimization_pass_manager
report = create_optimization_pass_manager("all").run(program2)
print(
f" Optimizer: {report.total_changes} change(s) "
f"across {len(report.executions)} passes"
)
codegen2 = LLVMCodegen(program2)
print("After optimization (fold + dce + peephole):")
print(codegen2.emit()[:400])
Expand Down
23 changes: 9 additions & 14 deletions examples/llvm_optimization_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ def main():
parser2 = DSLParser()
program2 = parser2.parse(dsl_source)

from scratchv.optimizer.constant_folding import ConstantFolder
from scratchv.optimizer.dead_code import DeadCodeEliminator
folder = ConstantFolder(program2)
folded = folder.run()
elim = DeadCodeEliminator(program2)
eliminated = elim.run()
from scratchv.compiler import create_optimization_pass_manager
report = create_optimization_pass_manager("basic").run(program2)
changes_by_name = {
execution.name: execution.changes for execution in report.executions
}
folded = changes_by_name["constant-folding"]
eliminated = changes_by_name["dead-code-elim"]
print(f" Folded: {folded}, Eliminated: {eliminated}")

codegen2 = LLVMCodegen(program2)
Expand All @@ -64,14 +65,8 @@ def main():
parser3 = DSLParser()
program3 = parser3.parse(dsl_source)

folder3 = ConstantFolder(program3)
folder3.run()
elim3 = DeadCodeEliminator(program3)
elim3.run()
from scratchv.optimizer.peephole import IRPeepholeOptimizer
peep = IRPeepholeOptimizer(program3)
peeped = peep.run()
print(f" Folded+DCE+Peephole: {peeped} optimizations")
report = create_optimization_pass_manager("all").run(program3)
print(f" Full pipeline: {report.total_changes} optimizations")

codegen3 = LLVMCodegen(program3)
opt_ir = codegen3.emit()
Expand Down
13 changes: 7 additions & 6 deletions examples/onnx_llvm_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ def main():

# Step 2: Optimize
print("\n[2/5] Optimizing IR...")
from scratchv.optimizer.constant_folding import ConstantFolder
from scratchv.optimizer.dead_code import DeadCodeEliminator
folder = ConstantFolder(program)
folded = folder.run()
elim = DeadCodeEliminator(program)
eliminated = elim.run()
from scratchv.compiler import create_optimization_pass_manager
report = create_optimization_pass_manager("basic").run(program)
changes_by_name = {
execution.name: execution.changes for execution in report.executions
}
folded = changes_by_name["constant-folding"]
eliminated = changes_by_name["dead-code-elim"]
print(f" Folded: {folded}, Eliminated: {eliminated}")

# Step 3: Generate LLVM IR
Expand Down
Loading
Loading