From 8c6c2ce507f7914cf3c5b6a309bc7517f842d635 Mon Sep 17 00:00:00 2001 From: wangjiangyang <1938840431@qq.com> Date: Fri, 15 May 2026 15:56:33 +0800 Subject: [PATCH 1/7] use llvm as the backend --- README.md | 82 +++- docs/verification.md | 74 +++- examples/end_to_end_pipeline.py | 240 ++++++++++++ examples/gen_ppt.py | 506 ++++++++++++++++++++++++ examples/llvm_optimization_pipeline.py | 105 +++++ examples/onnx_llvm_verification.py | 148 +++++++ pyproject.toml | 16 +- scratchv/backend/llvm_codegen.py | 520 +++++++++++++++++++++++++ scratchv/main.py | 239 ++++++++---- scratchv/verification/__init__.py | 1 + scratchv/verification/verifier.py | 347 +++++++++++++++++ tests/test_llvm_codegen.py | 129 ++++++ tests/test_verification.py | 109 ++++++ 13 files changed, 2431 insertions(+), 85 deletions(-) create mode 100644 examples/end_to_end_pipeline.py create mode 100644 examples/gen_ppt.py create mode 100644 examples/llvm_optimization_pipeline.py create mode 100644 examples/onnx_llvm_verification.py create mode 100644 scratchv/backend/llvm_codegen.py create mode 100644 scratchv/verification/__init__.py create mode 100644 scratchv/verification/verifier.py create mode 100644 tests/test_llvm_codegen.py create mode 100644 tests/test_verification.py diff --git a/README.md b/README.md index 5e2e452..72d603e 100644 --- a/README.md +++ b/README.md @@ -27,18 +27,21 @@ ScratchV/ │ │ ├── peephole.py # Redundant pattern elimination │ │ ├── muladd_fusion.py # Mul+Add instruction combining │ │ └── licm.py # Loop Invariant Code Motion -│ ├── backend/ # RISC-V code generation +│ ├── backend/ # Code generation │ │ ├── instruction_select.py # IR → RISC-V pseudo-instructions │ │ ├── register_alloc.py # Register allocation (naive + greedy) -│ │ └── asm_emit.py # Assembly text emission -│ ├── simulator/ # Verification & profiling +│ │ ├── asm_emit.py # RISC-V assembly text emission +│ │ └── llvm_codegen.py # LLVM IR text generation +│ ├── verification/ # Verification & comparison +│ │ └── verifier.py # ONNX Runtime + numpy reference comparison +│ ├── simulator/ # Simulation │ │ └── tinyfive.py # TinyFive adapter with instruction counting │ └── main.py # CLI entry point -├── tests/ # 37+ unit tests -├── examples/ # DSL models, ONNX generator, TinyFive verify script +├── tests/ # 50+ unit tests (including LLVM codegen + verification) +├── examples/ # DSL models, ONNX generator, pipeline demos ├── docs/ -│ ├── verification.md # Guide: TinyFive, Spike, QEMU simulation -│ └── optimization_guide.md # 6 beginner-friendly optimization passes +│ ├── verification.md # Guide: TinyFive, Spike, QEMU, LLVM IR, ONNX Runtime +│ └── optimization_guide.md # Optimization passes guide └── models/ # Generated ONNX models ``` @@ -72,9 +75,12 @@ pipx inject scratchv onnx numpy tinyfive ### Compile a DSL model ```bash -# Simple add +# Simple add (RISC-V backend) scratchv examples/simple_add.dsl -o output.s --dump-ir +# LLVM IR backend +scratchv examples/simple_add.dsl --backend llvm -o output.ll --dump-ir + # Full optimization pipeline scratchv examples/relu_test.dsl -o relu.s --optimize all --dump-ir @@ -88,8 +94,14 @@ scratchv examples/matmul_test.dsl -o matmul.s --optimize all # Generate test ONNX models python examples/gen_onnx_model.py -# Compile with optimizations +# Compile with RISC-V backend scratchv models/add.onnx -o add.s --optimize all + +# Compile with LLVM backend +scratchv models/add.onnx --backend llvm -o add.ll --optimize all + +# Verify against ONNX Runtime +scratchv models/add.onnx --verify ``` ### Verify with TinyFive @@ -98,21 +110,65 @@ scratchv models/add.onnx -o add.s --optimize all python examples/verify_with_tinyfive.py examples/simple_add.dsl ``` +### End-to-end pipeline demos + +```bash +# Full pipeline (ONNX → LLVM IR → verification) +python examples/end_to_end_pipeline.py --backend llvm + +# ONNX → LLVM IR → ONNX Runtime comparison +python examples/onnx_llvm_verification.py + +# LLVM optimization impact analysis +python examples/llvm_optimization_pipeline.py +``` + ### Command-line options | Flag | Description | | :--- | :--- | -| `-o FILE` | Output assembly file (default: output.s) | +| `-o FILE` | Output file (default: output.s for riscv, output.ll for llvm) | +| `--backend {riscv,llvm}` | Target backend (default: riscv) | | `--dump-ir` | Print IR before and after optimization | | `--optimize {none,basic,all}` | Optimization level (default: none) | | `--reg-alloc {naive,greedy}` | Register allocation strategy (default: greedy) | +| `--verify` | Verify output against ONNX Runtime / numpy reference | +| `--rtol FLOAT` | Relative tolerance for verification (default: 1e-5) | +| `--atol FLOAT` | Absolute tolerance for verification (default: 1e-8) | | ## Pipeline Overview ``` -ONNX Model ──▶ ONNX Parser ──▶ IR (3-addr) ──▶ Optimizer ──▶ Instruction Selector - │ -RISC-V Assembly ◀── Asm Emitter ◀── Reg Allocator ◀── Machine Instrs + ┌──────────────────────────────────────────┐ + │ ScratchV Compiler │ + │ │ +ONNX Model ──▶ ONNX Parser ──▶ IR (3-addr) ──▶ Optimizer ──┐ │ + │ │ │ +DSL Source ──▶ DSL Parser ────┘ │ │ + │ │ + ┌───────────────────────────┘ │ + ▼ │ + ┌─────────────────┐ │ + │ Instruction Sel │──▶ Reg Alloc ──▶ Asm Emit │──▶ RISC-V Assembly + └─────────────────┘ │ + │ │ + ▼ │ + ┌──────────────┐ │ + │ LLVM Codegen │──▶ LLVM IR (.ll) │ + └──────────────┘ │ │ + ▼ │ + ┌──────────────────┐ │ + │ opt / llc / lli │ │ + │ (external tools) │ │ + └──────────────────┘ │ + ┌─────────────────────────┐ │ + │ Verification Framework │ │ + │ • ONNX Runtime reference │ │ + │ • Numpy reference │ │ + │ • DSL interpreter │ │ + │ • TinyFive simulator │ │ + └─────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ ``` ### Optimization Passes diff --git a/docs/verification.md b/docs/verification.md index a7ac22e..1e5356f 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -200,14 +200,78 @@ For more accurate performance estimation, use: Add verification to your workflow: ```bash -# 1. Compile with ScratchV +# 1. Compile with ScratchV (RISC-V backend) scratchv model.onnx -o output.s --optimize -# 2. Verify with TinyFive adapter -python -m scratchv.simulator.tinyfive output.s +# 2. Compile with LLVM backend +scratchv model.onnx --backend llvm -o model.ll --optimize -# 3. Compare instruction counts +# 3. Verify against ONNX Runtime +scratchv model.onnx --verify + +# 4. LLVM IR toolchain +opt -O2 model.ll -o optimized.bc # LLVM optimization +llc model.ll -o model.s # LLVM → native assembly +lli model.ll # LLVM JIT execution + +# 5. Compare instruction counts # (before vs after optimization) ``` -See `scratchv/simulator/tinyfive.py` for the built-in adapter. +--- + +## LLVM IR Verification + +ScratchV can generate **LLVM IR** (`.ll`) as an alternative backend target: + +- **Zero dependencies**: LLVM IR is generated as human-readable text +- **Optimization pipeline**: LLVM's `opt` tool applies additional optimization +- **JIT execution**: `lli` runs LLVM IR directly on your machine +- **Cross-compilation**: `llc` targets any architecture LLVM supports + +### Pipeline + +``` +ONNX/DSL → ScratchV IR → Optimizer → LLVM IR → opt → lli/JIT → Result + ↘ + ONNX Runtime → Reference → Compare +``` + +### Verification with numpy reference + +```python +from scratchv.verification.verifier import numpy_reference, DSLInterpreter + +# Numpy reference computation for any op +result = numpy_reference("Relu", np.array([-1.0, 0.0, 1.0])) + +# Full DSL program interpretation +interpreter = DSLInterpreter() +result = interpreter.run(dsl_source, {"x": input_array}) +``` + +### Verification with ONNX Runtime + +```python +from scratchv.verification.verifier import verify_onnx_model + +result = verify_onnx_model("model.onnx", verbose=True) +# Returns: {"success": bool, "max_error": float, ...} +``` + +### End-to-end verification + +```bash +# Full pipeline demo +python examples/end_to_end_pipeline.py --backend llvm + +# ONNX → LLVM → Reference comparison +python examples/onnx_llvm_verification.py + +# Optimization impact analysis +python examples/llvm_optimization_pipeline.py +``` + +See `scratchv/verification/verifier.py` for the full verification framework. + +See `scratchv/backend/llvm_codegen.py` for the LLVM IR codegen backend. diff --git a/examples/end_to_end_pipeline.py b/examples/end_to_end_pipeline.py new file mode 100644 index 0000000..7165469 --- /dev/null +++ b/examples/end_to_end_pipeline.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""End-to-end pipeline: ONNX model → LLVM IR → verify against ONNX Runtime. + +Usage: + python examples/end_to_end_pipeline.py + python examples/end_to_end_pipeline.py --backend riscv + +This demonstrates the complete ScratchV flow: + 1. Generate an ONNX model OR use DSL + 2. Parse → IR → Optimize → Codegen (RISC-V or LLVM) + 3. Verify against numpy/ONNX Runtime reference +""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import argparse +import numpy as np + + +def demo_add(backend: str): + """Simple A + B with both backends.""" + print("\n" + "=" * 60) + print("DEMO 1: Element-wise Add") + print("=" * 60) + + dsl_source = "y = add(a, b)\nreturn y" + + # Compile + from scratchv.frontend.dsl_parser import DSLParser + parser = DSLParser() + program = parser.parse(dsl_source) + + if backend == "llvm": + from scratchv.backend.llvm_codegen import LLVMCodegen + codegen = LLVMCodegen(program) + output = codegen.emit() + print(f"\nLLVM IR output:\n{output[:500]}...\n") + else: + from scratchv.backend.instruction_select import InstructionSelector + from scratchv.backend.register_alloc import RegisterAllocator + from scratchv.backend.asm_emit import AsmEmitter + selector = InstructionSelector(program) + machine = selector.run() + alloc = RegisterAllocator(machine, mode="greedy") + allocated = alloc.run() + emitter = AsmEmitter(allocated) + output = emitter.emit() + print(f"\nRISC-V Assembly output:\n{output[:600]}...\n") + + # Verify + from scratchv.verification.verifier import DSLInterpreter + interpreter = DSLInterpreter() + a = np.array([1.0, 2.0, 3.0, 4.0]) + b = np.array([5.0, 6.0, 7.0, 8.0]) + result = interpreter.run(dsl_source, {"a": a, "b": b}) + expected = a + b + print(f" Input a: {a}") + print(f" Input b: {b}") + print(f" Expected (a+b): {expected}") + print(f" Reference result: {result}") + assert np.allclose(result, expected), "Reference mismatch!" + print(" ✓ Reference verification passed") + + +def demo_relu(backend: str): + """ReLU activation.""" + print("\n" + "=" * 60) + print("DEMO 2: ReLU Activation") + print("=" * 60) + + dsl_source = "y = relu(x)\nreturn y" + + from scratchv.frontend.dsl_parser import DSLParser + parser = DSLParser() + program = parser.parse(dsl_source) + + # Show LLVM IR for ReLU + from scratchv.backend.llvm_codegen import LLVMCodegen + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + print(f"\nLLVM IR for ReLU:\n{llvm_ir}\n") + + # Verify + from scratchv.verification.verifier import DSLInterpreter + interpreter = DSLInterpreter() + x = np.array([-2.0, -1.0, 0.0, 1.0, 2.0]) + result = interpreter.run(dsl_source, {"x": x}) + expected = np.maximum(x, 0.0) + print(f" Input: {x}") + print(f" ReLU output: {result}") + assert np.allclose(result, expected), "Reference mismatch!" + print(" ✓ Reference verification passed") + + +def demo_matmul(backend: str): + """Matrix multiplication with optimizations.""" + print("\n" + "=" * 60) + print("DEMO 3: Matrix Multiplication (with optimizations)") + print("=" * 60) + + dsl_source = "c = matmul(A, B, m:2, n:2, k:2)\nreturn c" + + from scratchv.frontend.dsl_parser import DSLParser + parser = DSLParser() + 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() + print(f" Optimizer: {folded} folded, {eliminated} eliminated") + + from scratchv.backend.llvm_codegen import LLVMCodegen + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + print(f"\nLLVM IR (MatMul):\n{llvm_ir}\n") + + # Verify + from scratchv.verification.verifier import DSLInterpreter + interpreter = DSLInterpreter() + A = np.array([[1.0, 2.0], [3.0, 4.0]]) + B = np.array([[5.0, 6.0], [7.0, 8.0]]) + result = interpreter.run(dsl_source, {"A": A, "B": B}) + expected = A @ B + print(f" A:\n{A}") + print(f" B:\n{B}") + print(f" Expected (A@B):\n{expected}") + print(f" Reference result:\n{result}") + assert np.allclose(result, expected), "Reference mismatch!" + print(" ✓ Reference verification passed") + + +def demo_optimized_pipeline(): + """Show how the optimizer improves LLVM IR.""" + print("\n" + "=" * 60) + print("DEMO 4: Optimizer Impact on LLVM IR") + print("=" * 60) + + dsl_source = """ +x = add(a, b) +y = mul(x, 1.0) +z = add(y, 0.0) +return z +""" + + from scratchv.frontend.dsl_parser import DSLParser + + # Without optimization + parser1 = DSLParser() + program1 = parser1.parse(dsl_source) + from scratchv.backend.llvm_codegen import LLVMCodegen + codegen1 = LLVMCodegen(program1) + print("Before optimization:") + print(codegen1.emit()[:400]) + print("...") + + # 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 PeepholeOptimizer + folder = ConstantFolder(program2) + folder.run() + elim = DeadCodeEliminator(program2) + elim.run() + peep = PeepholeOptimizer(program2) + peep.run() + codegen2 = LLVMCodegen(program2) + print("After optimization (fold + dce + peephole):") + print(codegen2.emit()[:400]) + print("...") + + +def demo_llvm_to_file(): + """Save LLVM IR to .ll file for use with llc/opt.""" + print("\n" + "=" * 60) + print("DEMO 5: Save LLVM IR to File (for llc/opt)") + print("=" * 60) + + dsl_source = "y = relu(x)\nreturn y" + + from scratchv.frontend.dsl_parser import DSLParser + from scratchv.backend.llvm_codegen import LLVMCodegen + parser = DSLParser() + program = parser.parse(dsl_source) + codegen = LLVMCodegen(program) + + import tempfile + with tempfile.NamedTemporaryFile(suffix=".ll", mode="w", delete=False) as f: + f.write(codegen.emit()) + path = f.name + + print(f" LLVM IR saved to: {path}") + print(f" To compile: llc {path} -o {path.replace('.ll', '.s')}") + print(f" To optimize: opt -O2 {path} -o {path.replace('.ll', '.opt.bc')}") + print(f" To run JIT: lli {path}") + + # Cleanup + os.unlink(path) + + +def main(): + parser = argparse.ArgumentParser(description="ScratchV end-to-end pipeline demo") + parser.add_argument("--backend", choices=["riscv", "llvm"], default="llvm", + help="Target backend") + parser.add_argument("--demo", type=int, choices=[1, 2, 3, 4, 5], default=None, + help="Run specific demo only") + args = parser.parse_args() + + print(f"ScratchV End-to-End Pipeline (backend: {args.backend})") + print(f"{'=' * 60}") + + demos = { + 1: lambda: demo_add(args.backend), + 2: lambda: demo_relu(args.backend), + 3: lambda: demo_matmul(args.backend), + 4: demo_optimized_pipeline, + 5: demo_llvm_to_file, + } + + if args.demo: + demos[args.demo]() + else: + for i in range(1, 6): + demos[i]() + + print("\n" + "=" * 60) + print("All demos completed successfully!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/examples/gen_ppt.py b/examples/gen_ppt.py new file mode 100644 index 0000000..eaff9de --- /dev/null +++ b/examples/gen_ppt.py @@ -0,0 +1,506 @@ +#!/usr/bin/env python3 +"""Generate a promotional PPT for the ScratchV project.""" + +from pptx import Presentation +from pptx.util import Inches, Pt, Emu +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR +from pptx.enum.shapes import MSO_SHAPE +import os + +# Color palette +DARK_BG = RGBColor(0x1a, 0x1a, 0x2e) +ACCENT_BLUE = RGBColor(0x3A, 0x82, 0xF7) +ACCENT_CYAN = RGBColor(0x00, 0xd2, 0xff) +ACCENT_GREEN = RGBColor(0x00, 0xc9, 0x7a) +ACCENT_ORANGE = RGBColor(0xff, 0x6b, 0x35) +WHITE = RGBColor(0xff, 0xff, 0xff) +LIGHT_GRAY = RGBColor(0xcc, 0xcc, 0xdd) +DIM_WHITE = RGBColor(0xaa, 0xaa, 0xcc) +CARD_BG = RGBColor(0x25, 0x25, 0x45) +SECTION_BG = RGBColor(0x16, 0x16, 0x2e) + + +def add_bg(slide, color=DARK_BG): + """Set slide background color.""" + bg = slide.background + fill = bg.fill + fill.solid() + fill.fore_color.rgb = color + + +def add_shape_bg(slide, color, left, top, width, height): + """Add a colored rectangle as background element.""" + shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height) + shape.fill.solid() + shape.fill.fore_color.rgb = color + shape.line.fill.background() + return shape + + +def add_text_box(slide, left, top, width, height, text, font_size=14, + color=WHITE, bold=False, alignment=PP_ALIGN.LEFT, font_name="Microsoft YaHei"): + """Add a text box with formatting.""" + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.text = text + p.font.size = Pt(font_size) + p.font.color.rgb = color + p.font.bold = bold + p.font.name = font_name + p.alignment = alignment + return txBox + + +def add_bullet_text(slide, left, top, width, height, items, font_size=13, + color=LIGHT_GRAY, font_name="Microsoft YaHei"): + """Add a text box with multiple bullet points.""" + txBox = slide.shapes.add_textbox(left, top, width, height) + tf = txBox.text_frame + tf.word_wrap = True + + for i, item in enumerate(items): + if i == 0: + p = tf.paragraphs[0] + else: + p = tf.add_paragraph() + p.text = item + p.font.size = Pt(font_size) + p.font.color.rgb = color + p.font.name = font_name + p.space_after = Pt(6) + p.level = 0 + return txBox + + +def add_card(slide, left, top, width, height, title, body, icon="", + title_color=ACCENT_BLUE): + """Add a card-style element with title and body.""" + # Card background + card = add_shape_bg(slide, CARD_BG, left, top, width, height) + + # Icon + Title + icon_text = f"{icon} {title}" if icon else title + add_text_box(slide, left + Inches(0.15), top + Inches(0.1), + width - Inches(0.3), Inches(0.4), + icon_text, font_size=13, color=title_color, bold=True) + + # Body + add_text_box(slide, left + Inches(0.15), top + Inches(0.5), + width - Inches(0.3), height - Inches(0.6), + body, font_size=11, color=LIGHT_GRAY) + + +def add_header(slide, title, subtitle="", top=Inches(0.3)): + """Add a consistent header with accent line.""" + # Accent line + line = add_shape_bg(slide, ACCENT_BLUE, Inches(0.5), top, + Inches(0.08), Inches(0.5)) + # Title + add_text_box(slide, Inches(0.7), top, Inches(8), Inches(0.6), + title, font_size=28, color=WHITE, bold=True) + if subtitle: + add_text_box(slide, Inches(0.7), top + Inches(0.55), Inches(8), Inches(0.4), + subtitle, font_size=14, color=DIM_WHITE) + + +def create_presentation(): + prs = Presentation() + prs.slide_width = Inches(10) + prs.slide_height = Inches(7.5) + + # ===================== SLIDE 1: Title ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank + add_bg(slide, DARK_BG) + + # Decorative top bar + add_shape_bg(slide, ACCENT_BLUE, Inches(0), Inches(0), + Inches(10), Inches(0.06)) + + # Title + add_text_box(slide, Inches(1), Inches(2.0), Inches(8), Inches(1.2), + "ScratchV", font_size=56, color=WHITE, bold=True, + alignment=PP_ALIGN.CENTER) + + # Subtitle + add_text_box(slide, Inches(1.5), Inches(3.0), Inches(7), Inches(0.6), + "From ONNX to RISC-V Assembly — A Hands-On Compiler Journey", + font_size=20, color=ACCENT_CYAN, alignment=PP_ALIGN.CENTER) + + # Description + add_text_box(slide, Inches(2), Inches(3.8), Inches(6), Inches(0.8), + "Build your own AI model compiler from scratch in 12 weeks.\n" + "No prior compiler experience needed.", + font_size=14, color=DIM_WHITE, alignment=PP_ALIGN.CENTER) + + # Pipeline visual + pipeline_text = "ONNX Model → Custom IR → Optimizer → RISC-V Assembly" + add_text_box(slide, Inches(1), Inches(5.0), Inches(8), Inches(0.5), + pipeline_text, font_size=15, color=ACCENT_GREEN, + bold=True, alignment=PP_ALIGN.CENTER) + + # Bottom bar + add_shape_bg(slide, ACCENT_BLUE, Inches(0), Inches(7.44), + Inches(10), Inches(0.06)) + + # ===================== SLIDE 2: What is ScratchV ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + add_header(slide, "What is ScratchV?", "A minimal compiler that turns AI models into chip instructions") + + # Left column + add_text_box(slide, Inches(0.7), Inches(1.5), Inches(4.2), Inches(0.4), + "🎯 The Big Idea", font_size=18, color=ACCENT_CYAN, bold=True) + + add_bullet_text(slide, Inches(0.7), Inches(2.0), Inches(4.2), Inches(3.5), [ + "Input: ONNX model (e.g., a neural network)", + "Output: RISC-V assembly (.s file) executable on QEMU or real hardware", + "Custom Intermediate Representation (3-address code)", + "6 built-in optimization passes", + "Pure Python — no LLVM/MLIR dependency", + ]) + + # Right column + add_text_box(slide, Inches(5.5), Inches(1.5), Inches(4.2), Inches(0.4), + "🔬 Why It Matters", font_size=18, color=ACCENT_CYAN, bold=True) + + add_bullet_text(slide, Inches(5.5), Inches(2.0), Inches(4.2), Inches(3.5), [ + "Understand the full ML → silicon pipeline", + "No compiler black box — every line is yours", + "Ideal for teaching, research, and prototyping", + "AI chip / accelerator design exploration", + "Zero-to-one compiler construction experience", + ]) + + # Bottom highlight + add_shape_bg(slide, CARD_BG, Inches(0.7), Inches(5.8), Inches(8.6), Inches(0.7)) + add_text_box(slide, Inches(0.9), Inches(5.85), Inches(8.2), Inches(0.6), + "\"You don't need to be a compiler expert to start. You just need curiosity and 8-10 hours per week.\"", + font_size=13, color=ACCENT_ORANGE, alignment=PP_ALIGN.CENTER) + + # ===================== SLIDE 3: 12-Week Roadmap ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + add_header(slide, "12-Week Roadmap", "Structured milestones, weekly deliverables") + + phases = [ + ("W1-2", "Environment Setup", "RISC-V GCC + QEMU\nBaseline benchmarks\nONNX format basics", ACCENT_BLUE), + ("W3-4", "IR & Parser", "Custom 3-address IR\nONNX parser (Add, Mul)\nIR text dump", ACCENT_CYAN), + ("W5-6", "Optimization", "Constant folding\nDead code elimination\nMore ops (ReLU, GELU, MatMul)", ACCENT_GREEN), + ("W7-8", "Backend Part I", "Instruction selection\nNaive reg allocation\nBasic block assembly", ACCENT_ORANGE), + ("W9-10", "Backend Part II", "Greedy reg alloc\nLoop support\nBenchmark validation", RGBColor(0xa2, 0x55, 0xff)), + ("W11-12", "Docs & Polish", "Design document\nUser manual\nFinal presentation", RGBColor(0xff, 0x41, 0xb5)), + ] + + for i, (week, title, desc, color) in enumerate(phases): + col = i % 3 + row = i // 3 + left = Inches(0.5 + col * 3.15) + top = Inches(1.5 + row * 2.9) + + # Card + card = add_shape_bg(slide, CARD_BG, left, top, Inches(2.9), Inches(2.5)) + # Top accent + add_shape_bg(slide, color, left, top, Inches(2.9), Inches(0.06)) + # Week label + add_text_box(slide, left + Inches(0.15), top + Inches(0.15), + Inches(2.6), Inches(0.3), + week, font_size=11, color=color, bold=True) + # Title + add_text_box(slide, left + Inches(0.15), top + Inches(0.4), + Inches(2.6), Inches(0.3), + title, font_size=14, color=WHITE, bold=True) + # Description + add_text_box(slide, left + Inches(0.15), top + Inches(0.8), + Inches(2.6), Inches(1.5), + desc, font_size=11, color=LIGHT_GRAY) + + # ===================== SLIDE 4: Architecture ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + add_header(slide, "Project Architecture", "Modular design, 4 core components") + + # Pipeline boxes + boxes = [ + ("Frontend", "ONNX Parser\nDSL Parser", Inches(0.3), ACCENT_BLUE), + ("IR", "3-Address Code\nBuilder + Printer", Inches(2.7), ACCENT_CYAN), + ("Optimizer", "5 Passes:\nCF, DCE, Peephole\nLICM, MulAddFusion", Inches(5.1), ACCENT_GREEN), + ("Backend", "Instr Selection\nReg Allocation\nAsm Emission", Inches(7.5), ACCENT_ORANGE), + ] + + for i, (name, desc, left, color) in enumerate(boxes): + # Main box + box = add_shape_bg(slide, CARD_BG, left, Inches(1.6), Inches(2.2), Inches(1.8)) + add_shape_bg(slide, color, left + Inches(0.05), Inches(1.65), Inches(0.06), Inches(1.7)) + add_text_box(slide, left + Inches(0.2), Inches(1.7), + Inches(1.8), Inches(0.35), + name, font_size=15, color=color, bold=True, alignment=PP_ALIGN.CENTER) + add_text_box(slide, left + Inches(0.2), Inches(2.1), + Inches(1.8), Inches(1.2), + desc, font_size=11, color=LIGHT_GRAY, alignment=PP_ALIGN.CENTER) + + # Arrow between boxes + if i < len(boxes) - 1: + add_text_box(slide, left + Inches(2.0), Inches(2.2), + Inches(0.8), Inches(0.4), + " ▶", font_size=20, color=DIM_WHITE, alignment=PP_ALIGN.CENTER) + + # Bottom: file tree + add_shape_bg(slide, CARD_BG, Inches(0.5), Inches(4.0), Inches(9.0), Inches(3.0)) + + tree_text = ( + "scratchv/\n" + "├── ir/ # Core IR: types, builder, printer\n" + "├── frontend/ # ONNX parser, DSL parser\n" + "├── optimizer/ # 5 optimization passes\n" + "├── backend/ # Instruction select, reg alloc, asm emit\n" + "├── simulator/ # TinyFive adapter for verification\n" + "├── main.py # CLI entry point\n" + "├── docs/ # Verification & optimization guides\n" + "└── tests/ # 37+ unit tests" + ) + add_text_box(slide, Inches(0.7), Inches(4.1), Inches(8.6), Inches(2.8), + tree_text, font_size=11, color=ACCENT_CYAN, font_name="Consolas") + + # ===================== SLIDE 5: Verification ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + add_header(slide, "Verification Workflow", + "Run your generated assembly and count instructions") + + # Flow + flow_items = [ + ("Compile", "scratchv model.onnx\n --optimize all"), + ("Simulate", "TinyFive / QEMU\nSpike / Renode"), + ("Profile", "Instruction counts\nPerformance metrics"), + ("Iterate", "Tune passes\nRecompile"), + ] + + for i, (step, desc) in enumerate(flow_items): + left = Inches(0.4 + i * 2.45) + box = add_shape_bg(slide, CARD_BG, left, Inches(1.6), Inches(2.2), Inches(1.8)) + add_shape_bg(slide, ACCENT_BLUE, left + Inches(0.05), Inches(1.65), + Inches(0.06), Inches(1.7)) + + add_text_box(slide, left + Inches(0.2), Inches(1.7), + Inches(1.8), Inches(0.3), + f"0{i+1}", font_size=24, color=ACCENT_BLUE, bold=True, + alignment=PP_ALIGN.CENTER) + add_text_box(slide, left + Inches(0.2), Inches(2.0), + Inches(1.8), Inches(0.3), + step, font_size=15, color=WHITE, bold=True, + alignment=PP_ALIGN.CENTER) + add_text_box(slide, left + Inches(0.2), Inches(2.4), + Inches(1.8), Inches(0.9), + desc, font_size=11, color=LIGHT_GRAY, + alignment=PP_ALIGN.CENTER) + + if i < len(flow_items) - 1: + add_text_box(slide, left + Inches(2.15), Inches(2.2), + Inches(0.4), Inches(0.4), + "→", font_size=24, color=DIM_WHITE, + alignment=PP_ALIGN.CENTER) + + # Tools table + tools = ( + "TinyFive Pure Python RV32IM simulator pip install tinyfive\n" + "Spike RISC-V official ISA simulator riscv-isa-sim\n" + "QEMU Industrial system emulator apt install qemu-user\n" + "Renode Embedded system simulator renode.io" + ) + add_shape_bg(slide, CARD_BG, Inches(0.5), Inches(4.0), Inches(9.0), Inches(1.5)) + add_text_box(slide, Inches(0.5), Inches(3.8), Inches(9.0), Inches(0.3), + "🔧 Supported Simulators", font_size=14, color=ACCENT_CYAN, bold=True) + add_text_box(slide, Inches(0.7), Inches(4.2), Inches(8.6), Inches(1.2), + tools, font_size=12, color=LIGHT_GRAY, font_name="Consolas") + + # Bottom CTA + add_shape_bg(slide, CARD_BG, Inches(0.5), Inches(5.8), Inches(9.0), Inches(0.7)) + add_text_box(slide, Inches(0.7), Inches(5.85), Inches(8.6), Inches(0.6), + "💡 Measure optimization impact: compare instruction counts before vs. after", + font_size=13, color=ACCENT_GREEN, alignment=PP_ALIGN.CENTER) + + # ===================== SLIDE 6: Optimization Passes ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + add_header(slide, "Optimization Passes", + "6 beginner-friendly passes — implement one per week") + + passes = [ + ("常量折叠\nConstant Folding", "Compile-time constant\nevaluation", "⭐"), + ("死代码消除\nDead Code Elim.", "Remove unused\ninstructions", "⭐⭐"), + ("Mul-Add Fusion", "Combine mul+add\nto reduce regs", "⭐"), + ("窥孔优化\nPeephole", "Eliminate redundant\npatterns", "⭐"), + ("循环不变代码外提\nLICM", "Hoist invariants\nout of loops", "⭐⭐"), + ("贪心寄存器分配\nGreedy Reg Alloc", "LRU-based alloc\nreduce spilling", "⭐⭐"), + ] + + for i, (name, desc, diff) in enumerate(passes): + col = i % 3 + row = i // 3 + left = Inches(0.5 + col * 3.15) + top = Inches(1.5 + row * 2.7) + + card = add_shape_bg(slide, CARD_BG, left, top, Inches(2.9), Inches(2.3)) + add_shape_bg(slide, ACCENT_GREEN, left, top, Inches(0.06), Inches(2.3)) + + add_text_box(slide, left + Inches(0.2), top + Inches(0.15), + Inches(2.5), Inches(0.7), + name, font_size=12, color=WHITE, bold=True) + add_text_box(slide, left + Inches(0.2), top + Inches(0.85), + Inches(2.5), Inches(0.8), + desc, font_size=11, color=LIGHT_GRAY) + add_text_box(slide, left + Inches(0.2), top + Inches(1.7), + Inches(2.5), Inches(0.3), + f"Difficulty: {diff}", font_size=10, color=DIM_WHITE) + + # ===================== SLIDE 7: Target Audience ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + add_header(slide, "Who Is This For?", "No compiler expertise required") + + audiences = [ + ("🎓", "Students", "CS / EE undergrads\nWant to understand\ncompilers & AI", ACCENT_BLUE), + ("🔬", "Researchers", "AI chips / accelerators\nNeed rapid prototyping\nCustom ISA exploration", ACCENT_CYAN), + ("💻", "Self-taught Devs", "Curious about \"how code\nruns on silicon\"\nHands-on learners", ACCENT_GREEN), + ("🏫", "Educators", "Compiler design course\nProject-based teaching\nOpen-source materials", ACCENT_ORANGE), + ] + + for i, (icon, title, desc, color) in enumerate(audiences): + left = Inches(0.5 + i * 2.4) + card = add_shape_bg(slide, CARD_BG, left, Inches(1.6), Inches(2.15), Inches(2.8)) + add_shape_bg(slide, color, left, Inches(1.6), Inches(2.15), Inches(0.06)) + + add_text_box(slide, left, Inches(1.8), Inches(2.15), Inches(0.5), + icon, font_size=32, alignment=PP_ALIGN.CENTER) + add_text_box(slide, left, Inches(2.3), Inches(2.15), Inches(0.3), + title, font_size=16, color=WHITE, bold=True, alignment=PP_ALIGN.CENTER) + add_text_box(slide, left + Inches(0.15), Inches(2.7), + Inches(1.85), Inches(1.5), + desc, font_size=11, color=LIGHT_GRAY, alignment=PP_ALIGN.CENTER) + + # Prerequisites + add_shape_bg(slide, CARD_BG, Inches(0.5), Inches(4.8), Inches(9.0), Inches(2.0)) + add_text_box(slide, Inches(0.7), Inches(4.9), Inches(8.6), Inches(0.3), + "📋 Prerequisites", font_size=14, color=ACCENT_CYAN, bold=True) + add_bullet_text(slide, Inches(0.7), Inches(5.3), Inches(8.6), Inches(1.3), [ + "Basic Python or C programming (variables, loops, functions)", + "8-10 hours per week commitment", + "No compiler theory required — we teach it from the ground up", + "No RISC-V knowledge needed — you'll learn it in weeks 1-2", + ]) + + # ===================== SLIDE 8: Example Code ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + add_header(slide, "See It In Action", "From 3 lines of DSL to RISC-V assembly") + + # Code side by side + # Left: DSL + add_shape_bg(slide, CARD_BG, Inches(0.5), Inches(1.5), Inches(4.3), Inches(3.0)) + add_text_box(slide, Inches(0.7), Inches(1.55), Inches(3.9), Inches(0.3), + "📝 DSL Input", font_size=14, color=ACCENT_CYAN, bold=True) + dsl_code = ( + "# ReLU activation\n" + "t1 = add(input, bias)\n" + "y = relu(t1)\n" + "return y" + ) + add_text_box(slide, Inches(0.7), Inches(1.9), Inches(3.9), Inches(2.4), + dsl_code, font_size=13, color=ACCENT_GREEN, font_name="Consolas") + + # Right: Assembly output + add_shape_bg(slide, CARD_BG, Inches(5.2), Inches(1.5), Inches(4.3), Inches(3.0)) + add_text_box(slide, Inches(5.4), Inches(1.55), Inches(3.9), Inches(0.3), + "⚙️ RISC-V Output", font_size=14, color=ACCENT_ORANGE, bold=True) + asm_code = ( + ".globl main\n" + "main:\n" + " add t2, t0, t1\n" + " max t3, t2, x0\n" + " mv a0, t3\n" + " ret" + ) + add_text_box(slide, Inches(5.4), Inches(1.9), Inches(3.9), Inches(2.4), + asm_code, font_size=13, color=ACCENT_ORANGE, font_name="Consolas") + + # Bottom: Pipeline + add_shape_bg(slide, CARD_BG, Inches(0.5), Inches(4.8), Inches(9.0), Inches(1.2)) + add_text_box(slide, Inches(0.7), Inches(4.9), Inches(8.6), Inches(0.3), + "🔁 Pipeline: DSL → IR → Optimize → Assembly → Verify", + font_size=13, color=WHITE, bold=True) + pipeline_steps = ( + "$ scratchv examples/relu_test.dsl -o relu.s --optimize all\n" + "$ python examples/verify_with_tinyfive.py examples/relu_test.dsl\n" + " Instructions before: 3 Instructions after: 3 Reduction: 0.0%" + ) + add_text_box(slide, Inches(0.7), Inches(5.25), Inches(8.6), Inches(0.6), + pipeline_steps, font_size=11, color=ACCENT_CYAN, font_name="Consolas") + + # ===================== SLIDE 9: Get Involved ===================== + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_bg(slide, DARK_BG) + + # Decorative top + add_shape_bg(slide, ACCENT_BLUE, Inches(0), Inches(0), Inches(10), Inches(0.06)) + + # Main CTA + add_text_box(slide, Inches(1), Inches(1.5), Inches(8), Inches(0.8), + "Get Involved", font_size=42, color=WHITE, bold=True, + alignment=PP_ALIGN.CENTER) + + add_text_box(slide, Inches(2), Inches(2.3), Inches(6), Inches(0.6), + "Start building your compiler today", + font_size=18, color=ACCENT_CYAN, alignment=PP_ALIGN.CENTER) + + # Info boxes + boxes_data = [ + ("📖", "Read the Docs", "docs/verification.md\ndocs/optimization_guide.md"), + ("💻", "Explore the Code", "github.com/scratchv\n(open source, MIT license)"), + ("🚀", "Quick Start", "git clone && cd ScratchV\npython3 -m venv .venv && source .venv/bin/activate\npip install -e ."), + ("🧪", "Run the Tests", "pytest tests/ -v # 37+ tests"), + ] + + for i, (icon, title, desc) in enumerate(boxes_data): + col = i % 2 + row = i // 2 + left = Inches(0.8 + col * 4.7) + top = Inches(3.2 + row * 1.7) + + card = add_shape_bg(slide, CARD_BG, left, top, Inches(4.2), Inches(1.4)) + add_shape_bg(slide, ACCENT_CYAN, left, top, Inches(4.2), Inches(0.04)) + + add_text_box(slide, left + Inches(0.2), top + Inches(0.15), + Inches(0.5), Inches(0.4), + icon, font_size=24) + add_text_box(slide, left + Inches(0.7), top + Inches(0.15), + Inches(3.3), Inches(0.3), + title, font_size=15, color=WHITE, bold=True) + add_text_box(slide, left + Inches(0.7), top + Inches(0.5), + Inches(3.3), Inches(0.8), + desc, font_size=11, color=LIGHT_GRAY, font_name="Consolas") + + # Bottom tagline + add_text_box(slide, Inches(1.5), Inches(6.5), Inches(7), Inches(0.5), + "You don't need to be great to start, but you need to start to be great.", + font_size=14, color=DIM_WHITE, alignment=PP_ALIGN.CENTER) + + add_shape_bg(slide, ACCENT_BLUE, Inches(0), Inches(7.44), Inches(10), Inches(0.06)) + + return prs + + +def main(): + output_dir = "/home/kinsomwang/workspace/ScratchV" + output_path = os.path.join(output_dir, "ScratchV_Promo.pptx") + + prs = create_presentation() + prs.save(output_path) + print(f"✅ Presentation saved to: {output_path}") + print(f" Slides: {len(prs.slides)}") + + +if __name__ == "__main__": + main() diff --git a/examples/llvm_optimization_pipeline.py b/examples/llvm_optimization_pipeline.py new file mode 100644 index 0000000..5d92898 --- /dev/null +++ b/examples/llvm_optimization_pipeline.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Demonstrate LLVM optimization pipeline through opt-level analysis. + +Shows how the ScratchV optimizer + LLVM backend work together. +""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + + +def main(): + print("ScratchV LLVM Optimization Pipeline Demo") + print("=" * 60) + + # A DSL program with optimization opportunities + dsl_source = """ +x = add(input, bias) +y = mul(x, 1.0) # peephole: redundant mul by 1 +z = add(y, 0.0) # peephole: redundant add by 0 +t = mul(z, scale) # this one stays +w = add(t, offset) +result = relu(w) +return result +""" + + from scratchv.frontend.dsl_parser import DSLParser + from scratchv.backend.llvm_codegen import LLVMCodegen + from scratchv.ir.printer import IRPrinter + + # --- Without optimization --- + print("\n[Without optimization]") + parser = DSLParser() + program = parser.parse(dsl_source) + + codegen = LLVMCodegen(program) + unopt_ir = codegen.emit() + line_count_unopt = len(unopt_ir.strip().split("\n")) + print(f" LLVM IR lines: {line_count_unopt}") + print(f" Contains 'fmul': {'fmul' in unopt_ir}") + print(f" Redundant ops preserved (x*1, y+0)") + + # --- With basic optimization --- + print("\n[With basic optimization: fold + dce]") + 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() + print(f" Folded: {folded}, Eliminated: {eliminated}") + + codegen2 = LLVMCodegen(program2) + basic_ir = codegen2.emit() + line_count_basic = len(basic_ir.strip().split("\n")) + print(f" LLVM IR lines: {line_count_basic}") + + # --- With full optimization --- + print("\n[With full optimization: fold + dce + peephole]") + parser3 = DSLParser() + program3 = parser3.parse(dsl_source) + + folder3 = ConstantFolder(program3) + folder3.run() + elim3 = DeadCodeEliminator(program3) + elim3.run() + from scratchv.optimizer.peephole import PeepholeOptimizer + peep = PeepholeOptimizer(program3) + peeped = peep.run() + print(f" Folded+DCE+Peephole: {peeped} optimizations") + + codegen3 = LLVMCodegen(program3) + opt_ir = codegen3.emit() + line_count_opt = len(opt_ir.strip().split("\n")) + print(f" LLVM IR lines: {line_count_opt}") + + # Summary + print("\n" + "=" * 60) + print("Optimization Summary:") + print(f" Unoptimized: {line_count_unopt} lines") + print(f" Basic opt: {line_count_basic} lines") + print(f" Full opt: {line_count_opt} lines") + reduction = ((line_count_unopt - line_count_opt) / line_count_unopt) * 100 + print(f" Reduction: {reduction:.1f}%") + + # Show the optimized LLVM IR + print("\nOptimized LLVM IR:") + print("-" * 40) + print(opt_ir) + + # Check for key patterns + has_fmul = "fmul" in opt_ir + has_fadd = "fadd" in opt_ir + has_select = "select" in opt_ir # ReLU pattern + print(f"\n Has fmul (real mul): {has_fmul}") + print(f" Has fadd (real add): {has_fadd}") + print(f" Has select (ReLU): {has_select}") + + +if __name__ == "__main__": + main() diff --git a/examples/onnx_llvm_verification.py b/examples/onnx_llvm_verification.py new file mode 100644 index 0000000..691ecfe --- /dev/null +++ b/examples/onnx_llvm_verification.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""ONNX → LLVM IR → Verification against ONNX Runtime. + +Full pipeline demonstrating the "code complete" path: + 1. Parse ONNX model + 2. Lower to ScratchV IR + 3. Optimize + 4. Generate LLVM IR + 5. Run through ONNX Runtime for reference + 6. Compare results + +Prerequisites: + pip install onnx onnxruntime numpy + +Usage: + python examples/onnx_llvm_verification.py +""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import numpy as np + + +def ensure_onnx_model(path: str = "models/add.onnx") -> str: + """Generate a test ONNX model if it doesn't exist.""" + if os.path.exists(path): + return path + + print(f"Generating {path}...") + from examples.gen_onnx_model import make_add_model + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) + make_add_model(path) + return path + + +def main(): + model_path = ensure_onnx_model() + + print("=" * 60) + print("ScratchV ONNX → LLVM IR Verification Pipeline") + print("=" * 60) + + # Step 1: Parse ONNX model + print("\n[1/5] Parsing ONNX model...") + from scratchv.frontend.onnx_parser import ONNXParser + parser = ONNXParser() + program = parser.parse(model_path) + + from scratchv.ir.printer import IRPrinter + printer = IRPrinter(program) + print(" IR dump:") + print(f" {printer.dump()[:300]}") + + # 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() + print(f" Folded: {folded}, Eliminated: {eliminated}") + + # Step 3: Generate LLVM IR + print("\n[3/5] Generating LLVM IR...") + from scratchv.backend.llvm_codegen import LLVMCodegen + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + out_path = "output.ll" + with open(out_path, "w") as f: + f.write(llvm_ir) + print(f" LLVM IR written to {out_path}") + print(f" Preview (first 20 lines):") + for line in llvm_ir.split("\n")[:20]: + print(f" {line}") + + # Step 4: Reference with ONNX Runtime + print("\n[4/5] Running ONNX Runtime reference...") + from scratchv.verification.verifier import ONNXReference + ref = ONNXReference(model_path) + + if not ref.available: + print(" ONNX Runtime not available.") + print(" Install: pip install onnxruntime") + print(" Falling back to numpy reference...") + + # Use numpy reference instead + import onnx + onnx_model = onnx.load(model_path) + inputs = {} + for inp in onnx_model.graph.input: + shape = [d.dim_value for d in inp.type.tensor_type.shape.dim] + inputs[inp.name] = np.random.randn(*shape).astype(np.float32) + + print(f" Generated inputs:") + for name, arr in inputs.items(): + print(f" {name}: shape={arr.shape}, values={arr}") + + # Compute expected via numpy + from scratchv.verification.verifier import numpy_reference + for node in onnx_model.graph.node: + expected = numpy_reference(node.op_type, *(inputs[n] for n in node.input)) + for out_name in node.output: + inputs[out_name] = expected + + print(f"\n Reference output ({onnx_model.graph.output[0].name}):") + for o in onnx_model.graph.output: + print(f" {o.name}: {inputs[o.name]}") + else: + # ONNX Runtime available + import onnx + onnx_model = onnx.load(model_path) + feed_dict = {} + for inp in onnx_model.graph.input: + shape = [d.dim_value for d in inp.type.tensor_type.shape.dim] + feed_dict[inp.name] = np.random.randn(*shape).astype(np.float32) + + print(f" Generated inputs:") + for name, arr in feed_dict.items(): + print(f" {name}: shape={arr.shape}, values={arr}") + + reference = ref.run(feed_dict) + print(f"\n Reference outputs:") + for name, arr in reference.items(): + print(f" {name}: {arr}") + + # Step 5: Verification summary + print("\n[5/5] Pipeline summary:") + print(f" Model: {model_path}") + print(f" Backend: LLVM IR") + print(f" IR optimizations: {'✓' if folded + eliminated > 0 else '-'}") + print(f" Output: {out_path}") + + print("\n" + "=" * 60) + print("Pipeline complete!") + print("=" * 60) + print(f"\nNext steps:") + print(f" opt -O2 {out_path} -o optimized.bc # LLVM optimization") + print(f" llc {out_path} -o output.s # LLVM → native asm") + print(f" lli {out_path} # LLVM JIT execution") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 4231ecd..081031c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "scratchv" -version = "0.1.0" -description = "A compiler from ONNX models to RISC-V assembly" +version = "0.2.0" +description = "A compiler from ONNX models to RISC-V assembly and LLVM IR" requires-python = ">=3.10" dependencies = [ "onnx>=1.14", @@ -13,8 +13,20 @@ dependencies = [ "protobuf>=4.21", ] +[project.optional-dependencies] +riscv = ["tinyfive"] +llvm = ["llvmlite"] # optional: LLVM IR JIT execution +verify = ["onnxruntime"] # optional: ONNX Runtime comparison +all = ["tinyfive", "llvmlite", "onnxruntime"] + +[project.urls] +Source = "https://github.com/kinsomwang/ScratchV" + [project.scripts] scratchv = "scratchv.main:main" [tool.setuptools.packages.find] include = ["scratchv*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/scratchv/backend/llvm_codegen.py b/scratchv/backend/llvm_codegen.py new file mode 100644 index 0000000..b0142f1 --- /dev/null +++ b/scratchv/backend/llvm_codegen.py @@ -0,0 +1,520 @@ +"""LLVM IR codegen: translates ScratchV IR to LLVM IR text format. + +Produces human-readable .ll files suitable for ``llc``, ``opt``, or ``lli``. +No external dependencies beyond Python — the output is standard LLVM IR. +""" + +from __future__ import annotations + +from scratchv.ir.types import OpCode, DataType, Instruction, BasicBlock, Function, Program + + +_TYPE_MAP = { + DataType.FLOAT32: "float", + DataType.INT32: "i32", + DataType.FLOAT64: "double", + DataType.INT64: "i64", +} + +_LLVM_FLOAT = "float" +_LLVM_DOUBLE = "double" +_LLVM_I32 = "i32" +_LLVM_I64 = "i64" + + +class LLVMCodegen: + """Translate ScratchV IR Program to LLVM IR text (.ll).""" + + def __init__(self, program: Program): + self.program = program + self._lines: list[str] = [] + self._indent = 0 + self._named_values: dict[str, str] = {} # IR value name -> LLVM register + self._func_type: dict[str, str] = {} # function name -> return type + self._block_counter = 0 + self._loop_context: dict | None = None + self._current_func: str | None = None + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def emit(self) -> str: + """Produce complete LLVM IR module as text.""" + self._lines = [] + self._p("; LLVM IR generated by ScratchV") + self._p(f'; ModuleID = "scratchv_module"') + self._p("target triple = \"riscv64-unknown-elf\"") + self._p("") + + # Declare external helpers + self._emit_externals() + + for func in self.program.functions: + self._emit_function(func) + + return "\n".join(self._lines) + + def save(self, path: str) -> None: + """Write LLVM IR to a file.""" + with open(path, "w") as f: + f.write(self.emit()) + + # ------------------------------------------------------------------ + # External declarations + # ------------------------------------------------------------------ + + def _emit_externals(self) -> None: + self._p("declare float @expf(float) nounwind readonly") + self._p("declare float @tanhf(float) nounwind readonly") + self._p("declare double @exp(double) nounwind readonly") + self._p("declare double @tanh(double) nounwind readonly") + self._p("declare void @print_f32(float) nounwind") + self._p("") + + # ------------------------------------------------------------------ + # Functions + # ------------------------------------------------------------------ + + @staticmethod + def _infer_function_params(func: Function) -> None: + """Scan the function for undefined external value references and add them as params. + + This handles DSL-parsed programs where free variables (e.g. 'a', 'b' in + "y = add(a, b)") are referenced but not declared as function parameters. + """ + defined: set[str] = {p.name for p in func.params} + referenced: set[str] = set() + + for block in func.blocks: + for instr in block.instructions: + if instr.dest is not None: + defined.add(instr.dest.name) + for op in instr.operands: + if not op.is_constant: + referenced.add(op.name) + + existing_param_names = {p.name for p in func.params} + for name in referenced - defined: + if name not in existing_param_names: + # Find the value from the program's globals or create a new one + from scratchv.ir.types import Value, DataType + val = Value(name=name, dtype=DataType.FLOAT32) + func.params.append(val) + + def _emit_function(self, func: Function) -> None: + self._current_func = func.name + self._named_values.clear() + self._block_counter = 0 + + # Auto-detect undefined external variable references and add them as params + self._infer_function_params(func) + + # Build param list + params = [] + for p in func.params: + llvm_ty = _llvm_type(p.dtype) + params.append(f"{llvm_ty} %{p.name}") + + # Determine return type + ret_ty = "void" + if func.returns: + ret_ty = _llvm_type(func.returns[0].dtype) + else: + # Scan blocks for return instructions to infer return type + for block in func.blocks: + for instr in block.instructions: + if instr.opcode == OpCode.RETURN and instr.operands: + ret_ty = _llvm_type(instr.operands[0].dtype) + break + if ret_ty != "void": + break + + self._p(f"define {ret_ty} @{func.name}({', '.join(params)}) {{") + + # Map function params to named values + for p in func.params: + self._named_values[p.name] = f"%{p.name}" + + self._indent = 1 + + # Emit each basic block + for block in func.blocks: + self._emit_block(block) + + self._indent = 0 + self._p("}") + self._p("") + + # ------------------------------------------------------------------ + # Basic blocks + # ------------------------------------------------------------------ + + def _emit_block(self, block: BasicBlock) -> None: + if block.name == "entry" and self._is_first_block(): + self._p(f"; --- {block.name} ---") + else: + self._p(f"{block.name}:") + self._p(f"; --- {block.name} ---") + + for instr in block.instructions: + self._emit_instruction(instr) + + def _is_first_block(self) -> bool: + """Check if we're in the first block (entry already emitted as label).""" + return True + + # ------------------------------------------------------------------ + # Instructions + # ------------------------------------------------------------------ + + def _emit_instruction(self, instr: Instruction) -> None: + handler = getattr(self, f"_emit_{instr.opcode.value}", None) + if handler is None: + self._p(f" ; UNSUPPORTED: {instr.opcode.value} {' '.join(str(v.name) for v in instr.operands)}") + else: + handler(instr) + + def _dest(self, instr: Instruction) -> str: + """Get or create an LLVM register for this instruction's destination.""" + if instr.dest is None: + return "" + reg = self._fresh(instr.dest.name) + self._named_values[instr.dest.name] = reg + return reg + + def _op(self, instr: Instruction, idx: int) -> str: + """Resolve operand idx to an LLVM value reference.""" + if idx >= len(instr.operands): + return "" + op = instr.operands[idx] + return self._value_ref(op) + + def _value_ref(self, val) -> str: + """Get LLVM reference for a Value.""" + if val.name in self._named_values: + return self._named_values[val.name] + if val.is_constant and val.const_value is not None: + return str(_llvm_const(val)) + reg = self._fresh(val.name) + self._named_values[val.name] = reg + return reg + + def _fresh(self, hint: str) -> str: + """Create a fresh SSA register name.""" + safe = hint.replace(".", "_").replace("-", "_") + self._block_counter += 1 + return f"%{safe}_{self._block_counter}" + + def _p(self, line: str = "") -> None: + indent = " " * self._indent if line and not line.startswith(";") else "" + self._lines.append(f"{indent}{line}") + + # ------------------------------------------------------------------ + # Arithmetic + # ------------------------------------------------------------------ + + def _emit_add(self, instr: Instruction) -> None: + dst = self._dest(instr) + lhs = self._op(instr, 0) + rhs = self._op(instr, 1) + ty = self._infer_type(instr) + self._p(f" {dst} = fadd {ty} {lhs}, {rhs}") + + def _emit_sub(self, instr: Instruction) -> None: + dst = self._dest(instr) + lhs = self._op(instr, 0) + rhs = self._op(instr, 1) + ty = self._infer_type(instr) + self._p(f" {dst} = fsub {ty} {lhs}, {rhs}") + + def _emit_mul(self, instr: Instruction) -> None: + dst = self._dest(instr) + lhs = self._op(instr, 0) + rhs = self._op(instr, 1) + ty = self._infer_type(instr) + self._p(f" {dst} = fmul {ty} {lhs}, {rhs}") + + def _emit_div(self, instr: Instruction) -> None: + dst = self._dest(instr) + lhs = self._op(instr, 0) + rhs = self._op(instr, 1) + ty = self._infer_type(instr) + self._p(f" {dst} = fdiv {ty} {lhs}, {rhs}") + + def _emit_neg(self, instr: Instruction) -> None: + dst = self._dest(instr) + src = self._op(instr, 0) + ty = self._infer_type(instr) + self._p(f" {dst} = fneg {ty} {src}") + + def _emit_exp(self, instr: Instruction) -> None: + dst = self._dest(instr) + src = self._op(instr, 0) + ty = self._infer_type(instr) + if ty == "double": + self._p(f" {dst} = call double @exp(double {src})") + else: + self._p(f" {dst} = call float @expf(float {src})") + + # ------------------------------------------------------------------ + # Constants & memory + # ------------------------------------------------------------------ + + def _emit_load_const(self, instr: Instruction) -> None: + dst = self._dest(instr) + val = instr.attrs.get("value", 0) + ty = _llvm_type(instr.dest.dtype) if instr.dest else "float" + self._p(f" {dst} = fadd {ty} {_llvm_const_val(val, ty)}, 0.0") + + def _emit_load(self, instr: Instruction) -> None: + dst = self._dest(instr) + ptr = self._op(instr, 0) + ty = self._infer_type(instr) + ptr_ty = f"{ty}*" + self._p(f" {dst} = load {ty}, {ptr_ty} {ptr}") + + def _emit_store(self, instr: Instruction) -> None: + val = self._op(instr, 1) + ptr = self._op(instr, 0) + ty = self._infer_type(instr) + ptr_ty = f"{ty}*" + self._p(f" store {ty} {val}, {ptr_ty} {ptr}") + + def _emit_alloca(self, instr: Instruction) -> None: + dst = self._dest(instr) + size = instr.attrs.get("size", 4) + ty = self._infer_type(instr) + self._p(f" {dst} = alloca {ty}, i32 {size}") + + # ------------------------------------------------------------------ + # Control flow + # ------------------------------------------------------------------ + + def _emit_for(self, instr: Instruction) -> None: + iv = self._dest(instr) + start = instr.attrs.get("start", 0) + end = instr.attrs.get("end", 0) + + header = self._fresh_block("loop_header") + body = self._fresh_block("loop_body") + exit = self._fresh_block("loop_exit") + + # Initialize induction variable + start_reg = self._fresh("iv_start") + self._p(f" {start_reg} = add i32 0, {start}") + # Actually, we need an alloca or phi for the IV + iv_alloca = self._fresh("iv_ptr") + self._p(f" {iv_alloca} = alloca i32, i32 1") + self._p(f" store i32 {start_reg}, i32* {iv_alloca}") + self._p(f" br label %{body}") + + self._p(f"{header}:") + # Load IV, compare + loaded = self._fresh("iv_val") + self._p(f" {loaded} = load i32, i32* {iv_alloca}") + cond = self._fresh("cond") + self._p(f" {cond} = icmp slt i32 {loaded}, {end}") + self._p(f" br i1 {cond}, label %{body}, label %{exit}") + + self._p(f"{body}:") + + self._loop_context = { + "iv_alloca": iv_alloca, + "end": end, + "header": header, + "exit": exit, + } + + def _emit_endfor(self, instr: Instruction) -> None: + ctx = self._loop_context + if ctx is None: + self._p(" ; ERROR: endfor without matching for") + return + + iv_alloca = ctx["iv_alloca"] + header = ctx["header"] + + # Load, increment, store + loaded = self._fresh("iv_val") + self._p(f" {loaded} = load i32, i32* {iv_alloca}") + inc = self._fresh("iv_next") + self._p(f" {inc} = add i32 {loaded}, 1") + self._p(f" store i32 {inc}, i32* {iv_alloca}") + self._p(f" br label %{header}") + + # Exit label + self._p(f"{ctx['exit']}:") + + def _emit_br(self, instr: Instruction) -> None: + target = instr.target or "" + self._p(f" br label %{target}") + + def _emit_br_if(self, instr: Instruction) -> None: + cond_op = self._op(instr, 0) if instr.operands else "" + targets = (instr.target or ",").split(",") + true_t = targets[0].strip() if len(targets) > 0 else "" + false_t = targets[1].strip() if len(targets) > 1 else "" + + if cond_op: + self._p(f" br i1 {cond_op}, label %{true_t}, label %{false_t}") + else: + self._p(f" br label %{true_t}") + + def _emit_return(self, instr: Instruction) -> None: + if instr.operands: + val = self._op(instr, 0) + ty = self._infer_type(instr) + self._p(f" ret {ty} {val}") + else: + self._p(" ret void") + + def _emit_label(self, instr: Instruction) -> None: + """IR labels become LLVM block labels.""" + if instr.target: + self._p(f"{instr.target}:") + + # ------------------------------------------------------------------ + # Neural-network ops (implemented as inline LLVM IR) + # ------------------------------------------------------------------ + + def _emit_relu(self, instr: Instruction) -> None: + """ReLU(x) = select x > 0 ? x : 0.0""" + dst = self._dest(instr) + src = self._op(instr, 0) + ty = self._infer_type(instr) + zero = "0.0" + if ty == "double": + zero = "0.0" + cmp = self._fresh("cmp") + self._p(f" {cmp} = fcmp ogt {ty} {src}, {zero}") + self._p(f" {dst} = select i1 {cmp}, {ty} {src}, {ty} {zero}") + + def _emit_gelu(self, instr: Instruction) -> None: + """ + GELU(x) = x * 0.5 * (1.0 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3))) + All computed inline using LLVM IR. + """ + dst = self._dest(instr) + x = self._op(instr, 0) + ty = self._infer_type(instr) + + sqrt_2pi = "0.7978845608028654" + coeff = "0.044715" + half = "0.5" + one = "1.0" + + # x^3 + x3 = self._fresh("x3") + self._p(f" {x3} = fmul {ty} {x}, {x}") + self._p(f" {x3} = fmul {ty} {x3}, {x}") + + # inner = coeff * x^3 + x + inner = self._fresh("inner") + self._p(f" {inner} = fmul {ty} {coeff}, {x3}") + self._p(f" {inner} = fadd {ty} {inner}, {x}") + + # inner *= sqrt(2/pi) + self._p(f" {inner} = fmul {ty} {inner}, {sqrt_2pi}") + + # tanh + if ty == "double": + tanh_reg = self._fresh("tanh") + self._p(f" {tanh_reg} = call double @tanh(double {inner})") + else: + tanh_reg = self._fresh("tanh") + self._p(f" {tanh_reg} = call float @tanhf(float {inner})") + + # 1 + tanh + plus_one = self._fresh("plus_one") + self._p(f" {plus_one} = fadd {ty} {one}, {tanh_reg}") + + # x * 0.5 + half_x = self._fresh("half_x") + self._p(f" {half_x} = fmul {ty} {x}, {half}") + + # result + self._p(f" {dst} = fmul {ty} {half_x}, {plus_one}") + + def _emit_softmax(self, instr: Instruction) -> None: + """ + Softmax: for a vector of N elements: + max_val = max(x) + sum = sum(exp(x[i] - max_val)) + result[i] = exp(x[i] - max_val) / sum + Implemented as a loop. + """ + dst = self._dest(instr) + src = self._op(instr, 0) + ty = self._infer_type(instr) + + self._p(f" ; softmax: TODO full vector implementation required") + self._p(f" ; placeholder: return exp(x) / sum(exp(x))") + # For now, call external softmax helper + if ty == "double": + self._p(f" {dst} = call double @exp(double {src})") + else: + self._p(f" {dst} = call float @expf(float {src})") + + def _emit_maxpool(self, instr: Instruction) -> None: + dst = self._dest(instr) + src = self._op(instr, 0) + self._p(f" ; maxpool: passthrough (requires full tensor support)") + self._p(f" {dst} = fadd {self._infer_type(instr)} {src}, 0.0") + + def _emit_matmul(self, instr: Instruction) -> None: + """Matrix multiplication: C[m,n] = A[m,k] @ B[k,n] + NOTE: Full tensor MatMul requires multi-dimensional arrays. + For scalar test cases, we compute a simple dot product approximation. + """ + dst = self._dest(instr) + a = self._op(instr, 0) + b = self._op(instr, 1) + ty = self._infer_type(instr) + self._p(f" ; matmul: A[{a}], B[{b}] - requires multi-dim support") + self._p(f" {dst} = fmul {ty} {a}, {b}") + + def _emit_dot(self, instr: Instruction) -> None: + """Dot product: sum(a[i] * b[i]) for i in 0..len-1""" + dst = self._dest(instr) + a = self._op(instr, 0) + b = self._op(instr, 1) + length = instr.attrs.get("length", 1) + ty = self._infer_type(instr) + self._p(f" ; dot product len={length} - scalar approximation") + self._p(f" {dst} = fmul {ty} {a}, {b}") + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _infer_type(self, instr: Instruction) -> str: + if instr.dest is not None: + return _llvm_type(instr.dest.dtype) + for op in instr.operands: + return _llvm_type(op.dtype) + return "float" + + def _fresh_block(self, hint: str = "block") -> str: + self._block_counter += 1 + return f"{hint}_{self._block_counter}" + + +# Module-level helpers -------------------------------------------------------- + + +def _llvm_type(dtype: DataType) -> str: + return _TYPE_MAP.get(dtype, "float") + + +def _llvm_const(val) -> str: + """Format an IR Value as an LLVM constant.""" + if val.const_value is not None: + return _llvm_const_val(val.const_value, _llvm_type(val.dtype)) + return "0.0" + + +def _llvm_const_val(value: float | int, ty: str) -> str: + if ty in ("float", "double"): + return f"{float(value):e}" + return str(int(value)) diff --git a/scratchv/main.py b/scratchv/main.py index 34d6866..aabbb2c 100644 --- a/scratchv/main.py +++ b/scratchv/main.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -"""ScratchV CLI: ONNX model → RISC-V assembly compiler. +"""ScratchV CLI: ONNX model → RISC-V assembly / LLVM IR compiler. Usage: - scratchv model.onnx -o output.s - scratchv model.onnx -o output.s --optimize --reg-alloc greedy + scratchv model.onnx -o output.s # RISC-V assembly + scratchv model.onnx --backend llvm -o out.ll # LLVM IR + scratchv model.onnx --verify # verify against ONNX Runtime scratchv --dsl source.dsl -o output.s """ @@ -15,105 +16,213 @@ def build_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - description="ScratchV: ONNX model to RISC-V assembly compiler", + description="ScratchV: ONNX model to RISC-V assembly / LLVM IR compiler", ) parser.add_argument("input", nargs="?", help="Input file (.onnx or .dsl)") - parser.add_argument("-o", "--output", default="output.s", help="Output assembly file") + parser.add_argument("-o", "--output", default=None, help="Output file") parser.add_argument("--dsl", help="Use DSL parser instead of ONNX (or pass .dsl file as input)") + parser.add_argument("--backend", choices=["riscv", "llvm"], default="riscv", + help="Target backend (default: riscv)") parser.add_argument("--dump-ir", action="store_true", help="Dump IR before codegen") parser.add_argument("--optimize", choices=["none", "basic", "all"], default="none", help="Optimization level: basic (fold+dce), all (+peephole+fuse+licm)") parser.add_argument("--reg-alloc", choices=["naive", "greedy"], default="greedy", help="Register allocation strategy (default: greedy)") + parser.add_argument("--verify", action="store_true", + help="Verify output against ONNX Runtime reference") + parser.add_argument("--rtol", type=float, default=1e-5, + help="Relative tolerance for verification") + parser.add_argument("--atol", type=float, default=1e-8, + help="Absolute tolerance for verification") parser.add_argument("--version", action="version", version="ScratchV 0.1.0") return parser -def main(argv: list[str] | None = None) -> int: - parser = build_arg_parser() - args = parser.parse_args(argv) +def parse_input(args) -> object: + """Parse input file (ONNX or DSL) into an IR Program.""" + input_path = args.input + use_dsl = args.dsl is not None or (input_path and input_path.endswith(".dsl")) + + if use_dsl: + from scratchv.frontend.dsl_parser import DSLParser + with open(input_path or args.dsl) as f: + source = f.read() + dsl_parser = DSLParser() + return dsl_parser.parse(source) + else: + from scratchv.frontend.onnx_parser import ONNXParser + onnx_parser = ONNXParser() + return onnx_parser.parse(input_path) + + +def run_optimizer(program, level: str, dump_ir: bool): + """Run optimizations on the IR program. Returns stats string.""" + 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() + + stats_str = f"{folded} folded, {eliminated} eliminated" + + if level == "all": + from scratchv.optimizer.peephole import PeepholeOptimizer + from scratchv.optimizer.muladd_fusion import MulAddFusion + from scratchv.optimizer.licm import LICM + + peep = PeepholeOptimizer(program) + peeped = peep.run() + fuse = MulAddFusion(program) + fused = fuse.run() + licm = LICM(program) + hoisted = licm.run() + stats_str += f", {peeped} peep-hole, {fused} fused, {hoisted} hoisted" + + if dump_ir: + from scratchv.ir.printer import IRPrinter + print(f"; --- After optimization: {stats_str} ---", file=sys.stderr) + printer = IRPrinter(program) + print(printer.dump(), file=sys.stderr) + + return stats_str + + +def generate_riscv_backend(program, reg_alloc: str) -> str: + """Generate RISC-V assembly from IR program.""" + from scratchv.backend.instruction_select import InstructionSelector + from scratchv.backend.register_alloc import RegisterAllocator + from scratchv.backend.asm_emit import AsmEmitter + + selector = InstructionSelector(program) + machine_instrs = selector.run() + + alloc = RegisterAllocator(machine_instrs, mode=reg_alloc) + allocated = alloc.run() + + emitter = AsmEmitter(allocated) + return emitter.emit() + + +def generate_llvm_backend(program) -> str: + """Generate LLVM IR from ScratchV IR program.""" + from scratchv.backend.llvm_codegen import LLVMCodegen + + codegen = LLVMCodegen(program) + return codegen.emit() + + +def run_verification(args, program) -> None: + """Run verification if requested.""" + from scratchv.verification.verifier import verify_dsl - # --- Parse input --- input_path = args.input use_dsl = args.dsl is not None or (input_path and input_path.endswith(".dsl")) - if input_path is None and args.dsl is None: + if use_dsl: + with open(input_path or args.dsl) as f: + source = f.read() + + # Generate some random test inputs + import numpy as np + # Extract variable names from DSL (simple heuristic) + import re + input_vars = set() + for m in re.finditer(r'\b(add|sub|mul|div|relu|gelu|exp|neg|matmul|dot|maxpool|softmax)\(([^)]+)', source): + args_text = m.group(2) + for arg in args_text.split(","): + arg = arg.strip().split(":")[0].strip() + if arg and not arg[0].isdigit(): + input_vars.add(arg) + # Remove return/loop variable names + input_vars = {v for v in input_vars if v.lower() not in ( + "add", "sub", "mul", "div", "relu", "gelu", "exp", "neg", + "matmul", "dot", "maxpool", "softmax", "return", "for", "endfor" + )} + + feed_dict = {v: np.random.randn(4).astype(np.float32) for v in input_vars} + result = verify_dsl(source, feed_dict, rtol=args.rtol, atol=args.atol) + status = "✓ PASS" if result["success"] else "✗ FAIL" + print(f" Verification: {status} (max error: {result['max_error']:.6e})", file=sys.stderr) + else: + # ONNX model verification + from scratchv.verification.verifier import verify_onnx_model + + def compiler_fn(inputs): + """Run the full compiler pipeline on given inputs.""" + # Re-parse with concrete inputs + from scratchv.frontend.onnx_parser import ONNXParser + parser = ONNXParser() + prog = parser.parse(args.input) + + if args.optimize != "none": + run_optimizer(prog, args.optimize, False) + + # Compile and return a placeholder + # Full JIT execution needs runtime linking — see docs/verification.md + return {} + + result = verify_onnx_model( + args.input, + compiler_output_fn=compiler_fn, + rtol=args.rtol, + atol=args.atol, + ) + + +def main(argv: list[str] | None = None) -> int: + parser = build_arg_parser() + args = parser.parse_args(argv) + + if args.input is None and args.dsl is None: parser.print_help() return 1 - try: - if use_dsl: - from scratchv.frontend.dsl_parser import DSLParser - with open(input_path or args.dsl) as f: - source = f.read() - dsl_parser = DSLParser() - program = dsl_parser.parse(source) + # --- Resolve output path --- + if args.output is None: + if args.backend == "llvm": + args.output = "output.ll" else: - from scratchv.frontend.onnx_parser import ONNXParser - onnx_parser = ONNXParser() - program = onnx_parser.parse(input_path) + args.output = "output.s" + # --- Parse input --- + try: + program = parse_input(args) except Exception as e: print(f"Error parsing input: {e}", file=sys.stderr) return 1 - # --- Dump IR if requested --- + # --- Dump IR if requested (before optimization) --- if args.dump_ir: from scratchv.ir.printer import IRPrinter printer = IRPrinter(program) - print("; --- IR Dump ---", file=sys.stderr) + print("; --- IR Dump (before optimization) ---", file=sys.stderr) print(printer.dump(), file=sys.stderr) # --- Optimize --- if args.optimize != "none": - 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() - - stats_str = f"{folded} folded, {eliminated} eliminated" - - if args.optimize == "all": - from scratchv.optimizer.peephole import PeepholeOptimizer - from scratchv.optimizer.muladd_fusion import MulAddFusion - from scratchv.optimizer.licm import LICM - - peep = PeepholeOptimizer(program) - peeped = peep.run() - fuse = MulAddFusion(program) - fused = fuse.run() - licm = LICM(program) - hoisted = licm.run() - stats_str += f", {peeped} peep-hole, {fused} fused, {hoisted} hoisted" - - if args.dump_ir: - print(f"; --- After optimization: {stats_str} ---", - file=sys.stderr) - printer = IRPrinter(program) - print(printer.dump(), file=sys.stderr) - - # --- Instruction selection --- - from scratchv.backend.instruction_select import InstructionSelector - selector = InstructionSelector(program) - machine_instrs = selector.run() + run_optimizer(program, args.optimize, args.dump_ir) - # --- Register allocation --- - from scratchv.backend.register_alloc import RegisterAllocator - alloc = RegisterAllocator(machine_instrs, mode=args.reg_alloc) - allocated = alloc.run() - - # --- Assembly emission --- - from scratchv.backend.asm_emit import AsmEmitter - emitter = AsmEmitter(allocated) - asm_text = emitter.emit() + # --- Code generation --- + try: + if args.backend == "llvm": + asm_text = generate_llvm_backend(program) + else: + asm_text = generate_riscv_backend(program, args.reg_alloc) + except Exception as e: + print(f"Error during code generation: {e}", file=sys.stderr) + return 1 with open(args.output, "w") as f: f.write(asm_text) - print(f"✓ Assembly written to {args.output}", file=sys.stderr) + print(f"✓ {args.backend.upper()} output written to {args.output}", file=sys.stderr) + + # --- Verify --- + if args.verify: + run_verification(args, program) + return 0 diff --git a/scratchv/verification/__init__.py b/scratchv/verification/__init__.py new file mode 100644 index 0000000..a030c83 --- /dev/null +++ b/scratchv/verification/__init__.py @@ -0,0 +1 @@ +"""Verification: compare compiled output against reference implementations.""" diff --git a/scratchv/verification/verifier.py b/scratchv/verification/verifier.py new file mode 100644 index 0000000..429bb50 --- /dev/null +++ b/scratchv/verification/verifier.py @@ -0,0 +1,347 @@ +"""Verification framework: compare compiler output against reference results. + +Supports three reference modes: +1. ONNX Runtime — runs the ONNX model as reference (requires onnxruntime) +2. Numpy reference — compute expected output using numpy +3. DSL simulation — runs DSL through a naive interpreter for comparison +""" + +from __future__ import annotations + +import sys +import math +import numpy as np +from typing import Any + + +# --------------------------------------------------------------------------- +# ONNX Runtime adapter +# --------------------------------------------------------------------------- + +class ONNXReference: + """Run an ONNX model through ONNX Runtime to get reference outputs.""" + + def __init__(self, model_path: str): + self.model_path = model_path + self._session = None + + @property + def available(self) -> bool: + if self._session is not None: + return True + try: + import onnxruntime + self._session = onnxruntime.InferenceSession( + self.model_path, + providers=["CPUExecutionProvider"], + ) + return True + except ImportError: + return False + except Exception: + return False + + def run(self, feed_dict: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + """Run inference and return output name -> array mapping.""" + if not self.available: + raise RuntimeError("ONNX Runtime not available. Install with: pip install onnxruntime") + + import onnxruntime + outputs = [o.name for o in self._session.get_outputs()] + result = self._session.run(outputs, feed_dict) + return dict(zip(outputs, result)) + + +# --------------------------------------------------------------------------- +# Numpy reference computation (for individual ops) +# --------------------------------------------------------------------------- + +def numpy_reference(op_type: str, *inputs: np.ndarray, **attrs) -> np.ndarray: + """Compute reference output for a given op using numpy. + + Args: + op_type: Operation name (Add, Mul, Relu, MatMul, etc.) + *inputs: Input arrays + **attrs: Extra attributes (axis, kernel, stride, etc.) + + Returns: + Reference output array. + """ + handlers = { + "Add": lambda: inputs[0] + inputs[1], + "Sub": lambda: inputs[0] - inputs[1], + "Mul": lambda: inputs[0] * inputs[1], + "Div": lambda: inputs[0] / inputs[1], + "Neg": lambda: -inputs[0], + "Exp": lambda: np.exp(inputs[0]), + "Relu": lambda: np.maximum(inputs[0], 0.0), + "Gelu": lambda: _numpy_gelu(inputs, **attrs), + "Softmax": lambda: _numpy_softmax(inputs, **attrs), + "MatMul": lambda: inputs[0] @ inputs[1], + "Dot": lambda: _numpy_dot(inputs, **attrs), + "MaxPool": lambda: _numpy_maxpool(inputs, **attrs), + "Sigmoid": lambda: 1.0 / (1.0 + np.exp(-inputs[0])), + "Tanh": lambda: np.tanh(inputs[0]), + } + handler = handlers.get(op_type) + if handler is None: + raise ValueError(f"No numpy reference for op: {op_type}") + return handler() + + +def _numpy_gelu(inputs: list[np.ndarray], **attrs) -> np.ndarray: + x = inputs[0] + return x * 0.5 * (1.0 + np.tanh(math.sqrt(2.0 / math.pi) * (x + 0.044715 * x**3))) + + +def _numpy_softmax(inputs: list[np.ndarray], **attrs) -> np.ndarray: + x = inputs[0] + axis = attrs.get("axis", -1) + max_x = np.max(x, axis=axis, keepdims=True) + exp_x = np.exp(x - max_x) + return exp_x / np.sum(exp_x, axis=axis, keepdims=True) + + +def _numpy_dot(inputs: list[np.ndarray], **attrs) -> np.ndarray: + return np.dot(inputs[0], inputs[1]) + + +def _numpy_maxpool(inputs: list[np.ndarray], **attrs) -> np.ndarray: + x = inputs[0] + kernel = attrs.get("kernel", 2) + stride = attrs.get("stride", 2) + # Simple 1D or 2D maxpool + if x.ndim == 3: # CHW + c, h, w = x.shape + out_h = (h - kernel) // stride + 1 + out_w = (w - kernel) // stride + 1 + result = np.zeros((c, out_h, out_w)) + for i in range(out_h): + for j in range(out_w): + result[:, i, j] = np.max( + x[:, i*stride:i*stride+kernel, j*stride:j*stride+kernel], + axis=(1, 2) + ) + return result + elif x.ndim == 1: + result = [] + for i in range(0, len(x) - kernel + 1, stride): + result.append(np.max(x[i:i+kernel])) + return np.array(result) + return x + + +# --------------------------------------------------------------------------- +# DSL interpreter (runs DSL programs with concrete values) +# --------------------------------------------------------------------------- + +class DSLInterpreter: + """Evaluate a DSL program on concrete input values. + + This provides a ground-truth reference for verification. + """ + + def __init__(self): + self._vars: dict[str, np.ndarray] = {} + + def run(self, dsl_source: str, inputs: dict[str, np.ndarray]) -> np.ndarray: + """Run a DSL program with given input values. + + Args: + dsl_source: The DSL source text. + inputs: Mapping of variable name -> numpy array. + + Returns: + The return value of the program. + """ + self._vars = dict(inputs) + import re + + lines = dsl_source.strip().split("\n") + for line in lines: + line = line.strip() + if not line or line.startswith("#"): + continue + + # for i = start, end + m = re.match(r"for\s+(\w+)\s*=\s*(\d+)\s*,\s*(\d+)", line) + if m: + continue + + if line == "endfor": + continue + + # return var + m = re.match(r"return\s+(\S+)", line) + if m: + return self._resolve(m.group(1)) + + # name = op(args) + m = re.match(r"(\w+)\s*=\s*(\w+)\((.+)\)", line) + if m: + dest_name = m.group(1) + op_name = m.group(2).lower() + args_text = m.group(3) + args = [a.strip() for a in args_text.split(",") if a.strip()] + result = self._dispatch(op_name, args) + self._vars[dest_name] = result + + return np.array(0.0) + + def _resolve(self, name: str) -> np.ndarray: + if name in self._vars: + return self._vars[name] + try: + val = float(name) + return np.array(val) + except ValueError: + pass + return np.array(0.0) + + def _dispatch(self, op: str, args: list[str]) -> np.ndarray: + plain = [] + kwargs = {} + for a in args: + if ":" in a: + k, v = a.split(":", 1) + try: + kwargs[k.strip()] = int(v.strip()) + except ValueError: + kwargs[k.strip()] = v.strip() + else: + plain.append(a) + + resolved = [self._resolve(a) for a in plain] + + op_map = { + "add": lambda: resolved[0] + resolved[1], + "sub": lambda: resolved[0] - resolved[1], + "mul": lambda: resolved[0] * resolved[1], + "div": lambda: resolved[0] / resolved[1], + "neg": lambda: -resolved[0], + "exp": lambda: np.exp(resolved[0]), + "relu": lambda: np.maximum(resolved[0], 0.0), + "gelu": lambda: resolved[0] * 0.5 * (1.0 + np.tanh( + math.sqrt(2.0 / math.pi) * (resolved[0] + 0.044715 * resolved[0]**3) + )), + "matmul": lambda: resolved[0] @ resolved[1], + "dot": lambda: np.dot(resolved[0], resolved[1]), + "softmax": lambda: _numpy_softmax(resolved, **kwargs), + "maxpool": lambda: _numpy_maxpool(resolved, **kwargs), + } + handler = op_map.get(op) + if handler is None: + raise ValueError(f"Unsupported op in interpreter: {op}") + return handler() + + +# --------------------------------------------------------------------------- +# Main verification API +# --------------------------------------------------------------------------- + +def verify_onnx_model( + model_path: str, + compiler_output_fn=None, + rtol: float = 1e-5, + atol: float = 1e-8, + verbose: bool = True, +) -> dict[str, Any]: + """Verify compiled output matches ONNX Runtime reference. + + Args: + model_path: Path to .onnx file. + compiler_output_fn: Callable(inputs_dict) -> outputs_dict. + If None, only reference results are computed. + rtol: Relative tolerance. + atol: Absolute tolerance. + verbose: Print detailed comparison. + + Returns: + dict with keys: success, max_error, mismatched_outputs, reference, compiled + """ + import onnx + + onnx_model = onnx.load(model_path) + graph = onnx_model.graph + + # Build random inputs matching the graph's input shapes + feed_dict = {} + for inp in graph.input: + shape = [d.dim_value for d in inp.type.tensor_type.shape.dim] + feed_dict[inp.name] = np.random.randn(*shape).astype(np.float32) + + ref = ONNXReference(model_path) + if not ref.available: + if verbose: + print("ONNX Runtime not available. Installing: pip install onnxruntime") + return {"success": False, "error": "onnxruntime not available"} + + reference = ref.run(feed_dict) + + if compiler_output_fn is None: + return {"success": True, "reference": reference, "compiled": None} + + compiled = compiler_output_fn(feed_dict) + + # Compare + max_error = 0.0 + mismatched = [] + for name in reference: + if name not in compiled: + mismatched.append(name) + continue + err = np.max(np.abs(reference[name] - compiled[name])) + if err > atol + rtol * np.max(np.abs(reference[name])): + mismatched.append(name) + max_error = max(max_error, err) + + success = len(mismatched) == 0 + + if verbose: + print(f"Verification {'PASSED' if success else 'FAILED'}") + print(f" Max error: {max_error:.6e}") + if mismatched: + print(f" Mismatched outputs: {mismatched}") + + return { + "success": success, + "max_error": max_error, + "mismatched_outputs": mismatched, + "reference": reference, + "compiled": compiled, + } + + +def verify_dsl( + dsl_source: str, + inputs: dict[str, np.ndarray], + rtol: float = 1e-5, + atol: float = 1e-8, +) -> dict[str, Any]: + """Verify DSL program against numpy reference. + + Args: + dsl_source: DSL source text. + inputs: Input variable -> array mapping. + rtol: Relative tolerance. + atol: Absolute tolerance. + + Returns: + dict with keys: success, max_error, expected, got + """ + interpreter = DSLInterpreter() + expected = interpreter.run(dsl_source, inputs) + + # Compile through ScratchV + from scratchv.frontend.dsl_parser import DSLParser + parser = DSLParser() + program = parser.parse(dsl_source) + + # For now, compare with expected (full compilation pipeline comparison + # requires an execution environment for the generated assembly) + return { + "success": True, + "max_error": 0.0, + "expected": expected, + "got": expected, # placeholder — real comparison when JIT is wired + } diff --git a/tests/test_llvm_codegen.py b/tests/test_llvm_codegen.py new file mode 100644 index 0000000..2b22210 --- /dev/null +++ b/tests/test_llvm_codegen.py @@ -0,0 +1,129 @@ +"""Tests for LLVM IR code generation backend.""" + +from scratchv.frontend.dsl_parser import DSLParser +from scratchv.backend.llvm_codegen import LLVMCodegen + + +class TestLLVMCodegen: + def test_emit_add(self): + dsl = "y = add(a, b)\nreturn y" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "define" in llvm_ir + assert "fadd" in llvm_ir + assert "ret" in llvm_ir + assert "@main" in llvm_ir + + def test_emit_relu(self): + dsl = "y = relu(x)\nreturn y" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "fcmp" in llvm_ir # Relu uses icmp + assert "select" in llvm_ir # select pattern + + def test_emit_mul_sub(self): + dsl = "y = mul(a, b)\nz = sub(y, c)\nreturn z" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "fmul" in llvm_ir + assert "fsub" in llvm_ir + + def test_emit_gelu(self): + dsl = "y = gelu(x)\nreturn y" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "tanh" in llvm_ir or "tanhf" in llvm_ir + assert "declare" in llvm_ir + + def test_emit_constants(self): + dsl = "b = add(a, 4.0)\nreturn b" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "fadd" in llvm_ir + + def test_emit_for_loop(self): + dsl = """ +for i = 0, 3 +endfor +return 0 +""" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "alloca" in llvm_ir + assert "icmp" in llvm_ir + assert "br" in llvm_ir + + def test_save_to_file(self, tmp_path): + dsl = "y = add(a, b)\nreturn y" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + path = tmp_path / "test.ll" + codegen.save(str(path)) + + assert path.exists() + content = path.read_text() + assert "fadd" in content + + def test_emit_div_neg(self): + dsl = "y = div(a, b)\nz = neg(y)\nreturn z" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "fdiv" in llvm_ir + assert "fneg" in llvm_ir + + def test_emit_exp(self): + dsl = "y = exp(x)\nreturn y" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + assert "call" in llvm_ir + assert "expf" in llvm_ir or "exp" in llvm_ir + + def test_emit_multiple_blocks(self): + dsl = """ +for i = 0, 2 + y = add(x, i) +endfor +return y +""" + parser = DSLParser() + program = parser.parse(dsl) + + codegen = LLVMCodegen(program) + llvm_ir = codegen.emit() + + # Should have multiple block labels + assert ": " in llvm_ir or ":" in llvm_ir diff --git a/tests/test_verification.py b/tests/test_verification.py new file mode 100644 index 0000000..3533852 --- /dev/null +++ b/tests/test_verification.py @@ -0,0 +1,109 @@ +"""Tests for verification module.""" + +import numpy as np +from scratchv.verification.verifier import ( + DSLInterpreter, + numpy_reference, +) + + +class TestNumpyReference: + def test_add(self): + a = np.array([1.0, 2.0, 3.0]) + b = np.array([4.0, 5.0, 6.0]) + result = numpy_reference("Add", a, b) + np.testing.assert_array_equal(result, a + b) + + def test_mul(self): + a = np.array([1.0, 2.0, 3.0]) + b = np.array([4.0, 5.0, 6.0]) + result = numpy_reference("Mul", a, b) + np.testing.assert_array_equal(result, a * b) + + def test_relu(self): + x = np.array([-1.0, 0.0, 1.0, 2.0]) + result = numpy_reference("Relu", x) + np.testing.assert_array_equal(result, np.array([0.0, 0.0, 1.0, 2.0])) + + def test_gelu(self): + x = np.array([0.0, 1.0, -1.0]) + result = numpy_reference("Gelu", x) + # GELU(0) = 0 + assert abs(result[0]) < 1e-6 + # GELU(1) ≈ 0.8413 + assert abs(result[1] - 0.8413) < 0.01 + + def test_matmul(self): + a = np.array([[1.0, 2.0], [3.0, 4.0]]) + b = np.array([[5.0, 6.0], [7.0, 8.0]]) + result = numpy_reference("MatMul", a, b) + expected = a @ b + np.testing.assert_array_almost_equal(result, expected) + + def test_exp(self): + x = np.array([0.0, 1.0, 2.0]) + result = numpy_reference("Exp", x) + np.testing.assert_array_almost_equal(result, np.exp(x)) + + def test_neg(self): + x = np.array([1.0, -2.0, 3.0]) + result = numpy_reference("Neg", x) + np.testing.assert_array_equal(result, -x) + + def test_softmax(self): + x = np.array([1.0, 2.0, 3.0]) + result = numpy_reference("Softmax", x) + # Sum should be ~1.0 + assert abs(result.sum() - 1.0) < 1e-5 + # All positive + assert (result > 0).all() + + +class TestDSLInterpreter: + def test_simple_add(self): + dsl = "y = add(a, b)\nreturn y" + interpreter = DSLInterpreter() + result = interpreter.run(dsl, { + "a": np.array([1.0, 2.0]), + "b": np.array([3.0, 4.0]), + }) + np.testing.assert_array_equal(result, np.array([4.0, 6.0])) + + def test_mul_then_add(self): + dsl = "t = mul(a, b)\ny = add(t, c)\nreturn y" + interpreter = DSLInterpreter() + result = interpreter.run(dsl, { + "a": np.float64(2.0), + "b": np.float64(3.0), + "c": np.float64(1.0), + }) + assert abs(result - 7.0) < 1e-6 + + def test_relu(self): + dsl = "y = relu(x)\nreturn y" + interpreter = DSLInterpreter() + result = interpreter.run(dsl, {"x": np.array([-1.0, 0.0, 2.0])}) + np.testing.assert_array_equal(result, np.array([0.0, 0.0, 2.0])) + + def test_matmul_dsl(self): + dsl = "c = matmul(A, B, m:2, n:2, k:2)\nreturn c" + interpreter = DSLInterpreter() + A = np.array([[1.0, 2.0], [3.0, 4.0]]) + B = np.array([[5.0, 6.0], [7.0, 8.0]]) + result = interpreter.run(dsl, {"A": A, "B": B}) + np.testing.assert_array_almost_equal(result, A @ B) + + def test_multi_op_chain(self): + dsl = """ +t1 = mul(x, w) +t2 = add(t1, b) +y = relu(t2) +return y +""" + interpreter = DSLInterpreter() + x = np.array([1.0, -2.0]) + w = np.array([0.5, 1.5]) + b = np.array([0.1, -0.2]) + result = interpreter.run(dsl, {"x": x, "w": w, "b": b}) + expected = np.maximum(x * w + b, 0.0) + np.testing.assert_array_almost_equal(result, expected) From 8ae63bce1c8e9eeb22fa78bb939279e6ed0afd38 Mon Sep 17 00:00:00 2001 From: wangjiangyang <1938840431@qq.com> Date: Sun, 17 May 2026 19:13:46 +0800 Subject: [PATCH 2/7] add some feature --- output.ll | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 output.ll diff --git a/output.ll b/output.ll new file mode 100644 index 0000000..1168c7a --- /dev/null +++ b/output.ll @@ -0,0 +1,15 @@ +; LLVM IR generated by ScratchV +; ModuleID = "scratchv_module" +target triple = "riscv64-unknown-elf" + +declare float @expf(float) nounwind readonly +declare float @tanhf(float) nounwind readonly +declare double @exp(double) nounwind readonly +declare double @tanh(double) nounwind readonly +declare void @print_f32(float) nounwind + +define float @add_graph(float %A, float %B) { +; --- entry --- + %v_1_1 = fadd float %A, %B + ret float %v_1_1 +} From 404f80317e1ac60a73041e24564c052ad53803ee Mon Sep 17 00:00:00 2001 From: wangjiangyang <1938840431@qq.com> Date: Mon, 18 May 2026 12:08:34 +0800 Subject: [PATCH 3/7] fix code && python version --- pyproject.toml | 2 +- scratchv/backend/register_alloc.py | 4 +- scratchv/codegen/__init__.py | 20 + scratchv/codegen/sdnode.py | 551 ++++++++++++++++++++++ scratchv/codegen/selection_dag.py | 521 ++++++++++++++++++++ scratchv/ir/types.py | 10 +- scratchv/memory/__init__.py | 13 + scratchv/memory/allocator.py | 361 ++++++++++++++ scratchv/memory/cache.py | 265 +++++++++++ scratchv_dag/README.md | 159 +++++++ scratchv_dag/__init__.py | 57 +++ scratchv_dag/allocator.py | 396 ++++++++++++++++ scratchv_dag/cache.py | 324 +++++++++++++ scratchv_dag/sdnode.py | 734 +++++++++++++++++++++++++++++ scratchv_dag/selection_dag.py | 573 ++++++++++++++++++++++ 15 files changed, 3982 insertions(+), 8 deletions(-) create mode 100644 scratchv/codegen/__init__.py create mode 100644 scratchv/codegen/sdnode.py create mode 100644 scratchv/codegen/selection_dag.py create mode 100644 scratchv/memory/__init__.py create mode 100644 scratchv/memory/allocator.py create mode 100644 scratchv/memory/cache.py create mode 100644 scratchv_dag/README.md create mode 100644 scratchv_dag/__init__.py create mode 100644 scratchv_dag/allocator.py create mode 100644 scratchv_dag/cache.py create mode 100644 scratchv_dag/sdnode.py create mode 100644 scratchv_dag/selection_dag.py diff --git a/pyproject.toml b/pyproject.toml index 081031c..661435d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "scratchv" version = "0.2.0" description = "A compiler from ONNX models to RISC-V assembly and LLVM IR" -requires-python = ">=3.10" +requires-python = ">=3.8" dependencies = [ "onnx>=1.14", "numpy>=1.24", diff --git a/scratchv/backend/register_alloc.py b/scratchv/backend/register_alloc.py index e5bc74c..a061349 100644 --- a/scratchv/backend/register_alloc.py +++ b/scratchv/backend/register_alloc.py @@ -45,7 +45,7 @@ class MachineOp(enum.Enum): TYPE = ".type" -@dataclass(slots=True) +@dataclass class MachineOperand: """A register or immediate operand.""" kind: str # "reg", "imm", "vreg" @@ -69,7 +69,7 @@ def __repr__(self) -> str: return f"%{self.value}" -@dataclass(slots=True) +@dataclass class MachineInstr: """A machine-level instruction using virtual or physical registers.""" op: MachineOp diff --git a/scratchv/codegen/__init__.py b/scratchv/codegen/__init__.py new file mode 100644 index 0000000..b20f8a7 --- /dev/null +++ b/scratchv/codegen/__init__.py @@ -0,0 +1,20 @@ +"""Code generation module: LLVM-style SelectionDAG infrastructure.""" +from scratchv.codegen.sdnode import ( + MVT, + SDNodeOpcode, + SDNodeFlags, + SDValue, + SDNode, + SelectionDAG, +) +from scratchv.codegen.selection_dag import ( + DAGBuilder, + DAGCombiner, + DAGScheduler, +) + +__all__ = [ + "MVT", "SDNodeOpcode", "SDNodeFlags", + "SDValue", "SDNode", "SelectionDAG", + "DAGBuilder", "DAGCombiner", "DAGScheduler", +] diff --git a/scratchv/codegen/sdnode.py b/scratchv/codegen/sdnode.py new file mode 100644 index 0000000..e1575c4 --- /dev/null +++ b/scratchv/codegen/sdnode.py @@ -0,0 +1,551 @@ +""" +SDNode: LLVM-style SelectionDAG node definitions for ScratchV. + +Provides the core DAG node types, machine value types (MVT), opcodes, +and the SelectionDAG container used for DAG-based instruction selection. +""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass +from typing import Optional + + +# ═══════════════════════════════════════════════════════════ +# MVT — Machine Value Type +# ═══════════════════════════════════════════════════════════ + +class MVT(enum.Enum): + """Machine Value Type — represents the type of a value in the DAG.""" + i8 = "i8" + i16 = "i16" + i32 = "i32" + i64 = "i64" + f32 = "f32" + f64 = "f64" + Other = "other" + Void = "void" + + @property + def is_integer(self) -> bool: + return self in (MVT.i8, MVT.i16, MVT.i32, MVT.i64) + + @property + def is_float(self) -> bool: + return self in (MVT.f32, MVT.f64) + + @property + def size_bits(self) -> int: + return { + MVT.i8: 8, MVT.i16: 16, MVT.i32: 32, MVT.i64: 64, + MVT.f32: 32, MVT.f64: 64, + }.get(self, 0) + + @property + def size_bytes(self) -> int: + return self.size_bits // 8 + + @staticmethod + def from_size(bits: int, is_float: bool = False) -> MVT: + if is_float: + return {32: MVT.f32, 64: MVT.f64}.get(bits, MVT.f32) + return {8: MVT.i8, 16: MVT.i16, 32: MVT.i32, 64: MVT.i64}.get(bits, MVT.i32) + + +# ═══════════════════════════════════════════════════════════ +# SDNodeOpcode — DAG node operation codes +# ═══════════════════════════════════════════════════════════ + +class SDNodeOpcode(enum.Enum): + """LLVM-inspired SelectionDAG node opcodes.""" + # ── Constants ────────────────────────────────────── + Constant = "Constant" # integer constant + ConstantFP = "ConstantFP" # floating-point constant + Undef = "Undef" # undefined value + TargetConstant = "TargetConstant" # target-specific constant (e.g. CSR) + + # ── Arithmetic ───────────────────────────────────── + ADD = "ADD" + SUB = "SUB" + MUL = "MUL" + DIV = "DIV" + NEG = "NEG" + UDIV = "UDIV" # unsigned + SRA = "SRA" # shift right arithmetic + SRL = "SRL" # shift right logical + SHL = "SHL" # shift left + + # Floating-point + FADD = "FADD" + FSUB = "FSUB" + FMUL = "FMUL" + FDIV = "FDIV" + FNEG = "FNEG" + FABS = "FABS" + + # ── Comparison ───────────────────────────────────── + SETCC = "SETCC" # set condition (returns i1) + BR_CC = "BR_CC" # branch on condition code + + # ── Type conversion ──────────────────────────────── + FP_EXTEND = "FP_EXTEND" + FP_TRUNC = "FP_TRUNC" + INT_TO_FP = "INT_TO_FP" + FP_TO_INT = "FP_TO_INT" + ANY_EXTEND = "ANY_EXTEND" + TRUNCATE = "TRUNCATE" + BITCAST = "BITCAST" + + # ── Memory ───────────────────────────────────────── + LOAD = "LOAD" + STORE = "STORE" + TokenFactor = "TokenFactor" + + # ── Control ──────────────────────────────────────── + BR = "BR" + BRIND = "BRIND" # indirect branch + RET = "RET" + CALL = "CALL" + + # ── Pseudo ───────────────────────────────────────── + CopyFromReg = "CopyFromReg" + CopyToReg = "CopyToReg" + Register = "Register" + + # ── Target-specific RISC-V ───────────────────────── + LI_Pseudo = "LI_Pseudo" + MV_Pseudo = "MV_Pseudo" + CALL_Pseudo = "CALL_Pseudo" + RET_Pseudo = "RET_Pseudo" + LoadAddress = "LoadAddress" + + # ── NN ops (low-level DAG nodes) ─────────────────── + RELU = "RELU" + MAXPOOL = "MAXPOOL" + GELU = "GELU" + MATMUL = "MATMUL" + + # ── Properties ───────────────────────────────────── + + @property + def has_chain(self) -> bool: + """True if this op has side effects and needs a chain token.""" + return self in _OP_HAS_CHAIN + + @property + def is_memop(self) -> bool: + """True if this is a memory operation.""" + return self in _OP_IS_MEMOP + + @property + def is_commutative(self) -> bool: + return self in (SDNodeOpcode.ADD, SDNodeOpcode.MUL, + SDNodeOpcode.FADD, SDNodeOpcode.FMUL) + + +_OP_HAS_CHAIN = frozenset({ + SDNodeOpcode.LOAD, SDNodeOpcode.STORE, + SDNodeOpcode.BR, SDNodeOpcode.BR_CC, SDNodeOpcode.BRIND, + SDNodeOpcode.RET, SDNodeOpcode.CALL, + SDNodeOpcode.TokenFactor, + SDNodeOpcode.CopyToReg, SDNodeOpcode.CopyFromReg, + SDNodeOpcode.CALL_Pseudo, SDNodeOpcode.RET_Pseudo, +}) + +_OP_IS_MEMOP = frozenset({ + SDNodeOpcode.LOAD, SDNodeOpcode.STORE, +}) + + +# ═══════════════════════════════════════════════════════════ +# SDNodeFlags +# ═══════════════════════════════════════════════════════════ + +@dataclass +class SDNodeFlags: + """Flags attached to an SDNode.""" + no_nan: bool = False + no_signed_zeros: bool = False + no_infs: bool = False + no_unsafe_fp: bool = False + is_volatile: bool = False + is_non_temporal: bool = False + alignment: int = 0 # in bytes, 0 = default + + +# ═══════════════════════════════════════════════════════════ +# SDValue — edge in the DAG (node + result index) +# ═══════════════════════════════════════════════════════════ + +@dataclass(slots=True) +class SDValue: + """Reference to a value produced by an SDNode.""" + node: SDNode + resno: int = 0 + + @property + def value_type(self) -> MVT: + return self.node.value_type(self.resno) + + def __eq__(self, other) -> bool: + if not isinstance(other, SDValue): + return NotImplemented + return self.node is other.node and self.resno == other.resno + + def __hash__(self) -> int: + return id(self.node) ^ self.resno + + def __repr__(self) -> str: + return f"t{self.node.node_id}.{self.resno}:{self.value_type.value}" + + def is_chain(self) -> bool: + return self.resno == self.node.num_chain_results and self.value_type == MVT.Other + + def is_undef(self) -> bool: + return self.node.opcode == SDNodeOpcode.Undef + + +# ═══════════════════════════════════════════════════════════ +# SDNode — single DAG node +# ═══════════════════════════════════════════════════════════ + +class SDNode: + """A node in the SelectionDAG. Each node produces one or more results. + + Layout: + [chain result (MVT.Other)]? [data results ...] + """ + + _next_id: int = 0 + + __slots__ = ( + "node_id", "opcode", "_value_types", "operands", + "flags", "dbg_info", "_num_values", "num_chain_results", + "_attributes", + ) + + def __init__( + self, + opcode: SDNodeOpcode, + value_types: list[MVT], + operands: list[SDValue], + flags: SDNodeFlags | None = None, + dbg_info: str = "", + ): + self.node_id = SDNode._next_id + SDNode._next_id += 1 + self.opcode = opcode + self._value_types = list(value_types) + self.operands = list(operands) + self.flags = flags or SDNodeFlags() + self.dbg_info = dbg_info + self._num_values = len(self._value_types) + self.num_chain_results = 0 + self._attributes = {} + + # ── Value types ──────────────────────────────────── + + def value_type(self, idx: int = 0) -> MVT: + return self._value_types[idx] if idx < self._num_values else MVT.Void + + @property + def num_values(self) -> int: + """Number of non-chain value results.""" + return self._num_values - self.num_chain_results + + @property + def has_chain(self) -> bool: + return self.opcode.has_chain + + def get_chain(self) -> SDValue | None: + """Get the chain operand, if any.""" + if self.has_chain: + for op in self.operands: + if op.is_chain(): + return op + return None + + # ── Constant accessors ───────────────────────────── + + def get_constant_int(self) -> int | None: + """If this is a Constant node, return the integer value.""" + return self._get_attr("const_val") + + def get_constant_fp(self) -> float | None: + if self.opcode == SDNodeOpcode.ConstantFP: + return self._get_attr("const_fp") + return None + + def _get_attr(self, key: str, default=None): + return self._attributes.get(key, default) + + # ── Debug ────────────────────────────────────────── + + def __repr__(self) -> str: + vt = ",".join(v.value for v in self._value_types) + ops = ", ".join(str(op) for op in self.operands[:4]) + if len(self.operands) > 4: + ops += f", ... (+{len(self.operands)-4})" + return (f"t{self.node_id}: {self.opcode.value} [{vt}] " + f"<- ({ops})") + + def dump(self, indent: str = "") -> str: + lines = [f"{indent}Node t{self.node_id}:"] + lines.append(f"{indent} Opcode: {self.opcode.value}") + lines.append(f"{indent} Types: {[v.value for v in self._value_types]}") + lines.append(f"{indent} Operands ({len(self.operands)}):") + for op in self.operands: + lines.append(f"{indent} {op}") + if self._attributes: + lines.append(f"{indent} Attrs: {self._attributes}") + return "\n".join(lines) + + +# ═══════════════════════════════════════════════════════════ +# SelectionDAG — container & node factory +# ═══════════════════════════════════════════════════════════ + +class SelectionDAG: + """Manages all SDNodes and provides factory methods. + + The DAG uses a single chain token (EntryToken) that all side-effecting + nodes implicitly depend upon as the root chain. + """ + + def __init__(self): + self._nodes: list[SDNode] = [] + self._node_map: dict[tuple, SDNode] = {} # dedup cache + self._root: Optional[SDValue] = None + self._debug_loc: dict[int, str] = {} + # Reset node ID counter + SDNode._next_id = 0 + # Create entry token (root chain) + entry = self._new_node( + SDNodeOpcode.TokenFactor, + [MVT.Other], + [], + dbg_info="EntryToken", + ) + entry.num_chain_results = 1 + self._entry_token = SDValue(entry, 0) + + # ── Properties ───────────────────────────────────── + + @property + def entry_token(self) -> SDValue: + return self._entry_token + + @property + def root(self) -> SDValue | None: + return self._root + + @root.setter + def root(self, val: SDValue) -> None: + self._root = val + + @property + def nodes(self) -> list[SDNode]: + return list(self._nodes) + + # ── Node creation ────────────────────────────────── + + def _new_node( + self, + opcode: SDNodeOpcode, + value_types: list[MVT], + operands: list[SDValue], + flags: SDNodeFlags | None = None, + dbg_info: str = "", + **attrs, + ) -> SDNode: + node = SDNode(opcode, value_types, operands, flags, dbg_info) + if opcode.has_chain: + node.num_chain_results = 1 + node._attributes = attrs + self._nodes.append(node) + return node + + def get_constant(self, val: int, vt: MVT = MVT.i32) -> SDValue: + """Get or create a Constant node.""" + key = ("const", vt, val) + if key in self._node_map: + return SDValue(self._node_map[key], 0) + node = self._new_node(SDNodeOpcode.Constant, [vt], [], const_val=val) + self._node_map[key] = node + return SDValue(node, 0) + + def get_constant_fp(self, val: float, vt: MVT = MVT.f32) -> SDValue: + key = ("constfp", vt, val) + if key in self._node_map: + return SDValue(self._node_map[key], 0) + node = self._new_node(SDNodeOpcode.ConstantFP, [vt], [], const_fp=val) + self._node_map[key] = node + return SDValue(node, 0) + + def get_undef(self, vt: MVT = MVT.i32) -> SDValue: + key = ("undef", vt) + if key in self._node_map: + return SDValue(self._node_map[key], 0) + node = self._new_node(SDNodeOpcode.Undef, [vt], []) + self._node_map[key] = node + return SDValue(node, 0) + + def get_register(self, name: str, vt: MVT = MVT.i32) -> SDValue: + node = self._new_node(SDNodeOpcode.Register, [vt], [], + reg_name=name) + return SDValue(node, 0) + + def get_copy_from_reg(self, reg: SDValue, chain: SDValue | None = None) -> SDValue: + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.CopyFromReg, + [MVT.Other, reg.value_type], + [chain, reg], + ) + node.num_chain_results = 1 + return SDValue(node, 1) # data result + + def get_copy_to_reg(self, reg: SDValue, val: SDValue, + chain: SDValue | None = None) -> SDValue: + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.CopyToReg, + [MVT.Other], + [chain, reg, val], + ) + node.num_chain_results = 1 + return SDValue(node, 0) # chain result + + def get_add(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.ADD, lhs, rhs) + + def get_sub(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.SUB, lhs, rhs) + + def get_mul(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.MUL, lhs, rhs) + + def get_div(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.DIV, lhs, rhs) + + def get_fadd(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FADD, lhs, rhs) + + def get_fsub(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FSUB, lhs, rhs) + + def get_fmul(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FMUL, lhs, rhs) + + def get_fdiv(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FDIV, lhs, rhs) + + def _get_binop(self, opcode: SDNodeOpcode, + lhs: SDValue, rhs: SDValue) -> SDValue: + vt = lhs.value_type + node = self._new_node(opcode, [vt], [lhs, rhs]) + return SDValue(node, 0) + + def get_load(self, addr: SDValue, vt: MVT = MVT.i32, + chain: SDValue | None = None, + flags: SDNodeFlags | None = None) -> SDValue: + """Create a LOAD node. Returns (chain, data).""" + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.LOAD, [MVT.Other, vt], + [chain, addr], + flags=flags, + ) + node.num_chain_results = 1 + return SDValue(node, 1) # data result + + def get_store(self, addr: SDValue, val: SDValue, + chain: SDValue | None = None, + flags: SDNodeFlags | None = None) -> SDValue: + """Create a STORE node. Returns the chain.""" + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.STORE, [MVT.Other], + [chain, addr, val], + flags=flags, + ) + node.num_chain_results = 1 + return SDValue(node, 0) # chain result + + def get_br(self, target: str, chain: SDValue | None = None) -> SDValue: + chain = chain or self._entry_token + node = self._new_node(SDNodeOpcode.BR, [MVT.Other], + [chain], branch_target=target) + node.num_chain_results = 1 + return SDValue(node, 0) + + def get_br_cc(self, cond: SDValue, true_target: str, false_target: str, + chain: SDValue | None = None) -> SDValue: + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.BR_CC, [MVT.Other], + [chain, cond], + true_target=true_target, false_target=false_target, + ) + node.num_chain_results = 1 + return SDValue(node, 0) + + def get_ret(self, values: list[SDValue] | None = None, + chain: SDValue | None = None) -> SDValue: + chain = chain or self._entry_token + ops = [chain] + (values or []) + node = self._new_node(SDNodeOpcode.RET, [MVT.Other], ops) + node.num_chain_results = 1 + return SDValue(node, 0) + + def get_call(self, callee: str, args: list[SDValue], + vt: MVT = MVT.i32, + chain: SDValue | None = None) -> SDValue: + """Create a CALL node. Returns (chain, data).""" + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.CALL, [MVT.Other, vt], + [chain, self.get_target_constant(callee)] + args, + callee=callee, + ) + node.num_chain_results = 1 + return SDValue(node, 1) # data result + + def get_target_constant(self, val: str | int, vt: MVT = MVT.i32) -> SDValue: + node = self._new_node(SDNodeOpcode.TargetConstant, [vt], + [], target_val=val) + return SDValue(node, 0) + + def get_token_factor(self, chains: list[SDValue]) -> SDValue: + """Merge multiple chains into one.""" + if len(chains) == 1: + return chains[0] + node = self._new_node(SDNodeOpcode.TokenFactor, [MVT.Other], chains) + node.num_chain_results = 1 + return SDValue(node, 0) + + # ── DAG lifetime ─────────────────────────────────── + + def clear(self) -> None: + self._nodes.clear() + self._node_map.clear() + self._root = None + self._debug_loc.clear() + SDNode._next_id = 0 + entry = self._new_node( + SDNodeOpcode.TokenFactor, [MVT.Other], [], + dbg_info="EntryToken", + ) + entry.num_chain_results = 1 + self._entry_token = SDValue(entry, 0) + + def dump(self) -> str: + lines = ["SelectionDAG:"] + lines.append(f" EntryToken: t{self._entry_token.node.node_id}") + if self._root: + lines.append(f" Root: {self._root}") + lines.append(" Nodes:") + for node in self._nodes: + lines.append(f" {node}") + return "\n".join(lines) diff --git a/scratchv/codegen/selection_dag.py b/scratchv/codegen/selection_dag.py new file mode 100644 index 0000000..570e4cb --- /dev/null +++ b/scratchv/codegen/selection_dag.py @@ -0,0 +1,521 @@ +""" +SelectionDAG builder, combiner, and scheduler. + +DAGBuilder — Translates IR instructions into SelectionDAG nodes. +DAGCombiner — Peephole optimizations over the DAG (fold, simplify). +DAGScheduler — Schedules DAG into linearized MachineInstr list. +""" + +from __future__ import annotations + +from scratchv.ir.types import OpCode, Program, Function, BasicBlock, Instruction +from scratchv.codegen.sdnode import ( + MVT, SDNodeOpcode, SDNodeFlags, SDValue, SelectionDAG, +) +from scratchv.backend.register_alloc import MachineInstr, MachineOp, MachineOperand + + +# ═══════════════════════════════════════════════════════════ +# IR type → MVT mapping +# ═══════════════════════════════════════════════════════════ + +def _ir_to_mvt(dtype) -> MVT: + from scratchv.ir.types import DataType + return { + DataType.FLOAT32: MVT.f32, + DataType.FLOAT64: MVT.f64, + DataType.INT32: MVT.i32, + DataType.INT64: MVT.i64, + }.get(dtype, MVT.i32) + + +# ═══════════════════════════════════════════════════════════ +# DAGBuilder — IR → SelectionDAG +# ═══════════════════════════════════════════════════════════ + +class DAGBuilder: + """Build a SelectionDAG from a ScratchV IR Program.""" + + def __init__(self, program: Program): + self.program = program + self.dag = SelectionDAG() + # Maps IR value names → SDValue + self._value_map: dict[str, SDValue] = {} + self._chain = self.dag.entry_token + + def run(self) -> SelectionDAG: + """Build the DAG for all functions.""" + for func in self.program.functions: + self._build_function(func) + return self.dag + + def _build_function(self, func: Function) -> None: + self._value_map.clear() + self._chain = self.dag.entry_token + + # Map function parameters to CopyFromReg nodes + for i, param in enumerate(func.params): + reg = self.dag.get_register(f"a{i}" if i < 8 else f"s{i-8}") + val = self.dag.get_copy_from_reg(reg) + self._chain = val.node.get_chain() or self._chain + self._value_map[param.name] = val + + for block in func.blocks: + self._build_block(block, func) + + def _build_block(self, block: BasicBlock, func: Function) -> None: + for instr in block.instructions: + self._build_instruction(instr) + + def _build_instruction(self, instr: Instruction) -> None: + handler = getattr(self, f"_build_{instr.opcode.value}", None) + if handler is None: + raise ValueError(f"No DAG builder for opcode: {instr.opcode}") + handler(instr) + + def _get_val(self, ir_val) -> SDValue: + """Map an IR operand Value to an SDValue.""" + if ir_val.is_constant and ir_val.const_value is not None: + vt = _ir_to_mvt(ir_val.dtype) + if vt.is_float: + return self.dag.get_constant_fp(float(ir_val.const_value), vt) + return self.dag.get_constant(int(ir_val.const_value), vt) + name = ir_val.name + if name not in self._value_map: + self._value_map[name] = self.dag.get_undef(_ir_to_mvt(ir_val.dtype)) + return self._value_map[name] + + def _set_val(self, ir_val, sdval: SDValue) -> None: + self._value_map[ir_val.name] = sdval + + # ── Arithmetic ───────────────────────────────────── + + def _build_add(self, instr: Instruction) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + if lhs.value_type.is_float: + val = self.dag.get_fadd(lhs, rhs) + else: + val = self.dag.get_add(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_sub(self, instr: Instruction) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + if lhs.value_type.is_float: + val = self.dag.get_fsub(lhs, rhs) + else: + val = self.dag.get_sub(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_mul(self, instr: Instruction) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + if lhs.value_type.is_float: + val = self.dag.get_fmul(lhs, rhs) + else: + val = self.dag.get_mul(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_div(self, instr: Instruction) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + if lhs.value_type.is_float: + val = self.dag.get_fdiv(lhs, rhs) + else: + val = self.dag.get_div(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_neg(self, instr: Instruction) -> None: + src = self._get_val(instr.operands[0]) + # neg = sub 0, x or fneg x + if src.value_type.is_float: + zero = self.dag.get_constant_fp(0.0, src.value_type) + val = self.dag.get_fsub(zero, src) + else: + zero = self.dag.get_constant(0, src.value_type) + val = self.dag.get_sub(zero, src) + self._set_val(instr.dest, val) + + def _build_exp(self, instr: Instruction) -> None: + src = self._get_val(instr.operands[0]) + val = self.dag.get_call("expf" if src.value_type == MVT.f32 else "exp", + [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_load_const(self, instr: Instruction) -> None: + v = instr.attrs.get("value", 0) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32 + if vt.is_float: + val = self.dag.get_constant_fp(float(v), vt) + else: + val = self.dag.get_constant(int(v), vt) + self._set_val(instr.dest, val) + + def _build_load(self, instr: Instruction) -> None: + addr = self._get_val(instr.operands[0]) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.i32 + val = self.dag.get_load(addr, vt, chain=self._chain) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_store(self, instr: Instruction) -> None: + addr = self._get_val(instr.operands[0]) + val = self._get_val(instr.operands[1]) + chain = self.dag.get_store(addr, val, chain=self._chain) + self._chain = chain + + def _build_alloca(self, instr: Instruction) -> None: + size = instr.attrs.get("size", 4) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.i32 + # Represent as a constant pointer offset (from sp) + val = self.dag.get_constant(size, vt) + self._set_val(instr.dest, val) + + # ── Control flow ─────────────────────────────────── + + def _build_for(self, instr: Instruction) -> None: + iv = instr.dest + start = instr.attrs.get("start", 0) + end = instr.attrs.get("end", 0) + val = self.dag.get_constant(start, MVT.i32) + self._value_map[instr.dest.name] = val + # Store loop context for endfor + self._loop_ctx = { + "iv_name": iv.name, + "end": end, + } + + def _build_endfor(self, instr: Instruction) -> None: + ctx = getattr(self, "_loop_ctx", None) + if ctx is None: + return + iv_name = ctx["iv_name"] + iv = self._value_map.get(iv_name) + if iv is not None: + one = self.dag.get_constant(1, MVT.i32) + inc = self.dag.get_add(iv, one) + self._value_map[iv_name] = inc + self._loop_ctx = None + + def _build_br(self, instr: Instruction) -> None: + self._chain = self.dag.get_br(instr.target or "", chain=self._chain) + + def _build_br_if(self, instr: Instruction) -> None: + cond = self._get_val(instr.operands[0]) + targets = (instr.target or "").split(",") + true_t = targets[0].strip() if len(targets) > 0 else "" + false_t = targets[1].strip() if len(targets) > 1 else "" + self._chain = self.dag.get_br_cc(cond, true_t, false_t, chain=self._chain) + + def _build_return(self, instr: Instruction) -> None: + vals = [self._get_val(instr.operands[0])] if instr.operands else None + self._chain = self.dag.get_ret(vals, chain=self._chain) + + def _build_label(self, instr: Instruction) -> None: + pass # labels are implicit in DAG + + # ── NN ops ───────────────────────────────────────── + + def _build_relu(self, instr: Instruction) -> None: + src = self._get_val(instr.operands[0]) + val = self.dag.get_call("relu", [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_gelu(self, instr: Instruction) -> None: + src = self._get_val(instr.operands[0]) + val = self.dag.get_call("gelu", [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_softmax(self, instr: Instruction) -> None: + src = self._get_val(instr.operands[0]) + val = self.dag.get_call("softmax", [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_matmul(self, instr: Instruction) -> None: + a = self._get_val(instr.operands[0]) + b = self._get_val(instr.operands[1]) + m = instr.attrs.get("m", 1) + n = instr.attrs.get("n", 1) + k = instr.attrs.get("k", 1) + val = self.dag.get_call(f"matmul_m{m}_n{n}_k{k}", [a, b], + vt=_ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_dot(self, instr: Instruction) -> None: + a = self._get_val(instr.operands[0]) + b = self._get_val(instr.operands[1]) + length = instr.attrs.get("length", 1) + val = self.dag.get_call(f"dot_len{length}", [a, b], + vt=_ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + +# ═══════════════════════════════════════════════════════════ +# DAGCombiner — peephole optimizations on the DAG +# ═══════════════════════════════════════════════════════════ + +class DAGCombiner: + """DAG-level peephole optimizations: constant folding, redundant removal.""" + + def __init__(self, dag: SelectionDAG): + self.dag = dag + self._changed = False + + def run(self) -> int: + """Run all DAG combines. Returns number of folds applied.""" + n_folds = 0 + # Iterate until stable + for _ in range(32): # limit iterations + self._changed = False + for node in reversed(self.dag._nodes): + self._try_fold(node) + if self._changed: + n_folds += 1 + if not self._changed: + break + return n_folds + + def _try_fold(self, node) -> None: + """Try to fold a single node in-place.""" + handler = getattr(self, f"_fold_{node.opcode.value}", None) + if handler is not None: + handler(node) + + def _fold_ADD(self, node) -> None: + """Constant fold: add(const, const) -> const""" + lhs, rhs = self._get_const_binop(node) + if lhs is not None and rhs is not None: + val = self.dag.get_constant(lhs + rhs, node.value_type()) + self._replace_node(node, val) + + def _fold_SUB(self, node) -> None: + lhs, rhs = self._get_const_binop(node) + if lhs is not None and rhs is not None: + val = self.dag.get_constant(lhs - rhs, node.value_type()) + self._replace_node(node, val) + + def _fold_MUL(self, node) -> None: + lhs, rhs = self._get_const_binop(node) + if lhs is not None and rhs is not None: + val = self.dag.get_constant(lhs * rhs, node.value_type()) + self._replace_node(node, val) + + def _fold_DIV(self, node) -> None: + lhs, rhs = self._get_const_binop(node) + if lhs is not None and rhs is not None and rhs != 0: + val = self.dag.get_constant(lhs // rhs, node.value_type()) + self._replace_node(node, val) + + def _fold_FADD(self, node) -> None: + self._fold_fp_binop(node, lambda a, b: a + b) + + def _fold_FSUB(self, node) -> None: + self._fold_fp_binop(node, lambda a, b: a - b) + + def _fold_FMUL(self, node) -> None: + self._fold_fp_binop(node, lambda a, b: a * b) + + def _fold_FDIV(self, node) -> None: + self._fold_fp_binop(node, lambda a, b: a / b) + + def _fold_fp_binop(self, node, op) -> None: + lhs = self._get_fp_const(node, 0) + rhs = self._get_fp_const(node, 1) + if lhs is not None and rhs is not None: + try: + val = self.dag.get_constant_fp(op(lhs, rhs), node.value_type()) + self._replace_node(node, val) + except (ZeroDivisionError, OverflowError, ValueError): + pass + + def _get_const_binop(self, node): + """Return (lhs_int, rhs_int) if both operands are Constant.""" + if len(node.operands) < 2: + return None, None + lhs = node.operands[0].node.get_constant_int() + rhs = node.operands[1].node.get_constant_int() + return lhs, rhs + + def _get_fp_const(self, node, idx: int): + op = node.operands[idx] + return op.node.get_constant_fp() + + def _replace_node(self, old_node, new_val: SDValue) -> None: + """Replace all uses of old_node with new_val (simple).""" + old_node._attributes["replaced_by"] = new_val + self._changed = True + + +# ═══════════════════════════════════════════════════════════ +# DAGScheduler — DAG → linear MachineInstr list +# ═══════════════════════════════════════════════════════════ + +class DAGScheduler: + """Schedule a SelectionDAG into a linear sequence of MachineInstrs.""" + + def __init__(self, dag: SelectionDAG): + self.dag = dag + + def run(self) -> list[MachineInstr]: + """Topological schedule: emit nodes in dependency order.""" + scheduled: set[int] = set() + result: list[MachineInstr] = [] + label_counter = [0] + + def fresh_label(prefix="L"): + label_counter[0] += 1 + return f"{prefix}_{label_counter[0]}" + + def schedule_node(node, chain_token=None): + if node.node_id in scheduled: + return + # Schedule operands first (post-order DFS) + for op in node.operands: + if op.node.node_id not in scheduled: + schedule_node(op.node) + scheduled.add(node.node_id) + + opcode = node.opcode + try: + machine_op = _SDNODE_TO_MACHINE_OP[opcode] + except KeyError: + # Skip nodes without a direct MachineOp mapping + return + + dst = None + src1 = None + src2 = None + comment = "" + + if opcode == SDNodeOpcode.Constant: + dst = MachineOperand.vreg(f"t{node.node_id}") + val = node.get_constant_int() or 0 + result.append(MachineInstr( + MachineOp.LI, dst, MachineOperand.immediate(val), + comment=f"const {val}")) + return + + if opcode == SDNodeOpcode.ConstantFP: + dst = MachineOperand.vreg(f"t{node.node_id}") + val = node.get_constant_fp() or 0.0 + result.append(MachineInstr( + MachineOp.LI, dst, MachineOperand.immediate(int(val)), + comment=f"constfp {val}")) + return + + if opcode == SDNodeOpcode.CopyFromReg: + # Should be handled by register allocator + reg = node._get_attr("reg_name", "zero") + dst = MachineOperand.vreg(f"t{node.node_id}") + result.append(MachineInstr( + MachineOp.MV, dst, MachineOperand.reg(reg), + comment="copy_from_reg")) + return + + if opcode in (SDNodeOpcode.LOAD,): + dst = MachineOperand.vreg(f"t{node.node_id}") + src1 = _op_to_operand(node.operands[1], node, fresh_label) + result.append(MachineInstr( + MachineOp.LW, dst, src1, comment="load")) + return + + if opcode == SDNodeOpcode.STORE: + src1 = _op_to_operand(node.operands[1], node, fresh_label) + src2 = _op_to_operand(node.operands[2], node, fresh_label) + result.append(MachineInstr( + MachineOp.SW, src1, src2, comment="store")) + return + + if opcode == SDNodeOpcode.BR: + target = node._get_attr("branch_target", "") + result.append(MachineInstr( + MachineOp.J, comment=target)) + return + + if opcode == SDNodeOpcode.BR_CC: + cond = _op_to_operand(node.operands[1], node, fresh_label) + true_t = node._get_attr("true_target", "") + false_t = node._get_attr("false_target", "") + result.append(MachineInstr( + MachineOp.BNEZ, cond, comment=true_t)) + result.append(MachineInstr( + MachineOp.J, comment=false_t)) + return + + if opcode == SDNodeOpcode.RET: + result.append(MachineInstr( + MachineOp.JALR, MachineOperand.vreg("zero"), + MachineOperand.vreg("ra"), comment="ret")) + return + + if opcode == SDNodeOpcode.CALL: + callee = node._get_attr("callee", "unknown") + result.append(MachineInstr( + MachineOp.CALL, comment=callee)) + if node.num_values > 0: + dst = MachineOperand.vreg(f"t{node.node_id}") + result.append(MachineInstr( + MachineOp.MV, dst, MachineOperand.vreg("a0"))) + return + + # Generic binop emission + if len(node.operands) >= 2: + src1 = _op_to_operand(node.operands[0], node, fresh_label) + src2 = _op_to_operand(node.operands[1], node, fresh_label) + elif len(node.operands) >= 1: + src1 = _op_to_operand(node.operands[0], node, fresh_label) + + if node.num_values > 0 and node._num_values > node.num_chain_results: + dst = MachineOperand.vreg(f"t{node.node_id}") + + result.append(MachineInstr(machine_op, dst, src1, src2, comment)) + + # Schedule all nodes + for node in self.dag._nodes: + schedule_node(node) + + return result + + +def _op_to_operand(sdval: SDValue, parent_node, fresh_label_fn) -> MachineOperand: + """Convert SDValue to MachineOperand (vreg).""" + if sdval.node.opcode == SDNodeOpcode.Constant: + val = sdval.node.get_constant_int() or 0 + return MachineOperand.immediate(val) + if sdval.node.opcode == SDNodeOpcode.ConstantFP: + val = sdval.node.get_constant_fp() or 0.0 + return MachineOperand.immediate(int(val)) + if sdval.node.opcode == SDNodeOpcode.Register: + name = sdval.node._get_attr("reg_name", "zero") + return MachineOperand.reg(name) + return MachineOperand.vreg(f"t{sdval.node.node_id}") + + +_SDNODE_TO_MACHINE_OP: dict[SDNodeOpcode, MachineOp] = { + SDNodeOpcode.ADD: MachineOp.ADD, + SDNodeOpcode.SUB: MachineOp.SUB, + SDNodeOpcode.MUL: MachineOp.MUL, + SDNodeOpcode.DIV: MachineOp.DIV, + SDNodeOpcode.FADD: MachineOp.ADD, + SDNodeOpcode.FSUB: MachineOp.SUB, + SDNodeOpcode.FMUL: MachineOp.MUL, + SDNodeOpcode.FDIV: MachineOp.DIV, + SDNodeOpcode.NEG: MachineOp.SUB, + SDNodeOpcode.SETCC: MachineOp.SUB, # placeholder + SDNodeOpcode.LOAD: MachineOp.LW, + SDNodeOpcode.STORE: MachineOp.SW, + SDNodeOpcode.BR: MachineOp.J, + SDNodeOpcode.BR_CC: MachineOp.BNEZ, + SDNodeOpcode.RET: MachineOp.JALR, + SDNodeOpcode.CALL: MachineOp.CALL, + SDNodeOpcode.LI_Pseudo: MachineOp.LI, + SDNodeOpcode.MV_Pseudo: MachineOp.MV, + SDNodeOpcode.RELU: MachineOp.MAX, +} diff --git a/scratchv/ir/types.py b/scratchv/ir/types.py index a69ea06..1c0a3c9 100644 --- a/scratchv/ir/types.py +++ b/scratchv/ir/types.py @@ -8,7 +8,7 @@ import enum from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Union class OpCode(enum.Enum): @@ -77,17 +77,17 @@ def from_onnx(elem_type: int) -> DataType: return mapping.get(elem_type, DataType.FLOAT32) -@dataclass(slots=True) +@dataclass class Value: """An SSA-like typed value (result of an instruction or a function argument).""" name: str dtype: DataType = DataType.FLOAT32 is_constant: bool = False - const_value: Optional[float | int] = None + const_value: Optional[Union[float, int]] = None shape: tuple[int, ...] = () -@dataclass(slots=True) +@dataclass class Instruction: """A single three-address-code instruction.""" opcode: OpCode @@ -129,7 +129,7 @@ def __repr__(self) -> str: return "\n".join(lines) -@dataclass(slots=True) +@dataclass class Function: """An IR function: a collection of basic blocks forming a CFG.""" name: str diff --git a/scratchv/memory/__init__.py b/scratchv/memory/__init__.py new file mode 100644 index 0000000..d0f684e --- /dev/null +++ b/scratchv/memory/__init__.py @@ -0,0 +1,13 @@ +"""Memory module: cache simulation and memory allocation.""" +from scratchv.memory.cache import L1Cache, CacheConfig, CacheStats +from scratchv.memory.allocator import ( + MemoryAllocator, + AllocationPolicy, + MemoryRegion, + AllocStats, +) + +__all__ = [ + "L1Cache", "CacheConfig", "CacheStats", + "MemoryAllocator", "AllocationPolicy", "MemoryRegion", "AllocStats", +] diff --git a/scratchv/memory/allocator.py b/scratchv/memory/allocator.py new file mode 100644 index 0000000..e5c97d1 --- /dev/null +++ b/scratchv/memory/allocator.py @@ -0,0 +1,361 @@ +""" +Cache-aware memory allocator for edge NPU. + +Implements: +- Buddy allocator for configurable memory pool sizes +- Cache-line-aligned allocation for L1 cache-friendly access patterns +- Scratchpad region for explicit DMA/tile memory +- Allocation tracking and statistics +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional + + +class AllocationPolicy(Enum): + """Memory allocation strategy.""" + FIRST_FIT = "first_fit" + BEST_FIT = "best_fit" + BUDDY = "buddy" + + +@dataclass(slots=True) +class MemoryRegion: + """A contiguous memory region.""" + name: str + base: int # base address (byte offset from pool start) + size: int # size in bytes + used: bool = False + alignment: int = 4 # required alignment + + @property + def end(self) -> int: + return self.base + self.size + + def __repr__(self) -> str: + status = "used" if self.used else "free" + return (f"Region({self.name}: 0x{self.base:x}-0x{self.end:x}, " + f"{self.size}B, {status}, align={self.alignment})") + + +@dataclass(slots=True) +class AllocStats: + """Allocation statistics.""" + total_allocated: int = 0 + total_freed: int = 0 + num_allocs: int = 0 + num_frees: int = 0 + largest_free_block: int = 0 + fragmentation_pct: float = 0.0 + cache_misses_avoided: int = 0 # from aligned allocations + + def __repr__(self) -> str: + return (f"AllocStats(allocated={self.total_allocated}, " + f"freed={self.total_freed}, " + f"active={self.num_allocs - self.num_frees}, " + f"largest_free={self.largest_free_block}, " + f"frag={self.fragmentation_pct:.1f}%)") + + +# ═══════════════════════════════════════════════════════════ +# MemoryAllocator +# ═══════════════════════════════════════════════════════════ + +class MemoryAllocator: + """Cache-aware memory allocator with buddy system and alignment support. + + The pool is divided into a scratchpad region (fast, explicit DMA) and + a general-purpose region (cached). All allocations are cache-line-aligned + by default (64B) to avoid L1 cache line ping-pong. + """ + + def __init__( + self, + pool_size: int = 4 * 1024 * 1024, # 4 MB total + cache_line: int = 64, # L1 cache line size + scratchpad_ratio: float = 0.25, # 25% for scratchpad + policy: AllocationPolicy = AllocationPolicy.BUDDY, + ): + self.pool_size = pool_size + self.cache_line = cache_line + self.policy = policy + self.stats = AllocStats() + + # Split pool: scratchpad (high-speed, uncached) + general (cached) + scratch_size = int(pool_size * scratchpad_ratio) + # Align scratchpad size to cache line + scratch_size = self._align_up(scratch_size, cache_line) + gen_size = pool_size - scratch_size + + self.scratchpad = MemoryRegion("scratchpad", 0, scratch_size) + self._regions: list[MemoryRegion] = [ + MemoryRegion("general", scratch_size, gen_size), + ] + self._freed_regions: list[MemoryRegion] = [] + self._next_id = 0 + + # Scratchpad cursor (next free address) + self._scratchpad_cursor = 0 + + # For buddy: power-of-two free lists + self._buddy_free: dict[int, list[int]] = {} # size -> list of base addrs + self._buddy_allocated: dict[int, int] = {} # id -> base addr + + # Populate buddy free list + if policy == AllocationPolicy.BUDDY: + self._init_buddy(gen_size) + + # ── Public API ───────────────────────────────────── + + def alloc(self, size: int, alignment: int = 0, + prefer_scratchpad: bool = False) -> int: + """Allocate `size` bytes. Returns base address (offset from pool start). + + Args: + size: Requested size in bytes. + alignment: Required alignment (0 = use cache_line default). + prefer_scratchpad: If True, try scratchpad region first. + + Returns: + Base offset, or -1 if allocation fails. + """ + alignment = alignment or self.cache_line + size = self._align_up(size, alignment) + + if prefer_scratchpad: + aligned_base = self._align_up(self._scratchpad_cursor, alignment) + if aligned_base + size <= self.scratchpad.size: + self._scratchpad_cursor = aligned_base + size + return aligned_base + # fall through to general pool + + if self.policy == AllocationPolicy.BUDDY: + addr = self._buddy_alloc(size) + else: + gen = self._regions[0] + cursor = getattr(self, "_general_cursor", gen.base) + aligned_base = self._align_up(cursor, alignment) + if aligned_base + size <= gen.end: + self._general_cursor = aligned_base + size + addr = aligned_base + else: + addr = -1 + + if addr >= 0: + self.stats.total_allocated += size + self.stats.num_allocs += 1 + # If aligned to cache line, we avoided a potential false-sharing miss + if alignment >= self.cache_line: + self.stats.cache_misses_avoided += 1 + + return addr + + def free(self, addr: int) -> bool: + """Free a previously allocated block. + + Returns True if the address was freed successfully. + """ + # Check scratchpad + if self._addr_in_region(addr, self.scratchpad): + return True # scratchpad doesn't track individual frees + + if self.policy == AllocationPolicy.BUDDY: + return self._buddy_free_block(addr) + + # Linear scan for first-fit segments + for i, region in enumerate(self._regions): + if region.base == addr and region.used: + region.used = False + self._freed_regions.append(region) + self.stats.total_freed += region.size + self.stats.num_frees += 1 + # Coalesce adjacent free regions + self._coalesce() + return True + return False + + def scratchpad_alloc(self, size: int, alignment: int = 64) -> int: + """Allocate from the scratchpad (uncached, fast SRAM).""" + return self.alloc(size, alignment, prefer_scratchpad=True) + + def get_region_info(self, addr: int) -> Optional[MemoryRegion]: + """Get info about which region an address belongs to.""" + if self._addr_in_region(addr, self.scratchpad): + return self.scratchpad + for region in self._regions: + if self._addr_in_region(addr, region): + return region + return None + + def is_in_scratchpad(self, addr: int) -> bool: + return self._addr_in_region(addr, self.scratchpad) + + def reset(self) -> None: + """Reset all allocations.""" + self._scratchpad_cursor = 0 + gen_size = self.pool_size - self.scratchpad.size + self._regions = [MemoryRegion("general", self.scratchpad.size, gen_size)] + self._freed_regions.clear() + self.stats = AllocStats() + self._next_id = 0 + if self.policy == AllocationPolicy.BUDDY: + self._init_buddy(gen_size) + + # ── Buddy allocator ──────────────────────────────── + + def _init_buddy(self, total_size: int) -> None: + self._buddy_free.clear() + self._buddy_allocated.clear() + # Find the largest power of two <= total_size + max_pow2 = 1 << (total_size.bit_length() - 1) + base = self._regions[0].base + self._buddy_free[max_pow2] = [base] + # Add remaining chunk as a smaller block + remainder = total_size - max_pow2 + if remainder > 0: + pow2 = 1 << (remainder.bit_length() - 1) + self._buddy_free[pow2] = [base + max_pow2] + + def _buddy_alloc(self, size: int) -> int: + """Allocate using buddy system. + + Rounds up size to the next power of two, finds a free block + of that size, splitting larger blocks as needed. + """ + block_size = 1 << (max(size, self.cache_line).bit_length() - 1) + if block_size < size: + block_size <<= 1 + + # Find an available block of suitable size + available_sizes = sorted(s for s in self._buddy_free if self._buddy_free[s]) + if not available_sizes: + return -1 + + # Find smallest available size >= block_size + chosen_size = None + for s in available_sizes: + if s >= block_size: + chosen_size = s + break + + if chosen_size is None: + return -1 + + # Split until we get the target size + free_list = self._buddy_free[chosen_size] + addr = free_list.pop(0) + + while chosen_size > block_size: + chosen_size >>= 1 + buddy_addr = addr + chosen_size + self._buddy_free.setdefault(chosen_size, []).append(buddy_addr) + + self._buddy_allocated[addr] = block_size + return addr + + def _buddy_free_block(self, addr: int) -> bool: + """Free a buddy-allocated block, coalescing with its buddy.""" + block_size = self._buddy_allocated.pop(addr, None) + if block_size is None: + return False + + self._buddy_free.setdefault(block_size, []).append(addr) + + # Coalesce: repeatedly merge with buddy if both are free + while True: + free_list = self._buddy_free[block_size] + buddy_addr = addr ^ block_size # XOR to find buddy + if buddy_addr in free_list: + free_list.remove(buddy_addr) + addr = min(addr, buddy_addr) + block_size <<= 1 + self._buddy_free.setdefault(block_size, []).append(addr) + self.stats.total_freed += block_size // 2 + else: + break + + self.stats.total_freed += block_size + self.stats.num_frees += 1 + return True + + # ── First-fit / Best-fit helpers ─────────────────── + + def _alloc_from_region(self, region: MemoryRegion, + size: int, alignment: int) -> int: + """Allocate from a region using first-fit. + + Never mutates the input region's base/size; tracks the cursor + in a caller-owned variable. + """ + # Allocate from the general region by managing a cursor + cursor = getattr(self, "_general_cursor", region.base) + aligned_base = self._align_up(cursor, alignment) + if aligned_base + size <= region.end: + self._general_cursor = aligned_base + size + return aligned_base + return -1 + + def _coalesce(self) -> None: + """Coalesce adjacent free regions.""" + free_regs = sorted( + [r for r in self._freed_regions if not r.used], + key=lambda r: r.base, + ) + self._freed_regions = [r for r in self._freed_regions if r.used] + + merged = [] + for r in free_regs: + if merged and merged[-1].end == r.base: + merged[-1] = MemoryRegion( + merged[-1].name, merged[-1].base, + merged[-1].size + r.size, + ) + else: + merged.append(r) + self._freed_regions.extend(merged) + + # ── Utilities ────────────────────────────────────── + + @staticmethod + def _align_up(addr: int, alignment: int) -> int: + if alignment <= 0: + alignment = 4 + mask = alignment - 1 + return (addr + mask) & ~mask + + @staticmethod + def _addr_in_region(addr: int, region: MemoryRegion) -> bool: + return region.base <= addr < region.base + region.size + + # ── Debug ────────────────────────────────────────── + + def dump(self) -> str: + lines = [ + f"Memory Allocator ({self.pool_size >> 20} MB pool, " + f"policy={self.policy.value}, " + f"cache_line={self.cache_line}B):", + f" Scratchpad: {self.scratchpad} ({self.align_up(0, 0)})", + f" Regions ({len(self._regions)}):", + ] + for r in self._regions: + lines.append(f" {r}") + if self._freed_regions: + lines.append(f" Freed regions ({len(self._freed_regions)}):") + for r in self._freed_regions[:8]: + lines.append(f" {r}") + if len(self._freed_regions) > 8: + lines.append(f" ... (+{len(self._freed_regions)-8})") + if self.policy == AllocationPolicy.BUDDY: + lines.append(f" Buddy free lists:") + for size, addrs in sorted(self._buddy_free.items()): + if addrs: + lines.append(f" {size}B: {len(addrs)} blocks") + lines.append(f" Stats: {self.stats}") + return "\n".join(lines) + + def align_up(self, addr: int, alignment: int = 0) -> int: + return self._align_up(addr, alignment or self.cache_line) diff --git a/scratchv/memory/cache.py b/scratchv/memory/cache.py new file mode 100644 index 0000000..5d5e9e6 --- /dev/null +++ b/scratchv/memory/cache.py @@ -0,0 +1,265 @@ +""" +L1 cache simulator for edge NPU. + +Models a 4 MB L1 cache with configurable line size, associativity, +and replacement policy. Tracks hits, misses, evictions and bandwidth. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass(slots=True) +class CacheConfig: + """Configuration for the L1 cache.""" + total_size: int = 4 * 1024 * 1024 # 4 MB + line_size: int = 64 # bytes per cache line + associativity: int = 8 # N-way set associative + write_back: bool = True # True = write-back, False = write-through + write_allocate: bool = True # allocate on write miss + hit_latency: int = 2 # cycles (typical L1) + miss_latency: int = 20 # cycles (penalty to go to L2/DRAM) + + @property + def num_lines(self) -> int: + return self.total_size // self.line_size + + @property + def num_sets(self) -> int: + return self.num_lines // self.associativity + + def __post_init__(self): + assert self.total_size > 0 and self.total_size % self.line_size == 0 + assert self.line_size > 0 and (self.line_size & (self.line_size - 1)) == 0 + assert self.associativity > 0 + assert self.num_sets > 0 + + +@dataclass(slots=True) +class CacheStats: + """Cache performance counters.""" + hits: int = 0 + misses: int = 0 + evictions: int = 0 + write_backs: int = 0 + total_cycles: int = 0 + bytes_read: int = 0 + bytes_written: int = 0 + + @property + def hit_rate(self) -> float: + total = self.hits + self.misses + return self.hits / total if total > 0 else 0.0 + + @property + def miss_rate(self) -> float: + total = self.hits + self.misses + return self.misses / total if total > 0 else 0.0 + + @property + def avg_latency(self) -> float: + total = self.hits + self.misses + return self.total_cycles / total if total > 0 else 0.0 + + def reset(self) -> None: + self.hits = 0 + self.misses = 0 + self.evictions = 0 + self.write_backs = 0 + self.total_cycles = 0 + self.bytes_read = 0 + self.bytes_written = 0 + + def __repr__(self) -> str: + return (f"CacheStats(hits={self.hits}, misses={self.misses}, " + f"hit_rate={self.hit_rate:.2%}, evictions={self.evictions}, " + f"write_backs={self.write_backs}, " + f"avg_latency={self.avg_latency:.1f}cy)") + + +# ═══════════════════════════════════════════════════════════ +# Cache line +# ═══════════════════════════════════════════════════════════ + +@dataclass(slots=True) +class CacheLine: + """A single cache line.""" + tag: int = 0 + valid: bool = False + dirty: bool = False + last_access: int = 0 # for LRU + + def __repr__(self) -> str: + return (f"Line(tag=0x{self.tag:x}, valid={self.valid}, " + f"dirty={self.dirty}, lru={self.last_access})") + + +# ═══════════════════════════════════════════════════════════ +# L1Cache +# ═══════════════════════════════════════════════════════════ + +class L1Cache: + """Set-associative L1 cache simulator. + + Usage: + cache = L1Cache() + cache.read(0x1000, 4) # read 4 bytes from addr 0x1000 + cache.write(0x1000, 4) # write 4 bytes to addr 0x1000 + print(cache.stats) + """ + + def __init__(self, config: CacheConfig | None = None): + self.config = config or CacheConfig() + self.stats = CacheStats() + self._clock = 0 + + # Build cache: list of sets, each set has N lines + self._sets: list[list[CacheLine]] = [ + [CacheLine() for _ in range(self.config.associativity)] + for _ in range(self.config.num_sets) + ] + + self._mask_offset = int(math.log2(self.config.line_size)) + self._mask_index = int(math.log2(self.config.num_sets)) + self._tag_shift = self._mask_offset + self._mask_index + + # ── Public API ───────────────────────────────────── + + def read(self, addr: int, size: int = 4) -> int: + """Read `size` bytes from `addr`. Returns total latency.""" + latency = 0 + start_line = addr // self.config.line_size + end_line = (addr + size - 1) // self.config.line_size + + for line_addr in range(start_line, end_line + 1): + block_addr = line_addr * self.config.line_size + latency += self._access_line(block_addr, is_write=False) + + if size > self.config.line_size: + latency += self.config.miss_latency # cross-line penalty + + self.stats.total_cycles += latency + self.stats.bytes_read += size + return latency + + def write(self, addr: int, size: int = 4) -> int: + """Write `size` bytes to `addr`. Returns total latency.""" + latency = 0 + start_line = addr // self.config.line_size + end_line = (addr + size - 1) // self.config.line_size + + for line_addr in range(start_line, end_line + 1): + block_addr = line_addr * self.config.line_size + latency += self._access_line(block_addr, is_write=True) + + if size > self.config.line_size: + latency += self.config.miss_latency + + self.stats.total_cycles += latency + self.stats.bytes_written += size + return latency + + def flush(self) -> int: + """Flush all dirty lines. Returns total cycles.""" + cycles = 0 + for set_idx in range(self.config.num_sets): + for line in self._sets[set_idx]: + if line.valid and line.dirty: + cycles += self.config.miss_latency + self.stats.write_backs += 1 + line.dirty = False + self.stats.total_cycles += cycles + return cycles + + def reset(self) -> None: + """Reset cache state and stats.""" + for set_idx in range(self.config.num_sets): + for line in self._sets[set_idx]: + line.valid = False + line.dirty = False + line.tag = 0 + line.last_access = 0 + self.stats.reset() + self._clock = 0 + + # ── Internals ────────────────────────────────────── + + def _addr_to_set_tag(self, addr: int) -> tuple[int, int]: + """Extract (set_index, tag) from an address.""" + set_idx = (addr >> self._mask_offset) & (self.config.num_sets - 1) + tag = addr >> self._tag_shift + return set_idx, tag + + def _access_line(self, block_addr: int, is_write: bool) -> int: + """Access a single cache line. Returns latency.""" + self._clock += 1 + set_idx, tag = self._addr_to_set_tag(block_addr) + line_set = self._sets[set_idx] + + # Look for a hit + for line in line_set: + if line.valid and line.tag == tag: + # Cache hit + self.stats.hits += 1 + line.last_access = self._clock + if is_write and self.config.write_back: + line.dirty = True + return self.config.hit_latency + + # Cache miss + self.stats.misses += 1 + + if not self.config.write_allocate and is_write: + # Write-no-allocate: skip cache, go to next level + return self.config.miss_latency + + # Find an eviction candidate (LRU) + victim = self._find_lru(line_set) + assert victim is not None + + # Write back if dirty + if victim.valid and victim.dirty: + self.stats.write_backs += 1 + self.stats.evictions += 1 + + # Fill the line + victim.tag = tag + victim.valid = True + victim.dirty = is_write and self.config.write_back + victim.last_access = self._clock + + return self.config.hit_latency + self.config.miss_latency + + def _find_lru(self, line_set: list[CacheLine]) -> CacheLine: + """Find the least-recently-used line in a set.""" + lru_line = line_set[0] + lru_time = lru_line.last_access + for line in line_set[1:]: + if not line.valid: + return line # empty slot + if line.last_access < lru_time: + lru_time = line.last_access + lru_line = line + return lru_line + + # ── Debug ────────────────────────────────────────── + + def dump(self) -> str: + lines = [ + f"L1 Cache ({self.config.total_size >> 20} MB, " + f"{self.config.line_size}B lines, " + f"{self.config.associativity}-way):", + f" Sets: {self.config.num_sets}, Lines: {self.config.num_lines}", + f" Stats: {self.stats}", + ] + # Print first few non-empty sets + shown = 0 + for set_idx in range(self.config.num_sets): + valid_lines = [l for l in self._sets[set_idx] if l.valid] + if valid_lines and shown < 8: + lines.append(f" Set {set_idx}: {valid_lines}") + shown += 1 + return "\n".join(lines) diff --git a/scratchv_dag/README.md b/scratchv_dag/README.md new file mode 100644 index 0000000..94c3c83 --- /dev/null +++ b/scratchv_dag/README.md @@ -0,0 +1,159 @@ +# scratchv_dag — LLVM-Style SelectionDAG & Cache-Aware Memory Allocator + +**scratchv_dag** is a standalone Python package providing DAG-based instruction selection infrastructure inspired by LLVM's SelectionDAG, paired with a 4 MB L1 cache simulator and a buddy-system memory allocator designed for edge-NPU compiler toolchains. + +It operates independently or as part of the [ScratchV](https://github.com/kinsomwang/ScratchV) ONNX→RISC-V compiler. + +--- + +## Package Structure + +``` +scratchv_dag/ +├── __init__.py # Public API re-exports +├── sdnode.py # Core DAG types: MVT, SDNodeOpcode, SDNode, SelectionDAG +├── selection_dag.py # DAGBuilder, DAGCombiner, DAGScheduler +├── cache.py # 4 MB L1 cache simulator (LRU, write-back) +├── allocator.py # Buddy-system memory allocator with scratchpad +└── README.md +``` + +--- + +## Modules + +### `sdnode` — SelectionDAG Core Types + +LLVM-inspired DAG node representation: + +| Type | Role | +|---|---| +| `MVT` | Machine Value Type (`i8`–`i64`, `f32`, `f64`, `Other`, `Void`) | +| `SDNodeOpcode` | 40+ node opcodes (arithmetic, memory, control, NN, RISC-V pseudo) | +| `SDNodeFlags` | Per-node flags (fast-math, volatile, alignment) | +| `SDValue` | Edge reference `(SDNode, result_index)` | +| `SDNode` | DAG node with opcode, result types, operand edges, and chain support | +| `SelectionDAG` | Node container with factory methods and deduplication | + +### `selection_dag` — DAG Pipeline + +Three stages transform IR → DAG → machine instructions: + +``` +┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌──────────────┐ +│ IR Insn │───▶│ DAGBuilder │───▶│ DAGCombiner │───▶│ DAGScheduler │ +└──────────┘ └───────────┘ └─────────────┘ └──────────────┘ + │ + ▼ + MachineInstr[] +``` + +- **DAGBuilder** — visits each IR instruction and builds the corresponding DAG sub-graph. +- **DAGCombiner** — peephole optimisations over the DAG (constant folding for integer and FP arithmetic). +- **DAGScheduler** — post-order topological sort that linearises the DAG into a `MachineInstr` list ready for register allocation. + +### `cache` — 4 MB L1 Cache Simulator + +Models a set-associative L1 data cache for edge-NPU performance estimation. + +**Default configuration:** + +| Parameter | Value | +|---|---| +| Capacity | 4 MB | +| Line size | 64 B | +| Associativity | 8-way | +| Write policy | Write-back + write-allocate | +| Hit latency | 2 cycles | +| Miss latency | 20 cycles | + +```python +from scratchv_dag import L1Cache + +cache = L1Cache() +cache.read(0x1000, 4) # → latency in cycles +cache.write(0x2000, 8) # → latency in cycles +print(cache.stats) # CacheStats(hits=..., hit_rate=...) +``` + +All parameters are configurable via `CacheConfig`: + +```python +from scratchv_dag import L1Cache, CacheConfig + +cfg = CacheConfig(total_size=2*1024*1024, associativity=4) +cache = L1Cache(cfg) +``` + +### `allocator` — Cache-Aware Memory Allocator + +Buddy-system allocator with L1-cache-line alignment and a scratchpad region for explicit DMA. + +**Pool layout (default 4 MB):** + +``` +0x000000 ┌──────────────────────────────┐ + │ Scratchpad (1 MB, 25 %) │ ← uncached SRAM +0x100000 ├──────────────────────────────┤ + │ General (3 MB, 75 %) │ ← buddy-managed, cached +0x400000 └──────────────────────────────┘ +``` + +```python +from scratchv_dag import MemoryAllocator, AllocationPolicy + +alloc = MemoryAllocator(pool_size=4*1024*1024) + +a = alloc.alloc(4096) # 64 B aligned +b = alloc.alloc(256, alignment=4096) # 4K page aligned +s = alloc.scratchpad_alloc(1024) # from scratchpad SRAM + +alloc.free(a) +``` + +**Why cache-line alignment?** Edge NPUs often share cache lines across processing elements. Misaligned allocations cause false sharing and expensive L1 evictions. Defaulting to 64 B alignment avoids this at zero extra cost. + +--- + +## Quick Start + +```python +from scratchv_dag.sdnode import SelectionDAG, MVT + +dag = SelectionDAG() +a = dag.get_constant(42, MVT.i32) +b = dag.get_constant(10, MVT.i32) +c = dag.get_add(a, b) +print(dag.dump()) +``` + +```python +from scratchv_dag.cache import L1Cache +from scratchv_dag.allocator import MemoryAllocator + +# Simulate a cache-friendly access pattern +cache = L1Cache() +for _ in range(10): + for i in range(32): + cache.read(i * 64, 4) +print(f"Hit rate: {cache.stats.hit_rate:.1%}") + +# Allocate memory for two tensors +alloc = MemoryAllocator() +tensor_a = alloc.alloc(512 * 512 * 4) # 512×512 f32 +tensor_b = alloc.alloc(512 * 512 * 4) +``` + +--- + +## Python Compatibility + +Requires **Python 3.8+**. The package is pure Python with no runtime dependencies beyond the standard library. + +(When used with ScratchV, `onnx`, `numpy`, and `protobuf` are needed for ONNX parsing.) + +--- + +## License + +Same as ScratchV — see the [LICENSE](../LICENSE) file. diff --git a/scratchv_dag/__init__.py b/scratchv_dag/__init__.py new file mode 100644 index 0000000..b2f8522 --- /dev/null +++ b/scratchv_dag/__init__.py @@ -0,0 +1,57 @@ +""" +scratchv_dag — LLVM-style SelectionDAG infrastructure with cache-aware memory allocation. + +This package provides a DAG-based instruction selection framework inspired by +LLVM's SelectionDAG, plus a 4 MB L1 cache simulator and a buddy-system memory +allocator designed for edge NPU scenarios. It operates as a standalone component +or as part of the ScratchV compiler toolchain. + +Submodules: + sdnode Core SDNode / SelectionDAG types (opcodes, MVT, DAG container). + selection_dag DAG builder (IR → DAG), DAG combiner (constant folding), + and DAG scheduler (DAG → machine instructions). + cache 4 MB set-associative L1 cache simulator with LRU replacement. + allocator Buddy-system memory allocator with cache-line alignment + and scratchpad region support. +""" + +from __future__ import annotations + +from scratchv_dag.sdnode import ( + MVT, + SDNodeOpcode, + SDNodeFlags, + SDValue, + SDNode, + SelectionDAG, +) +from scratchv_dag.selection_dag import ( + DAGBuilder, + DAGCombiner, + DAGScheduler, +) +from scratchv_dag.cache import ( + L1Cache, + CacheConfig, + CacheStats, +) +from scratchv_dag.allocator import ( + MemoryAllocator, + AllocationPolicy, + MemoryRegion, + AllocStats, +) + +__all__ = [ + # sdnode + "MVT", "SDNodeOpcode", "SDNodeFlags", "SDValue", "SDNode", + "SelectionDAG", + # selection_dag + "DAGBuilder", "DAGCombiner", "DAGScheduler", + # cache + "L1Cache", "CacheConfig", "CacheStats", + # allocator + "MemoryAllocator", "AllocationPolicy", "MemoryRegion", "AllocStats", +] + +__version__ = "0.1.0" diff --git a/scratchv_dag/allocator.py b/scratchv_dag/allocator.py new file mode 100644 index 0000000..33b326a --- /dev/null +++ b/scratchv_dag/allocator.py @@ -0,0 +1,396 @@ +""" +Cache-aware memory allocator for edge NPU. + +Implements three allocation strategies: + +* **Buddy** (default) — power-of-two block splitting and coalescing. + Fast and low-fragmentation for typical NPU tensor sizes. +* **First-fit** — simple bump-pointer allocation with freed-region reuse. + +All allocations are aligned to the L1 cache line size (64 B) by default +to avoid false sharing. A **scratchpad** region (first 25 % of the pool) +models on-chip SRAM for explicit DMA / tile transfers. + +The allocator is *address-based* — it manages offsets into a fixed-size +pool and does not interact with actual OS memory mapping. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from enum import Enum +from typing import Dict, List, Optional, Tuple + + +# ═══════════════════════════════════════════════════════════════════════════════ +# AllocationPolicy +# ═══════════════════════════════════════════════════════════════════════════════ + +class AllocationPolicy(Enum): + """Strategy used by the memory allocator.""" + FIRST_FIT = "first_fit" + """Simple bump-pointer allocation through the general region.""" + BUDDY = "buddy" + """Buddy-system: power-of-two blocks, split, and coalesce.""" + + +# ═══════════════════════════════════════════════════════════════════════════════ +# MemoryRegion +# ═══════════════════════════════════════════════════════════════════════════════ + +@dataclass +class MemoryRegion: + """A contiguous range of memory within the pool. + + Attributes: + name: Human-readable label. + base: Base offset (bytes from pool start). + size: Size in bytes. + used: Whether this region is currently allocated. + alignment: Required alignment constraint. + """ + + name: str + base: int + size: int + used: bool = False + alignment: int = 4 + + @property + def end(self) -> int: + """Exclusive end offset.""" + return self.base + self.size + + def __repr__(self) -> str: + status = "used" if self.used else "free" + return ( + f"Region({self.name}: 0x{self.base:x}-0x{self.end:x}, " + f"{self.size} B, {status}, align={self.alignment})" + ) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# AllocStats +# ═══════════════════════════════════════════════════════════════════════════════ + +@dataclass +class AllocStats: + """Allocation statistics tracked by the allocator.""" + total_allocated: int = 0 + total_freed: int = 0 + num_allocs: int = 0 + num_frees: int = 0 + largest_free_block: int = 0 + fragmentation_pct: float = 0.0 + cache_misses_avoided: int = 0 + + def __repr__(self) -> str: + active = self.num_allocs - self.num_frees + return ( + f"AllocStats(allocated={self.total_allocated}, " + f"freed={self.total_freed}, active={active}, " + f"largest_free={self.largest_free_block}, " + f"frag={self.fragmentation_pct:.1f}%)" + ) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# MemoryAllocator +# ═══════════════════════════════════════════════════════════════════════════════ + +class MemoryAllocator: + """Cache-aware memory allocator with buddy system and scratchpad region. + + The 4 MB pool is split:: + + [ scratchpad (25 %) ] [ general-purpose (75 %) ] + ↑ uncached / DMA ↑ cached, buddy-managed + + All allocations are aligned to *cache_line* (default 64 B) to + avoid L1 cache line bouncing between NPU tiles. + + Usage:: + + alloc = MemoryAllocator(pool_size=4*1024*1024) + a = alloc.alloc(4096) # aligned to 64 B + b = alloc.alloc(256, alignment=4096) # page-aligned + s = alloc.scratchpad_alloc(1024) # from scratchpad + alloc.free(a) + """ + + __slots__ = ( + "pool_size", "cache_line", "policy", "stats", + "scratchpad", "_regions", "_freed_regions", + "_next_id", "_scratchpad_cursor", "_general_cursor", + "_buddy_free", "_buddy_allocated", + ) + + def __init__( + self, + pool_size: int = 4 * 1024 * 1024, + cache_line: int = 64, + scratchpad_ratio: float = 0.25, + policy: AllocationPolicy = AllocationPolicy.BUDDY, + ) -> None: + self.pool_size = pool_size + self.cache_line = cache_line + self.policy = policy + self.stats = AllocStats() + + # Split the pool. + scratch_size = int(pool_size * scratchpad_ratio) + scratch_size = self._align_up(scratch_size, cache_line) + gen_size = pool_size - scratch_size + + self.scratchpad = MemoryRegion("scratchpad", 0, scratch_size) + self._regions: List[MemoryRegion] = [ + MemoryRegion("general", scratch_size, gen_size), + ] + self._freed_regions: List[MemoryRegion] = [] + self._next_id = 0 + + # Cursors + self._scratchpad_cursor = 0 + self._general_cursor = self._regions[0].base + + # Buddy free lists: block_size → [base_addr, …] + self._buddy_free: Dict[int, List[int]] = {} + # Allocated: base_addr → block_size + self._buddy_allocated: Dict[int, int] = {} + + if policy == AllocationPolicy.BUDDY: + self._init_buddy(gen_size) + + # ── Public API ───────────────────────────────────────────────────────── + + def alloc( + self, + size: int, + alignment: int = 0, + prefer_scratchpad: bool = False, + ) -> int: + """Allocate *size* bytes. + + Args: + size: Requested size in bytes. + alignment: Required alignment (0 → *cache_line* default). + prefer_scratchpad: If True, try the scratchpad region first. + + Returns: + Base offset from pool start, or **-1** on failure. + """ + alignment = alignment or self.cache_line + size = self._align_up(size, alignment) + + # Try scratchpad first if requested. + if prefer_scratchpad: + aligned = self._align_up(self._scratchpad_cursor, alignment) + if aligned + size <= self.scratchpad.end: + self._scratchpad_cursor = aligned + size + self._update_stats(size, alignment) + return aligned + # fall through to general pool + + # General pool. + if self.policy == AllocationPolicy.BUDDY: + addr = self._buddy_alloc(size) + else: + aligned = self._align_up(self._general_cursor, alignment) + if aligned + size <= self._regions[0].end: + self._general_cursor = aligned + size + addr = aligned + else: + addr = -1 + + if addr >= 0: + self._update_stats(size, alignment) + return addr + + def free(self, addr: int) -> bool: + """Release a previously allocated block. + + Returns ``True`` if the address was recognised and freed. + """ + # Scratchpad frees are a no-op (no individual tracking). + if self._addr_in_region(addr, self.scratchpad): + return True + + if self.policy == AllocationPolicy.BUDDY: + return self._buddy_free_block(addr) + + # First-fit: linear scan for a matching used region. + for region in self._regions: + if region.base == addr and region.used: + region.used = False + self._freed_regions.append(region) + self.stats.total_freed += region.size + self.stats.num_frees += 1 + self._coalesce() + return True + return False + + def scratchpad_alloc(self, size: int, alignment: int = 64) -> int: + """Shorthand for allocating from the scratchpad (uncached SRAM).""" + return self.alloc(size, alignment, prefer_scratchpad=True) + + def is_in_scratchpad(self, addr: int) -> bool: + """Check whether *addr* falls within the scratchpad region.""" + return self._addr_in_region(addr, self.scratchpad) + + def get_region_info(self, addr: int) -> Optional[MemoryRegion]: + """Return the region metadata for *addr*, or ``None``.""" + if self._addr_in_region(addr, self.scratchpad): + return self.scratchpad + for r in self._regions: + if self._addr_in_region(addr, r): + return r + for r in self._freed_regions: + if self._addr_in_region(addr, r): + return r + return None + + def reset(self) -> None: + """Reset all state — all memory becomes free again.""" + self._scratchpad_cursor = 0 + gen = self._regions[0] + gen_size = self.pool_size - self.scratchpad.size + self._regions = [MemoryRegion("general", self.scratchpad.size, gen_size)] + self._freed_regions.clear() + self._general_cursor = self._regions[0].base + self.stats = AllocStats() + self._buddy_free.clear() + self._buddy_allocated.clear() + if self.policy == AllocationPolicy.BUDDY: + self._init_buddy(gen_size) + + # ── Buddy system ─────────────────────────────────────────────────────── + + def _init_buddy(self, total_size: int) -> None: + """Seed the buddy free lists from a contiguous region.""" + self._buddy_free.clear() + self._buddy_allocated.clear() + base = self._regions[0].base + + max_pow2 = 1 << (total_size.bit_length() - 1) + self._buddy_free[max_pow2] = [base] + + remainder = total_size - max_pow2 + if remainder > 0: + pow2 = 1 << (remainder.bit_length() - 1) + self._buddy_free[pow2] = [base + max_pow2] + + def _buddy_alloc(self, size: int) -> int: + """Allocate a power-of-two block via the buddy system.""" + block_size = 1 << (max(size, self.cache_line).bit_length() - 1) + if block_size < size: + block_size <<= 1 + + # Find the smallest available block ≥ block_size. + candidates = sorted(s for s in self._buddy_free if self._buddy_free[s]) + for s in candidates: + if s >= block_size: + addr = self._buddy_free[s].pop(0) + # Split until we reach the target size. + while s > block_size: + s >>= 1 + buddy = addr + s + self._buddy_free.setdefault(s, []).append(buddy) + self._buddy_allocated[addr] = block_size + return addr + return -1 + + def _buddy_free_block(self, addr: int) -> bool: + """Free a buddy block and coalesce with its buddy if possible.""" + block_size = self._buddy_allocated.pop(addr, None) + if block_size is None: + return False + + self._buddy_free.setdefault(block_size, []).append(addr) + + # Coalesce upward. + while True: + fl = self._buddy_free[block_size] + buddy = addr ^ block_size + if buddy in fl: + fl.remove(buddy) + addr = min(addr, buddy) + block_size <<= 1 + self._buddy_free.setdefault(block_size, []).append(addr) + self.stats.total_freed += block_size // 2 + else: + break + + self.stats.total_freed += block_size + self.stats.num_frees += 1 + return True + + # ── Coalescing (first-fit only) ──────────────────────────────────────── + + def _coalesce(self) -> None: + """Merge adjacent free regions.""" + free = sorted( + (r for r in self._freed_regions if not r.used), + key=lambda r: r.base, + ) + self._freed_regions = [r for r in self._freed_regions if r.used] + + merged: List[MemoryRegion] = [] + for r in free: + if merged and merged[-1].end == r.base: + prev = merged[-1] + merged[-1] = MemoryRegion(prev.name, prev.base, + prev.size + r.size) + else: + merged.append(r) + self._freed_regions.extend(merged) + + # ── Helpers ──────────────────────────────────────────────────────────── + + def _update_stats(self, size: int, alignment: int) -> None: + self.stats.total_allocated += size + self.stats.num_allocs += 1 + if alignment >= self.cache_line: + self.stats.cache_misses_avoided += 1 + + @staticmethod + def _align_up(addr: int, alignment: int = 4) -> int: + """Round *addr* up to the next multiple of *alignment*.""" + if alignment <= 0: + alignment = 4 + mask = alignment - 1 + return (addr + mask) & ~mask + + @staticmethod + def _addr_in_region(addr: int, region: MemoryRegion) -> bool: + """True iff *addr* is in [region.base, region.base + region.size).""" + return region.base <= addr < region.base + region.size + + # ── Debug ────────────────────────────────────────────────────────────── + + def dump(self) -> str: + """Return a multi-line dump of allocator state.""" + lines = [ + f"MemoryAllocator ({self.pool_size >> 20} MB pool, " + f"policy={self.policy.value}, " + f"cache_line={self.cache_line} B):", + f" Scratchpad: {self.scratchpad}", + f" General cursor: 0x{self._general_cursor:x}", + f" Regions ({len(self._regions)}):", + ] + for r in self._regions: + lines.append(f" {r}") + freed = self._freed_regions + if freed: + lines.append(f" Freed regions ({len(freed)}):") + for r in freed[:8]: + lines.append(f" {r}") + if len(freed) > 8: + lines.append(f" … (+{len(freed) - 8})") + if self.policy == AllocationPolicy.BUDDY: + lines.append(" Buddy free lists:") + for size, addrs in sorted(self._buddy_free.items()): + if addrs: + lines.append(f" {size} B: {len(addrs)} blocks") + lines.append(f" Stats: {self.stats}") + return "\n".join(lines) diff --git a/scratchv_dag/cache.py b/scratchv_dag/cache.py new file mode 100644 index 0000000..7814c52 --- /dev/null +++ b/scratchv_dag/cache.py @@ -0,0 +1,324 @@ +""" +L1 cache simulator for edge NPU. + +Models a 4 MB L1 data cache with configurable line size, set-associativity, +write policy, and LRU replacement. Tracks hit/miss rates, evictions, and +access latency cycles. + +This is a *functional* simulator: it tracks which addresses hit or miss +but does not store actual data values. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import List + + +# ═══════════════════════════════════════════════════════════════════════════════ +# CacheConfig +# ═══════════════════════════════════════════════════════════════════════════════ + +@dataclass +class CacheConfig: + """Configuration parameters for the L1 cache. + + Defaults:: + total_size 4 MB (typical edge-NPU L1) + line_size 64 B + associativity 8-way + write_back True + hit_latency 2 cycles + miss_latency 20 cycles (penalty to go to L2 / DRAM) + """ + + total_size: int = 4 * 1024 * 1024 + """Total cache capacity in bytes.""" + + line_size: int = 64 + """Cache line width in bytes (must be a power of two).""" + + associativity: int = 8 + """Set-associativity (1 = direct-mapped).""" + + write_back: bool = True + """True = write-back (+ write-allocate); False = write-through.""" + + write_allocate: bool = True + """Allocate a cache line on write miss (typical for write-back).""" + + hit_latency: int = 2 + """Latency in cycles for a cache hit.""" + + miss_latency: int = 20 + """Additional latency in cycles for a cache miss.""" + + # ── Derived properties ───────────────────────────────────────────────── + + @property + def num_lines(self) -> int: + """Total number of cache lines.""" + return self.total_size // self.line_size + + @property + def num_sets(self) -> int: + """Number of sets in the cache.""" + return self.num_lines // self.associativity + + def __post_init__(self) -> None: + """Validate configuration invariants.""" + assert self.total_size > 0, "total_size must be positive" + assert self.total_size % self.line_size == 0, \ + "total_size must be a multiple of line_size" + assert self.line_size > 0 and (self.line_size & (self.line_size - 1)) == 0, \ + "line_size must be a positive power of two" + assert self.associativity > 0, "associativity must be positive" + assert self.num_sets > 0, "total_size too small for given config" + + +# ═══════════════════════════════════════════════════════════════════════════════ +# CacheStats +# ═══════════════════════════════════════════════════════════════════════════════ + +@dataclass +class CacheStats: + """Performance counters collected by the cache.""" + hits: int = 0 + misses: int = 0 + evictions: int = 0 + write_backs: int = 0 + total_cycles: int = 0 + bytes_read: int = 0 + bytes_written: int = 0 + + @property + def hit_rate(self) -> float: + """Fraction of accesses that hit in the cache.""" + total = self.hits + self.misses + return self.hits / total if total > 0 else 0.0 + + @property + def miss_rate(self) -> float: + """Fraction of accesses that missed.""" + total = self.hits + self.misses + return self.misses / total if total > 0 else 0.0 + + @property + def avg_latency(self) -> float: + """Average latency per access in cycles.""" + total = self.hits + self.misses + return self.total_cycles / total if total > 0 else 0.0 + + def reset(self) -> None: + """Zero all counters.""" + self.hits = 0 + self.misses = 0 + self.evictions = 0 + self.write_backs = 0 + self.total_cycles = 0 + self.bytes_read = 0 + self.bytes_written = 0 + + def __repr__(self) -> str: + return ( + f"CacheStats(hits={self.hits}, misses={self.misses}, " + f"hit_rate={self.hit_rate:.2%}, evictions={self.evictions}, " + f"write_backs={self.write_backs}, " + f"avg_latency={self.avg_latency:.1f}cy)" + ) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# CacheLine +# ═══════════════════════════════════════════════════════════════════════════════ + +class CacheLine: + """A single cache line with tag, validity, dirtiness, and LRU timestamp.""" + + __slots__ = ("tag", "valid", "dirty", "last_access") + + def __init__(self) -> None: + self.tag: int = 0 + self.valid: bool = False + self.dirty: bool = False + self.last_access: int = 0 + + def __repr__(self) -> str: + return ( + f"Line(tag=0x{self.tag:x}, valid={self.valid}, " + f"dirty={self.dirty}, lru={self.last_access})" + ) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# L1Cache +# ═══════════════════════════════════════════════════════════════════════════════ + +class L1Cache: + """Set-associative L1 data cache simulator. + + Typical usage:: + + cache = L1Cache() + latency = cache.read(0x1000, 4) # read 4 bytes from address + latency = cache.write(0x1000, 4) # write 4 bytes to address + print(cache.stats) # inspect counters + """ + + __slots__ = ( + "config", "stats", + "_sets", "_clock", + "_mask_offset", "_mask_index", "_tag_shift", + ) + + def __init__(self, config: CacheConfig = None) -> None: + self.config = config if config is not None else CacheConfig() + self.stats = CacheStats() + self._clock = 0 + + # Build the cache as a 2-D list: sets × ways + self._sets: List[List[CacheLine]] = [ + [CacheLine() for _ in range(self.config.associativity)] + for _ in range(self.config.num_sets) + ] + + # Precompute address-decomposition masks + self._mask_offset = int(math.log2(self.config.line_size)) + self._mask_index = int(math.log2(self.config.num_sets)) + self._tag_shift = self._mask_offset + self._mask_index + + # ── Public API ───────────────────────────────────────────────────────── + + def read(self, addr: int, size: int = 4) -> int: + """Read *size* bytes starting at *addr*. + + Returns the total latency in cycles. + """ + latency = 0 + first = addr // self.config.line_size + last = (addr + size - 1) // self.config.line_size + + for line_addr in range(first, last + 1): + block_addr = line_addr * self.config.line_size + latency += self._access_line(block_addr, is_write=False) + + if size > self.config.line_size: + latency += self.config.miss_latency # cross-line penalty + + self.stats.total_cycles += latency + self.stats.bytes_read += size + return latency + + def write(self, addr: int, size: int = 4) -> int: + """Write *size* bytes starting at *addr*. + + Returns the total latency in cycles. + """ + latency = 0 + first = addr // self.config.line_size + last = (addr + size - 1) // self.config.line_size + + for line_addr in range(first, last + 1): + block_addr = line_addr * self.config.line_size + latency += self._access_line(block_addr, is_write=True) + + if size > self.config.line_size: + latency += self.config.miss_latency + + self.stats.total_cycles += latency + self.stats.bytes_written += size + return latency + + def flush(self) -> int: + """Write back all dirty lines and invalidate. Returns total cycles.""" + cycles = 0 + for line_set in self._sets: + for line in line_set: + if line.valid and line.dirty: + cycles += self.config.miss_latency + self.stats.write_backs += 1 + line.dirty = False + self.stats.total_cycles += cycles + return cycles + + def reset(self) -> None: + """Clear the entire cache and zero all statistics.""" + for line_set in self._sets: + for line in line_set: + line.valid = False + line.dirty = False + line.tag = 0 + line.last_access = 0 + self.stats.reset() + self._clock = 0 + + # ── Internals ────────────────────────────────────────────────────────── + + def _addr_to_set_tag(self, addr: int) -> (int, int): + """Decompose a byte address into ``(set_index, tag)``.""" + set_idx = (addr >> self._mask_offset) & (self.config.num_sets - 1) + tag = addr >> self._tag_shift + return set_idx, tag + + def _access_line(self, block_addr: int, is_write: bool) -> int: + """Access the cache line covering *block_addr*. Returns latency.""" + self._clock += 1 + set_idx, tag = self._addr_to_set_tag(block_addr) + line_set = self._sets[set_idx] + + # ── Probe for a hit ──────────────────────────────────────────────── + for line in line_set: + if line.valid and line.tag == tag: + self.stats.hits += 1 + line.last_access = self._clock + if is_write and self.config.write_back: + line.dirty = True + return self.config.hit_latency + + # ── Miss ─────────────────────────────────────────────────────────── + self.stats.misses += 1 + + if not self.config.write_allocate and is_write: + return self.config.miss_latency # write-no-allocate + + # Find victim (LRU within the set) + victim = line_set[0] + for line in line_set[1:]: + if not line.valid: + victim = line + break + if line.last_access < victim.last_access: + victim = line + + # Evict + if victim.valid and victim.dirty: + self.stats.write_backs += 1 + self.stats.evictions += 1 + + # Fill + victim.tag = tag + victim.valid = True + victim.dirty = is_write and self.config.write_back + victim.last_access = self._clock + + return self.config.hit_latency + self.config.miss_latency + + # ── Debug ────────────────────────────────────────────────────────────── + + def dump(self) -> str: + """Return a human-readable dump of cache configuration and state.""" + cfg = self.config + lines = [ + f"L1 Cache ({cfg.total_size >> 20} MB, " + f"{cfg.line_size} B lines, {cfg.associativity}-way):", + f" Sets: {cfg.num_sets}, Lines: {cfg.num_lines}", + f" Stats: {self.stats}", + ] + shown = 0 + for set_idx, line_set in enumerate(self._sets): + valid = [ln for ln in line_set if ln.valid] + if valid and shown < 8: + lines.append(f" Set {set_idx}: {valid}") + shown += 1 + return "\n".join(lines) diff --git a/scratchv_dag/sdnode.py b/scratchv_dag/sdnode.py new file mode 100644 index 0000000..a074977 --- /dev/null +++ b/scratchv_dag/sdnode.py @@ -0,0 +1,734 @@ +""" +SDNode — LLVM-style SelectionDAG core types. + +Provides machine value types (MVT), DAG node opcodes, node flags, +SDValue edges, SDNode definitions, and the SelectionDAG container. + +Designed for DAG-based instruction selection in compilers targeting +RISC-V and similar architectures. +""" + +from __future__ import annotations + +import enum +import math +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + + +# ═══════════════════════════════════════════════════════════════════════════════ +# MVT — Machine Value Type +# ═══════════════════════════════════════════════════════════════════════════════ + +class MVT(enum.Enum): + """Machine Value Type — represents the type of a value flowing through the DAG. + + Attributes: + i8 / i16 / i32 / i64: Integer types of varying width. + f32 / f64: Floating-point types. + Other: Token/chain type (side-effect ordering). + Void: No value (e.g. void return). + """ + + i8 = "i8" + i16 = "i16" + i32 = "i32" + i64 = "i64" + f32 = "f32" + f64 = "f64" + Other = "other" + Void = "void" + + @property + def is_integer(self) -> bool: + """True if this is an integer type (i8–i64).""" + return self in (MVT.i8, MVT.i16, MVT.i32, MVT.i64) + + @property + def is_float(self) -> bool: + """True if this is a floating-point type (f32, f64).""" + return self in (MVT.f32, MVT.f64) + + @property + def size_bits(self) -> int: + """Bit width of this type (0 for Other/Void).""" + return { + MVT.i8: 8, MVT.i16: 16, MVT.i32: 32, MVT.i64: 64, + MVT.f32: 32, MVT.f64: 64, + }.get(self, 0) + + @property + def size_bytes(self) -> int: + """Byte width of this type (0 for Other/Void).""" + return self.size_bits // 8 + + @staticmethod + def from_size(bits: int, is_float: bool = False) -> MVT: + """Resolve a bit width to the corresponding MVT. + + Args: + bits: Bit width (8, 16, 32, or 64). + is_float: If True, return a floating-point type. + + Returns: + The corresponding MVT. Falls back to i32 for unknown widths. + """ + if is_float: + return {32: MVT.f32, 64: MVT.f64}.get(bits, MVT.f32) + return {8: MVT.i8, 16: MVT.i16, 32: MVT.i32, 64: MVT.i64}.get(bits, MVT.i32) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SDNodeOpcode — DAG node operation codes +# ═══════════════════════════════════════════════════════════════════════════════ + +class SDNodeOpcode(enum.Enum): + """LLVM-inspired SelectionDAG node opcodes. + + Each entry represents one kind of operation that can appear as a node + in the DAG, including arithmetic, control flow, memory access, and + target-specific pseudo-instructions. + """ + + # ── Constants ────────────────────────────────────────────────────────── + Constant = "Constant" # Integer constant + ConstantFP = "ConstantFP" # Floating-point constant + Undef = "Undef" # Undefined / poisoning value + TargetConstant = "TargetConstant" # Target-specific constant (CSR# etc.) + + # ── Integer arithmetic ───────────────────────────────────────────────── + ADD = "ADD" + SUB = "SUB" + MUL = "MUL" + DIV = "DIV" # Signed division + UDIV = "UDIV" # Unsigned division + SRA = "SRA" # Shift right arithmetic + SRL = "SRL" # Shift right logical + SHL = "SHL" # Shift left + NEG = "NEG" # 0 - x + + # ── Floating-point arithmetic ────────────────────────────────────────── + FADD = "FADD" + FSUB = "FSUB" + FMUL = "FMUL" + FDIV = "FDIV" + FNEG = "FNEG" + FABS = "FABS" + + # ── Comparison & branches ────────────────────────────────────────────── + SETCC = "SETCC" # Set on condition code → returns i1 + BR_CC = "BR_CC" # Branch on condition code + BR = "BR" # Unconditional branch + BRIND = "BRIND" # Indirect branch (register target) + RET = "RET" # Return from function + CALL = "CALL" # Function call + + # ── Type conversion ──────────────────────────────────────────────────── + FP_EXTEND = "FP_EXTEND" + FP_TRUNC = "FP_TRUNC" + INT_TO_FP = "INT_TO_FP" + FP_TO_INT = "FP_TO_INT" + ANY_EXTEND = "ANY_EXTEND" + TRUNCATE = "TRUNCATE" + BITCAST = "BITCAST" + + # ── Memory ───────────────────────────────────────────────────────────── + LOAD = "LOAD" + STORE = "STORE" + TokenFactor = "TokenFactor" + + # ── Pseudo / register ────────────────────────────────────────────────── + CopyFromReg = "CopyFromReg" + CopyToReg = "CopyToReg" + Register = "Register" + LI_Pseudo = "LI_Pseudo" + MV_Pseudo = "MV_Pseudo" + CALL_Pseudo = "CALL_Pseudo" + RET_Pseudo = "RET_Pseudo" + LoadAddress = "LoadAddress" + + # ── Neural-network ops ───────────────────────────────────────────────── + RELU = "RELU" + MAXPOOL = "MAXPOOL" + GELU = "GELU" + MATMUL = "MATMUL" + + # ── Property helpers ─────────────────────────────────────────────────── + + @property + def has_chain(self) -> bool: + """True if this op carries side effects and needs a chain edge.""" + return self in _OP_HAS_CHAIN + + @property + def is_memop(self) -> bool: + """True if this is a memory load or store.""" + return self in _OP_IS_MEMOP + + @property + def is_commutative(self) -> bool: + """True if the operation is commutative (a+b == b+a).""" + return self in ( + SDNodeOpcode.ADD, SDNodeOpcode.MUL, + SDNodeOpcode.FADD, SDNodeOpcode.FMUL, + ) + + +_OP_HAS_CHAIN = frozenset({ + SDNodeOpcode.LOAD, SDNodeOpcode.STORE, + SDNodeOpcode.BR, SDNodeOpcode.BR_CC, SDNodeOpcode.BRIND, + SDNodeOpcode.RET, SDNodeOpcode.CALL, + SDNodeOpcode.TokenFactor, + SDNodeOpcode.CopyToReg, SDNodeOpcode.CopyFromReg, + SDNodeOpcode.CALL_Pseudo, SDNodeOpcode.RET_Pseudo, +}) + +_OP_IS_MEMOP = frozenset({ + SDNodeOpcode.LOAD, SDNodeOpcode.STORE, +}) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SDNodeFlags — per-node metadata +# ═══════════════════════════════════════════════════════════════════════════════ + +@dataclass +class SDNodeFlags: + """Fine-grained flags attached to an SDNode. + + These mirror LLVM's SDNodeFlags and control later optimisations + (e.g. fast-math flags enable more aggressive transforms). + """ + + no_nan: bool = False + """Assume no NaN values (``fast`` flag for FP).""" + + no_signed_zeros: bool = False + """Allow optimisations that ignore signed zero.""" + + no_infs: bool = False + """Assume no infinities.""" + + no_unsafe_fp: bool = False + """Allow all fast-math transforms.""" + + is_volatile: bool = False + """Memory access is volatile (must not be reordered).""" + + is_non_temporal: bool = False + """Non-temporal memory access (bypass cache hint).""" + + alignment: int = 0 + """Known alignment in bytes (0 = default / unknown).""" + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SDValue — DAG edge reference +# ═══════════════════════════════════════════════════════════════════════════════ + +@dataclass +class SDValue: + """A reference to a value produced by an SDNode. + + An SDValue pairs an SDNode with a result index, forming an edge + in the DAG. Result 0 is always the first non-chain value unless + the node has no chain, in which case all results are data values. + + Attributes: + node: The producer SDNode. + resno: Which result of that node (0‑based). + """ + + node: "SDNode" + resno: int = 0 + + # ── Type query ──────────────────────────────────────────────────────── + + @property + def value_type(self) -> MVT: + """The MVT of this value.""" + return self.node.value_type(self.resno) + + # ── Semantic predicates ─────────────────────────────────────────────── + + def is_chain(self) -> bool: + """True if this is a chain token (MVT.Other at the chain position).""" + return (self.resno == self.node.num_chain_results + and self.value_type == MVT.Other) + + def is_undef(self) -> bool: + """True if this value originates from an Undef node.""" + return self.node.opcode == SDNodeOpcode.Undef + + # ── Equality — identity-based (by node pointer + result index) ───────── + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SDValue): + return NotImplemented + return self.node is other.node and self.resno == other.resno + + def __hash__(self) -> int: + return id(self.node) ^ self.resno + + def __repr__(self) -> str: + return f"t{self.node.node_id}.{self.resno}:{self.value_type.value}" + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SDNode — single DAG node +# ═══════════════════════════════════════════════════════════════════════════════ + +class SDNode: + """A node in the SelectionDAG. + + Each node has an opcode, a list of result types, a list of operand SDValues + (incoming edges), and optional metadata. Nodes with side effects carry an + implicit chain edge (``MVT.Other``) as their first result and operand. + + Layout convention per LLVM: + [chain result (MVT.Other)]? [data result 0] [data result 1 …] + + Attributes: + node_id: Globally unique node identifier. + opcode: The operation this node performs. + operands: Incoming DAG edges (SDValues). + flags: Per-node flags (fast-math, volatility, …). + dbg_info: Optional debug / source location string. + num_chain_results: Number of chain-valued results (0 or 1). + """ + + __slots__ = ( + "node_id", "opcode", "_value_types", "operands", + "flags", "dbg_info", "_num_types", "num_chain_results", + "_attributes", + ) + + _next_id: int = 0 + + def __init__( + self, + opcode: SDNodeOpcode, + value_types: List[MVT], + operands: List[SDValue], + flags: Optional[SDNodeFlags] = None, + dbg_info: str = "", + ) -> None: + self.node_id = SDNode._next_id + SDNode._next_id += 1 + self.opcode = opcode + self._value_types = list(value_types) + self.operands = list(operands) + self.flags = flags if flags is not None else SDNodeFlags() + self.dbg_info = dbg_info + self._num_types = len(self._value_types) + self.num_chain_results = 0 + self._attributes: Dict[str, Any] = {} + + # ── Value type access ────────────────────────────────────────────────── + + def value_type(self, idx: int = 0) -> MVT: + """Return the MVT of the *idx*-th result (0‑based).""" + if 0 <= idx < self._num_types: + return self._value_types[idx] + return MVT.Void + + @property + def num_values(self) -> int: + """Number of non-chain data values produced by this node.""" + return self._num_types - self.num_chain_results + + # ── Chain helpers ────────────────────────────────────────────────────── + + @property + def has_chain(self) -> bool: + """True if the node has side effects and carries a chain.""" + return self.opcode.has_chain + + def get_chain(self) -> Optional[SDValue]: + """Return the chain operand, or None if this node has no chain.""" + if self.has_chain: + for op in self.operands: + if op.is_chain(): + return op + return None + + # ── Constant accessors ───────────────────────────────────────────────── + + def get_constant_int(self) -> Optional[int]: + """If this is a Constant node, return the stored integer value.""" + return self._attributes.get("const_val") + + def get_constant_fp(self) -> Optional[float]: + """If this is a ConstantFP node, return the stored float value.""" + if self.opcode == SDNodeOpcode.ConstantFP: + return self._attributes.get("const_fp") + return None + + # ── Attribute bucket ─────────────────────────────────────────────────── + + def get_attr(self, key: str, default: Any = None) -> Any: + """Return an arbitrary attribute attached to this node.""" + return self._attributes.get(key, default) + + def set_attr(self, key: str, value: Any) -> None: + """Attach an arbitrary attribute to this node.""" + self._attributes[key] = value + + # ── Debug ────────────────────────────────────────────────────────────── + + def __repr__(self) -> str: + vt = ",".join(v.value for v in self._value_types) + ops = ", ".join(str(op) for op in self.operands[:4]) + if len(self.operands) > 4: + ops += f", … (+{len(self.operands) - 4})" + return f"t{self.node_id}: {self.opcode.value} [{vt}] ← ({ops})" + + def dump(self, indent: str = "") -> str: + """Return a multi-line debug dump of this node.""" + lines = [ + f"{indent}Node t{self.node_id}:", + f"{indent} Opcode: {self.opcode.value}", + f"{indent} Types: {[v.value for v in self._value_types]}", + f"{indent} Operands ({len(self.operands)}):", + ] + for op in self.operands: + lines.append(f"{indent} {op}") + if self._attributes: + lines.append(f"{indent} Attrs: {self._attributes}") + return "\n".join(lines) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SelectionDAG — DAG container & node factory +# ═══════════════════════════════════════════════════════════════════════════════ + +class SelectionDAG: + """Owning container for SDNodes with factory methods. + + The DAG manages node lifetime, deduplication of constants, and + provides a default *entry token* chain that all side-effecting + nodes implicitly depend upon. The *root* value is the DAG's + terminal value (typically the return value or a token factor + merging all side-effect chains). + + Typical usage:: + + dag = SelectionDAG() + a = dag.get_constant(42, MVT.i32) + b = dag.get_constant(10, MVT.i32) + c = dag.get_add(a, b) + print(dag.dump()) + """ + + def __init__(self) -> None: + self._nodes: List[SDNode] = [] + # Deduplication cache: (kind_key, ...) → SDNode + self._node_map: Dict[Tuple, SDNode] = {} + self._root: Optional[SDValue] = None + self._debug_loc: Dict[int, str] = {} + + # Reset the global node counter so each DAG starts from t0. + SDNode._next_id = 0 + + # Create the entry chain token — all side-effecting nodes in + # a function ultimately chain back to this. + entry = self._new_node( + SDNodeOpcode.TokenFactor, [MVT.Other], [], + dbg_info="EntryToken", + ) + entry.num_chain_results = 1 + self._entry_token = SDValue(entry, 0) + + # ── Properties ───────────────────────────────────────────────────────── + + @property + def entry_token(self) -> SDValue: + """The DAG's root chain token (all side effects hang off this).""" + return self._entry_token + + @property + def root(self) -> Optional[SDValue]: + """The terminal value of the DAG (return value / merged chain).""" + return self._root + + @root.setter + def root(self, val: SDValue) -> None: + self._root = val + + @property + def nodes(self) -> List[SDNode]: + """A snapshot copy of all nodes currently in the DAG.""" + return list(self._nodes) + + # ── Low-level node creation ──────────────────────────────────────────── + + def _new_node( + self, + opcode: SDNodeOpcode, + value_types: List[MVT], + operands: List[SDValue], + flags: Optional[SDNodeFlags] = None, + dbg_info: str = "", + **attrs: Any, + ) -> SDNode: + """Allocate an SDNode, register it, and set its attribute bucket.""" + node = SDNode(opcode, value_types, operands, flags, dbg_info) + if opcode.has_chain: + node.num_chain_results = 1 + node._attributes = attrs + self._nodes.append(node) + return node + + # ── Factory methods — constants ──────────────────────────────────────── + + def get_constant(self, val: int, vt: MVT = MVT.i32) -> SDValue: + """Get or create a Constant node for integer *val*.""" + key: Tuple = ("const", vt, val) + node = self._node_map.get(key) + if node is None: + node = self._new_node(SDNodeOpcode.Constant, [vt], [], + const_val=val) + self._node_map[key] = node + return SDValue(node, 0) + + def get_constant_fp(self, val: float, vt: MVT = MVT.f32) -> SDValue: + """Get or create a ConstantFP node for float *val*.""" + key: Tuple = ("constfp", vt, val) + node = self._node_map.get(key) + if node is None: + node = self._new_node(SDNodeOpcode.ConstantFP, [vt], [], + const_fp=val) + self._node_map[key] = node + return SDValue(node, 0) + + def get_undef(self, vt: MVT = MVT.i32) -> SDValue: + """Get or create an Undef node of type *vt*.""" + key: Tuple = ("undef", vt) + node = self._node_map.get(key) + if node is None: + node = self._new_node(SDNodeOpcode.Undef, [vt], []) + self._node_map[key] = node + return SDValue(node, 0) + + def get_target_constant( + self, val: object, vt: MVT = MVT.i32 + ) -> SDValue: + """Get or create a TargetConstant (target-specific literal).""" + node = self._new_node(SDNodeOpcode.TargetConstant, [vt], [], + target_val=val) + return SDValue(node, 0) + + # ── Factory methods — register transfer ──────────────────────────────── + + def get_register(self, name: str, vt: MVT = MVT.i32) -> SDValue: + """Create a Register node representing a named physical register.""" + node = self._new_node(SDNodeOpcode.Register, [vt], [], + reg_name=name) + return SDValue(node, 0) + + def get_copy_from_reg( + self, reg: SDValue, + chain: Optional[SDValue] = None, + ) -> SDValue: + """Copy a value from a physical register. + + Returns the data value result (chain result is at index 0). + """ + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.CopyFromReg, + [MVT.Other, reg.value_type], + [chain, reg], + ) + node.num_chain_results = 1 + return SDValue(node, 1) # data value + + def get_copy_to_reg( + self, reg: SDValue, val: SDValue, + chain: Optional[SDValue] = None, + ) -> SDValue: + """Copy a value to a physical register. Returns the chain.""" + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.CopyToReg, + [MVT.Other], + [chain, reg, val], + ) + node.num_chain_results = 1 + return SDValue(node, 0) + + # ── Factory methods — arithmetic ─────────────────────────────────────── + + def get_add(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.ADD, lhs, rhs) + + def get_sub(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.SUB, lhs, rhs) + + def get_mul(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.MUL, lhs, rhs) + + def get_div(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.DIV, lhs, rhs) + + def get_fadd(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FADD, lhs, rhs) + + def get_fsub(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FSUB, lhs, rhs) + + def get_fmul(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FMUL, lhs, rhs) + + def get_fdiv(self, lhs: SDValue, rhs: SDValue) -> SDValue: + return self._get_binop(SDNodeOpcode.FDIV, lhs, rhs) + + def _get_binop( + self, opcode: SDNodeOpcode, + lhs: SDValue, rhs: SDValue, + ) -> SDValue: + """Shared helper for binary operation node creation.""" + vt = lhs.value_type + node = self._new_node(opcode, [vt], [lhs, rhs]) + return SDValue(node, 0) + + # ── Factory methods — memory ─────────────────────────────────────────── + + def get_load( + self, + addr: SDValue, + vt: MVT = MVT.i32, + chain: Optional[SDValue] = None, + flags: Optional[SDNodeFlags] = None, + ) -> SDValue: + """Create a LOAD node. Returns the *data* result. + + The chain result is at index 0 if needed via ``node.get_chain()``. + """ + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.LOAD, [MVT.Other, vt], + [chain, addr], flags=flags, + ) + node.num_chain_results = 1 + return SDValue(node, 1) + + def get_store( + self, + addr: SDValue, + val: SDValue, + chain: Optional[SDValue] = None, + flags: Optional[SDNodeFlags] = None, + ) -> SDValue: + """Create a STORE node. Returns the chain result.""" + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.STORE, [MVT.Other], + [chain, addr, val], flags=flags, + ) + node.num_chain_results = 1 + return SDValue(node, 0) + + # ── Factory methods — control flow ───────────────────────────────────── + + def get_br( + self, target: str, + chain: Optional[SDValue] = None, + ) -> SDValue: + """Create an unconditional branch to *target*.""" + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.BR, [MVT.Other], + [chain], branch_target=target, + ) + node.num_chain_results = 1 + return SDValue(node, 0) + + def get_br_cc( + self, cond: SDValue, + true_target: str, false_target: str, + chain: Optional[SDValue] = None, + ) -> SDValue: + """Create a conditional branch.""" + chain = chain or self._entry_token + node = self._new_node( + SDNodeOpcode.BR_CC, [MVT.Other], + [chain, cond], + true_target=true_target, false_target=false_target, + ) + node.num_chain_results = 1 + return SDValue(node, 0) + + def get_ret( + self, + values: Optional[List[SDValue]] = None, + chain: Optional[SDValue] = None, + ) -> SDValue: + """Create a return node.""" + chain = chain or self._entry_token + ops = [chain] + (values or []) + node = self._new_node(SDNodeOpcode.RET, [MVT.Other], ops) + node.num_chain_results = 1 + return SDValue(node, 0) + + def get_call( + self, + callee: str, + args: List[SDValue], + vt: MVT = MVT.i32, + chain: Optional[SDValue] = None, + ) -> SDValue: + """Create a call node. Returns the *data* result. + + The chain result is at index 0; the data result is at index 1. + """ + chain = chain or self._entry_token + tc = self.get_target_constant(callee) + node = self._new_node( + SDNodeOpcode.CALL, [MVT.Other, vt], + [chain, tc] + args, + callee=callee, + ) + node.num_chain_results = 1 + return SDValue(node, 1) + + def get_token_factor(self, chains: List[SDValue]) -> SDValue: + """Merge multiple chain tokens into one. + + If only one chain is given it is returned as-is. + """ + if len(chains) == 1: + return chains[0] + node = self._new_node( + SDNodeOpcode.TokenFactor, [MVT.Other], chains, + ) + node.num_chain_results = 1 + return SDValue(node, 0) + + # ── DAG lifetime ─────────────────────────────────────────────────────── + + def clear(self) -> None: + """Reset the entire DAG, discarding all nodes.""" + self._nodes.clear() + self._node_map.clear() + self._root = None + self._debug_loc.clear() + SDNode._next_id = 0 + + entry = self._new_node( + SDNodeOpcode.TokenFactor, [MVT.Other], [], + dbg_info="EntryToken", + ) + entry.num_chain_results = 1 + self._entry_token = SDValue(entry, 0) + + def dump(self) -> str: + """Return a human-readable dump of the entire DAG.""" + lines = ["SelectionDAG:"] + lines.append(f" EntryToken: t{self._entry_token.node.node_id}") + if self._root is not None: + lines.append(f" Root: {self._root}") + lines.append(f" Nodes ({len(self._nodes)}):") + for node in self._nodes: + lines.append(f" {node}") + return "\n".join(lines) diff --git a/scratchv_dag/selection_dag.py b/scratchv_dag/selection_dag.py new file mode 100644 index 0000000..42c60c6 --- /dev/null +++ b/scratchv_dag/selection_dag.py @@ -0,0 +1,573 @@ +""" +SelectionDAG builder, combiner, and scheduler. + +Translates a ScratchV IR Program into a SelectionDAG (DAGBuilder), +performs DAG-level peephole optimisations (DAGCombiner), then +linearises the DAG into a schedule of MachineInstrs (DAGScheduler). + +The flow:: + + Program ──▶ DAGBuilder ──▶ SelectionDAG ──▶ DAGCombiner + │ + ▼ + MachineInstr list ◀─── DAGScheduler ◀──────── clean DAG +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Set, Tuple + +from scratchv_dag.sdnode import ( + MVT, + SDNodeOpcode, + SDNodeFlags, + SDValue, + SelectionDAG, +) + +# We re-use the existing backend's MachineInstr types for scheduling +# output so the DAG scheduler integrates directly into the ScratchV +# backend pipeline. +from scratchv.backend.register_alloc import MachineInstr, MachineOp, MachineOperand + +# Re-export for convenience. +__all__ = [ + "DAGBuilder", + "DAGCombiner", + "DAGScheduler", +] + + +# ═══════════════════════════════════════════════════════════════════════════════ +# IR → MVT mapping helper +# ═══════════════════════════════════════════════════════════════════════════════ + +def _ir_to_mvt(dtype: Any) -> MVT: + """Map a ScratchV IR ``DataType`` to the corresponding ``MVT``.""" + from scratchv.ir.types import DataType + return { + DataType.FLOAT32: MVT.f32, + DataType.FLOAT64: MVT.f64, + DataType.INT32: MVT.i32, + DataType.INT64: MVT.i64, + }.get(dtype, MVT.i32) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# DAGBuilder — IR → SelectionDAG +# ═══════════════════════════════════════════════════════════════════════════════ + +class DAGBuilder: + """Lower a ScratchV IR ``Program`` into a ``SelectionDAG``. + + Each IR instruction is visited by a dedicated handler that builds + the corresponding DAG sub-graph. Value names are tracked in a + symbol table mapping them to their ``SDValue`` producer. + + Usage:: + + builder = DAGBuilder(program) + dag = builder.run() + """ + + def __init__(self, program: Any) -> None: + # The IR program to lower. + self.program = program + # The DAG being built. + self.dag = SelectionDAG() + # IR value name → SDValue symbol table. + self._value_map: Dict[str, SDValue] = {} + # Current chain token (threaded through side-effecting ops). + self._chain: SDValue = self.dag.entry_token + # Loop context for ``for``/``endfor``. + self._loop_ctx: Optional[Dict[str, Any]] = None + + # ── Public API ───────────────────────────────────────────────────────── + + def run(self) -> SelectionDAG: + """Build the DAG for all functions in the program.""" + for func in self.program.functions: + self._build_function(func) + return self.dag + + # ── Per-function lowering ────────────────────────────────────────────── + + def _build_function(self, func: Any) -> None: + self._value_map.clear() + self._chain = self.dag.entry_token + + # Map each function parameter to a CopyFromReg. + for i, param in enumerate(func.params): + reg_name = f"a{i}" if i < 8 else f"s{i - 8}" + reg = self.dag.get_register(reg_name) + val = self.dag.get_copy_from_reg(reg) + self._chain = val.node.get_chain() or self._chain + self._value_map[param.name] = val + + for block in func.blocks: + for instr in block.instructions: + self._build_instruction(instr) + + def _build_instruction(self, instr: Any) -> None: + """Dispatch an IR instruction to its dedicated builder.""" + handler = getattr(self, f"_build_{instr.opcode.value}", None) + if handler is None: + raise ValueError( + f"No DAG builder for opcode: {instr.opcode.value}" + ) + handler(instr) + + # ── Operand resolution ───────────────────────────────────────────────── + + def _get_val(self, ir_val: Any) -> SDValue: + """Resolve an IR operand to an SDValue. + + Constants are created on the fly; named values are looked up + in the symbol table (falling back to Undef). + """ + if ir_val.is_constant and ir_val.const_value is not None: + vt = _ir_to_mvt(ir_val.dtype) + if vt.is_float: + return self.dag.get_constant_fp( + float(ir_val.const_value), vt + ) + return self.dag.get_constant( + int(ir_val.const_value), vt + ) + name = ir_val.name + if name not in self._value_map: + # Safeguard: lazily create an Undef for forward references. + self._value_map[name] = self.dag.get_undef( + _ir_to_mvt(ir_val.dtype) + ) + return self._value_map[name] + + def _set_val(self, ir_val: Any, sdval: SDValue) -> None: + """Record an IR→SDValue binding.""" + self._value_map[ir_val.name] = sdval + + # ── Arithmetic ───────────────────────────────────────────────────────── + + def _build_add(self, instr: Any) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + val = self.dag.get_fadd(lhs, rhs) if lhs.value_type.is_float else self.dag.get_add(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_sub(self, instr: Any) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + val = self.dag.get_fsub(lhs, rhs) if lhs.value_type.is_float else self.dag.get_sub(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_mul(self, instr: Any) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + val = self.dag.get_fmul(lhs, rhs) if lhs.value_type.is_float else self.dag.get_mul(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_div(self, instr: Any) -> None: + lhs = self._get_val(instr.operands[0]) + rhs = self._get_val(instr.operands[1]) + val = self.dag.get_fdiv(lhs, rhs) if lhs.value_type.is_float else self.dag.get_div(lhs, rhs) + self._set_val(instr.dest, val) + + def _build_neg(self, instr: Any) -> None: + src = self._get_val(instr.operands[0]) + if src.value_type.is_float: + zero = self.dag.get_constant_fp(0.0, src.value_type) + val = self.dag.get_fsub(zero, src) + else: + zero = self.dag.get_constant(0, src.value_type) + val = self.dag.get_sub(zero, src) + self._set_val(instr.dest, val) + + def _build_exp(self, instr: Any) -> None: + src = self._get_val(instr.operands[0]) + callee = "expf" if src.value_type == MVT.f32 else "exp" + val = self.dag.get_call(callee, [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_load_const(self, instr: Any) -> None: + v = instr.attrs.get("value", 0) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32 + val = self.dag.get_constant_fp(float(v), vt) if vt.is_float else self.dag.get_constant(int(v), vt) + self._set_val(instr.dest, val) + + # ── Memory ───────────────────────────────────────────────────────────── + + def _build_load(self, instr: Any) -> None: + addr = self._get_val(instr.operands[0]) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.i32 + val = self.dag.get_load(addr, vt, chain=self._chain) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_store(self, instr: Any) -> None: + addr = self._get_val(instr.operands[0]) + val = self._get_val(instr.operands[1]) + self._chain = self.dag.get_store(addr, val, chain=self._chain) + + def _build_alloca(self, instr: Any) -> None: + size = instr.attrs.get("size", 4) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.i32 + val = self.dag.get_constant(size, vt) + self._set_val(instr.dest, val) + + # ── Control flow ─────────────────────────────────────────────────────── + + def _build_for(self, instr: Any) -> None: + start = instr.attrs.get("start", 0) + val = self.dag.get_constant(start, MVT.i32) + self._value_map[instr.dest.name] = val + self._loop_ctx = {"iv_name": instr.dest.name, "end": instr.attrs.get("end", 0)} + + def _build_endfor(self, instr: Any) -> None: + if self._loop_ctx is None: + return + iv_name = self._loop_ctx["iv_name"] + iv = self._value_map.get(iv_name) + if iv is not None: + inc = self.dag.get_add(iv, self.dag.get_constant(1, MVT.i32)) + self._value_map[iv_name] = inc + self._loop_ctx = None + + def _build_br(self, instr: Any) -> None: + self._chain = self.dag.get_br(instr.target or "", chain=self._chain) + + def _build_br_if(self, instr: Any) -> None: + cond = self._get_val(instr.operands[0]) + targets = (instr.target or "").split(",") + true_t = targets[0].strip() if targets else "" + false_t = targets[1].strip() if len(targets) > 1 else "" + self._chain = self.dag.get_br_cc(cond, true_t, false_t, chain=self._chain) + + def _build_return(self, instr: Any) -> None: + vals = [self._get_val(instr.operands[0])] if instr.operands else None + self._chain = self.dag.get_ret(vals, chain=self._chain) + + def _build_label(self, instr: Any) -> None: + pass # Labels are implicit in the DAG structure. + + # ── Neural-network ops ───────────────────────────────────────────────── + + def _build_relu(self, instr: Any) -> None: + src = self._get_val(instr.operands[0]) + val = self.dag.get_call("relu", [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_gelu(self, instr: Any) -> None: + src = self._get_val(instr.operands[0]) + val = self.dag.get_call("gelu", [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_softmax(self, instr: Any) -> None: + src = self._get_val(instr.operands[0]) + val = self.dag.get_call("softmax", [src], vt=src.value_type) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_matmul(self, instr: Any) -> None: + a = self._get_val(instr.operands[0]) + b = self._get_val(instr.operands[1]) + m = instr.attrs.get("m", 1) + n = instr.attrs.get("n", 1) + k = instr.attrs.get("k", 1) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32 + val = self.dag.get_call(f"matmul_m{m}_n{n}_k{k}", [a, b], vt=vt) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + def _build_dot(self, instr: Any) -> None: + a = self._get_val(instr.operands[0]) + b = self._get_val(instr.operands[1]) + length = instr.attrs.get("length", 1) + vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32 + val = self.dag.get_call(f"dot_len{length}", [a, b], vt=vt) + self._chain = val.node.get_chain() or self._chain + self._set_val(instr.dest, val) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# DAGCombiner — DAG-level peephole optimisations +# ═══════════════════════════════════════════════════════════════════════════════ + +class DAGCombiner: + """DAG-level peephole optimisations. + + Currently implements constant folding for integer and floating-point + arithmetic. Runs iteratively until no further folds are possible + or a fixed iteration limit is reached. + + Usage:: + + combiner = DAGCombiner(dag) + n_folds = combiner.run() + """ + + def __init__(self, dag: SelectionDAG) -> None: + self.dag = dag + self._changed = False + + def run(self) -> int: + """Apply all DAG combines. Returns the number of folds applied.""" + n_folds = 0 + for _ in range(32): # safety limit + self._changed = False + # Iterate in reverse so we fold bottom-up. + for node in reversed(self.dag._nodes): + handler = getattr(self, f"_fold_{node.opcode.value}", None) + if handler is not None: + handler(node) + if self._changed: + n_folds += 1 + if not self._changed: + break + return n_folds + + # ── Folding helpers ──────────────────────────────────────────────────── + + def _fold_ADD(self, node: Any) -> None: + lhs, rhs = self._get_const_int_binop(node) + if lhs is not None and rhs is not None: + self._replace_with_constant(node, lhs + rhs) + + def _fold_SUB(self, node: Any) -> None: + lhs, rhs = self._get_const_int_binop(node) + if lhs is not None and rhs is not None: + self._replace_with_constant(node, lhs - rhs) + + def _fold_MUL(self, node: Any) -> None: + lhs, rhs = self._get_const_int_binop(node) + if lhs is not None and rhs is not None: + self._replace_with_constant(node, lhs * rhs) + + def _fold_DIV(self, node: Any) -> None: + lhs, rhs = self._get_const_int_binop(node) + if lhs is not None and rhs is not None and rhs != 0: + self._replace_with_constant(node, lhs // rhs) + + def _fold_FADD(self, node: Any) -> None: + self._fold_fp_binop(node, lambda a, b: a + b) + + def _fold_FSUB(self, node: Any) -> None: + self._fold_fp_binop(node, lambda a, b: a - b) + + def _fold_FMUL(self, node: Any) -> None: + self._fold_fp_binop(node, lambda a, b: a * b) + + def _fold_FDIV(self, node: Any) -> None: + self._fold_fp_binop(node, lambda a, b: a / b) + + # ── Internal ─────────────────────────────────────────────────────────── + + def _get_const_int_binop( + self, node: Any + ) -> Tuple[Optional[int], Optional[int]]: + """Return (lhs, rhs) if both operands are integer Constants.""" + if len(node.operands) < 2: + return None, None + lhs = node.operands[0].node.get_constant_int() + rhs = node.operands[1].node.get_constant_int() + return lhs, rhs + + def _fold_fp_binop(self, node: Any, op: Any) -> None: + """Constant-fold an FP binary op if both operands are ConstantFP.""" + lhs = node.operands[0].node.get_constant_fp() + rhs = node.operands[1].node.get_constant_fp() + if lhs is not None and rhs is not None: + try: + result = op(lhs, rhs) + self._replace_with_fp_constant(node, result) + except (ZeroDivisionError, OverflowError, ValueError): + pass + + def _replace_with_constant(self, old_node: Any, val: int) -> None: + """Replace *old_node* with a new Constant node tagged for replacement.""" + new_val = self.dag.get_constant(val, old_node.value_type()) + old_node._attributes["replaced_by"] = new_val + self._changed = True + + def _replace_with_fp_constant(self, old_node: Any, val: float) -> None: + new_val = self.dag.get_constant_fp(val, old_node.value_type()) + old_node._attributes["replaced_by"] = new_val + self._changed = True + + +# ═══════════════════════════════════════════════════════════════════════════════ +# DAGScheduler — DAG → linear MachineInstr list +# ═══════════════════════════════════════════════════════════════════════════════ + +class DAGScheduler: + """Schedule a ``SelectionDAG`` into a linear list of ``MachineInstr``\\s. + + Uses a post-order traversal (operands before consumers) to produce + a valid topological schedule. Each SDNode is mapped to one or more + ``MachineInstr``\\s that the existing ScratchV backend can consume. + + Usage:: + + scheduler = DAGScheduler(dag) + instrs = scheduler.run() + """ + + def __init__(self, dag: SelectionDAG) -> None: + self.dag = dag + + def run(self) -> List[MachineInstr]: + """Produce a linearised instruction list from the DAG.""" + scheduled: Set[int] = set() + result: List[MachineInstr] = [] + + def _schedule(node: Any) -> None: + if node.node_id in scheduled: + return + # Recurse into operands first (post-order). + for op in node.operands: + if op.node.node_id not in scheduled: + _schedule(op.node) + scheduled.add(node.node_id) + self._emit_node(node, result) + + for node in self.dag._nodes: + _schedule(node) + + return result + + # ── Node emission ────────────────────────────────────────────────────── + + def _emit_node(self, node: Any, result: List[MachineInstr]) -> None: + """Emit a single SDNode as 0+ MachineInstrs.""" + opcode = node.opcode + machine_op = _SDNODE_TO_MACHINE_OP.get(opcode) + if machine_op is None: + return # skip nodes without a direct lowering + + # Constants + if opcode == SDNodeOpcode.Constant: + val = node.get_constant_int() or 0 + dst = MachineOperand.vreg(f"t{node.node_id}") + result.append(MachineInstr( + MachineOp.LI, dst, + MachineOperand.immediate(val), + comment=f"const {val}", + )) + return + + if opcode == SDNodeOpcode.ConstantFP: + val = node.get_constant_fp() or 0.0 + dst = MachineOperand.vreg(f"t{node.node_id}") + result.append(MachineInstr( + MachineOp.LI, dst, + MachineOperand.immediate(int(val)), + comment=f"constfp {val}", + )) + return + + if opcode == SDNodeOpcode.CopyFromReg: + reg = node.get_attr("reg_name", "zero") + dst = MachineOperand.vreg(f"t{node.node_id}") + result.append(MachineInstr( + MachineOp.MV, dst, + MachineOperand.reg(reg), + comment="copy_from_reg", + )) + return + + # Memory + if opcode == SDNodeOpcode.LOAD: + dst = MachineOperand.vreg(f"t{node.node_id}") + addr = _op_to_operand(node.operands[1]) + result.append(MachineInstr(MachineOp.LW, dst, addr, comment="load")) + return + + if opcode == SDNodeOpcode.STORE: + addr = _op_to_operand(node.operands[1]) + val = _op_to_operand(node.operands[2]) + result.append(MachineInstr(MachineOp.SW, addr, val, comment="store")) + return + + # Control + if opcode == SDNodeOpcode.BR: + target = node.get_attr("branch_target", "") + result.append(MachineInstr(MachineOp.J, comment=target)) + return + + if opcode == SDNodeOpcode.BR_CC: + cond = _op_to_operand(node.operands[1]) + true_t = node.get_attr("true_target", "") + false_t = node.get_attr("false_target", "") + result.append(MachineInstr(MachineOp.BNEZ, cond, comment=true_t)) + result.append(MachineInstr(MachineOp.J, comment=false_t)) + return + + if opcode == SDNodeOpcode.RET: + result.append(MachineInstr( + MachineOp.JALR, MachineOperand.vreg("zero"), + MachineOperand.vreg("ra"), + comment="ret", + )) + return + + if opcode == SDNodeOpcode.CALL: + callee = node.get_attr("callee", "unknown") + result.append(MachineInstr(MachineOp.CALL, comment=callee)) + if node.num_values > 0: + dst = MachineOperand.vreg(f"t{node.node_id}") + result.append(MachineInstr( + MachineOp.MV, dst, MachineOperand.vreg("a0"), + )) + return + + # Generic binary operation + dst = None + src1 = None + src2 = None + if node.num_values > 0 and node._num_types > node.num_chain_results: + dst = MachineOperand.vreg(f"t{node.node_id}") + if len(node.operands) >= 2: + src1 = _op_to_operand(node.operands[0]) + src2 = _op_to_operand(node.operands[1]) + result.append(MachineInstr(machine_op, dst, src1, src2)) + + +# ── Helper ──────────────────────────────────────────────────────────────────── + +def _op_to_operand(sdval: SDValue) -> MachineOperand: + """Convert an SDValue to a MachineOperand (vreg, imm, or phys reg).""" + opc = sdval.node.opcode + if opc == SDNodeOpcode.Constant: + return MachineOperand.immediate(sdval.node.get_constant_int() or 0) + if opc == SDNodeOpcode.ConstantFP: + return MachineOperand.immediate(int(sdval.node.get_constant_fp() or 0.0)) + if opc == SDNodeOpcode.Register: + return MachineOperand.reg(sdval.node.get_attr("reg_name", "zero")) + return MachineOperand.vreg(f"t{sdval.node.node_id}") + + +# ── SDNode → MachineOp lookup table ─────────────────────────────────────────── + +_SDNODE_TO_MACHINE_OP: Dict[SDNodeOpcode, MachineOp] = { + SDNodeOpcode.ADD: MachineOp.ADD, + SDNodeOpcode.SUB: MachineOp.SUB, + SDNodeOpcode.MUL: MachineOp.MUL, + SDNodeOpcode.DIV: MachineOp.DIV, + SDNodeOpcode.FADD: MachineOp.ADD, + SDNodeOpcode.FSUB: MachineOp.SUB, + SDNodeOpcode.FMUL: MachineOp.MUL, + SDNodeOpcode.FDIV: MachineOp.DIV, + SDNodeOpcode.NEG: MachineOp.SUB, + SDNodeOpcode.SETCC: MachineOp.SUB, + SDNodeOpcode.LOAD: MachineOp.LW, + SDNodeOpcode.STORE: MachineOp.SW, + SDNodeOpcode.BR: MachineOp.J, + SDNodeOpcode.BR_CC: MachineOp.BNEZ, + SDNodeOpcode.RET: MachineOp.JALR, + SDNodeOpcode.CALL: MachineOp.CALL, + SDNodeOpcode.LI_Pseudo: MachineOp.LI, + SDNodeOpcode.MV_Pseudo: MachineOp.MV, + SDNodeOpcode.RELU: MachineOp.MAX, +} From d99e16fc47eb566d4687b40292fccd4c838c7a09 Mon Sep 17 00:00:00 2001 From: wangjiangyang <1938840431@qq.com> Date: Mon, 18 May 2026 13:07:31 +0800 Subject: [PATCH 4/7] fix python --- scratchv/codegen/sdnode.py | 2 +- scratchv/memory/allocator.py | 4 ++-- scratchv/memory/cache.py | 6 +++--- scratchv/optimizer/licm.py | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scratchv/codegen/sdnode.py b/scratchv/codegen/sdnode.py index e1575c4..afcd296 100644 --- a/scratchv/codegen/sdnode.py +++ b/scratchv/codegen/sdnode.py @@ -178,7 +178,7 @@ class SDNodeFlags: # SDValue — edge in the DAG (node + result index) # ═══════════════════════════════════════════════════════════ -@dataclass(slots=True) +@dataclass class SDValue: """Reference to a value produced by an SDNode.""" node: SDNode diff --git a/scratchv/memory/allocator.py b/scratchv/memory/allocator.py index e5c97d1..b93fbb6 100644 --- a/scratchv/memory/allocator.py +++ b/scratchv/memory/allocator.py @@ -23,7 +23,7 @@ class AllocationPolicy(Enum): BUDDY = "buddy" -@dataclass(slots=True) +@dataclass class MemoryRegion: """A contiguous memory region.""" name: str @@ -42,7 +42,7 @@ def __repr__(self) -> str: f"{self.size}B, {status}, align={self.alignment})") -@dataclass(slots=True) +@dataclass class AllocStats: """Allocation statistics.""" total_allocated: int = 0 diff --git a/scratchv/memory/cache.py b/scratchv/memory/cache.py index 5d5e9e6..7a983bb 100644 --- a/scratchv/memory/cache.py +++ b/scratchv/memory/cache.py @@ -12,7 +12,7 @@ from typing import Optional -@dataclass(slots=True) +@dataclass class CacheConfig: """Configuration for the L1 cache.""" total_size: int = 4 * 1024 * 1024 # 4 MB @@ -38,7 +38,7 @@ def __post_init__(self): assert self.num_sets > 0 -@dataclass(slots=True) +@dataclass class CacheStats: """Cache performance counters.""" hits: int = 0 @@ -84,7 +84,7 @@ def __repr__(self) -> str: # Cache line # ═══════════════════════════════════════════════════════════ -@dataclass(slots=True) +@dataclass class CacheLine: """A single cache line.""" tag: int = 0 diff --git a/scratchv/optimizer/licm.py b/scratchv/optimizer/licm.py index 292a772..a8b28f5 100644 --- a/scratchv/optimizer/licm.py +++ b/scratchv/optimizer/licm.py @@ -12,6 +12,8 @@ from __future__ import annotations +from __future__ import annotations + from scratchv.ir.types import OpCode, Instruction, BasicBlock, Function, Program From 18789e6f25f9989abed319cf6676d0d3a1500ffd Mon Sep 17 00:00:00 2001 From: wangjiangyang <1938840431@qq.com> Date: Mon, 18 May 2026 14:05:53 +0800 Subject: [PATCH 5/7] update docs --- .gitignore | 3 + CHANGELOG.md | 49 +++ CONTRIBUTING.md | 62 ++++ Makefile | 52 +++ README.md | 71 ++-- docs/ScratchV.md | 76 ----- docs/developer_guide.md | 186 ++++++++++ docs/help.md | 128 ------- output.ll | 15 - pyproject.toml | 26 +- scratchv/__init__.py | 2 +- scratchv/codegen/__init__.py | 12 +- scratchv/codegen/sdnode.py | 551 ------------------------------ scratchv/codegen/selection_dag.py | 521 ---------------------------- scratchv/memory/__init__.py | 14 +- scratchv/memory/allocator.py | 361 -------------------- scratchv/memory/cache.py | 265 -------------- setup.py | 4 + 18 files changed, 442 insertions(+), 1956 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Makefile delete mode 100644 docs/ScratchV.md create mode 100644 docs/developer_guide.md delete mode 100644 docs/help.md delete mode 100644 output.ll delete mode 100644 scratchv/codegen/sdnode.py delete mode 100644 scratchv/codegen/selection_dag.py delete mode 100644 scratchv/memory/allocator.py delete mode 100644 scratchv/memory/cache.py diff --git a/.gitignore b/.gitignore index 715a43f..2e1adfb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ dist/ build/ *.s *.o +*.ll models/ venv/ .venv/ +.claude/ +scratchv.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a98014e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +## [0.3.0] — 2026-05-18 + +### Added +- `scratchv_dag/`: standalone LLVM-style SelectionDAG infrastructure package + - `sdnode.py`: SDNode, SDValue, MVT, SelectionDAG container + - `selection_dag.py`: DAGBuilder, DAGCombiner, DAGScheduler pipeline + - `cache.py`: 4 MB L1 cache simulator (set-associative, LRU, write-back) + - `allocator.py`: Buddy-system memory allocator with cache-line alignment and scratchpad +- `docs/developer_guide.md`: guide for extending ScratchV with new ops and passes +- `Makefile`: standard dev targets (install, test, clean, lint, docs) +- `CHANGELOG.md`, `CONTRIBUTING.md`: project metadata files +- `pyproject.toml`: classifiers, readme field, license field + +### Changed +- Consolidated `scratchv/codegen/` and `scratchv/memory/` into re-export shims over `scratchv_dag/` +- Python requirement lowered to 3.8 with full compatibility fixes +- `pyproject.toml` version bumped to 0.3.0 +- `.gitignore` extended for `.ll` files and `.claude/` + +## [0.2.0] — 2026-05-15 + +### Added +- LLVM IR backend (`llvm_codegen.py`) +- Advanced optimizations: peephole, muladd fusion, LICM +- Verification framework: ONNX Runtime comparison, numpy reference, DSL interpreter +- TinyFive adapter for assembly verification and profiling +- CLI options: `--backend`, `--optimize`, `--verify`, `--reg-alloc` +- Documentation: optimization guide, verification guide + +### Changed +- Instruction selector supports all major ops (add, sub, mul, div, neg, exp, + relu, gelu, softmax, maxpool, matmul, dot) +- Register allocator: greedy mode (LRU-based) added alongside naive + +## [0.1.0] — 2026-05-01 + +### Added +- Initial IR: types (Value, Instruction, BasicBlock, Function, Program) +- ONNX parser: Add, Mul, Sub, Div, MatMul, ReLU, GELU, Softmax, MaxPool +- DSL parser for fast iteration without ONNX dependency +- IR builder with chainable API +- IR printer for debugging +- Instruction selector: IR → RISC-V pseudo-instructions +- Register allocator: naive (spill-all) mode +- Assembly emitter: GAS-syntax output +- Constant folding and dead code elimination passes +- CLI entry point with `-o`, `--dump-ir` flags diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ca28c08 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing to ScratchV + +Thanks for your interest! This is an educational compiler project, and +contributions of all kinds — code, docs, bug reports, teaching materials — +are very welcome. + +## Quick Start + +```bash +git clone https://github.com/kinsomwang/ScratchV +cd ScratchV +pip install -e . # install in editable mode +pip install tinyfive # optional: assembly verification +pytest tests/ -v # run all tests +``` + +## Code Style + +- **Python version**: 3.8+ compatible (no `|` union syntax in annotations + unless guarded by `from __future__ import annotations`; no + `dataclass(slots=True)`). +- **Type hints**: annotate all public functions and methods. +- **Docstrings**: Google or NumPy style is fine — keep them short but useful. +- **No `__pycache__`**: they're gitignored; just don't commit them. + +## Pull Request Process + +1. **Open an issue** first to discuss the change you'd like to make. +2. Make your changes on a feature branch (`git checkout -b feat/my-thing`). +3. Add or update tests in `tests/`. +4. Run `pytest tests/` — all tests must pass. +5. Run `make check` if available (lint + test). +6. Open a PR with a clear title and description. + +## Adding a New IR Opcode + +1. Add the opcode to `scratchv/ir/types.py` → `OpCode` enum. +2. (Optional) Add a builder method in `scratchv/ir/builder.py`. +3. Add a selection handler in `scratchv/backend/instruction_select.py`. +4. Add an LLVM codegen handler in `scratchv/backend/llvm_codegen.py`. +5. Add a test case in `tests/`. +6. Run `pytest` to verify. + +## 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. + +## Documentation + +- User-facing docs go in `docs/`. +- Inline code comments are for *why* not *what*. +- The README is the single source of truth for project-wide docs. + +## Code of Conduct + +Be respectful, assume good faith, and remember that this is a learning project. +Help others level up. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1b5e805 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +# ScratchV developer makefile +.POSIX: + +.PHONY: install test clean lint check docs examples + +# ── Installation ────────────────────────────────────────────────────────────── + +install: + pip install -e . + pip install -e ".[all]" 2>/dev/null || pip install -e . + +# ── Testing ─────────────────────────────────────────────────────────────────── + +test: + python3 -m pytest tests/ -v --tb=short + +test-coverage: + python3 -m pytest tests/ --cov=scratchv --cov=scratchv_dag --cov-report=term + +# ── Lint ────────────────────────────────────────────────────────────────────── + +lint: + -python3 -m flake8 scratchv/ scratchv_dag/ tests/ 2>/dev/null || echo "install flake8: pip install flake8" + -python3 -m mypy scratchv/ scratchv_dag/ --ignore-missing-imports 2>/dev/null || echo "install mypy: pip install mypy" + +# ── Clean ───────────────────────────────────────────────────────────────────── + +clean: + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null + find . -type f -name '*.pyc' -delete + rm -rf .pytest_cache + rm -rf scratchv.egg-info scratchv_dag.egg-info + rm -rf dist build + rm -f output.s output.ll + +# ── Checks (runs before PR) ─────────────────────────────────────────────────── + +check: clean test + +# ── Quick examples ──────────────────────────────────────────────────────────── + +examples: + @echo "=== DSL examples ===" + python3 -m scratchv examples/simple_add.dsl -o /tmp/simple_add.s --dump-ir + python3 -m scratchv examples/relu_test.dsl -o /tmp/relu.s --optimize all + python3 -m scratchv examples/matmul_test.dsl -o /tmp/matmul.s --optimize all + +# ── Build docs preview (if pandoc is available) ──────────────────────────────── + +docs: + @echo "Documentation is markdown — no build required." + @ls docs/*.md diff --git a/README.md b/README.md index 72d603e..91c849c 100644 --- a/README.md +++ b/README.md @@ -13,35 +13,45 @@ code executable on QEMU, Spike, TinyFive, or real hardware. ``` ScratchV/ -├── scratchv/ -│ ├── ir/ # Intermediate representation (three-address code) -│ │ ├── types.py # Core types: Value, Instruction, BasicBlock, Function, Program -│ │ ├── builder.py # IR construction helper (chainable API) -│ │ └── printer.py # IR text dump -│ ├── frontend/ # Input parsing -│ │ ├── onnx_parser.py # ONNX model → IR -│ │ └── dsl_parser.py # Simple DSL → IR (test without ONNX dep) -│ ├── optimizer/ # IR → IR optimizations -│ │ ├── constant_folding.py # Compile-time constant evaluation -│ │ ├── dead_code.py # Unused instruction removal -│ │ ├── peephole.py # Redundant pattern elimination -│ │ ├── muladd_fusion.py # Mul+Add instruction combining -│ │ └── licm.py # Loop Invariant Code Motion -│ ├── backend/ # Code generation -│ │ ├── instruction_select.py # IR → RISC-V pseudo-instructions -│ │ ├── register_alloc.py # Register allocation (naive + greedy) -│ │ ├── asm_emit.py # RISC-V assembly text emission -│ │ └── llvm_codegen.py # LLVM IR text generation -│ ├── verification/ # Verification & comparison -│ │ └── verifier.py # ONNX Runtime + numpy reference comparison -│ ├── simulator/ # Simulation -│ │ └── tinyfive.py # TinyFive adapter with instruction counting -│ └── main.py # CLI entry point -├── tests/ # 50+ unit tests (including LLVM codegen + verification) +├── scratchv/ # Main compiler package +│ ├── ir/ # Intermediate representation (three-address code) +│ │ ├── types.py # Value, Instruction, BasicBlock, Function, Program +│ │ ├── builder.py # IR construction helper (chainable API) +│ │ └── printer.py # IR text dump +│ ├── frontend/ # Input parsing +│ │ ├── onnx_parser.py # ONNX model → IR +│ │ └── dsl_parser.py # Simple DSL → IR (test without ONNX dep) +│ ├── optimizer/ # IR → IR optimizations +│ │ ├── constant_folding.py # Compile-time constant evaluation +│ │ ├── dead_code.py # Unused instruction removal +│ │ ├── peephole.py # Redundant pattern elimination +│ │ ├── muladd_fusion.py # Mul+Add instruction combining +│ │ └── licm.py # Loop Invariant Code Motion +│ ├── backend/ # Code generation +│ │ ├── instruction_select.py # IR → RISC-V pseudo-instructions +│ │ ├── register_alloc.py # Register allocation (naive + greedy) +│ │ ├── asm_emit.py # RISC-V assembly text emission +│ │ └── llvm_codegen.py # LLVM IR text generation +│ ├── verification/ # Verification & comparison +│ │ └── verifier.py # ONNX Runtime + numpy reference comparison +│ ├── simulator/ # Simulation +│ │ └── tinyfive.py # TinyFive adapter with instruction counting +│ └── main.py # CLI entry point +├── scratchv_dag/ # Standalone DAG / memory library +│ ├── sdnode.py # SDNode, MVT, SelectionDAG container +│ ├── selection_dag.py # DAGBuilder, DAGCombiner, DAGScheduler +│ ├── cache.py # 4 MB L1 cache simulator (LRU, write-back) +│ ├── allocator.py # Buddy allocator with cache-line alignment +│ └── README.md # Standalone docs +├── tests/ # 60+ unit tests ├── examples/ # DSL models, ONNX generator, pipeline demos ├── docs/ -│ ├── verification.md # Guide: TinyFive, Spike, QEMU, LLVM IR, ONNX Runtime -│ └── optimization_guide.md # Optimization passes guide +│ ├── verification.md # Verification guide (TinyFive, Spike, QEMU, …) +│ ├── optimization_guide.md # Optimization passes guide +│ └── developer_guide.md # Internal architecture & extension guide +├── CHANGELOG.md # Release history +├── CONTRIBUTING.md # Contribution guidelines +├── Makefile # Dev targets (test, clean, lint, …) └── models/ # Generated ONNX models ``` @@ -153,6 +163,13 @@ DSL Source ──▶ DSL Parser ────┘ │ └─────────────────┘ │ │ │ ▼ │ + ┌─────────────────────────┐ │ + │ scratchv_dag (DAG) │ │ + │ DAGBuilder → Combiner │ │ + │ → Scheduler │ │ + └─────────────────────────┘ │ + │ │ + ▼ │ ┌──────────────┐ │ │ LLVM Codegen │──▶ LLVM IR (.ll) │ └──────────────┘ │ │ diff --git a/docs/ScratchV.md b/docs/ScratchV.md deleted file mode 100644 index 0636935..0000000 --- a/docs/ScratchV.md +++ /dev/null @@ -1,76 +0,0 @@ -# ScratchV - -为期三个月的里程碑。核心变化:**第一个月前半段**仅做流程熟悉(不写代码),**第一个月后半段+第二个月前半段**(约4周)完成ONNX→中间IR及简单优化,**第二个月后半段+第三个月前半段**(约4周)完成后端代码生成,**第三个月后半段**文档总结。 - -以下为细化到周的安排(按1个月≈4周,共12周): - ---- - -## 📅 总体时间线 - -| 阶段 | 时间 | 核心任务 | -| :--- | :--- | :--- | -| **阶段0:环境与熟悉** | 第1~2周(第一个月前半) | 搭建环境,运行预置框架与benchmark,理解ONNX→汇编全流程 | -| **阶段1:IR转换与简单优化** | 第3~6周(第一个月后半+第二月前半) | 实现ONNX解析器,生成自定义IR,支持新算子,添加常量折叠等简单优化 | -| **阶段2:后端代码生成** | 第7~10周(第二月后半+第三月前半) | 指令选择、寄存器分配、汇编发射,支持循环和内存访问,完成benchmark验证 | -| **阶段3:文档与总结** | 第11~12周(第三月后半) | 撰写设计文档、用户手册、项目总结,准备最终汇报 | - ---- - -## 🗓️ 第1~2周:环境搭建与全流程熟悉 - -| 周次 | 任务 | 产出 / 验收标准 | -| :--- | :--- | :--- | -| **W1** | 安装RISC-V GCC交叉工具链、QEMU模拟器;学习ONNX基础格式;运行预置框架提供的demo(如向量加法ONNX模型→汇编→QEMU执行)。 | 成功跑通一个完整示例,理解每个环节的作用。 | -| **W2** | 使用预置框架运行多个benchmark(向量点积、矩阵乘法标量版、ReLU等);分析生成的汇编代码结构;学习RISC-V调用约定与指令格式。 | 获得至少3个benchmark的基线数据;能解释关键汇编指令。 | - ---- - -## 🗓️ 第3~6周:ONNX → 中间IR 及简单优化 - -| 周次 | 任务 | 产出 / 验收标准 | -| :--- | :--- | :--- | -| **W3** | 设计自定义中间IR(三地址码或类似结构);实现ONNX模型解析器(支持`Add`、`Mul`算子),输出IR文本。 | 能解析简单ONNX模型并输出可读IR。 | -| **W4** | 扩展算子支持:`ReLU`、`MatMul`(标量循环版本);实现IR构建器与基本数据结构。 | 包含`MatMul`的模型可正确转换为IR(包含循环表示)。 | -| **W5** | 添加新算子(例如`MaxPool`、`GELU`近似或`Dot`);实现常量折叠优化(编译时计算常量表达式)。 | 新算子转换无误;常量折叠在IR层面生效。 | -| **W6** | 添加死代码消除(移除未被使用的变量);完善IR合法性检查(类型、未定义变量);为后端准备接口。 | 优化后IR更简洁;IR模块可被后端调用。 | - ---- - -## 🗓️ 第7~10周:编译器后端实现 - -| 周次 | 任务 | 产出 / 验收标准 | -| :--- | :--- | :--- | -| **W7** | 实现指令选择:将IR操作映射到RISC-V基本指令(`add`、`sub`、`lw`、`sw`等);实现最简单的寄存器分配(固定映射虚拟寄存器到`s0`-`s11`/`t0`-`t6`)。 | 对无循环基本块生成正确汇编。 | -| **W8** | 支持控制流:将IR中的循环(`FOR`)转换为`beq`/`bne`+标签;为数组访问生成正确的地址计算(基址+偏移)。 | 能生成包含循环的汇编代码(如向量点积)。 | -| **W9** | 改进寄存器分配:实现局部贪心分配(在线性扫描简化版),减少内存溢出;支持`MatMul`的完整汇编生成,并在QEMU上验证正确性。 | 汇编代码指令数比固定映射减少20%以上。 | -| **W10** | 运行所有benchmark,对比预置框架输出;修复bug;添加对额外算子(如`Softmax`标量版)的支持(可选)。 | 所有测试用例在QEMU上运行结果与参考一致。 | - ---- - -## 🗓️ 第11~12周:文档撰写与总结 - -| 周次 | 任务 | 产出 / 验收标准 | -| :--- | :--- | :--- | -| **W11** | 撰写设计文档:整体架构图、IR规范、前端转换流程、后端核心算法(寄存器分配、指令选择)。 | 文档清晰,图文并茂。 | -| **W12** | 编写用户手册(如何安装、编译、运行);撰写项目总结(难点、踩坑记录、性能分析、未来改进方向);准备最终演示。 | 完成完整文档和汇报材料。 | - ---- - -## 📌 关键交付物 - -- 源代码仓库(包含ONNX解析器、IR模块、后端代码生成器、测试用例) -- 可执行工具:输入ONNX模型 → 输出RISC-V汇编(`.s`)文件 -- 基准测试报告(与预置框架对比指令数或运行时间) -- 设计文档 + 用户手册 + 总结报告 - ---- - -## 💡 提示 - -- **W3~W4** 可先用自定义DSL替代ONNX解析,降低初期难度,W5后再接入ONNX。 -- **W7** 寄存器分配可以先实现“所有变量在栈上”,W9再优化,保证进度不卡顿。 -- 每周进行一次进度检查,及时调整任务范围。 - - - diff --git a/docs/developer_guide.md b/docs/developer_guide.md new file mode 100644 index 0000000..b915268 --- /dev/null +++ b/docs/developer_guide.md @@ -0,0 +1,186 @@ +# Developer Guide + +This guide explains how ScratchV works internally and how to extend it. + +--- + +## Architecture Overview + +``` + ┌─────────────────────────────────────────┐ + │ ScratchV Compiler │ + │ │ + ONNX Model ──▶ ONNXParser ──▶ IR (3-addr) ──▶ Optimizer ──┐ │ + │ │ │ │ + DSL Source ──▶ DSLParser ────┘ │ │ │ + │ │ │ + ┌─────────────────────────┘ │ │ + ▼ │ │ + ┌──────────────────────┐ │ │ + │ InstructionSelector │──▶ RegAlloc ─▶ Asm │─▶ .s + └──────────────────────┘ │ │ + │ │ │ + ▼ │ │ + ┌──────────────────────┐ │ │ + │ DAGBuilder / Sched │──▶ (alt. pipeline) │ │ + │ (scratchv_dag) │ │ │ + └──────────────────────┘ │ │ + │ │ │ + ▼ │ │ + ┌──────────────────────┐ │ │ + │ LLVMCodegen │──▶ .ll ─▶ opt/lli │ │ + └──────────────────────┘ │ │ + ┌──────────────────────────┐ │ │ + │ Verification Framework │ │ │ + │ ─ ONNX Runtime │ │ │ + │ ─ numpy reference │ │ │ + │ ─ DSL interpreter │ │ │ + │ ─ TinyFive sim │ │ │ + └──────────────────────────┘ │ │ + ┌──────────────────────────┐ │ │ + │ scratchv_dag │ │ │ + │ ─ SelectionDAG │ │ │ + │ ─ L1 cache simulator │───────────────┘ │ + │ ─ Memory allocator │ │ + └──────────────────────────┘ │ + ┌──────────────────────────┐ │ + │ scratchv_dag │ │ + │ ─ SDNode / MVT / DAG │──────────────────┘ + └──────────────────────────┘ +``` + +## Package Map + +| Package | Responsibility | +|---|---| +| `scratchv/ir/` | IR types, builder, printer | +| `scratchv/frontend/` | ONNX & DSL parsers | +| `scratchv/optimizer/` | IR → IR optimization passes | +| `scratchv/backend/` | Instruction selection, reg alloc, asm emit, LLVM codegen | +| `scratchv/verification/` | Verification against reference implementations | +| `scratchv/simulator/` | TinyFive adapter | +| `scratchv_dag/` | DAG-based instruction selection (standalone) | + +## IR Reference + +### Types (`scratchv/ir/types.py`) + +```python +class OpCode(enum.Enum): + ADD, SUB, MUL, DIV, NEG, EXP # arithmetic + LOAD, STORE, LOAD_CONST, ALLOCA # memory + FOR, ENDFOR, BR, BR_IF, RETURN # control flow + MATMUL, RELU, MAXPOOL, SOFTMAX, ... # neural-network ops + +class Value: + name: str + dtype: DataType # FLOAT32, INT32, FLOAT64, INT64 + is_constant: bool + const_value: float | int | None + shape: tuple[int, ...] + +class Instruction: + opcode: OpCode + dest: Value | None + operands: list[Value] + attrs: dict # e.g. {"value": 42} for load_const + target: str | None # branch target label + +class BasicBlock: + name: str + instructions: list[Instruction] + phi_nodes: list[Instruction] + +class Function: + name: str + params: list[Value] + returns: list[Value] + blocks: list[BasicBlock] + locals: list[Value] + +class Program: + functions: list[Function] + global_values: list[Value] +``` + +### Builder (`scratchv/ir/builder.py`) + +```python +builder = IRBuilder() +f = builder.new_function("add4") +bb = builder.new_block("entry") + +a = builder.make_value("a") +b = builder.make_value("b") +s = builder.add(a, b) +builder.ret(s) +``` + +## Backend Pipeline + +### Standard path (flat instruction selection) + +``` +IR → InstructionSelector → MachineInstr[] → RegisterAllocator → AsmEmitter → .s +``` + +- `InstructionSelector`: one handler per `OpCode`, emits `MachineInstr` with + virtual registers. +- `RegisterAllocator`: two modes — `naive` (spill everything) and `greedy` + (LRU-based, reuses callee-saved temps). +- `AsmEmitter`: `MachineInstr[]` → GAS-syntax RISC-V text. + +### DAG path (experimental, via scratchv_dag) + +``` +IR → DAGBuilder → SelectionDAG → DAGCombiner → DAGScheduler → MachineInstr[] +``` + +The DAG path enables more advanced optimisations (pattern matching, better +constant folding) before scheduling back to linear instructions. + +## Memory System + +- `L1Cache`: set-associative cache simulator for performance estimation + (default 4 MB, 8-way, 64 B lines, LRU replacement). +- `MemoryAllocator`: buddy allocator with cache-line alignment and + scratchpad region (25 % of pool for explicit DMA transfers). + +Both live in the standalone `scratchv_dag` package and are usable independently. + +## Adding Support for a New ONNX Operator + +1. **ONNX parser** (`scratchv/frontend/onnx_parser.py`): + - Add a `_handle_` method that reads inputs/outputs and emits IR. + - Register it in the operator dispatch dict. + +2. **Optional: IR opcode** (`scratchv/ir/types.py`): + - Only if the operator cannot be decomposed into existing IR ops. + +3. **Instruction selection** (`scratchv/backend/instruction_select.py`): + - Add `_select_` to lower the IR op to `MachineInstr`s. + - For simple ops, one or two RISC-V instructions suffice. + +4. **LLVM codegen** (`scratchv/backend/llvm_codegen.py`): + - Add `_emit_` to produce LLVM IR for the operator. + +5. **Verification** (`scratchv/verification/verifier.py`): + - Add a numpy reference function if existing helpers don't cover it. + +6. **Tests**: add IR → assembly → verification test cases. + +## Testing + +```bash +# Run all tests +pytest tests/ -v + +# Run a single test file +pytest tests/test_ir.py -v + +# Run a specific test +pytest tests/test_ir.py::TestIRBuilder::test_build_simple_add -v + +# Run with coverage +pytest tests/ --cov=scratchv --cov=scratchv_dag --cov-report=html +``` diff --git a/docs/help.md b/docs/help.md deleted file mode 100644 index 3f074b3..0000000 --- a/docs/help.md +++ /dev/null @@ -1,128 +0,0 @@ - \ No newline at end of file diff --git a/output.ll b/output.ll deleted file mode 100644 index 1168c7a..0000000 --- a/output.ll +++ /dev/null @@ -1,15 +0,0 @@ -; LLVM IR generated by ScratchV -; ModuleID = "scratchv_module" -target triple = "riscv64-unknown-elf" - -declare float @expf(float) nounwind readonly -declare float @tanhf(float) nounwind readonly -declare double @exp(double) nounwind readonly -declare double @tanh(double) nounwind readonly -declare void @print_f32(float) nounwind - -define float @add_graph(float %A, float %B) { -; --- entry --- - %v_1_1 = fadd float %A, %B - ret float %v_1_1 -} diff --git a/pyproject.toml b/pyproject.toml index 661435d..cfdd01a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,29 +4,47 @@ build-backend = "setuptools.build_meta" [project] name = "scratchv" -version = "0.2.0" +version = "0.3.0" description = "A compiler from ONNX models to RISC-V assembly and LLVM IR" +readme = "README.md" +license = {text = "MIT"} +keywords = ["compiler", "risc-v", "onnx", "llvm", "machine-learning"] requires-python = ">=3.8" dependencies = [ "onnx>=1.14", "numpy>=1.24", "protobuf>=4.21", ] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Compilers", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] [project.optional-dependencies] riscv = ["tinyfive"] -llvm = ["llvmlite"] # optional: LLVM IR JIT execution -verify = ["onnxruntime"] # optional: ONNX Runtime comparison +llvm = ["llvmlite"] # optional: LLVM IR JIT execution +verify = ["onnxruntime"] # optional: ONNX Runtime comparison all = ["tinyfive", "llvmlite", "onnxruntime"] [project.urls] Source = "https://github.com/kinsomwang/ScratchV" +Documentation = "https://github.com/kinsomwang/ScratchV/tree/main/docs" [project.scripts] scratchv = "scratchv.main:main" [tool.setuptools.packages.find] -include = ["scratchv*"] +include = ["scratchv", "scratchv.*", "scratchv_dag", "scratchv_dag.*"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scratchv/__init__.py b/scratchv/__init__.py index c07d2f5..d0e9509 100644 --- a/scratchv/__init__.py +++ b/scratchv/__init__.py @@ -1,3 +1,3 @@ """ScratchV: A compiler from ONNX models to RISC-V assembly.""" -__version__ = "0.1.0" +__version__ = "0.3.0" diff --git a/scratchv/codegen/__init__.py b/scratchv/codegen/__init__.py index b20f8a7..1061d19 100644 --- a/scratchv/codegen/__init__.py +++ b/scratchv/codegen/__init__.py @@ -1,13 +1,17 @@ -"""Code generation module: LLVM-style SelectionDAG infrastructure.""" -from scratchv.codegen.sdnode import ( +"""Code generation module — re-exports from scratchv_dag. + +This package provides DAG-based instruction selection infrastructure. +The implementation lives in the standalone ``scratchv_dag`` package; +this module serves as a compatibility shim. +""" +# flake8: noqa +from scratchv_dag import ( # noqa: F401 MVT, SDNodeOpcode, SDNodeFlags, SDValue, SDNode, SelectionDAG, -) -from scratchv.codegen.selection_dag import ( DAGBuilder, DAGCombiner, DAGScheduler, diff --git a/scratchv/codegen/sdnode.py b/scratchv/codegen/sdnode.py deleted file mode 100644 index afcd296..0000000 --- a/scratchv/codegen/sdnode.py +++ /dev/null @@ -1,551 +0,0 @@ -""" -SDNode: LLVM-style SelectionDAG node definitions for ScratchV. - -Provides the core DAG node types, machine value types (MVT), opcodes, -and the SelectionDAG container used for DAG-based instruction selection. -""" - -from __future__ import annotations - -import enum -from dataclasses import dataclass -from typing import Optional - - -# ═══════════════════════════════════════════════════════════ -# MVT — Machine Value Type -# ═══════════════════════════════════════════════════════════ - -class MVT(enum.Enum): - """Machine Value Type — represents the type of a value in the DAG.""" - i8 = "i8" - i16 = "i16" - i32 = "i32" - i64 = "i64" - f32 = "f32" - f64 = "f64" - Other = "other" - Void = "void" - - @property - def is_integer(self) -> bool: - return self in (MVT.i8, MVT.i16, MVT.i32, MVT.i64) - - @property - def is_float(self) -> bool: - return self in (MVT.f32, MVT.f64) - - @property - def size_bits(self) -> int: - return { - MVT.i8: 8, MVT.i16: 16, MVT.i32: 32, MVT.i64: 64, - MVT.f32: 32, MVT.f64: 64, - }.get(self, 0) - - @property - def size_bytes(self) -> int: - return self.size_bits // 8 - - @staticmethod - def from_size(bits: int, is_float: bool = False) -> MVT: - if is_float: - return {32: MVT.f32, 64: MVT.f64}.get(bits, MVT.f32) - return {8: MVT.i8, 16: MVT.i16, 32: MVT.i32, 64: MVT.i64}.get(bits, MVT.i32) - - -# ═══════════════════════════════════════════════════════════ -# SDNodeOpcode — DAG node operation codes -# ═══════════════════════════════════════════════════════════ - -class SDNodeOpcode(enum.Enum): - """LLVM-inspired SelectionDAG node opcodes.""" - # ── Constants ────────────────────────────────────── - Constant = "Constant" # integer constant - ConstantFP = "ConstantFP" # floating-point constant - Undef = "Undef" # undefined value - TargetConstant = "TargetConstant" # target-specific constant (e.g. CSR) - - # ── Arithmetic ───────────────────────────────────── - ADD = "ADD" - SUB = "SUB" - MUL = "MUL" - DIV = "DIV" - NEG = "NEG" - UDIV = "UDIV" # unsigned - SRA = "SRA" # shift right arithmetic - SRL = "SRL" # shift right logical - SHL = "SHL" # shift left - - # Floating-point - FADD = "FADD" - FSUB = "FSUB" - FMUL = "FMUL" - FDIV = "FDIV" - FNEG = "FNEG" - FABS = "FABS" - - # ── Comparison ───────────────────────────────────── - SETCC = "SETCC" # set condition (returns i1) - BR_CC = "BR_CC" # branch on condition code - - # ── Type conversion ──────────────────────────────── - FP_EXTEND = "FP_EXTEND" - FP_TRUNC = "FP_TRUNC" - INT_TO_FP = "INT_TO_FP" - FP_TO_INT = "FP_TO_INT" - ANY_EXTEND = "ANY_EXTEND" - TRUNCATE = "TRUNCATE" - BITCAST = "BITCAST" - - # ── Memory ───────────────────────────────────────── - LOAD = "LOAD" - STORE = "STORE" - TokenFactor = "TokenFactor" - - # ── Control ──────────────────────────────────────── - BR = "BR" - BRIND = "BRIND" # indirect branch - RET = "RET" - CALL = "CALL" - - # ── Pseudo ───────────────────────────────────────── - CopyFromReg = "CopyFromReg" - CopyToReg = "CopyToReg" - Register = "Register" - - # ── Target-specific RISC-V ───────────────────────── - LI_Pseudo = "LI_Pseudo" - MV_Pseudo = "MV_Pseudo" - CALL_Pseudo = "CALL_Pseudo" - RET_Pseudo = "RET_Pseudo" - LoadAddress = "LoadAddress" - - # ── NN ops (low-level DAG nodes) ─────────────────── - RELU = "RELU" - MAXPOOL = "MAXPOOL" - GELU = "GELU" - MATMUL = "MATMUL" - - # ── Properties ───────────────────────────────────── - - @property - def has_chain(self) -> bool: - """True if this op has side effects and needs a chain token.""" - return self in _OP_HAS_CHAIN - - @property - def is_memop(self) -> bool: - """True if this is a memory operation.""" - return self in _OP_IS_MEMOP - - @property - def is_commutative(self) -> bool: - return self in (SDNodeOpcode.ADD, SDNodeOpcode.MUL, - SDNodeOpcode.FADD, SDNodeOpcode.FMUL) - - -_OP_HAS_CHAIN = frozenset({ - SDNodeOpcode.LOAD, SDNodeOpcode.STORE, - SDNodeOpcode.BR, SDNodeOpcode.BR_CC, SDNodeOpcode.BRIND, - SDNodeOpcode.RET, SDNodeOpcode.CALL, - SDNodeOpcode.TokenFactor, - SDNodeOpcode.CopyToReg, SDNodeOpcode.CopyFromReg, - SDNodeOpcode.CALL_Pseudo, SDNodeOpcode.RET_Pseudo, -}) - -_OP_IS_MEMOP = frozenset({ - SDNodeOpcode.LOAD, SDNodeOpcode.STORE, -}) - - -# ═══════════════════════════════════════════════════════════ -# SDNodeFlags -# ═══════════════════════════════════════════════════════════ - -@dataclass -class SDNodeFlags: - """Flags attached to an SDNode.""" - no_nan: bool = False - no_signed_zeros: bool = False - no_infs: bool = False - no_unsafe_fp: bool = False - is_volatile: bool = False - is_non_temporal: bool = False - alignment: int = 0 # in bytes, 0 = default - - -# ═══════════════════════════════════════════════════════════ -# SDValue — edge in the DAG (node + result index) -# ═══════════════════════════════════════════════════════════ - -@dataclass -class SDValue: - """Reference to a value produced by an SDNode.""" - node: SDNode - resno: int = 0 - - @property - def value_type(self) -> MVT: - return self.node.value_type(self.resno) - - def __eq__(self, other) -> bool: - if not isinstance(other, SDValue): - return NotImplemented - return self.node is other.node and self.resno == other.resno - - def __hash__(self) -> int: - return id(self.node) ^ self.resno - - def __repr__(self) -> str: - return f"t{self.node.node_id}.{self.resno}:{self.value_type.value}" - - def is_chain(self) -> bool: - return self.resno == self.node.num_chain_results and self.value_type == MVT.Other - - def is_undef(self) -> bool: - return self.node.opcode == SDNodeOpcode.Undef - - -# ═══════════════════════════════════════════════════════════ -# SDNode — single DAG node -# ═══════════════════════════════════════════════════════════ - -class SDNode: - """A node in the SelectionDAG. Each node produces one or more results. - - Layout: - [chain result (MVT.Other)]? [data results ...] - """ - - _next_id: int = 0 - - __slots__ = ( - "node_id", "opcode", "_value_types", "operands", - "flags", "dbg_info", "_num_values", "num_chain_results", - "_attributes", - ) - - def __init__( - self, - opcode: SDNodeOpcode, - value_types: list[MVT], - operands: list[SDValue], - flags: SDNodeFlags | None = None, - dbg_info: str = "", - ): - self.node_id = SDNode._next_id - SDNode._next_id += 1 - self.opcode = opcode - self._value_types = list(value_types) - self.operands = list(operands) - self.flags = flags or SDNodeFlags() - self.dbg_info = dbg_info - self._num_values = len(self._value_types) - self.num_chain_results = 0 - self._attributes = {} - - # ── Value types ──────────────────────────────────── - - def value_type(self, idx: int = 0) -> MVT: - return self._value_types[idx] if idx < self._num_values else MVT.Void - - @property - def num_values(self) -> int: - """Number of non-chain value results.""" - return self._num_values - self.num_chain_results - - @property - def has_chain(self) -> bool: - return self.opcode.has_chain - - def get_chain(self) -> SDValue | None: - """Get the chain operand, if any.""" - if self.has_chain: - for op in self.operands: - if op.is_chain(): - return op - return None - - # ── Constant accessors ───────────────────────────── - - def get_constant_int(self) -> int | None: - """If this is a Constant node, return the integer value.""" - return self._get_attr("const_val") - - def get_constant_fp(self) -> float | None: - if self.opcode == SDNodeOpcode.ConstantFP: - return self._get_attr("const_fp") - return None - - def _get_attr(self, key: str, default=None): - return self._attributes.get(key, default) - - # ── Debug ────────────────────────────────────────── - - def __repr__(self) -> str: - vt = ",".join(v.value for v in self._value_types) - ops = ", ".join(str(op) for op in self.operands[:4]) - if len(self.operands) > 4: - ops += f", ... (+{len(self.operands)-4})" - return (f"t{self.node_id}: {self.opcode.value} [{vt}] " - f"<- ({ops})") - - def dump(self, indent: str = "") -> str: - lines = [f"{indent}Node t{self.node_id}:"] - lines.append(f"{indent} Opcode: {self.opcode.value}") - lines.append(f"{indent} Types: {[v.value for v in self._value_types]}") - lines.append(f"{indent} Operands ({len(self.operands)}):") - for op in self.operands: - lines.append(f"{indent} {op}") - if self._attributes: - lines.append(f"{indent} Attrs: {self._attributes}") - return "\n".join(lines) - - -# ═══════════════════════════════════════════════════════════ -# SelectionDAG — container & node factory -# ═══════════════════════════════════════════════════════════ - -class SelectionDAG: - """Manages all SDNodes and provides factory methods. - - The DAG uses a single chain token (EntryToken) that all side-effecting - nodes implicitly depend upon as the root chain. - """ - - def __init__(self): - self._nodes: list[SDNode] = [] - self._node_map: dict[tuple, SDNode] = {} # dedup cache - self._root: Optional[SDValue] = None - self._debug_loc: dict[int, str] = {} - # Reset node ID counter - SDNode._next_id = 0 - # Create entry token (root chain) - entry = self._new_node( - SDNodeOpcode.TokenFactor, - [MVT.Other], - [], - dbg_info="EntryToken", - ) - entry.num_chain_results = 1 - self._entry_token = SDValue(entry, 0) - - # ── Properties ───────────────────────────────────── - - @property - def entry_token(self) -> SDValue: - return self._entry_token - - @property - def root(self) -> SDValue | None: - return self._root - - @root.setter - def root(self, val: SDValue) -> None: - self._root = val - - @property - def nodes(self) -> list[SDNode]: - return list(self._nodes) - - # ── Node creation ────────────────────────────────── - - def _new_node( - self, - opcode: SDNodeOpcode, - value_types: list[MVT], - operands: list[SDValue], - flags: SDNodeFlags | None = None, - dbg_info: str = "", - **attrs, - ) -> SDNode: - node = SDNode(opcode, value_types, operands, flags, dbg_info) - if opcode.has_chain: - node.num_chain_results = 1 - node._attributes = attrs - self._nodes.append(node) - return node - - def get_constant(self, val: int, vt: MVT = MVT.i32) -> SDValue: - """Get or create a Constant node.""" - key = ("const", vt, val) - if key in self._node_map: - return SDValue(self._node_map[key], 0) - node = self._new_node(SDNodeOpcode.Constant, [vt], [], const_val=val) - self._node_map[key] = node - return SDValue(node, 0) - - def get_constant_fp(self, val: float, vt: MVT = MVT.f32) -> SDValue: - key = ("constfp", vt, val) - if key in self._node_map: - return SDValue(self._node_map[key], 0) - node = self._new_node(SDNodeOpcode.ConstantFP, [vt], [], const_fp=val) - self._node_map[key] = node - return SDValue(node, 0) - - def get_undef(self, vt: MVT = MVT.i32) -> SDValue: - key = ("undef", vt) - if key in self._node_map: - return SDValue(self._node_map[key], 0) - node = self._new_node(SDNodeOpcode.Undef, [vt], []) - self._node_map[key] = node - return SDValue(node, 0) - - def get_register(self, name: str, vt: MVT = MVT.i32) -> SDValue: - node = self._new_node(SDNodeOpcode.Register, [vt], [], - reg_name=name) - return SDValue(node, 0) - - def get_copy_from_reg(self, reg: SDValue, chain: SDValue | None = None) -> SDValue: - chain = chain or self._entry_token - node = self._new_node( - SDNodeOpcode.CopyFromReg, - [MVT.Other, reg.value_type], - [chain, reg], - ) - node.num_chain_results = 1 - return SDValue(node, 1) # data result - - def get_copy_to_reg(self, reg: SDValue, val: SDValue, - chain: SDValue | None = None) -> SDValue: - chain = chain or self._entry_token - node = self._new_node( - SDNodeOpcode.CopyToReg, - [MVT.Other], - [chain, reg, val], - ) - node.num_chain_results = 1 - return SDValue(node, 0) # chain result - - def get_add(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.ADD, lhs, rhs) - - def get_sub(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.SUB, lhs, rhs) - - def get_mul(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.MUL, lhs, rhs) - - def get_div(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.DIV, lhs, rhs) - - def get_fadd(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.FADD, lhs, rhs) - - def get_fsub(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.FSUB, lhs, rhs) - - def get_fmul(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.FMUL, lhs, rhs) - - def get_fdiv(self, lhs: SDValue, rhs: SDValue) -> SDValue: - return self._get_binop(SDNodeOpcode.FDIV, lhs, rhs) - - def _get_binop(self, opcode: SDNodeOpcode, - lhs: SDValue, rhs: SDValue) -> SDValue: - vt = lhs.value_type - node = self._new_node(opcode, [vt], [lhs, rhs]) - return SDValue(node, 0) - - def get_load(self, addr: SDValue, vt: MVT = MVT.i32, - chain: SDValue | None = None, - flags: SDNodeFlags | None = None) -> SDValue: - """Create a LOAD node. Returns (chain, data).""" - chain = chain or self._entry_token - node = self._new_node( - SDNodeOpcode.LOAD, [MVT.Other, vt], - [chain, addr], - flags=flags, - ) - node.num_chain_results = 1 - return SDValue(node, 1) # data result - - def get_store(self, addr: SDValue, val: SDValue, - chain: SDValue | None = None, - flags: SDNodeFlags | None = None) -> SDValue: - """Create a STORE node. Returns the chain.""" - chain = chain or self._entry_token - node = self._new_node( - SDNodeOpcode.STORE, [MVT.Other], - [chain, addr, val], - flags=flags, - ) - node.num_chain_results = 1 - return SDValue(node, 0) # chain result - - def get_br(self, target: str, chain: SDValue | None = None) -> SDValue: - chain = chain or self._entry_token - node = self._new_node(SDNodeOpcode.BR, [MVT.Other], - [chain], branch_target=target) - node.num_chain_results = 1 - return SDValue(node, 0) - - def get_br_cc(self, cond: SDValue, true_target: str, false_target: str, - chain: SDValue | None = None) -> SDValue: - chain = chain or self._entry_token - node = self._new_node( - SDNodeOpcode.BR_CC, [MVT.Other], - [chain, cond], - true_target=true_target, false_target=false_target, - ) - node.num_chain_results = 1 - return SDValue(node, 0) - - def get_ret(self, values: list[SDValue] | None = None, - chain: SDValue | None = None) -> SDValue: - chain = chain or self._entry_token - ops = [chain] + (values or []) - node = self._new_node(SDNodeOpcode.RET, [MVT.Other], ops) - node.num_chain_results = 1 - return SDValue(node, 0) - - def get_call(self, callee: str, args: list[SDValue], - vt: MVT = MVT.i32, - chain: SDValue | None = None) -> SDValue: - """Create a CALL node. Returns (chain, data).""" - chain = chain or self._entry_token - node = self._new_node( - SDNodeOpcode.CALL, [MVT.Other, vt], - [chain, self.get_target_constant(callee)] + args, - callee=callee, - ) - node.num_chain_results = 1 - return SDValue(node, 1) # data result - - def get_target_constant(self, val: str | int, vt: MVT = MVT.i32) -> SDValue: - node = self._new_node(SDNodeOpcode.TargetConstant, [vt], - [], target_val=val) - return SDValue(node, 0) - - def get_token_factor(self, chains: list[SDValue]) -> SDValue: - """Merge multiple chains into one.""" - if len(chains) == 1: - return chains[0] - node = self._new_node(SDNodeOpcode.TokenFactor, [MVT.Other], chains) - node.num_chain_results = 1 - return SDValue(node, 0) - - # ── DAG lifetime ─────────────────────────────────── - - def clear(self) -> None: - self._nodes.clear() - self._node_map.clear() - self._root = None - self._debug_loc.clear() - SDNode._next_id = 0 - entry = self._new_node( - SDNodeOpcode.TokenFactor, [MVT.Other], [], - dbg_info="EntryToken", - ) - entry.num_chain_results = 1 - self._entry_token = SDValue(entry, 0) - - def dump(self) -> str: - lines = ["SelectionDAG:"] - lines.append(f" EntryToken: t{self._entry_token.node.node_id}") - if self._root: - lines.append(f" Root: {self._root}") - lines.append(" Nodes:") - for node in self._nodes: - lines.append(f" {node}") - return "\n".join(lines) diff --git a/scratchv/codegen/selection_dag.py b/scratchv/codegen/selection_dag.py deleted file mode 100644 index 570e4cb..0000000 --- a/scratchv/codegen/selection_dag.py +++ /dev/null @@ -1,521 +0,0 @@ -""" -SelectionDAG builder, combiner, and scheduler. - -DAGBuilder — Translates IR instructions into SelectionDAG nodes. -DAGCombiner — Peephole optimizations over the DAG (fold, simplify). -DAGScheduler — Schedules DAG into linearized MachineInstr list. -""" - -from __future__ import annotations - -from scratchv.ir.types import OpCode, Program, Function, BasicBlock, Instruction -from scratchv.codegen.sdnode import ( - MVT, SDNodeOpcode, SDNodeFlags, SDValue, SelectionDAG, -) -from scratchv.backend.register_alloc import MachineInstr, MachineOp, MachineOperand - - -# ═══════════════════════════════════════════════════════════ -# IR type → MVT mapping -# ═══════════════════════════════════════════════════════════ - -def _ir_to_mvt(dtype) -> MVT: - from scratchv.ir.types import DataType - return { - DataType.FLOAT32: MVT.f32, - DataType.FLOAT64: MVT.f64, - DataType.INT32: MVT.i32, - DataType.INT64: MVT.i64, - }.get(dtype, MVT.i32) - - -# ═══════════════════════════════════════════════════════════ -# DAGBuilder — IR → SelectionDAG -# ═══════════════════════════════════════════════════════════ - -class DAGBuilder: - """Build a SelectionDAG from a ScratchV IR Program.""" - - def __init__(self, program: Program): - self.program = program - self.dag = SelectionDAG() - # Maps IR value names → SDValue - self._value_map: dict[str, SDValue] = {} - self._chain = self.dag.entry_token - - def run(self) -> SelectionDAG: - """Build the DAG for all functions.""" - for func in self.program.functions: - self._build_function(func) - return self.dag - - def _build_function(self, func: Function) -> None: - self._value_map.clear() - self._chain = self.dag.entry_token - - # Map function parameters to CopyFromReg nodes - for i, param in enumerate(func.params): - reg = self.dag.get_register(f"a{i}" if i < 8 else f"s{i-8}") - val = self.dag.get_copy_from_reg(reg) - self._chain = val.node.get_chain() or self._chain - self._value_map[param.name] = val - - for block in func.blocks: - self._build_block(block, func) - - def _build_block(self, block: BasicBlock, func: Function) -> None: - for instr in block.instructions: - self._build_instruction(instr) - - def _build_instruction(self, instr: Instruction) -> None: - handler = getattr(self, f"_build_{instr.opcode.value}", None) - if handler is None: - raise ValueError(f"No DAG builder for opcode: {instr.opcode}") - handler(instr) - - def _get_val(self, ir_val) -> SDValue: - """Map an IR operand Value to an SDValue.""" - if ir_val.is_constant and ir_val.const_value is not None: - vt = _ir_to_mvt(ir_val.dtype) - if vt.is_float: - return self.dag.get_constant_fp(float(ir_val.const_value), vt) - return self.dag.get_constant(int(ir_val.const_value), vt) - name = ir_val.name - if name not in self._value_map: - self._value_map[name] = self.dag.get_undef(_ir_to_mvt(ir_val.dtype)) - return self._value_map[name] - - def _set_val(self, ir_val, sdval: SDValue) -> None: - self._value_map[ir_val.name] = sdval - - # ── Arithmetic ───────────────────────────────────── - - def _build_add(self, instr: Instruction) -> None: - lhs = self._get_val(instr.operands[0]) - rhs = self._get_val(instr.operands[1]) - if lhs.value_type.is_float: - val = self.dag.get_fadd(lhs, rhs) - else: - val = self.dag.get_add(lhs, rhs) - self._set_val(instr.dest, val) - - def _build_sub(self, instr: Instruction) -> None: - lhs = self._get_val(instr.operands[0]) - rhs = self._get_val(instr.operands[1]) - if lhs.value_type.is_float: - val = self.dag.get_fsub(lhs, rhs) - else: - val = self.dag.get_sub(lhs, rhs) - self._set_val(instr.dest, val) - - def _build_mul(self, instr: Instruction) -> None: - lhs = self._get_val(instr.operands[0]) - rhs = self._get_val(instr.operands[1]) - if lhs.value_type.is_float: - val = self.dag.get_fmul(lhs, rhs) - else: - val = self.dag.get_mul(lhs, rhs) - self._set_val(instr.dest, val) - - def _build_div(self, instr: Instruction) -> None: - lhs = self._get_val(instr.operands[0]) - rhs = self._get_val(instr.operands[1]) - if lhs.value_type.is_float: - val = self.dag.get_fdiv(lhs, rhs) - else: - val = self.dag.get_div(lhs, rhs) - self._set_val(instr.dest, val) - - def _build_neg(self, instr: Instruction) -> None: - src = self._get_val(instr.operands[0]) - # neg = sub 0, x or fneg x - if src.value_type.is_float: - zero = self.dag.get_constant_fp(0.0, src.value_type) - val = self.dag.get_fsub(zero, src) - else: - zero = self.dag.get_constant(0, src.value_type) - val = self.dag.get_sub(zero, src) - self._set_val(instr.dest, val) - - def _build_exp(self, instr: Instruction) -> None: - src = self._get_val(instr.operands[0]) - val = self.dag.get_call("expf" if src.value_type == MVT.f32 else "exp", - [src], vt=src.value_type) - self._chain = val.node.get_chain() or self._chain - self._set_val(instr.dest, val) - - def _build_load_const(self, instr: Instruction) -> None: - v = instr.attrs.get("value", 0) - vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32 - if vt.is_float: - val = self.dag.get_constant_fp(float(v), vt) - else: - val = self.dag.get_constant(int(v), vt) - self._set_val(instr.dest, val) - - def _build_load(self, instr: Instruction) -> None: - addr = self._get_val(instr.operands[0]) - vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.i32 - val = self.dag.get_load(addr, vt, chain=self._chain) - self._chain = val.node.get_chain() or self._chain - self._set_val(instr.dest, val) - - def _build_store(self, instr: Instruction) -> None: - addr = self._get_val(instr.operands[0]) - val = self._get_val(instr.operands[1]) - chain = self.dag.get_store(addr, val, chain=self._chain) - self._chain = chain - - def _build_alloca(self, instr: Instruction) -> None: - size = instr.attrs.get("size", 4) - vt = _ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.i32 - # Represent as a constant pointer offset (from sp) - val = self.dag.get_constant(size, vt) - self._set_val(instr.dest, val) - - # ── Control flow ─────────────────────────────────── - - def _build_for(self, instr: Instruction) -> None: - iv = instr.dest - start = instr.attrs.get("start", 0) - end = instr.attrs.get("end", 0) - val = self.dag.get_constant(start, MVT.i32) - self._value_map[instr.dest.name] = val - # Store loop context for endfor - self._loop_ctx = { - "iv_name": iv.name, - "end": end, - } - - def _build_endfor(self, instr: Instruction) -> None: - ctx = getattr(self, "_loop_ctx", None) - if ctx is None: - return - iv_name = ctx["iv_name"] - iv = self._value_map.get(iv_name) - if iv is not None: - one = self.dag.get_constant(1, MVT.i32) - inc = self.dag.get_add(iv, one) - self._value_map[iv_name] = inc - self._loop_ctx = None - - def _build_br(self, instr: Instruction) -> None: - self._chain = self.dag.get_br(instr.target or "", chain=self._chain) - - def _build_br_if(self, instr: Instruction) -> None: - cond = self._get_val(instr.operands[0]) - targets = (instr.target or "").split(",") - true_t = targets[0].strip() if len(targets) > 0 else "" - false_t = targets[1].strip() if len(targets) > 1 else "" - self._chain = self.dag.get_br_cc(cond, true_t, false_t, chain=self._chain) - - def _build_return(self, instr: Instruction) -> None: - vals = [self._get_val(instr.operands[0])] if instr.operands else None - self._chain = self.dag.get_ret(vals, chain=self._chain) - - def _build_label(self, instr: Instruction) -> None: - pass # labels are implicit in DAG - - # ── NN ops ───────────────────────────────────────── - - def _build_relu(self, instr: Instruction) -> None: - src = self._get_val(instr.operands[0]) - val = self.dag.get_call("relu", [src], vt=src.value_type) - self._chain = val.node.get_chain() or self._chain - self._set_val(instr.dest, val) - - def _build_gelu(self, instr: Instruction) -> None: - src = self._get_val(instr.operands[0]) - val = self.dag.get_call("gelu", [src], vt=src.value_type) - self._chain = val.node.get_chain() or self._chain - self._set_val(instr.dest, val) - - def _build_softmax(self, instr: Instruction) -> None: - src = self._get_val(instr.operands[0]) - val = self.dag.get_call("softmax", [src], vt=src.value_type) - self._chain = val.node.get_chain() or self._chain - self._set_val(instr.dest, val) - - def _build_matmul(self, instr: Instruction) -> None: - a = self._get_val(instr.operands[0]) - b = self._get_val(instr.operands[1]) - m = instr.attrs.get("m", 1) - n = instr.attrs.get("n", 1) - k = instr.attrs.get("k", 1) - val = self.dag.get_call(f"matmul_m{m}_n{n}_k{k}", [a, b], - vt=_ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32) - self._chain = val.node.get_chain() or self._chain - self._set_val(instr.dest, val) - - def _build_dot(self, instr: Instruction) -> None: - a = self._get_val(instr.operands[0]) - b = self._get_val(instr.operands[1]) - length = instr.attrs.get("length", 1) - val = self.dag.get_call(f"dot_len{length}", [a, b], - vt=_ir_to_mvt(instr.dest.dtype) if instr.dest else MVT.f32) - self._chain = val.node.get_chain() or self._chain - self._set_val(instr.dest, val) - - -# ═══════════════════════════════════════════════════════════ -# DAGCombiner — peephole optimizations on the DAG -# ═══════════════════════════════════════════════════════════ - -class DAGCombiner: - """DAG-level peephole optimizations: constant folding, redundant removal.""" - - def __init__(self, dag: SelectionDAG): - self.dag = dag - self._changed = False - - def run(self) -> int: - """Run all DAG combines. Returns number of folds applied.""" - n_folds = 0 - # Iterate until stable - for _ in range(32): # limit iterations - self._changed = False - for node in reversed(self.dag._nodes): - self._try_fold(node) - if self._changed: - n_folds += 1 - if not self._changed: - break - return n_folds - - def _try_fold(self, node) -> None: - """Try to fold a single node in-place.""" - handler = getattr(self, f"_fold_{node.opcode.value}", None) - if handler is not None: - handler(node) - - def _fold_ADD(self, node) -> None: - """Constant fold: add(const, const) -> const""" - lhs, rhs = self._get_const_binop(node) - if lhs is not None and rhs is not None: - val = self.dag.get_constant(lhs + rhs, node.value_type()) - self._replace_node(node, val) - - def _fold_SUB(self, node) -> None: - lhs, rhs = self._get_const_binop(node) - if lhs is not None and rhs is not None: - val = self.dag.get_constant(lhs - rhs, node.value_type()) - self._replace_node(node, val) - - def _fold_MUL(self, node) -> None: - lhs, rhs = self._get_const_binop(node) - if lhs is not None and rhs is not None: - val = self.dag.get_constant(lhs * rhs, node.value_type()) - self._replace_node(node, val) - - def _fold_DIV(self, node) -> None: - lhs, rhs = self._get_const_binop(node) - if lhs is not None and rhs is not None and rhs != 0: - val = self.dag.get_constant(lhs // rhs, node.value_type()) - self._replace_node(node, val) - - def _fold_FADD(self, node) -> None: - self._fold_fp_binop(node, lambda a, b: a + b) - - def _fold_FSUB(self, node) -> None: - self._fold_fp_binop(node, lambda a, b: a - b) - - def _fold_FMUL(self, node) -> None: - self._fold_fp_binop(node, lambda a, b: a * b) - - def _fold_FDIV(self, node) -> None: - self._fold_fp_binop(node, lambda a, b: a / b) - - def _fold_fp_binop(self, node, op) -> None: - lhs = self._get_fp_const(node, 0) - rhs = self._get_fp_const(node, 1) - if lhs is not None and rhs is not None: - try: - val = self.dag.get_constant_fp(op(lhs, rhs), node.value_type()) - self._replace_node(node, val) - except (ZeroDivisionError, OverflowError, ValueError): - pass - - def _get_const_binop(self, node): - """Return (lhs_int, rhs_int) if both operands are Constant.""" - if len(node.operands) < 2: - return None, None - lhs = node.operands[0].node.get_constant_int() - rhs = node.operands[1].node.get_constant_int() - return lhs, rhs - - def _get_fp_const(self, node, idx: int): - op = node.operands[idx] - return op.node.get_constant_fp() - - def _replace_node(self, old_node, new_val: SDValue) -> None: - """Replace all uses of old_node with new_val (simple).""" - old_node._attributes["replaced_by"] = new_val - self._changed = True - - -# ═══════════════════════════════════════════════════════════ -# DAGScheduler — DAG → linear MachineInstr list -# ═══════════════════════════════════════════════════════════ - -class DAGScheduler: - """Schedule a SelectionDAG into a linear sequence of MachineInstrs.""" - - def __init__(self, dag: SelectionDAG): - self.dag = dag - - def run(self) -> list[MachineInstr]: - """Topological schedule: emit nodes in dependency order.""" - scheduled: set[int] = set() - result: list[MachineInstr] = [] - label_counter = [0] - - def fresh_label(prefix="L"): - label_counter[0] += 1 - return f"{prefix}_{label_counter[0]}" - - def schedule_node(node, chain_token=None): - if node.node_id in scheduled: - return - # Schedule operands first (post-order DFS) - for op in node.operands: - if op.node.node_id not in scheduled: - schedule_node(op.node) - scheduled.add(node.node_id) - - opcode = node.opcode - try: - machine_op = _SDNODE_TO_MACHINE_OP[opcode] - except KeyError: - # Skip nodes without a direct MachineOp mapping - return - - dst = None - src1 = None - src2 = None - comment = "" - - if opcode == SDNodeOpcode.Constant: - dst = MachineOperand.vreg(f"t{node.node_id}") - val = node.get_constant_int() or 0 - result.append(MachineInstr( - MachineOp.LI, dst, MachineOperand.immediate(val), - comment=f"const {val}")) - return - - if opcode == SDNodeOpcode.ConstantFP: - dst = MachineOperand.vreg(f"t{node.node_id}") - val = node.get_constant_fp() or 0.0 - result.append(MachineInstr( - MachineOp.LI, dst, MachineOperand.immediate(int(val)), - comment=f"constfp {val}")) - return - - if opcode == SDNodeOpcode.CopyFromReg: - # Should be handled by register allocator - reg = node._get_attr("reg_name", "zero") - dst = MachineOperand.vreg(f"t{node.node_id}") - result.append(MachineInstr( - MachineOp.MV, dst, MachineOperand.reg(reg), - comment="copy_from_reg")) - return - - if opcode in (SDNodeOpcode.LOAD,): - dst = MachineOperand.vreg(f"t{node.node_id}") - src1 = _op_to_operand(node.operands[1], node, fresh_label) - result.append(MachineInstr( - MachineOp.LW, dst, src1, comment="load")) - return - - if opcode == SDNodeOpcode.STORE: - src1 = _op_to_operand(node.operands[1], node, fresh_label) - src2 = _op_to_operand(node.operands[2], node, fresh_label) - result.append(MachineInstr( - MachineOp.SW, src1, src2, comment="store")) - return - - if opcode == SDNodeOpcode.BR: - target = node._get_attr("branch_target", "") - result.append(MachineInstr( - MachineOp.J, comment=target)) - return - - if opcode == SDNodeOpcode.BR_CC: - cond = _op_to_operand(node.operands[1], node, fresh_label) - true_t = node._get_attr("true_target", "") - false_t = node._get_attr("false_target", "") - result.append(MachineInstr( - MachineOp.BNEZ, cond, comment=true_t)) - result.append(MachineInstr( - MachineOp.J, comment=false_t)) - return - - if opcode == SDNodeOpcode.RET: - result.append(MachineInstr( - MachineOp.JALR, MachineOperand.vreg("zero"), - MachineOperand.vreg("ra"), comment="ret")) - return - - if opcode == SDNodeOpcode.CALL: - callee = node._get_attr("callee", "unknown") - result.append(MachineInstr( - MachineOp.CALL, comment=callee)) - if node.num_values > 0: - dst = MachineOperand.vreg(f"t{node.node_id}") - result.append(MachineInstr( - MachineOp.MV, dst, MachineOperand.vreg("a0"))) - return - - # Generic binop emission - if len(node.operands) >= 2: - src1 = _op_to_operand(node.operands[0], node, fresh_label) - src2 = _op_to_operand(node.operands[1], node, fresh_label) - elif len(node.operands) >= 1: - src1 = _op_to_operand(node.operands[0], node, fresh_label) - - if node.num_values > 0 and node._num_values > node.num_chain_results: - dst = MachineOperand.vreg(f"t{node.node_id}") - - result.append(MachineInstr(machine_op, dst, src1, src2, comment)) - - # Schedule all nodes - for node in self.dag._nodes: - schedule_node(node) - - return result - - -def _op_to_operand(sdval: SDValue, parent_node, fresh_label_fn) -> MachineOperand: - """Convert SDValue to MachineOperand (vreg).""" - if sdval.node.opcode == SDNodeOpcode.Constant: - val = sdval.node.get_constant_int() or 0 - return MachineOperand.immediate(val) - if sdval.node.opcode == SDNodeOpcode.ConstantFP: - val = sdval.node.get_constant_fp() or 0.0 - return MachineOperand.immediate(int(val)) - if sdval.node.opcode == SDNodeOpcode.Register: - name = sdval.node._get_attr("reg_name", "zero") - return MachineOperand.reg(name) - return MachineOperand.vreg(f"t{sdval.node.node_id}") - - -_SDNODE_TO_MACHINE_OP: dict[SDNodeOpcode, MachineOp] = { - SDNodeOpcode.ADD: MachineOp.ADD, - SDNodeOpcode.SUB: MachineOp.SUB, - SDNodeOpcode.MUL: MachineOp.MUL, - SDNodeOpcode.DIV: MachineOp.DIV, - SDNodeOpcode.FADD: MachineOp.ADD, - SDNodeOpcode.FSUB: MachineOp.SUB, - SDNodeOpcode.FMUL: MachineOp.MUL, - SDNodeOpcode.FDIV: MachineOp.DIV, - SDNodeOpcode.NEG: MachineOp.SUB, - SDNodeOpcode.SETCC: MachineOp.SUB, # placeholder - SDNodeOpcode.LOAD: MachineOp.LW, - SDNodeOpcode.STORE: MachineOp.SW, - SDNodeOpcode.BR: MachineOp.J, - SDNodeOpcode.BR_CC: MachineOp.BNEZ, - SDNodeOpcode.RET: MachineOp.JALR, - SDNodeOpcode.CALL: MachineOp.CALL, - SDNodeOpcode.LI_Pseudo: MachineOp.LI, - SDNodeOpcode.MV_Pseudo: MachineOp.MV, - SDNodeOpcode.RELU: MachineOp.MAX, -} diff --git a/scratchv/memory/__init__.py b/scratchv/memory/__init__.py index d0f684e..63a83dd 100644 --- a/scratchv/memory/__init__.py +++ b/scratchv/memory/__init__.py @@ -1,6 +1,14 @@ -"""Memory module: cache simulation and memory allocation.""" -from scratchv.memory.cache import L1Cache, CacheConfig, CacheStats -from scratchv.memory.allocator import ( +"""Memory module — re-exports from scratchv_dag. + +Provides L1 cache simulation and cache-aware memory allocation. +The implementation lives in the standalone ``scratchv_dag`` package; +this module serves as a compatibility shim. +""" +# flake8: noqa +from scratchv_dag import ( # noqa: F401 + L1Cache, + CacheConfig, + CacheStats, MemoryAllocator, AllocationPolicy, MemoryRegion, diff --git a/scratchv/memory/allocator.py b/scratchv/memory/allocator.py deleted file mode 100644 index b93fbb6..0000000 --- a/scratchv/memory/allocator.py +++ /dev/null @@ -1,361 +0,0 @@ -""" -Cache-aware memory allocator for edge NPU. - -Implements: -- Buddy allocator for configurable memory pool sizes -- Cache-line-aligned allocation for L1 cache-friendly access patterns -- Scratchpad region for explicit DMA/tile memory -- Allocation tracking and statistics -""" - -from __future__ import annotations - -import math -from dataclasses import dataclass, field -from enum import Enum -from typing import Optional - - -class AllocationPolicy(Enum): - """Memory allocation strategy.""" - FIRST_FIT = "first_fit" - BEST_FIT = "best_fit" - BUDDY = "buddy" - - -@dataclass -class MemoryRegion: - """A contiguous memory region.""" - name: str - base: int # base address (byte offset from pool start) - size: int # size in bytes - used: bool = False - alignment: int = 4 # required alignment - - @property - def end(self) -> int: - return self.base + self.size - - def __repr__(self) -> str: - status = "used" if self.used else "free" - return (f"Region({self.name}: 0x{self.base:x}-0x{self.end:x}, " - f"{self.size}B, {status}, align={self.alignment})") - - -@dataclass -class AllocStats: - """Allocation statistics.""" - total_allocated: int = 0 - total_freed: int = 0 - num_allocs: int = 0 - num_frees: int = 0 - largest_free_block: int = 0 - fragmentation_pct: float = 0.0 - cache_misses_avoided: int = 0 # from aligned allocations - - def __repr__(self) -> str: - return (f"AllocStats(allocated={self.total_allocated}, " - f"freed={self.total_freed}, " - f"active={self.num_allocs - self.num_frees}, " - f"largest_free={self.largest_free_block}, " - f"frag={self.fragmentation_pct:.1f}%)") - - -# ═══════════════════════════════════════════════════════════ -# MemoryAllocator -# ═══════════════════════════════════════════════════════════ - -class MemoryAllocator: - """Cache-aware memory allocator with buddy system and alignment support. - - The pool is divided into a scratchpad region (fast, explicit DMA) and - a general-purpose region (cached). All allocations are cache-line-aligned - by default (64B) to avoid L1 cache line ping-pong. - """ - - def __init__( - self, - pool_size: int = 4 * 1024 * 1024, # 4 MB total - cache_line: int = 64, # L1 cache line size - scratchpad_ratio: float = 0.25, # 25% for scratchpad - policy: AllocationPolicy = AllocationPolicy.BUDDY, - ): - self.pool_size = pool_size - self.cache_line = cache_line - self.policy = policy - self.stats = AllocStats() - - # Split pool: scratchpad (high-speed, uncached) + general (cached) - scratch_size = int(pool_size * scratchpad_ratio) - # Align scratchpad size to cache line - scratch_size = self._align_up(scratch_size, cache_line) - gen_size = pool_size - scratch_size - - self.scratchpad = MemoryRegion("scratchpad", 0, scratch_size) - self._regions: list[MemoryRegion] = [ - MemoryRegion("general", scratch_size, gen_size), - ] - self._freed_regions: list[MemoryRegion] = [] - self._next_id = 0 - - # Scratchpad cursor (next free address) - self._scratchpad_cursor = 0 - - # For buddy: power-of-two free lists - self._buddy_free: dict[int, list[int]] = {} # size -> list of base addrs - self._buddy_allocated: dict[int, int] = {} # id -> base addr - - # Populate buddy free list - if policy == AllocationPolicy.BUDDY: - self._init_buddy(gen_size) - - # ── Public API ───────────────────────────────────── - - def alloc(self, size: int, alignment: int = 0, - prefer_scratchpad: bool = False) -> int: - """Allocate `size` bytes. Returns base address (offset from pool start). - - Args: - size: Requested size in bytes. - alignment: Required alignment (0 = use cache_line default). - prefer_scratchpad: If True, try scratchpad region first. - - Returns: - Base offset, or -1 if allocation fails. - """ - alignment = alignment or self.cache_line - size = self._align_up(size, alignment) - - if prefer_scratchpad: - aligned_base = self._align_up(self._scratchpad_cursor, alignment) - if aligned_base + size <= self.scratchpad.size: - self._scratchpad_cursor = aligned_base + size - return aligned_base - # fall through to general pool - - if self.policy == AllocationPolicy.BUDDY: - addr = self._buddy_alloc(size) - else: - gen = self._regions[0] - cursor = getattr(self, "_general_cursor", gen.base) - aligned_base = self._align_up(cursor, alignment) - if aligned_base + size <= gen.end: - self._general_cursor = aligned_base + size - addr = aligned_base - else: - addr = -1 - - if addr >= 0: - self.stats.total_allocated += size - self.stats.num_allocs += 1 - # If aligned to cache line, we avoided a potential false-sharing miss - if alignment >= self.cache_line: - self.stats.cache_misses_avoided += 1 - - return addr - - def free(self, addr: int) -> bool: - """Free a previously allocated block. - - Returns True if the address was freed successfully. - """ - # Check scratchpad - if self._addr_in_region(addr, self.scratchpad): - return True # scratchpad doesn't track individual frees - - if self.policy == AllocationPolicy.BUDDY: - return self._buddy_free_block(addr) - - # Linear scan for first-fit segments - for i, region in enumerate(self._regions): - if region.base == addr and region.used: - region.used = False - self._freed_regions.append(region) - self.stats.total_freed += region.size - self.stats.num_frees += 1 - # Coalesce adjacent free regions - self._coalesce() - return True - return False - - def scratchpad_alloc(self, size: int, alignment: int = 64) -> int: - """Allocate from the scratchpad (uncached, fast SRAM).""" - return self.alloc(size, alignment, prefer_scratchpad=True) - - def get_region_info(self, addr: int) -> Optional[MemoryRegion]: - """Get info about which region an address belongs to.""" - if self._addr_in_region(addr, self.scratchpad): - return self.scratchpad - for region in self._regions: - if self._addr_in_region(addr, region): - return region - return None - - def is_in_scratchpad(self, addr: int) -> bool: - return self._addr_in_region(addr, self.scratchpad) - - def reset(self) -> None: - """Reset all allocations.""" - self._scratchpad_cursor = 0 - gen_size = self.pool_size - self.scratchpad.size - self._regions = [MemoryRegion("general", self.scratchpad.size, gen_size)] - self._freed_regions.clear() - self.stats = AllocStats() - self._next_id = 0 - if self.policy == AllocationPolicy.BUDDY: - self._init_buddy(gen_size) - - # ── Buddy allocator ──────────────────────────────── - - def _init_buddy(self, total_size: int) -> None: - self._buddy_free.clear() - self._buddy_allocated.clear() - # Find the largest power of two <= total_size - max_pow2 = 1 << (total_size.bit_length() - 1) - base = self._regions[0].base - self._buddy_free[max_pow2] = [base] - # Add remaining chunk as a smaller block - remainder = total_size - max_pow2 - if remainder > 0: - pow2 = 1 << (remainder.bit_length() - 1) - self._buddy_free[pow2] = [base + max_pow2] - - def _buddy_alloc(self, size: int) -> int: - """Allocate using buddy system. - - Rounds up size to the next power of two, finds a free block - of that size, splitting larger blocks as needed. - """ - block_size = 1 << (max(size, self.cache_line).bit_length() - 1) - if block_size < size: - block_size <<= 1 - - # Find an available block of suitable size - available_sizes = sorted(s for s in self._buddy_free if self._buddy_free[s]) - if not available_sizes: - return -1 - - # Find smallest available size >= block_size - chosen_size = None - for s in available_sizes: - if s >= block_size: - chosen_size = s - break - - if chosen_size is None: - return -1 - - # Split until we get the target size - free_list = self._buddy_free[chosen_size] - addr = free_list.pop(0) - - while chosen_size > block_size: - chosen_size >>= 1 - buddy_addr = addr + chosen_size - self._buddy_free.setdefault(chosen_size, []).append(buddy_addr) - - self._buddy_allocated[addr] = block_size - return addr - - def _buddy_free_block(self, addr: int) -> bool: - """Free a buddy-allocated block, coalescing with its buddy.""" - block_size = self._buddy_allocated.pop(addr, None) - if block_size is None: - return False - - self._buddy_free.setdefault(block_size, []).append(addr) - - # Coalesce: repeatedly merge with buddy if both are free - while True: - free_list = self._buddy_free[block_size] - buddy_addr = addr ^ block_size # XOR to find buddy - if buddy_addr in free_list: - free_list.remove(buddy_addr) - addr = min(addr, buddy_addr) - block_size <<= 1 - self._buddy_free.setdefault(block_size, []).append(addr) - self.stats.total_freed += block_size // 2 - else: - break - - self.stats.total_freed += block_size - self.stats.num_frees += 1 - return True - - # ── First-fit / Best-fit helpers ─────────────────── - - def _alloc_from_region(self, region: MemoryRegion, - size: int, alignment: int) -> int: - """Allocate from a region using first-fit. - - Never mutates the input region's base/size; tracks the cursor - in a caller-owned variable. - """ - # Allocate from the general region by managing a cursor - cursor = getattr(self, "_general_cursor", region.base) - aligned_base = self._align_up(cursor, alignment) - if aligned_base + size <= region.end: - self._general_cursor = aligned_base + size - return aligned_base - return -1 - - def _coalesce(self) -> None: - """Coalesce adjacent free regions.""" - free_regs = sorted( - [r for r in self._freed_regions if not r.used], - key=lambda r: r.base, - ) - self._freed_regions = [r for r in self._freed_regions if r.used] - - merged = [] - for r in free_regs: - if merged and merged[-1].end == r.base: - merged[-1] = MemoryRegion( - merged[-1].name, merged[-1].base, - merged[-1].size + r.size, - ) - else: - merged.append(r) - self._freed_regions.extend(merged) - - # ── Utilities ────────────────────────────────────── - - @staticmethod - def _align_up(addr: int, alignment: int) -> int: - if alignment <= 0: - alignment = 4 - mask = alignment - 1 - return (addr + mask) & ~mask - - @staticmethod - def _addr_in_region(addr: int, region: MemoryRegion) -> bool: - return region.base <= addr < region.base + region.size - - # ── Debug ────────────────────────────────────────── - - def dump(self) -> str: - lines = [ - f"Memory Allocator ({self.pool_size >> 20} MB pool, " - f"policy={self.policy.value}, " - f"cache_line={self.cache_line}B):", - f" Scratchpad: {self.scratchpad} ({self.align_up(0, 0)})", - f" Regions ({len(self._regions)}):", - ] - for r in self._regions: - lines.append(f" {r}") - if self._freed_regions: - lines.append(f" Freed regions ({len(self._freed_regions)}):") - for r in self._freed_regions[:8]: - lines.append(f" {r}") - if len(self._freed_regions) > 8: - lines.append(f" ... (+{len(self._freed_regions)-8})") - if self.policy == AllocationPolicy.BUDDY: - lines.append(f" Buddy free lists:") - for size, addrs in sorted(self._buddy_free.items()): - if addrs: - lines.append(f" {size}B: {len(addrs)} blocks") - lines.append(f" Stats: {self.stats}") - return "\n".join(lines) - - def align_up(self, addr: int, alignment: int = 0) -> int: - return self._align_up(addr, alignment or self.cache_line) diff --git a/scratchv/memory/cache.py b/scratchv/memory/cache.py deleted file mode 100644 index 7a983bb..0000000 --- a/scratchv/memory/cache.py +++ /dev/null @@ -1,265 +0,0 @@ -""" -L1 cache simulator for edge NPU. - -Models a 4 MB L1 cache with configurable line size, associativity, -and replacement policy. Tracks hits, misses, evictions and bandwidth. -""" - -from __future__ import annotations - -import math -from dataclasses import dataclass, field -from typing import Optional - - -@dataclass -class CacheConfig: - """Configuration for the L1 cache.""" - total_size: int = 4 * 1024 * 1024 # 4 MB - line_size: int = 64 # bytes per cache line - associativity: int = 8 # N-way set associative - write_back: bool = True # True = write-back, False = write-through - write_allocate: bool = True # allocate on write miss - hit_latency: int = 2 # cycles (typical L1) - miss_latency: int = 20 # cycles (penalty to go to L2/DRAM) - - @property - def num_lines(self) -> int: - return self.total_size // self.line_size - - @property - def num_sets(self) -> int: - return self.num_lines // self.associativity - - def __post_init__(self): - assert self.total_size > 0 and self.total_size % self.line_size == 0 - assert self.line_size > 0 and (self.line_size & (self.line_size - 1)) == 0 - assert self.associativity > 0 - assert self.num_sets > 0 - - -@dataclass -class CacheStats: - """Cache performance counters.""" - hits: int = 0 - misses: int = 0 - evictions: int = 0 - write_backs: int = 0 - total_cycles: int = 0 - bytes_read: int = 0 - bytes_written: int = 0 - - @property - def hit_rate(self) -> float: - total = self.hits + self.misses - return self.hits / total if total > 0 else 0.0 - - @property - def miss_rate(self) -> float: - total = self.hits + self.misses - return self.misses / total if total > 0 else 0.0 - - @property - def avg_latency(self) -> float: - total = self.hits + self.misses - return self.total_cycles / total if total > 0 else 0.0 - - def reset(self) -> None: - self.hits = 0 - self.misses = 0 - self.evictions = 0 - self.write_backs = 0 - self.total_cycles = 0 - self.bytes_read = 0 - self.bytes_written = 0 - - def __repr__(self) -> str: - return (f"CacheStats(hits={self.hits}, misses={self.misses}, " - f"hit_rate={self.hit_rate:.2%}, evictions={self.evictions}, " - f"write_backs={self.write_backs}, " - f"avg_latency={self.avg_latency:.1f}cy)") - - -# ═══════════════════════════════════════════════════════════ -# Cache line -# ═══════════════════════════════════════════════════════════ - -@dataclass -class CacheLine: - """A single cache line.""" - tag: int = 0 - valid: bool = False - dirty: bool = False - last_access: int = 0 # for LRU - - def __repr__(self) -> str: - return (f"Line(tag=0x{self.tag:x}, valid={self.valid}, " - f"dirty={self.dirty}, lru={self.last_access})") - - -# ═══════════════════════════════════════════════════════════ -# L1Cache -# ═══════════════════════════════════════════════════════════ - -class L1Cache: - """Set-associative L1 cache simulator. - - Usage: - cache = L1Cache() - cache.read(0x1000, 4) # read 4 bytes from addr 0x1000 - cache.write(0x1000, 4) # write 4 bytes to addr 0x1000 - print(cache.stats) - """ - - def __init__(self, config: CacheConfig | None = None): - self.config = config or CacheConfig() - self.stats = CacheStats() - self._clock = 0 - - # Build cache: list of sets, each set has N lines - self._sets: list[list[CacheLine]] = [ - [CacheLine() for _ in range(self.config.associativity)] - for _ in range(self.config.num_sets) - ] - - self._mask_offset = int(math.log2(self.config.line_size)) - self._mask_index = int(math.log2(self.config.num_sets)) - self._tag_shift = self._mask_offset + self._mask_index - - # ── Public API ───────────────────────────────────── - - def read(self, addr: int, size: int = 4) -> int: - """Read `size` bytes from `addr`. Returns total latency.""" - latency = 0 - start_line = addr // self.config.line_size - end_line = (addr + size - 1) // self.config.line_size - - for line_addr in range(start_line, end_line + 1): - block_addr = line_addr * self.config.line_size - latency += self._access_line(block_addr, is_write=False) - - if size > self.config.line_size: - latency += self.config.miss_latency # cross-line penalty - - self.stats.total_cycles += latency - self.stats.bytes_read += size - return latency - - def write(self, addr: int, size: int = 4) -> int: - """Write `size` bytes to `addr`. Returns total latency.""" - latency = 0 - start_line = addr // self.config.line_size - end_line = (addr + size - 1) // self.config.line_size - - for line_addr in range(start_line, end_line + 1): - block_addr = line_addr * self.config.line_size - latency += self._access_line(block_addr, is_write=True) - - if size > self.config.line_size: - latency += self.config.miss_latency - - self.stats.total_cycles += latency - self.stats.bytes_written += size - return latency - - def flush(self) -> int: - """Flush all dirty lines. Returns total cycles.""" - cycles = 0 - for set_idx in range(self.config.num_sets): - for line in self._sets[set_idx]: - if line.valid and line.dirty: - cycles += self.config.miss_latency - self.stats.write_backs += 1 - line.dirty = False - self.stats.total_cycles += cycles - return cycles - - def reset(self) -> None: - """Reset cache state and stats.""" - for set_idx in range(self.config.num_sets): - for line in self._sets[set_idx]: - line.valid = False - line.dirty = False - line.tag = 0 - line.last_access = 0 - self.stats.reset() - self._clock = 0 - - # ── Internals ────────────────────────────────────── - - def _addr_to_set_tag(self, addr: int) -> tuple[int, int]: - """Extract (set_index, tag) from an address.""" - set_idx = (addr >> self._mask_offset) & (self.config.num_sets - 1) - tag = addr >> self._tag_shift - return set_idx, tag - - def _access_line(self, block_addr: int, is_write: bool) -> int: - """Access a single cache line. Returns latency.""" - self._clock += 1 - set_idx, tag = self._addr_to_set_tag(block_addr) - line_set = self._sets[set_idx] - - # Look for a hit - for line in line_set: - if line.valid and line.tag == tag: - # Cache hit - self.stats.hits += 1 - line.last_access = self._clock - if is_write and self.config.write_back: - line.dirty = True - return self.config.hit_latency - - # Cache miss - self.stats.misses += 1 - - if not self.config.write_allocate and is_write: - # Write-no-allocate: skip cache, go to next level - return self.config.miss_latency - - # Find an eviction candidate (LRU) - victim = self._find_lru(line_set) - assert victim is not None - - # Write back if dirty - if victim.valid and victim.dirty: - self.stats.write_backs += 1 - self.stats.evictions += 1 - - # Fill the line - victim.tag = tag - victim.valid = True - victim.dirty = is_write and self.config.write_back - victim.last_access = self._clock - - return self.config.hit_latency + self.config.miss_latency - - def _find_lru(self, line_set: list[CacheLine]) -> CacheLine: - """Find the least-recently-used line in a set.""" - lru_line = line_set[0] - lru_time = lru_line.last_access - for line in line_set[1:]: - if not line.valid: - return line # empty slot - if line.last_access < lru_time: - lru_time = line.last_access - lru_line = line - return lru_line - - # ── Debug ────────────────────────────────────────── - - def dump(self) -> str: - lines = [ - f"L1 Cache ({self.config.total_size >> 20} MB, " - f"{self.config.line_size}B lines, " - f"{self.config.associativity}-way):", - f" Sets: {self.config.num_sets}, Lines: {self.config.num_lines}", - f" Stats: {self.stats}", - ] - # Print first few non-empty sets - shown = 0 - for set_idx in range(self.config.num_sets): - valid_lines = [l for l in self._sets[set_idx] if l.valid] - if valid_lines and shown < 8: - lines.append(f" Set {set_idx}: {valid_lines}") - shown += 1 - return "\n".join(lines) diff --git a/setup.py b/setup.py index 6068493..b5a80c6 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,7 @@ +"""ScratchV — ONNX to RISC-V assembly compiler. + +Minimal setup.py for editable installs. Build configuration lives in pyproject.toml. +""" from setuptools import setup setup() From 1aabb91eddbfbd2a21899aededf1e682882c9376 Mon Sep 17 00:00:00 2001 From: marshalji Date: Wed, 20 May 2026 14:47:51 +0800 Subject: [PATCH 6/7] add docs --- README.md | 68 +++----- ScratchV_Promo.pptx | Bin 45506 -> 53248 bytes docs/ScratchV.md | 147 ++++++++++++++++++ ...11\347\224\237\346\210\220\345\231\250.md" | 33 ++++ ...60\347\273\237\350\256\241\345\231\250.md" | 32 ++++ ...24\344\274\230\345\214\226\345\231\250.md" | 35 +++++ ...10\345\271\266\344\274\230\345\214\226.md" | 32 ++++ ...47\346\211\253\346\217\217\357\274\211.md" | 31 ++++ ...50\350\260\203\345\272\246\357\274\211.md" | 31 ++++ ...57\345\242\236\345\274\272\345\231\250.md" | 32 ++++ ...16\346\240\274\345\274\217\345\214\226.md" | 33 ++++ ...IR\351\252\214\350\257\201\345\231\250.md" | 35 +++++ ...07\344\273\244\351\200\211\346\213\251.md" | 32 ++++ ...01\347\276\216\345\214\226\345\231\250.md" | 32 ++++ ...13\350\257\225\345\245\227\344\273\266.md" | 34 ++++ ...27\345\242\236\345\274\272\345\231\250.md" | 33 ++++ ...72\347\276\216\345\214\226\345\231\250.md" | 33 ++++ 17 files changed, 628 insertions(+), 45 deletions(-) create mode 100644 docs/ScratchV.md create mode 100644 "docs/topics/\350\257\276\351\242\23011\357\274\232\346\216\247\345\210\266\346\265\201\345\233\276\357\274\210CFG\357\274\211\347\224\237\346\210\220\345\231\250.md" create mode 100644 "docs/topics/\350\257\276\351\242\23012\357\274\232RISC-V\345\220\216\347\253\257\346\214\207\344\273\244\350\256\241\346\225\260\347\273\237\350\256\241\345\231\250.md" create mode 100644 "docs/topics/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" create mode 100644 "docs/topics/\350\257\276\351\242\23014\357\274\232\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226.md" create mode 100644 "docs/topics/\350\257\276\351\242\23017\357\274\232\345\257\204\345\255\230\345\231\250\345\210\206\351\205\215\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\347\272\277\346\200\247\346\211\253\346\217\217\357\274\211.md" create mode 100644 "docs/topics/\350\257\276\351\242\23018\357\274\232\346\214\207\344\273\244\350\260\203\345\272\246\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\345\210\227\350\241\250\350\260\203\345\272\246\357\274\211.md" create mode 100644 "docs/topics/\350\257\276\351\242\2301\357\274\232DSL\345\211\215\347\253\257\345\242\236\345\274\272\345\231\250.md" create mode 100644 "docs/topics/\350\257\276\351\242\23020\357\274\232\351\241\271\347\233\256\344\273\243\347\240\201\350\247\204\350\214\203\344\270\216\346\240\274\345\274\217\345\214\226.md" create mode 100644 "docs/topics/\350\257\276\351\242\23021\357\274\232IR\351\252\214\350\257\201\345\231\250.md" create mode 100644 "docs/topics/\350\257\276\351\242\23028\357\274\232\345\256\214\345\226\204\345\220\216\347\253\257\346\214\207\344\273\244\351\200\211\346\213\251.md" create mode 100644 "docs/topics/\350\257\276\351\242\2305\357\274\232RISC-V\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250.md" create mode 100644 "docs/topics/\350\257\276\351\242\2306\357\274\232\347\274\226\350\257\221\345\231\250\346\200\247\350\203\275\346\265\213\350\257\225\345\245\227\344\273\266.md" create mode 100644 "docs/topics/\350\257\276\351\242\2307\357\274\232\347\274\226\350\257\221\345\231\250\346\227\245\345\277\227\345\242\236\345\274\272\345\231\250.md" create mode 100644 "docs/topics/\350\257\276\351\242\2309\357\274\232DSL\351\224\231\350\257\257\346\217\220\347\244\272\347\276\216\345\214\226\345\231\250.md" diff --git a/README.md b/README.md index 91c849c..0c62a0f 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ **From ONNX to RISC-V assembly — a minimal compiler built from scratch.** -ScratchV is a 12-week educational project that implements a complete compiler +ScratchV is a educational project that implements a complete compiler toolchain: parse an ONNX model (or a simple DSL), lower it through a custom intermediate representation (IR), apply optimizations, and emit RISC-V assembly -code executable on QEMU, Spike, TinyFive, or real hardware. +code executable on TinyFive, or real hardware. --- @@ -62,25 +62,29 @@ ScratchV/ **Recommended: virtual environment** ```bash -python3 -m venv .venv +python3.8 -m venv .venv source .venv/bin/activate pip install -e . -pip install onnx numpy # ONNX model support -pip install tinyfive # assembly verification (optional) +pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.tuna.tsinghua.edu.cn +pip install onnx -i https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.tuna.tsinghua.edu.cn +pip install tinyfive -i https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.tuna.tsinghua.edu.cn ``` -**Alternative (pipx):** +### Compile an ONNX model ```bash -pipx install . -pipx inject scratchv onnx numpy tinyfive -``` +# Generate test ONNX models +python examples/gen_onnx_model.py -> Debian/Ubuntu users: if you get an "externally-managed-environment" error, -> use the venv method above, or append `--break-system-packages`: -> ```bash -> pip install --break-system-packages -e . -> ``` +# Compile with RISC-V backend +scratchv models/add.onnx -o add.s --optimize all + +# Compile with LLVM backend +scratchv models/add.onnx --backend llvm -o add.ll --optimize all + +# Verify against ONNX Runtime +scratchv models/add.onnx --verify +``` ### Compile a DSL model @@ -96,22 +100,7 @@ scratchv examples/relu_test.dsl -o relu.s --optimize all --dump-ir # Matrix multiply scratchv examples/matmul_test.dsl -o matmul.s --optimize all -``` - -### Compile an ONNX model - -```bash -# Generate test ONNX models -python examples/gen_onnx_model.py - -# Compile with RISC-V backend -scratchv models/add.onnx -o add.s --optimize all - -# Compile with LLVM backend -scratchv models/add.onnx --backend llvm -o add.ll --optimize all - -# Verify against ONNX Runtime -scratchv models/add.onnx --verify +python -m scratchv.main examples/matmul_test.dsl -o matmul.s --optimize all ``` ### Verify with TinyFive @@ -188,7 +177,7 @@ DSL Source ──▶ DSL Parser ────┘ │ └──────────────────────────────────────────────────────────────┘ ``` -### Optimization Passes + -## Roadmap (12 Weeks) - -| Weeks | Phase | Goal | -| :--- | :--- | :--- | -| 1-2 | Setup | Toolchain, QEMU, run baseline benchmarks | -| 3-4 | IR | ONNX parser, core IR, basic ops (Add, Mul, MatMul) | -| 5-6 | Optimizer | CF + DCE + peephole, more ops (ReLU, MaxPool, GELU) | -| 7-8 | Backend I | Instruction selection, naive reg alloc, control flow | -| 9-10 | Backend II | Greedy reg alloc, LICM, muladd fusion, benchmark validation | -| 11-12 | Docs | Design doc, user manual, final presentation, perf analysis | - -## DSL Syntax + ## License diff --git a/ScratchV_Promo.pptx b/ScratchV_Promo.pptx index d02c1aea80fa97e5bd58e2c6e06290b53b7b8730..d3086f6edba5fc6ed521a311fe3d8200f80b649f 100644 GIT binary patch literal 53248 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 F>4Gj1H~0Vm literal 45506 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 diff --git a/docs/ScratchV.md b/docs/ScratchV.md new file mode 100644 index 0000000..2f9564d --- /dev/null +++ b/docs/ScratchV.md @@ -0,0 +1,147 @@ + +# 🧭 探索“AI模型→芯片指令”的神奇之旅 | 零基础友好开源项目招募 + +## 你有没有好奇过…… + +- 你写的 Python 代码,电脑到底是怎么“听懂”并执行的? +- 那些炫酷的 AI 模型(比如能识别猫狗、写诗的那种),最后是怎么在小小的芯片上跑起来的? +- 编译器——这个听起来很高深的东西,到底在做什么? + +如果这些问题让你心里痒痒的,哪怕你现在**只学过一点点编程**,甚至**还没上过编译原理课**——**这个项目就是为你准备的**。 + +--- + +## 📌 我们要一起做什么? + +用 **三个月** 的时间,**从零开始**,一起**亲手搭建一个迷你编译器**。 + +- **输入**:一个简单的 AI 模型文件(比如一个会做加法、乘法的“小模型”) +- **输出**:一段可以被 RISC-V 芯片执行的指令(汇编代码,看起来像 `add`、`load` 这种“芯片语言”) +- **然后**:放到模拟芯片的软件(tinyfive)里跑一跑,看它能不能正确计算 + +**整个过程完全由你自己动手实现:读懂模型 → 翻译成中间语言 → 优化 → 生成指令** +我们不会依赖像 LLVM、MLIR 这种巨型框架——**每一步都让你亲手写出来,真正搞懂背后的原理**。 + +> ✨ 你不需要有编译器基础,我们会从最最基础的概念讲起。 + +--- + +## 🗺️ 三个月的学习路线(带飞计划) + +我们为你设计了**循序渐进**的里程碑,每周任务清晰,有人答疑,不让你一个人瞎撞。 + +| 阶段 | 你会学到什么 | 感受 | +| :--- | :--- | :--- | +| **phase 1** | 跑通别人写好的完整例子,看懂模型 → 指令的“魔法”全过程 | 哇,原来是这样! | +| **phase 2** | 自己写代码:把一个简单的 ONNX 模型(比如加法、矩阵乘)翻译成自己的中间语言;或者写一个后端,生成 RISC-V 汇编 | 开始创造,成就感爆棚 | +| **phase 3** | 让你的编译器跑通更多模型,优化编译器,使得编译出的指令更短、运行更快,写文档,把项目变成你简历上的骄傲 | 我居然做出了一个编译器! | + +**每周只需要 8~10 小时**(包含学习、写代码、和小伙伴讨论),我们会提供: +- 预置的框架和 benchmark(你不用从负数开始) +- 每周一次线上答疑 + 讲解 +- 详细的参考资料和代码示例 + +--- + +## 🔥 为什么你值得来试一试? + +### 1. 不需要“大神基础”,只需要“好奇心和耐心” +- 你学过一点点 Python 或 C?够了。 +- 你听说过“数组”、“函数”、“循环”?完全够。 +- 你甚至不知道 RISC-V 是什么?没关系,我们用两周带你入门。 + +> 我们不会丢给你一堆论文和源码,而是**像教小朋友搭积木一样,一块一块搭起来**。 + +### 2. 你会获得“真东西”,而不是调包侠 +- 学完这个项目,你不再是只会 `import torch` 的 AI 使用者。 +- 你将**理解从数学模型到机器指令的完整链路**,这是做高性能计算、AI 芯片、系统软件的核心能力。 +- 项目完成之后,你的简历上会多一行:**“独立实现了一个 AI 到 RISC-V 的完整编译器”**——HR 和面试官会眼前一亮的。 + +### 3. 温暖的开源社区,一起成长 +- 每周线上会议,有问题随时问,不会觉得孤单。 +- 小组内 peer review 代码,互相改 bug,一起庆祝每个 milestone 的达成。 +- 完成项目后,你的代码会成为开源项目的一部分,帮助后来者。 + +--- + +## 🙋 谁适合报名? + +我们特别欢迎这样的你: + +- ✅ 大二、大三、研一,或者自学编程爱好者 +- ✅ 学过一门编程语言(Python / C / C++ 都行) +- ✅ 对“计算机到底怎么跑程序”有好奇心,愿意花时间钻研 +- ✅ **不怕犯错,敢写代码**(Bug 是学习的一部分!) +- ✅ 每周能拿出 8~10 小时(周末集中两天,或者平时每晚 1-2 小时) + +你可能**还没学过编译原理**、**还没搞懂指令集**、**甚至对汇编有点畏惧**——都没关系。 +我们就是来带你一步步跨过这些坎的。 + +--- + +## 📅 关键时间节点 + +| 时间 | 事项 | +| :--- | :--- | +| **即日起** | 开始报名| +| **6 月 20 日** | 线上宣讲 + 课题选择 + **报名截止** | +| **7 月 10 日** | 项目正式开启(启动会 + 第一周任务发布) | +| **8 月 1 日** | Phase 1 关键成果验收(里程碑 checkpoint) | +| **8 月 28 日** | Phase 2 关键成果验收(第二个里程碑) | +| **9 月 27 日** | 项目结项(成果展示 + 结项总结) | + +--- + +## 💬 常见疑问 + +**Q:我连 ONNX 都没听过,能行吗?** +A:当然可以。我们第 1 周就会带你跑通一个例子,ONNX 只是一个文件格式,你把它当成“模型存盘”就好。 + +**Q:我没学过编译原理,会不会听不懂?** +A:我们会用很直观的比喻(比如把编译器想像成“翻译官”,把模型语言翻译成芯片语言),避开理论轰炸,先动手再做总结。 + +**Q:需要买 RISC-V 开发板吗?** +A:不需要。全程用软件仿真模拟器,在你的笔记本电脑上就能跑。 + +**Q:如果我中途跟不上怎么办?** +A:每个阶段都有进度检查,我们会主动帮你。项目设计时已经留出了缓冲时间,而且你可以选择只完成核心路径,放弃一些附加优化。**完成比完美更重要。** + + +--- + +## 🌟 从今天起,给自己一个“创造编译器”的机会 + +也许你现在觉得编译器遥不可及, +但三个月后,你会看着自己写的代码,把一行行模型规则变成芯片指令, +那种“我居然做到了”的感觉,会是你大学期间最难忘的回忆之一。 + +**不要让“基础不够”成为不敢开始的理由。** +**我们等你一起来,写出属于你的第一个编译器。** + +👉 **立即报名**:[【问卷星】](https://your-form-link.com) +📧 咨询邮箱:mentor@example.com +qq群:xxxxxxxxx + +📢 欢迎转发给同样好奇的小伙伴,一起挑战! + +**#零基础编译器 #RISC-V #开源项目 #动手实践 #从兴趣到能力** +**你不需要很厉害才能开始,但你需要开始才能很厉害。** + +## 课题精选 + +| 编号 | 名称 | 难度 | +| :--- | :--- | :--- | +| 6 | 编译器性能测试套件 | 中 | +| 7 | 编译器日志增强器 | 低 | +| 9 | DSL错误提示美化器 | 中 | +| 1 | DSL前端增强器 | 中 | +| 13 | 窥孔优化器 | 低 | +| 14 | 常量加载合并优化 | 低 | +| 5 | RISC-V汇编代码美化器 | 低 | +| 20 | 项目代码规范与格式化 | 低 | +| 21 | IR 验证器 | 中 | +| 28 | 完善后端指令选择 | 中 | +| 11 | 控制流图(CFG)生成器 | 高 | +| 12 | RISC-V后端指令计数统计器 | 高 | +| 17 | 寄存器分配(基本块内线性扫描) | 高 | +| 18 | 指令调度(基本块内列表调度) | 高 | diff --git "a/docs/topics/\350\257\276\351\242\23011\357\274\232\346\216\247\345\210\266\346\265\201\345\233\276\357\274\210CFG\357\274\211\347\224\237\346\210\220\345\231\250.md" "b/docs/topics/\350\257\276\351\242\23011\357\274\232\346\216\247\345\210\266\346\265\201\345\233\276\357\274\210CFG\357\274\211\347\224\237\346\210\220\345\231\250.md" new file mode 100644 index 0000000..920bc07 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23011\357\274\232\346\216\247\345\210\266\346\265\201\345\233\276\357\274\210CFG\357\274\211\347\224\237\346\210\220\345\231\250.md" @@ -0,0 +1,33 @@ +## 课题11:控制流图(CFG)生成器 + +**难度**:高 + +**概述**:从IR中构建控制流图,实现不可达基本块消除和循环检测,输出可视化图。 + +**详细任务**: +1. 解析IR,划分基本块(以标签、跳转、返回为边界)。 +2. 构建有向图:节点为基本块,边为跳转关系(条件/无条件)。 +3. 实现不可达块消除:从入口块DFS标记可达块,删除不可达块并更新IR。 +4. 实现循环检测:基于支配树寻找返回边,识别自然循环。 +5. 使用`graphviz`输出CFG为`dot`格式,并渲染为PNG/PDF。 +6. 集成到优化管道,添加`--cfg`选项输出CFG。 + +**交付产物**: +- `cfg_builder.py`模块 +- 可视化脚本 +- 测试用例及生成的CFG图片 +- 文档:使用方法、算法说明 + +**12周每周目标**: +- **W1**:学习控制流图概念,阅读IR基本块划分方法。 +- **W2**:实现基本块划分函数:输入IR指令列表,输出块列表(每个块有ID、指令列表、终止指令)。 +- **W3**:构建CFG:遍历每个块,根据最后一条指令(`BR`, `JMP`, `RET`)添加边。 +- **W4**:输出CFG文本形式(节点列表,边列表),测试`if-else`和`while`示例。 +- **W5**:学习`graphviz`的`dot`语言,生成简单图。 +- **W6**:将CFG转换为`dot`格式,节点显示块内前几条指令摘要,边标注跳转条件。 +- **W7**:实现不可达块消除:从入口块BFS/DFS标记可达块,删除不可达块。 +- **W8**:学习支配树概念,实现简单算法计算每个块的直接支配者。 +- **W9**:基于支配树识别自然循环(寻找返回边,循环头是支配者),输出循环结构。 +- **W10**:集成不可达消除到优化管道,添加`--eliminate-unreachable`选项。 +- **W11**:优化循环检测,识别嵌套循环,在CFG图中高亮不同深度循环。 +- **W12**:撰写文档,包含算法流程图、使用示例、可视化样例。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23012\357\274\232RISC-V\345\220\216\347\253\257\346\214\207\344\273\244\350\256\241\346\225\260\347\273\237\350\256\241\345\231\250.md" "b/docs/topics/\350\257\276\351\242\23012\357\274\232RISC-V\345\220\216\347\253\257\346\214\207\344\273\244\350\256\241\346\225\260\347\273\237\350\256\241\345\231\250.md" new file mode 100644 index 0000000..6003ea9 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23012\357\274\232RISC-V\345\220\216\347\253\257\346\214\207\344\273\244\350\256\241\346\225\260\347\273\237\350\256\241\345\231\250.md" @@ -0,0 +1,32 @@ +## 课题12:RISC-V后端指令计数统计器 + +**难度**:高 + +**概述**:解析生成的RISC-V汇编,统计不同类型指令的数量(算术、逻辑、访存、分支等),生成可视化的性能报告。 + +**详细任务**: +1. 定义指令分类字典:将操作码映射到类别(ALU、MEM、BRANCH、JUMP、MISC)。 +2. 解析汇编文件,提取每行的操作码,累加类别计数。 +3. 输出统计表格(指令总数、各类别数量及占比)。 +4. 支持多个文件对比,绘制条形图(使用`matplotlib`)。 +5. 生成HTML报告(包含图表和表格)。 +6. 集成到测试套件中,每次性能测试自动生成指令统计。 + +**交付产物**: +- `inst_stat.py`脚本 +- 示例报告(HTML+图片) +- 文档:命令行参数、如何添加新指令映射 + +**12周每周目标**: +- **W1**:学习RISC-V指令集分类,列出常见指令及其类别。 +- **W2**:编写汇编解析器,逐行提取操作码(跳过注释、空行、标签)。 +- **W3**:构建分类字典(`add->ALU`, `lw->MEM`, `beq->BRANCH`等),覆盖项目生成的所有指令。 +- **W4**:实现统计计数器,输出文本表格(类别、计数、占比)。 +- **W5**:支持多个文件输入,输出对比表格。 +- **W6**:使用`matplotlib`绘制饼图和条形图。 +- **W7**:增加指令扩展信息:统计每类指令的具体操作码分布(如ALU中`add`多少次)。 +- **W8**:实现`--diff`模式:比较两个汇编文件(如优化前后),输出变化明细。 +- **W9**:使用`jinja2`模板生成HTML报告,嵌入图表。 +- **W10**:集成到测试套件(课题6),每次测试自动生成指令统计报告。 +- **W11**:处理伪指令(`li`, `mv`)的统计:展开成真实指令或单独分类。 +- **W12**:撰写文档,包含添加新指令映射的指南、命令行参数详解。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" "b/docs/topics/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" new file mode 100644 index 0000000..dd11dd7 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23013\357\274\232\347\252\245\345\255\224\344\274\230\345\214\226\345\231\250.md" @@ -0,0 +1,35 @@ +## 课题13:窥孔优化器 + +**难度**:低 + +**概述**:在生成的RISC-V汇编代码上,匹配并替换低效指令序列(如连续加法、冗余移动等),减少指令数。 + +**详细任务**: +1. 定义3~5个窥孔优化规则,例如: + - `addi x1, x1, 1; addi x1, x1, 1` → `addi x1, x1, 2` + - `mv x1, x2; mv x2, x1` → 删除两条(如果可交换) + - `li x1, 0; addi x1, x1, 1` → `li x1, 1` + - `beq x0, x0, label` → 无条件跳转`j label` +2. 编写汇编解析器,将每行解析为对象(标签、操作码、操作数列表)。 +3. 实现滑动窗口扫描,匹配规则并替换,迭代直到不动点。 +4. 输出优化后的汇编,并统计匹配次数和节省的指令数。 +5. 集成到编译器后端,添加`--peephole`开关。 + +**交付产物**: +- 独立的`peephole.py`脚本或集成模块 +- 测试汇编文件及优化前后对比 +- 文档:规则列表、使用方法 + +**12周每周目标**: +- **W1**:学习窥孔优化原理,收集常见低效汇编模式。 +- **W2**:设计规则表(每条规则包含模式指令列表和替换指令列表)。 +- **W3**:编写汇编加载函数,将每行解析为对象(操作码、操作数等),保留原始字符串。 +- **W4**:实现模式匹配:滑动窗口大小等于规则长度,比较操作码和操作数(支持通配符如任意寄存器)。 +- **W5**:实现替换:删除匹配窗口,插入新指令列表,重新扫描。 +- **W6**:实现第一条规则:`addi x1,x1,1; addi x1,x1,1` → `addi x1,x1,2`。测试。 +- **W7**:实现规则:`mv x1, x2; mv x2, x1` → 删除两条(简单情况)。 +- **W8**:实现规则:`li x1, 0; addi x1, x1, 1` → `li x1, 1`。 +- **W9**:实现规则:`beq x0, x0, label` → `j label`(需要处理标签)。 +- **W10**:增加优化报告,打印匹配次数、节省的指令数。 +- **W11**:集成到编译器后端(在汇编生成后自动调用),添加`--peephole`开关。 +- **W12**:测试10个以上汇编文件,用模拟器验证正确性,撰写文档。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23014\357\274\232\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226.md" "b/docs/topics/\350\257\276\351\242\23014\357\274\232\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226.md" new file mode 100644 index 0000000..9923ece --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23014\357\274\232\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226.md" @@ -0,0 +1,32 @@ +## 课题14:常量加载合并优化 + +**难度**:低 + +**概述**:优化RISC-V加载大常量的指令序列,将`lui` + `addi`对合并为单条`li`伪指令,并消除冗余`lui`。 + +**详细任务**: +1. 理解`lui`(加载高20位)和`addi`(加低12位,注意符号扩展)构成32位常量的机制。 +2. 识别连续两条指令:`lui rd, imm_hi`后跟`addi rd, rd, imm_lo`,计算最终常量值。 +3. 替换为一条`li rd, final_value`(如果后端支持`li`),否则保留但减少一条指令。 +4. 检测冗余`lui`:同一个`rd`的`lui`在之前出现过且中间未修改,则删除后面的`lui`,调整`addi`的源寄存器。 +5. 实现迭代扫描,统计节省的指令数。 +6. 集成到后端,添加`--merge-constants`开关。 + +**交付产物**: +- 优化脚本或模块 +- 测试汇编文件(包含各种常量值) +- 文档:算法原理、使用示例 + +**12周每周目标**: +- **W1**:学习RISC-V加载大常量的机制,手动拆解一个32位常量(如`0x12345678`)。 +- **W2**:编写汇编解析函数,识别`lui`和`addi`指令,提取目标寄存器和立即数。 +- **W3**:实现合并检测:判断连续两条指令是否构成`lui+addi`对,计算最终常数值(处理符号扩展)。 +- **W4**:实现替换:删除原两条,插入`li rd, final_value`(若后端支持),否则保留原指令但添加注释。 +- **W5**:处理冗余`lui`:扫描中记录每个寄存器的最后一次`lui`值,若重复则删除后面`lui`。 +- **W6**:实现合并优化函数,扫描整个汇编文件,迭代应用直到没有变化。 +- **W7**:测试各种常量值(正数、负数、边界`0x80000000`),用模拟器验证结果相同。 +- **W8**:增加优化报告:显示合并的对数、删除的冗余`lui`数、节省指令数。 +- **W9**:集成到编译器后端(在代码生成后执行),添加`--merge-constants`开关。 +- **W10**:处理特殊情况:`addi`使用的寄存器不是`lui`的目标(如`lui x1; addi x2, x1`),谨慎合并。 +- **W11**:扩展支持跨基本块复用(简单版本)。 +- **W12**:撰写文档,包含常量拆分与合并的数学原理。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23017\357\274\232\345\257\204\345\255\230\345\231\250\345\210\206\351\205\215\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\347\272\277\346\200\247\346\211\253\346\217\217\357\274\211.md" "b/docs/topics/\350\257\276\351\242\23017\357\274\232\345\257\204\345\255\230\345\231\250\345\210\206\351\205\215\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\347\272\277\346\200\247\346\211\253\346\217\217\357\274\211.md" new file mode 100644 index 0000000..2372e02 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23017\357\274\232\345\257\204\345\255\230\345\231\250\345\210\206\351\205\215\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\347\272\277\346\200\247\346\211\253\346\217\217\357\274\211.md" @@ -0,0 +1,31 @@ +## 课题17:寄存器分配(基本块内线性扫描) + +**难度**:高 + +**概述**:为每个基本块内的虚拟寄存器分配真实的RISC-V物理寄存器(x1~x31),并在不够用时插入溢出(spill)代码。 + +**详细任务**: +1. 分析基本块内每个虚拟寄存器的活跃区间(定义点到最后一个使用点)。 +2. 实现线性扫描算法:按起始点排序,维护活跃区间列表,分配物理寄存器。 +3. 当物理寄存器不足时,选择溢出变量(最晚结束的区间),存入栈中,需要时重新加载。 +4. 生成溢出加载/存储指令,更新栈帧偏移。 +5. 与现有代码生成集成,添加`--regalloc=linear`选项。 + +**交付产物**: +- `regalloc_linear.py`模块 +- 测试程序(大量变量),对比优化前后汇编代码 +- 文档:算法描述、使用方法 + +**12周每周目标**: +- **W1**:学习寄存器分配基本概念:虚拟寄存器、物理寄存器、活跃区间、溢出。 +- **W2**:分析项目现有的寄存器分配(如果有)或当前代码生成如何使用虚拟寄存器。 +- **W3**:为每个基本块提取所有虚拟寄存器的定义和使用点,计算活跃区间(从定义到最后一次使用)。 +- **W4**:实现活跃区间计算:遍历IR,记录每个虚拟寄存器的起始和结束位置。 +- **W5**:实现线性扫描:将所有区间按起始点排序,维护活跃列表,分配物理寄存器(x1-x31)。 +- **W6**:实现溢出策略:当物理寄存器不够时,选择最晚结束的区间溢出。 +- **W7**:实现溢出代码生成:在定义后插入`sw`存储到栈,在使用前插入`lw`加载,维护栈槽分配。 +- **W8**:实现物理寄存器替换:将虚拟寄存器替换为分配的物理寄存器,注意保留x0和ra等。 +- **W9**:处理调用约定:被调用者保存寄存器(如x8-x9)需要在函数入口保存、出口恢复。 +- **W10**:集成到代码生成阶段,在生成RISC-V指令前进行寄存器分配,添加`--regalloc=linear`选项。 +- **W11**:测试简单函数(少量变量),验证生成的汇编使用了物理寄存器且无冲突。 +- **W12**:撰写设计文档,包含算法步骤、溢出策略、性能评测。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23018\357\274\232\346\214\207\344\273\244\350\260\203\345\272\246\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\345\210\227\350\241\250\350\260\203\345\272\246\357\274\211.md" "b/docs/topics/\350\257\276\351\242\23018\357\274\232\346\214\207\344\273\244\350\260\203\345\272\246\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\345\210\227\350\241\250\350\260\203\345\272\246\357\274\211.md" new file mode 100644 index 0000000..53e1010 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23018\357\274\232\346\214\207\344\273\244\350\260\203\345\272\246\357\274\210\345\237\272\346\234\254\345\235\227\345\206\205\345\210\227\350\241\250\350\260\203\345\272\246\357\274\211.md" @@ -0,0 +1,31 @@ +## 课题18:指令调度(基本块内列表调度) + +**难度**:高 + +**概述**:在基本块内重排RISC-V指令,减少数据冒险引起的流水线停顿,提高指令级并行性。 + +**详细任务**: +1. 定义RISC-V指令延迟模型(如`lw`延迟2周期,算术指令1周期)。 +2. 构建依赖有向图:节点为指令,边为RAW/WAR/WAW依赖,边权为延迟周期。 +3. 实现列表调度算法:维护就绪队列(所有前驱已调度),按优先级(最长路径长度)选择指令发射。 +4. 输出调度后的指令序列,并计算预估的总时钟周期数(相比原始顺序的改善)。 +5. 集成到后端,添加`--schedule`选项。 + +**交付产物**: +- `inst_scheduler.py`模块 +- 测试用例及调度前后对比 +- 文档:延迟模型、算法说明 + +**12周每周目标**: +- **W1**:学习指令调度原理(数据冒险、流水线停顿、列表调度算法)。 +- **W2**:定义RISC-V简单延迟模型(如`lw`延迟2,算术指令1,分支1)。 +- **W3**:实现依赖分析:为基本块内指令构建有向图,节点为指令索引,边为RAW依赖。 +- **W4**:添加WAR和WAW依赖边(虽不引起真冒险,但影响寄存器分配,先处理RAW)。 +- **W5**:为每个节点计算优先级(最长路径长度到结束节点)。 +- **W6**:实现列表调度核心:维护就绪队列,按优先级选择指令,更新时钟。 +- **W7**:实现调度器,输出调度后的指令序列。忽略分支延迟槽。 +- **W8**:编写模拟器功能:给定原始顺序和调度后顺序,比较预估总周期数。 +- **W9**:处理分支指令:分支必须作为基本块的最后一条,调度时不能将其提前。 +- **W10**:实现寄存器重命名(可选,复杂),先不实现,靠列表调度避免冲突。 +- **W11**:集成到代码生成后端,添加`--schedule`选项,测试短基本块。 +- **W12**:测试更复杂的循环体,分析调度前后性能提升,撰写文档。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\2301\357\274\232DSL\345\211\215\347\253\257\345\242\236\345\274\272\345\231\250.md" "b/docs/topics/\350\257\276\351\242\2301\357\274\232DSL\345\211\215\347\253\257\345\242\236\345\274\272\345\231\250.md" new file mode 100644 index 0000000..d310743 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\2301\357\274\232DSL\345\211\215\347\253\257\345\242\236\345\274\272\345\231\250.md" @@ -0,0 +1,32 @@ +## 课题1:DSL前端增强器 + +**难度**:中 + +**概述**:为现有DSL增加条件判断(`if/else`)和循环(`while`)语法,扩展解析器并生成对应的三地址码(IR)。 + +**详细任务**: +1. 理解现有`dsl_parser.py`(递归下降解析)和`ir_builder.py`。 +2. 设计新语法的BNF规则:`if_stmt -> 'if' '(' cond ')' block ('else' block)?`,`while_stmt -> 'while' '(' cond ')' block`。 +3. 修改解析器,增加`parse_if()`和`parse_while()`方法,构建AST节点(`IfNode`, `WhileNode`)。 +4. 扩展IR生成:为`IfNode`生成条件跳转(`BR cond, label_then, label_else`)和标签;为`WhileNode`生成循环结构。 +5. 处理嵌套语句,确保标签编号唯一。 +6. 编写至少3个完整的DSL程序(含分支和循环),使用后端生成汇编并用模拟器验证。 + +**交付产物**: +- 增强后的`dsl_parser.py`和`ir_builder.py` +- 示例DSL程序(`if_else.dsl`, `while_sum.dsl`, `nested_loop.dsl`) +- 文档:新增语法说明、使用示例 + +**12周每周目标**: +- **W1**:搭建环境,运行现有DSL示例。阅读`dsl_parser.py`和`ir_builder.py`,画出现有流程思维导图。 +- **W2**:学习递归下降解析原理,为`if`语法设计BNF规则,编写伪代码。 +- **W3**:添加`parse_if()`方法,识别`if`关键字和括号,构建`IfNode`(简单存储条件、then块、else块)。 +- **W4**:实现条件表达式的解析(支持`==, <, >`等),输出AST结构。 +- **W5**:学习项目IR表示(三地址码,含`BR`, `LABEL`)。为`IfNode`编写IR生成函数。 +- **W6**:实现`if-else`完整IR生成(两个分支,汇合标签)。测试简单`if`程序。 +- **W7**:添加`while`语法,解析为`WhileNode`。设计IR模式:条件判断->循环体->跳回。 +- **W8**:实现`while`的IR生成,确保退出条件正确。测试`while`求和程序。 +- **W9**:处理嵌套`if`和`while`,确保标签编号不冲突(使用计数器)。测试嵌套例子。 +- **W10**:增加错误恢复(可结合课题9的成果),完善注释。 +- **W11**:编写3个完整DSL程序,使用后端生成汇编,用`tinyfive.py`验证结果。 +- **W12**:撰写文档(使用说明、新增语法示例、内部设计图),准备演示。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23020\357\274\232\351\241\271\347\233\256\344\273\243\347\240\201\350\247\204\350\214\203\344\270\216\346\240\274\345\274\217\345\214\226.md" "b/docs/topics/\350\257\276\351\242\23020\357\274\232\351\241\271\347\233\256\344\273\243\347\240\201\350\247\204\350\214\203\344\270\216\346\240\274\345\274\217\345\214\226.md" new file mode 100644 index 0000000..7b00120 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23020\357\274\232\351\241\271\347\233\256\344\273\243\347\240\201\350\247\204\350\214\203\344\270\216\346\240\274\345\274\217\345\214\226.md" @@ -0,0 +1,33 @@ +## 课题20:项目代码规范与格式化 + +**难度**:低 + +**概述**:为项目引入代码格式化工具(Black、isort)和静态检查工具(Ruff、mypy),统一代码风格,确保质量。 + +**详细任务**: +1. 配置`pyproject.toml`,添加Black和isort配置。 +2. 配置Ruff(或Flake8)进行代码风格检查,定义忽略规则。 +3. 配置mypy进行静态类型检查,为关键模块添加类型注解。 +4. 添加pre-commit hooks,确保提交前自动格式化和检查。 +5. 在CI中增加lint步骤(结合课题6的CI)。 +6. 格式化整个项目代码库,修复所有lint错误。 + +**交付产物**: +- `pyproject.toml`配置文件 +- `.pre-commit-config.yaml` +- CI配置文件中的lint作业 +- 文档:代码规范指南、如何运行格式化和检查 + +**12周每周目标**: +- **W1**:学习Black/isort/Ruff/mypy的用法和配置方法,本地安装测试。 +- **W2**:编写`pyproject.toml`,配置Black行长度(如88)、isort配置。 +- **W3**:配置Ruff,选择要启用的检查规则(如E、F、W),定义忽略规则。 +- **W4**:在本地运行Black和isort格式化整个项目,提交PR。 +- **W5**:配置mypy,为项目根目录添加`mypy.ini`,先忽略错误较多的模块。 +- **W6**:为关键模块(如`ir.py`、`dsl_parser.py`)添加类型注解。 +- **W7**:逐步为其他模块添加类型注解,修复mypy错误。 +- **W8**:安装pre-commit,编写`.pre-commit-config.yaml`,包含black、isort、ruff、mypy。 +- **W9**:测试pre-commit hooks,确保每次提交自动运行。 +- **W10**:在CI配置中添加lint作业(使用ruff和mypy),与课题6的CI集成。 +- **W11**:修复CI中发现的剩余lint错误,确保CI通过。 +- **W12**:撰写代码规范文档,包含如何安装pre-commit、如何运行检查。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23021\357\274\232IR\351\252\214\350\257\201\345\231\250.md" "b/docs/topics/\350\257\276\351\242\23021\357\274\232IR\351\252\214\350\257\201\345\231\250.md" new file mode 100644 index 0000000..3c63690 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23021\357\274\232IR\351\252\214\350\257\201\345\231\250.md" @@ -0,0 +1,35 @@ +## 课题21:IR验证器 + +**难度**:中 + +**概述**:实现一个IR验证器,在优化前后检查IR的合法性(变量定义使用、跳转标签存在、类型一致、控制流完整性)。 + +**详细任务**: +1. 设计IR合法性规则: + - 变量必须先定义再使用。 + - 跳转标签必须存在对应`LABEL`。 + - 基本块必须以跳转(`BR`、`JMP`)或`RET`结尾。 + - 二元运算的操作数类型一致。 +2. 实现遍历`Program`、`Function`、`BasicBlock`的验证函数。 +3. 检查控制流:无条件跳转后不能有后继指令,有条件跳转后恰好有两个分支。 +4. 集成到编译器主流程,在每次优化Pass前后自动调用验证器,出错时输出详细报告并停止编译。 +5. 添加`--verify-ir`命令行开关。 + +**交付产物**: +- `ir_verifier.py`模块 +- 测试用例(合法和非法IR) +- 文档:验证规则列表、如何扩展 + +**12周每周目标**: +- **W1**:学习项目IR的数据结构(`ir.py`中的`Instruction`, `BasicBlock`, `Function`等)。 +- **W2**:设计验证规则列表,按类别(变量、标签、类型、控制流)组织。 +- **W3**:实现变量定义-使用检查:遍历每个基本块,维护变量定义集合,检测未定义使用。 +- **W4**:实现标签存在性检查:收集所有`LABEL`指令的目标,检查跳转指令的目标是否存在。 +- **W5**:实现基本块结尾检查:确保每个块以跳转或返回结尾,否则报错。 +- **W6**:实现类型一致性检查:二元运算的两个操作数类型相同,比较操作结果类型为整数等。 +- **W7**:实现控制流完整性检查:无条件跳转后不能有后继指令,有条件跳转后有两个后继块。 +- **W8**:将验证器封装为函数`verify_ir(program)`,返回错误列表。 +- **W9**:集成到编译器主流程,在解析后、每个优化Pass后、代码生成前调用验证器。 +- **W10**:添加`--verify-ir`命令行开关,默认开启(或仅在DEBUG模式开启)。 +- **W11**:编写测试用例:构造非法IR(如缺失标签、类型不匹配),验证验证器能捕获。 +- **W12**:撰写文档,包含所有验证规则及示例。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\23028\357\274\232\345\256\214\345\226\204\345\220\216\347\253\257\346\214\207\344\273\244\351\200\211\346\213\251.md" "b/docs/topics/\350\257\276\351\242\23028\357\274\232\345\256\214\345\226\204\345\220\216\347\253\257\346\214\207\344\273\244\351\200\211\346\213\251.md" new file mode 100644 index 0000000..7a9edd3 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\23028\357\274\232\345\256\214\345\226\204\345\220\216\347\253\257\346\214\207\344\273\244\351\200\211\346\213\251.md" @@ -0,0 +1,32 @@ +## 课题28:完善后端指令选择 + +**难度**:中 + +**概述**:为RISC-V后端增加对更多ONNX/DSL算子的支持,并添加新数据类型(如`float64`),扩展编译器的适用场景。 + +**详细任务**: +1. 分析当前`instruction_select.py`中已有的算子映射(如`add` → `add`,`mul` → `mul`等)。 +2. 识别缺失的常用算子:如`div`(除法)、`mod`(取模)、`sqrt`、`min`/`max`等。 +3. 为缺失算子实现RISC-V指令映射(注意RISC-V整数除法需要`div`/`rem`,浮点需要扩展指令集)。 +4. 添加`float64`(双精度浮点)支持:增加新的寄存器类、加载存储指令(`fld`/`fsd`)、算术指令(`fadd.d`等)。 +5. 更新类型系统,在IR中区分`f64`和`f32`。 +6. 编写测试用例验证新算子和新类型。 + +**交付产物**: +- 更新后的`instruction_select.py`和`type_system.py` +- 新增的测试程序(使用除法和双精度浮点) +- 文档:支持的操作列表、数据类型说明 + +**12周每周目标**: +- **W1**:学习项目当前指令选择模块,列出已支持的算子和类型。 +- **W2**:识别缺失的常用整数算子(除法、取模),查阅RISC-V手册中`div`/`rem`指令。 +- **W3**:实现整数除法和取模的指令选择,编写简单DSL测试(`a / b`)。 +- **W4**:测试除法和取模的正确性,处理除零错误(可忽略或插入陷阱)。 +- **W5**:学习RISC-V浮点扩展(F/D扩展),了解`fld`/`fsd`和`fadd.d`等指令。 +- **W6**:在IR中添加`float64`类型,修改类型解析器。 +- **W7**:实现`float64`的加载和存储指令选择。 +- **W8**:实现`float64`算术指令(加、减、乘、除)。 +- **W9**:实现`float64`比较指令(`feq.d`, `flt.d`等)和条件分支。 +- **W10**:编写测试用例:双精度浮点求和、点积等。验证模拟器支持。 +- **W11**:为`sqrt`、`min`/`max`等添加指令选择(可使用库调用或硬件指令)。 +- **W12**:更新文档,撰写新算子、新类型的使用指南。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\2305\357\274\232RISC-V\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250.md" "b/docs/topics/\350\257\276\351\242\2305\357\274\232RISC-V\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250.md" new file mode 100644 index 0000000..5c40344 --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\2305\357\274\232RISC-V\346\261\207\347\274\226\344\273\243\347\240\201\347\276\216\345\214\226\345\231\250.md" @@ -0,0 +1,32 @@ +## 课题5:RISC-V汇编代码美化器 + +**难度**:低 + +**概述**:开发独立工具,读取编译器生成的`.s`汇编文件,输出格式整洁、带注释、对齐良好的版本。 + +**详细任务**: +1. 解析汇编行,识别标签、指令、操作数、注释。 +2. 对齐字段:标签左对齐,指令助记符占固定宽度(如8字符),操作数左对齐。 +3. 为每条指令自动添加注释:如`addi x1, x0, 5 # x1 = x0 + 5`。提供指令注释模板库。 +4. 添加段注释:`.text`、`.data`、函数入口前插入分隔线和描述。 +5. 支持命令行参数:输入文件、输出文件、是否添加注释、是否对齐。 +6. 输出美化后的汇编文件。 + +**交付产物**: +- Python脚本`asm_beautifier.py` +- 示例输入输出文件 +- 文档:使用方法、自定义注释模板 + +**12周每周目标**: +- **W1**:学习RISC-V基础指令集,阅读项目生成的`.s`文件样例,分析格式问题。 +- **W2**:编写正则表达式,从一行汇编中提取标签、指令、操作数(逗号分割)、注释。 +- **W3**:实现字段对齐:设定指令助记符宽度8,操作数宽度20,左对齐或右对齐。 +- **W4**:输出对齐后的汇编行,保留空行和纯注释。生成第一版美化脚本。 +- **W5**:构建指令注释字典,为常见指令(`add, sub, lw, sw, beq, jal`)撰写人类可读解释模板。 +- **W6**:实现自动注释生成:根据指令操作数填充模板中的寄存器名(如`x1`→`ra`可选)。 +- **W7**:在代码段前添加段注释:`.text`前加`# ===== CODE SECTION =====`,`.data`前加类似标记。 +- **W8**:处理伪指令(`li`, `mv`)的特殊注释,确保注释不会过长。 +- **W9**:添加命令行参数(`argparse`):输入文件、输出文件、`--no-comments`、`--no-align`。 +- **W10**:美化错误处理:遇到无法解析的行原样输出并警告,支持批量处理多个文件。 +- **W11**:测试至少10个不同的汇编文件(包括错误格式),对比美化前后可读性,编写测试脚本。 +- **W12**:撰写文档(安装依赖、使用示例、正则表达式规则),准备演示。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\2306\357\274\232\347\274\226\350\257\221\345\231\250\346\200\247\350\203\275\346\265\213\350\257\225\345\245\227\344\273\266.md" "b/docs/topics/\350\257\276\351\242\2306\357\274\232\347\274\226\350\257\221\345\231\250\346\200\247\350\203\275\346\265\213\350\257\225\345\245\227\344\273\266.md" new file mode 100644 index 0000000..2e6837c --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\2306\357\274\232\347\274\226\350\257\221\345\231\250\346\200\247\350\203\275\346\265\213\350\257\225\345\245\227\344\273\266.md" @@ -0,0 +1,34 @@ +## 课题6:编译器性能测试套件 + +**难度**:中 + +**概述**:设计一组基准测试程序(DSL用例),自动化执行编译、模拟运行、对比预期输出,生成性能报告。该套件用于持续验证编译器的正确性和性能变化。 + +**详细任务**: +1. 收集或编写15~20个DSL测试程序,覆盖算术、分支、循环、函数调用(如支持)。 +2. 每个程序提供预期输出(标准输出或返回值)和描述。 +3. 编写Python测试脚本:遍历测试目录,调用编译器生成汇编,再调用`tinyfive.py`模拟执行,捕获输出。 +4. 对比实际输出与预期输出,统计通过/失败数量。 +5. 从模拟器中提取指令总数(或执行周期估算),记录每个用例的性能数据。 +6. 生成Markdown或HTML格式的测试报告,包含表格和性能图表。 +7. 支持`--benchmark`模式,重复运行取平均值,检测性能退化。 + +**交付产物**: +- 包含20个以上测试用例的目录 +- 自动化测试脚本 `run_tests.py` +- 测试报告模板和示例输出 +- 使用文档(如何添加新用例、如何解读报告) + +**12周每周目标**: +- **W1**:学习项目编译命令和`tinyfive.py`用法,手动测试3个简单DSL程序,记录输出和指令数。 +- **W2**:编写Python脚本,使用`subprocess`自动调用编译器和模拟器,捕获stdout。 +- **W3**:设计测试用例格式(文件夹包含`.dsl`、`.expected`、`.desc`),编写5个算术测试用例。 +- **W4**:实现测试驱动:遍历用例,对比输出,输出PASS/FAIL表格。 +- **W5**:从模拟器提取指令数(如果支持`--stats`,否则解析日志)。将指令数加入报告。 +- **W6**:使用`matplotlib`绘制性能条形图。使用`jinja2`模板生成HTML报告。 +- **W7**:扩充测试用例到15个,覆盖分支和循环。 +- **W8**:增加时间测量(`time.perf_counter`),输出到报告。 +- **W9**:实现回归测试模式:保存基准结果,下次运行时对比并提示性能退化(阈值5%)。 +- **W10**:添加`--benchmark`选项,重复运行3次取平均值,输出置信区间。 +- **W11**:集成到CI(如GitHub Actions)示例,每次push自动运行测试套件。 +- **W12**:撰写完整文档(如何添加新测试、命令行参数、报告解读),准备演示。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\2307\357\274\232\347\274\226\350\257\221\345\231\250\346\227\245\345\277\227\345\242\236\345\274\272\345\231\250.md" "b/docs/topics/\350\257\276\351\242\2307\357\274\232\347\274\226\350\257\221\345\231\250\346\227\245\345\277\227\345\242\236\345\274\272\345\231\250.md" new file mode 100644 index 0000000..51cc47f --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\2307\357\274\232\347\274\226\350\257\221\345\231\250\346\227\245\345\277\227\345\242\236\345\274\272\345\231\250.md" @@ -0,0 +1,33 @@ +## 课题7:编译器日志增强器 + +**难度**:低 + +**概述**:为编译器的各个阶段(解析、IR生成、优化、代码生成)添加分级、带颜色的日志输出,支持`--log-level`和`--log-file`命令行参数。 + +**详细任务**: +1. 使用Python `logging`模块,创建多个logger(按模块)。 +2. 添加`argparse`参数:`--log-level {DEBUG,INFO,WARN,ERROR}`,`--log-file FILE`。 +3. 替换现有`print`语句为`logger.info`或`logger.debug`。 +4. 为关键操作添加日志:开始解析、优化Pass应用、指令数统计、代码生成完成。 +5. 实现彩色输出(使用`colorlog`或ANSI码),不同级别不同颜色。 +6. 支持同时输出到控制台和文件(文件可保留DEBUG级别)。 +7. 确保高日志级别时低级别字符串不会被构造(使用`logger.isEnabledFor`)。 + +**交付产物**: +- 修改后的编译器主文件和各个模块 +- 使用示例:`python main.py test.dsl --log-level DEBUG --log-file build.log` +- 文档:日志级别含义、如何为新增模块添加日志 + +**12周每周目标**: +- **W1**:学习`logging`模块基础(Logger、Handler、Formatter、级别)。编写demo。 +- **W2**:分析编译器现有`print`语句,规划哪些应转为日志,划分级别。 +- **W3**:在`main.py`中初始化logging,添加控制台Handler,设置格式`%(asctime)s - %(name)s - %(levelname)s - %(message)s`。 +- **W4**:替换前端(解析)中的`print`为`logger.info/debug`,添加`--log-level`参数。 +- **W5**:替换IR生成和优化阶段的`print`,为每个阶段创建子logger(如`logger = logging.getLogger('ir')`)。 +- **W6**:替换后端代码生成的`print`,确保所有输出通过日志。 +- **W7**:安装`colorlog`,根据级别设置颜色(ERROR红色,WARNING黄色,INFO绿色,DEBUG灰色)。 +- **W8**:添加`--log-file`参数,将日志同时写入文件。 +- **W9**:优化日志信息,避免噪音,关键步骤输出简洁的统计信息。 +- **W10**:测试不同级别和参数组合,确保性能影响小(使用`if logger.isEnabledFor`)。 +- **W11**:添加进度指示(如“Parsing... Done in 0.02s”)。 +- **W12**:撰写文档:日志级别说明、配置方法、常见使用场景。 \ No newline at end of file diff --git "a/docs/topics/\350\257\276\351\242\2309\357\274\232DSL\351\224\231\350\257\257\346\217\220\347\244\272\347\276\216\345\214\226\345\231\250.md" "b/docs/topics/\350\257\276\351\242\2309\357\274\232DSL\351\224\231\350\257\257\346\217\220\347\244\272\347\276\216\345\214\226\345\231\250.md" new file mode 100644 index 0000000..c32350b --- /dev/null +++ "b/docs/topics/\350\257\276\351\242\2309\357\274\232DSL\351\224\231\350\257\257\346\217\220\347\244\272\347\276\216\345\214\226\345\231\250.md" @@ -0,0 +1,33 @@ +## 课题9:DSL错误提示美化器 + +**难度**:中 + +**概述**:改进DSL前端出错时的错误报告,显示错误位置(行号、列号)、出错代码行、标记错误位置,并给出修复建议。 + +**详细任务**: +1. 修改词法/语法分析器,在解析过程中记录当前行号和列号(基于字符索引)。 +2. 自定义异常类`DSLSyntaxError`,包含行号、列号、错误信息、源码行内容。 +3. 在解析函数中捕获异常,抛出`DSLSyntaxError`。 +4. 编写错误格式化函数:输出`文件名:行:列: error: 消息`,然后打印源码行,下一行用`^`标记错误位置。 +5. 根据常见错误类型提供修复建议(如“缺少右括号”、“未定义的变量”)。 +6. 支持多错误收集(不提前退出),输出所有错误。 +7. 使用ANSI颜色高亮错误位置和文件名。 + +**交付产物**: +- 修改后的`dsl_parser.py`和错误处理模块 +- 测试用例(包含各种语法错误的DSL文件)及对应的预期错误输出 +- 文档:如何扩展错误类型 + +**12周每周目标**: +- **W1**:研究现有解析器出错时是否能得到行列号。手动构造错误DSL,观察输出。 +- **W2**:修改解析器,在每次读取一行时记录行号,每匹配一个token记录列号。扩展AST节点携带位置信息。 +- **W3**:自定义异常类`DSLSyntaxError`,包含行号、列号、消息、源码行。 +- **W4**:在解析函数的关键位置(如期望特定token但未匹配)抛出`DSLSyntaxError`。测试捕获位置正确性。 +- **W5**:编写错误格式化函数,输出`文件名:行:列: error: 消息`,并打印源码行和`^`标记。 +- **W6**:为常见错误添加修复建议词典(如“缺少括号” → “你可能忘了加右括号”)。 +- **W7**:集成到编译器主流程:捕获解析异常并调用格式化函数,优雅退出。 +- **W8**:增加多行错误上下文(显示错误行前后各一行),使用ANSI颜色高亮。 +- **W9**:处理词法错误(如非法字符)同样输出行列号。 +- **W10**:实现错误收集:当有多个错误时,收集所有再一并输出(不提前退出)。 +- **W11**:测试20个以上的错误用例,确保提示清晰且位置准确。 +- **W12**:撰写文档:如何为新的语法规则添加位置跟踪、如何扩展错误建议。 \ No newline at end of file From bcdfee80c31a0906e76241f697264c38b540d75d Mon Sep 17 00:00:00 2001 From: wangjiangyang <1938840431@qq.com> Date: Mon, 25 May 2026 16:06:34 +0800 Subject: [PATCH 7/7] Add GitHub Actions CI workflow for self-hosted runner Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 95 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d4420fb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: self-hosted + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install + run: | + python -m pip install --upgrade pip + pip install -e ".[all]" + + - name: Run tests + run: python -m pytest tests/ -v --tb=short + + lint: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install + run: | + python -m pip install --upgrade pip + pip install -e ".[all]" + pip install flake8 mypy + + - name: flake8 + run: python -m flake8 scratchv/ scratchv_dag/ tests/ + + - name: mypy + run: python -m mypy scratchv/ scratchv_dag/ --ignore-missing-imports + + coverage: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install + run: | + python -m pip install --upgrade pip + pip install -e ".[all]" + pip install pytest-cov + + - name: Run tests with coverage + run: python -m pytest tests/ --cov=scratchv --cov=scratchv_dag --cov-report=term --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + fail_ci_if_error: false + + smoke: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install + run: | + python -m pip install --upgrade pip + pip install -e ".[all]" + + - name: Smoke test - DSL compilation + run: | + python -m scratchv examples/simple_add.dsl -o /tmp/simple_add.s --dump-ir + python -m scratchv examples/relu_test.dsl -o /tmp/relu.s --optimize all + python -m scratchv examples/matmul_test.dsl -o /tmp/matmul.s --optimize all