diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15aae4b..ae66f04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,13 @@ jobs: run: | python3.12 -m pytest tests/test_pr37_regression.py -v --tb=short + - name: Run topic16 LLVM-codegen regressions + run: | + python3.12 -m pytest \ + tests/test_llvm_codegen_topic16.py \ + tests/test_topic16_llvm_case_report.py \ + -v --tb=short + - name: Generate test visualization page if: github.ref == 'refs/heads/main' run: | @@ -218,6 +225,14 @@ jobs: --json benchmark_reports/const_merge_report.json \ --markdown benchmark_reports/const_merge_report.md + # ── 3.1.2 课题16:LLVM 代码生成 case 报告(结构 + llvm-as/opt) ── + - name: Topic 16 LLVM codegen case report + run: | + mkdir -p benchmark_reports + python3.12 benchmarks/run_topic16_llvm_case.py \ + --json benchmark_reports/llvm_codegen_report.json \ + --markdown benchmark_reports/llvm_codegen_report.md + # ── 3.2 DSL 用例编译 + 模拟基准 ──────────────────────────────────── - name: DSL case compilation benchmarks run: | @@ -363,6 +378,9 @@ jobs: if [ -f benchmark_reports/const_merge_report.md ]; then cat benchmark_reports/const_merge_report.md >> $GITHUB_STEP_SUMMARY fi + if [ -f benchmark_reports/llvm_codegen_report.md ]; then + cat benchmark_reports/llvm_codegen_report.md >> $GITHUB_STEP_SUMMARY + fi echo "" >> $GITHUB_STEP_SUMMARY if [ -f benchmark_reports/github_summary.md ]; then cat benchmark_reports/github_summary.md >> $GITHUB_STEP_SUMMARY diff --git a/benchmarks/cases/topic16_llvm_feature.dsl b/benchmarks/cases/topic16_llvm_feature.dsl new file mode 100644 index 0000000..390eab5 --- /dev/null +++ b/benchmarks/cases/topic16_llvm_feature.dsl @@ -0,0 +1,23 @@ +# Topic 16 LLVM-codegen feature case. +# +# Deterministic operator chain covering five NN operators (relu, dot, +# matmul, softmax, gelu). The tensor operators are deliberately +# scalar-degenerate (length/all dims = 1): the DSL frontend does not +# materialize operand shapes yet, so larger extents would be rejected by the +# LLVM backend's element-count checks. The canonical lowering structure +# (loop skeleton, getelementptr, MAC fmul/fadd, softmax three passes, gelu +# tanhf call) is emitted regardless of the trip count, and the case report +# asserts those markers. +# +# This case is compiled by the real CompilerDriver with backend="llvm"; it is +# a structural/feature case, not a performance workload. +y = relu(x) +d = dot(a, b, len:1) +c = matmul(m1, m2, m:1, n:1, k:1) +s = softmax(v) +g = gelu(z) +p1 = add(y, d) +p2 = add(c, s) +p3 = add(p2, g) +t = add(p1, p3) +return t diff --git a/benchmarks/run_topic16_llvm_case.py b/benchmarks/run_topic16_llvm_case.py new file mode 100644 index 0000000..b7bdb0a --- /dev/null +++ b/benchmarks/run_topic16_llvm_case.py @@ -0,0 +1,698 @@ +#!/usr/bin/env python3 +"""Run one Topic 16 LLVM-codegen feature case and emit auditable CI reports. + +The report proves three separate facts: + +1. the real ``CompilerDriver`` configured with ``backend="llvm"`` compiles the + deterministic DSL case into a non-empty LLVM IR module; +2. the emitted module satisfies text-level legality invariants (unique SSA + definitions and labels, every basic block terminated, positive metrics) and + still contains the canonical lowering of each NN operator in the case + (loop skeleton, ``getelementptr``, MAC ``fmul``/``fadd``, softmax three + passes, gelu ``tanhf``); +3. ``LLVMCodegen(target_triple=...)`` can stamp a target triple while leaving + the module body otherwise identical, and — when the LLVM tools are + installed — ``llvm-as``/``opt`` actually accept the module. + +Graceful degradation: when a tool is missing its row is reported as +``skipped`` with a ``skip_reason`` and it never counts as a hard failure and +no assemblability/execution claim is made. ``lli`` execution is skipped +unless the module exposes a runnable ``i32 @main()`` entry, so this report +makes no execution or performance claim. Real ONNX workload numbers remain +separate in ``run_benchmark.py``. +""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import subprocess +import tempfile +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from scratchv.backend.llvm_codegen import LLVMCodegen +from scratchv.compiler import CompilerConfig, CompilerDriver +from scratchv.frontend.dsl_extended import ExtendedDSLParser + +SCHEMA_VERSION = "topic16-llvm-case/1" +DEFAULT_CASE = ( + Path(__file__).parent / "cases" / "topic16_llvm_feature.dsl" +) +DEFAULT_JSON = Path("benchmark_reports/llvm_codegen_report.json") +DEFAULT_MARKDOWN = Path("benchmark_reports/llvm_codegen_report.md") +#: Triple used only to prove ``LLVMCodegen(target_triple=...)`` overridability; +#: the default driver path intentionally emits a triple-less module. +TARGET_TRIPLE = "riscv32-unknown-elf" +TOOL_NAMES = ("llvm-as", "opt", "lli") +TOOL_TIMEOUT_S = 60 +LLI_TIMEOUT_S = 30 + +_DEFINE_RE = re.compile(r"^define\s+([\w.*]+)\s+@([\w.$]+)\(") +_DEF_RE = re.compile(r"^\s*(%[\w.$]+)\s*=") +_LABEL_RE = re.compile(r"^([A-Za-z0-9_.$]+):") +_TERMINATOR_RE = re.compile(r"^(ret|br|unreachable)\b") +_EXECUTABLE_ENTRY_RE = re.compile(r"^define\s+i32\s+@main\(\)\s*\{", re.M) + +HONESTY = ( + "Deterministic structural feature case: the module is produced by the " + "real CompilerDriver with backend='llvm'. Text-level invariants (unique " + "SSA definitions, unique labels, terminated basic blocks, positive " + "metrics) and NN-operator lowering markers are verified without any " + "external tool. llvm-as/opt/lli results are claimed only when the " + "corresponding binary is present; a missing tool is reported as " + "'skipped' with a skip_reason and is not a hard failure. lli execution is " + "skipped unless the module exposes a runnable i32 @main() entry, so no " + "execution, timing or speedup claim is made." +) + + +# --------------------------------------------------------------------------- +# IR text analysis (no external tools) +# --------------------------------------------------------------------------- + +def _split_functions(ir: str) -> list[dict[str, Any]]: + """Split module text into ``define`` bodies.""" + functions: list[dict[str, Any]] = [] + current: dict[str, Any] | None = None + for raw in ir.splitlines(): + match = _DEFINE_RE.match(raw.strip()) + if match: + current = { + "name": match.group(2), + "return_type": match.group(1), + "lines": [], + } + functions.append(current) + continue + if current is None: + continue + if raw.strip() == "}": + current = None + continue + current["lines"].append(raw) + return functions + + +def _split_blocks(lines: list[str]) -> list[dict[str, Any]]: + """Split a function body into basic blocks (implicit entry included).""" + blocks: list[dict[str, Any]] = [] + current = {"label": "", "instructions": []} + for raw in lines: + stripped = raw.strip() + if not stripped or stripped.startswith(";"): + continue + match = _LABEL_RE.match(stripped) + if match: + if current["instructions"] or blocks: + blocks.append(current) + current = {"label": match.group(1), "instructions": []} + continue + current["instructions"].append(stripped) + if current["instructions"] or blocks: + blocks.append(current) + if (blocks and blocks[0]["label"] == "" + and not blocks[0]["instructions"]): + blocks.pop(0) + return blocks + + +def analyze_module(ir: str) -> dict[str, Any]: + """Compute module/function/block metrics and legality indicators.""" + duplicate_ssa: list[str] = [] + duplicate_labels: list[str] = [] + unterminated: list[str] = [] + empty_blocks: list[str] = [] + function_stats: list[dict[str, Any]] = [] + totals = { + "basic_block_count": 0, + "instruction_count": 0, + "ssa_definition_count": 0, + "load_count": 0, + "store_count": 0, + "alloca_count": 0, + "call_count": 0, + "gep_count": 0, + "terminator_count": 0, + } + + for func in _split_functions(ir): + blocks = _split_blocks(func["lines"]) + defs: list[str] = [] + counts = {key: 0 for key in totals} + counts["basic_block_count"] = len(blocks) + for block in blocks: + instructions = block["instructions"] + if not instructions and block["label"] != "": + empty_blocks.append(f"{func['name']}:{block['label']}") + if instructions and not _TERMINATOR_RE.match(instructions[-1]): + unterminated.append(f"{func['name']}:{block['label']}") + counts["terminator_count"] += sum( + 1 for line in instructions if _TERMINATOR_RE.match(line) + ) + for line in instructions: + counts["instruction_count"] += 1 + match = _DEF_RE.match(line) + if match: + defs.append(match.group(1)) + counts["ssa_definition_count"] += 1 + if "= load" in line: + counts["load_count"] += 1 + elif "= alloca" in line: + counts["alloca_count"] += 1 + elif "= call" in line: + counts["call_count"] += 1 + elif "= getelementptr" in line: + counts["gep_count"] += 1 + elif line.startswith("store "): + counts["store_count"] += 1 + seen: set[str] = set() + for name in defs: + if name in seen: + duplicate_ssa.append(f"{func['name']}:{name}") + seen.add(name) + labels: set[str] = set() + for block in blocks: + if block["label"] in labels: + duplicate_labels.append(f"{func['name']}:{block['label']}") + labels.add(block["label"]) + for key in totals: + totals[key] += counts[key] + function_stats.append({ + "name": func["name"], + "return_type": func["return_type"], + "basic_blocks": len(blocks), + "instructions": counts["instruction_count"], + "ssa_definitions": counts["ssa_definition_count"], + "loads": counts["load_count"], + "stores": counts["store_count"], + "allocas": counts["alloca_count"], + "calls": counts["call_count"], + "geps": counts["gep_count"], + "terminators": counts["terminator_count"], + }) + + return { + "ir_lines": len(ir.splitlines()), + "function_count": len(function_stats), + "function_names": [func["name"] for func in function_stats], + **totals, + "ssa_definitions_unique": not duplicate_ssa, + "labels_unique": not duplicate_labels, + "all_blocks_terminated": not unterminated, + "duplicate_ssa_definitions": duplicate_ssa, + "duplicate_labels": duplicate_labels, + "unterminated_blocks": unterminated, + "empty_blocks": empty_blocks, + "functions": function_stats, + } + + +# --------------------------------------------------------------------------- +# Lowering markers +# --------------------------------------------------------------------------- + +def _count(ir: str, pattern: str) -> int: + return len(re.findall(pattern, ir)) + + +def check_lowering(ir: str) -> dict[str, Any]: + """Check the canonical lowering of every operator used by the case.""" + counts = { + "getelementptr": _count(ir, r"\bgetelementptr\b"), + "fmul": _count(ir, r"=\s*fmul\b"), + "fadd": _count(ir, r"=\s*fadd\b"), + "fcmp_ogt": _count(ir, r"\bfcmp ogt\b"), + "select_i1": _count(ir, r"\bselect i1\b"), + "icmp_slt_i32": _count(ir, r"\bicmp slt i32\b"), + "br_i1": _count(ir, r"\bbr i1\b"), + "expf_calls": _count(ir, r"=\s*call float @expf\("), + "tanhf_calls": _count(ir, r"=\s*call float @tanhf\("), + } + header_labels = [ + label + for label in re.findall(r"^([A-Za-z0-9_.$]+):", ir, re.M) + if "_hdr" in label + ] + + def has_header(prefix: str) -> bool: + return any(label.startswith(prefix) for label in header_labels) + + markers = { + "relu_uses_fcmp_and_select": ( + counts["fcmp_ogt"] > 0 and counts["select_i1"] > 0 + ), + "dot_uses_loop_gep_and_mac": ( + has_header("dot_i_hdr") + and counts["getelementptr"] > 0 + and counts["fmul"] > 0 + and counts["fadd"] > 0 + ), + "matmul_uses_nested_loops_and_gep": ( + has_header("mm_i_hdr") + and has_header("mm_j_hdr") + and has_header("mm_k_hdr") + and counts["getelementptr"] > 0 + and counts["fmul"] > 0 + ), + "softmax_uses_three_passes_and_expf": ( + has_header("sm_max_i_hdr") + and has_header("sm_sum_i_hdr") + and has_header("sm_div_i_hdr") + and counts["expf_calls"] > 0 + ), + "gelu_uses_tanh": counts["tanhf_calls"] > 0, + "loops_have_icmp_and_conditional_branch": ( + counts["icmp_slt_i32"] >= 6 and counts["br_i1"] >= 6 + ), + } + return { + "counts": counts, + "loop_header_labels": sorted(header_labels), + "markers": markers, + } + + +# --------------------------------------------------------------------------- +# Real CompilerDriver compilation +# --------------------------------------------------------------------------- + +def compile_case(case_path: Path) -> dict[str, Any]: + """Compile the DSL case through ``CompilerDriver(backend="llvm")``.""" + source = case_path.read_text(encoding="utf-8") + started = time.perf_counter() + with tempfile.TemporaryDirectory() as tmp: + output = Path(tmp) / "topic16_case.ll" + result = CompilerDriver(CompilerConfig( + backend="llvm", + optimize_level="all", + dump_ir=True, + )).compile("", str(output), dsl_source=source) + elapsed_ms = (time.perf_counter() - started) * 1000 + ir_text = result.output_text + if output.is_file(): + ir_text = output.read_text(encoding="utf-8") + return { + "success": result.success, + "errors": list(result.errors), + "warnings": list(result.warnings), + "elapsed_ms": round(elapsed_ms, 3), + "opt_message": result.stats.get("opt_message", ""), + "ir_text": ir_text, + } + + +def check_target_triple(source: str) -> dict[str, Any]: + """Prove ``target_triple`` is optional and overridable.""" + try: + default_ir = LLVMCodegen(ExtendedDSLParser().parse(source)).emit() + explicit_ir = LLVMCodegen( + ExtendedDSLParser().parse(source), TARGET_TRIPLE + ).emit() + except Exception as exc: # pragma: no cover - exercised via bad cases + return { + "status": "error", + "error": f"{type(exc).__name__}: {exc}", + "default_has_triple": None, + "explicit_triple": TARGET_TRIPLE, + "explicit_has_triple": None, + "body_identical_without_triple": False, + } + + def without_triple(text: str) -> list[str]: + return [ + line for line in text.splitlines() if "target triple" not in line + ] + + return { + "status": "ok", + "error": None, + "default_has_triple": "target triple" in default_ir, + "explicit_triple": TARGET_TRIPLE, + "explicit_has_triple": ( + f'target triple = "{TARGET_TRIPLE}"' in explicit_ir + ), + "body_identical_without_triple": ( + without_triple(default_ir) == without_triple(explicit_ir) + ), + } + + +# --------------------------------------------------------------------------- +# External LLVM toolchain (gracefully optional) +# --------------------------------------------------------------------------- + +def _tool_row(name: str, path: str | None) -> dict[str, Any]: + return { + "tool": name, + "available": path is not None, + "path": path, + "status": "skipped", + "skip_reason": None, + "returncode": None, + "stdout": "", + "stderr": "", + "detail": "", + } + + +def _run(command: list[str], timeout: int) -> subprocess.CompletedProcess: + return subprocess.run( + command, capture_output=True, text=True, timeout=timeout, + ) + + +def run_toolchain(ir: str) -> dict[str, dict[str, Any]]: + """Assemble/optimise the module with whatever LLVM tools exist locally.""" + paths = {name: shutil.which(name) for name in TOOL_NAMES} + tools = {name: _tool_row(name, paths[name]) for name in TOOL_NAMES} + for name, row in tools.items(): + if not row["available"]: + row["skip_reason"] = f"{name} not installed" + if not ir.strip(): + for row in tools.values(): + row["skip_reason"] = "no IR module was produced" + return tools + + with tempfile.TemporaryDirectory() as tmp: + ll_path = Path(tmp) / "module.ll" + ll_path.write_text(ir, encoding="utf-8") + bc_path = Path(tmp) / "module.bc" + opt_path = Path(tmp) / "module.opt.ll" + + asm = tools["llvm-as"] + if asm["available"]: + try: + proc = _run( + [paths["llvm-as"], str(ll_path), "-o", str(bc_path)], + TOOL_TIMEOUT_S, + ) + asm["returncode"] = proc.returncode + asm["stderr"] = proc.stderr.strip() + if proc.returncode == 0: + asm["status"] = "assembled" + asm["detail"] = f"bitcode {bc_path.stat().st_size} bytes" + asm["skip_reason"] = None + else: + asm["status"] = "failed" + except subprocess.TimeoutExpired: + asm["status"] = "timeout" + asm["skip_reason"] = "llvm-as exceeded the time limit" + except OSError as exc: + asm["status"] = "error" + asm["skip_reason"] = str(exc) + + opt = tools["opt"] + if opt["available"]: + try: + proc = _run( + [ + paths["opt"], "-O3", "-S", + str(ll_path), "-o", str(opt_path), + ], + TOOL_TIMEOUT_S, + ) + opt["returncode"] = proc.returncode + opt["stderr"] = proc.stderr.strip() + if proc.returncode == 0: + opt["status"] = "optimized" + opt_lines = len( + opt_path.read_text(encoding="utf-8").splitlines() + ) + opt["detail"] = f"{opt_lines} lines after -O3" + if asm["available"]: + reparsed = _run( + [paths["llvm-as"], str(opt_path), + "-o", str(Path(tmp) / "module.opt.bc")], + TOOL_TIMEOUT_S, + ) + opt["detail"] += ( + f"; reassembly rc={reparsed.returncode}" + ) + else: + opt["status"] = "failed" + except subprocess.TimeoutExpired: + opt["status"] = "timeout" + opt["skip_reason"] = "opt exceeded the time limit" + except OSError as exc: + opt["status"] = "error" + opt["skip_reason"] = str(exc) + + lli = tools["lli"] + if lli["available"]: + if not _EXECUTABLE_ENTRY_RE.search(ir): + lli["skip_reason"] = ( + "module exposes no runnable i32 @main() entry " + "(kernel-only module); IR validity is covered by " + "llvm-as/opt" + ) + elif bc_path.is_file(): + try: + proc = _run([paths["lli"], str(bc_path)], LLI_TIMEOUT_S) + lli["returncode"] = proc.returncode + lli["stdout"] = proc.stdout.strip() + lli["stderr"] = proc.stderr.strip() + lli["status"] = ( + "executed" if proc.returncode == 0 else "failed" + ) + except subprocess.TimeoutExpired: + lli["status"] = "timeout" + lli["skip_reason"] = "lli exceeded the time limit" + except OSError as exc: + lli["status"] = "error" + lli["skip_reason"] = str(exc) + else: + lli["skip_reason"] = "no bitcode available (llvm-as missing)" + return tools + + +# --------------------------------------------------------------------------- +# Report assembly +# --------------------------------------------------------------------------- + +def evaluate(case_path: Path) -> dict[str, Any]: + """Build the full report payload and run the hard invariants.""" + source = case_path.read_text(encoding="utf-8") + compile_info = compile_case(case_path) + ir_text = compile_info.pop("ir_text") + module = analyze_module(ir_text) + lowering = check_lowering(ir_text) + triple = check_target_triple(source) + toolchain = run_toolchain(ir_text) + + ir_lines = len(ir_text.splitlines()) + compile_info["ir_lines"] = ir_lines + compile_info["ir_head"] = ir_text.splitlines()[:40] + + asm, opt, lli = toolchain["llvm-as"], toolchain["opt"], toolchain["lli"] + hard_checks = { + "case_compiles_via_llvm_driver": bool(compile_info["success"]), + "ir_module_is_nonempty": ir_lines > 0, + "ssa_definitions_unique_within_functions": ( + module["ssa_definitions_unique"]), + "block_labels_unique_within_functions": module["labels_unique"], + "every_basic_block_terminated": module["all_blocks_terminated"], + "module_metrics_are_positive": ( + module["function_count"] >= 1 + and module["instruction_count"] > 0 + and module["ssa_definition_count"] > 0 + and module["load_count"] > 0 + and module["store_count"] > 0 + and module["alloca_count"] > 0 + and module["gep_count"] > 0 + ), + "nn_operator_lowering_markers_present": all( + lowering["markers"].values()), + "target_triple_is_configurable": ( + triple["default_has_triple"] is False + and triple["explicit_has_triple"] is True + and triple["body_identical_without_triple"] + ), + "llvm_toolchain_accepts_module_when_available": ( + bool(compile_info["success"]) + and (not asm["available"] or asm["returncode"] == 0) + and (not opt["available"] or opt["returncode"] == 0) + and (not lli["available"] or lli["status"] != "failed") + ), + } + failed = sorted(name for name, ok in hard_checks.items() if not ok) + + return { + "schema_version": SCHEMA_VERSION, + "topic": "topic16-llvm-codegen", + "generated_at": datetime.now(timezone.utc).isoformat(), + "case": str(case_path), + "config": { + "backend": "llvm", + "optimize_level": "all", + "dump_ir": True, + }, + "compile": compile_info, + "module": module, + "lowering": lowering, + "target_triple": triple, + "toolchain": toolchain, + "hard_checks": hard_checks, + "hard_failures": failed, + "honesty": HONESTY, + } + + +def render_markdown(report: dict[str, Any]) -> str: + compile_info = report["compile"] + module = report["module"] + lowering = report["lowering"] + triple = report["target_triple"] + passed = not report["hard_failures"] + lines = [ + "# Topic 16 LLVM Codegen Feature Case", + "", + f"- Schema: `{report['schema_version']}`", + f"- Case: `{report['case']}`", + f"- Backend: `{report['config']['backend']}` " + f"(optimize_level=`{report['config']['optimize_level']}`)", + f"- Generated: {report['generated_at']}", + f"- Hard checks: {'PASS' if passed else 'FAIL'} " + f"({len(report['hard_checks']) - len(report['hard_failures'])}" + f"/{len(report['hard_checks'])})", + "", + "## Compile metrics", + "", + "| Metric | Value |", + "|--------|---:|", + f"| Compilation success | {compile_info['success']} |", + f"| IR lines | {module['ir_lines']} |", + f"| Functions | {module['function_count']} |", + f"| Basic blocks | {module['basic_block_count']} |", + f"| Instructions | {module['instruction_count']} |", + f"| SSA definitions | {module['ssa_definition_count']} |", + f"| load / store / alloca | {module['load_count']} / " + f"{module['store_count']} / {module['alloca_count']} |", + f"| calls / getelementptr | {module['call_count']} / " + f"{module['gep_count']} |", + f"| Terminators (br/ret/unreachable) | {module['terminator_count']} |", + f"| Compile time (ms) | {compile_info['elapsed_ms']:.3f} |", + "", + "## Module structure", + "", + "| Function | Return | Blocks | Instructions | SSA defs | " + "Terminated |", + "|----------|--------|-------:|-------------:|---------:|" + "-----------:|", + ] + for func in module["functions"]: + terminated = all( + not block.startswith(f"{func['name']}:") + for block in module["unterminated_blocks"] + ) + lines.append( + f"| `{func['name']}` | `{func['return_type']}` | " + f"{func['basic_blocks']} | {func['instructions']} | " + f"{func['ssa_definitions']} | {'yes' if terminated else 'no'} |" + ) + duplicate_ssa = module["duplicate_ssa_definitions"] or "none" + duplicate_labels = module["duplicate_labels"] or "none" + unterminated = module["unterminated_blocks"] or "none" + + def mark(name: str) -> str: + return "yes" if lowering["markers"][name] else "no" + + lines += [ + "", + f"- Duplicate SSA definitions: {duplicate_ssa}", + f"- Duplicate block labels: {duplicate_labels}", + f"- Unterminated blocks: {unterminated}", + f"- Optimizer pipeline: `{compile_info['opt_message'] or 'n/a'}`", + f"- Compilation errors: {compile_info['errors'] or 'none'}", + "", + "## NN operator lowering markers", + "", + "| Marker | Present | Evidence count |", + "|--------|---------|----------------|", + f"| relu: fcmp+select | {mark('relu_uses_fcmp_and_select')} " + f"| fcmp ogt={lowering['counts']['fcmp_ogt']}, " + f"select i1={lowering['counts']['select_i1']} |", + f"| dot: loop+GEP+MAC | {mark('dot_uses_loop_gep_and_mac')} " + f"| gep={lowering['counts']['getelementptr']}, " + f"fmul={lowering['counts']['fmul']}, " + f"fadd={lowering['counts']['fadd']} |", + f"| matmul: nested loops+GEP | " + f"{mark('matmul_uses_nested_loops_and_gep')} " + f"| icmp slt i32={lowering['counts']['icmp_slt_i32']}, " + f"br i1={lowering['counts']['br_i1']} |", + f"| softmax: 3 passes+expf | " + f"{mark('softmax_uses_three_passes_and_expf')} " + f"| expf calls={lowering['counts']['expf_calls']} |", + f"| gelu: tanhf | {mark('gelu_uses_tanh')} " + f"| tanhf calls={lowering['counts']['tanhf_calls']} |", + "", + f"- Loop header labels: " + f"{', '.join(lowering['loop_header_labels']) or 'none'}", + "", + "## Target triple", + "", + f"- Default module carries a triple: `{triple['default_has_triple']}`", + f"- Explicit `LLVMCodegen(target_triple=" + f"\"{triple['explicit_triple']}\")` emits it: " + f"`{triple['explicit_has_triple']}`", + f"- Module body identical apart from the triple line: " + f"`{triple['body_identical_without_triple']}`", + "", + "## Toolchain matrix", + "", + "| Tool | Available | Status | Return code | Detail |", + "|------|-----------|--------|------------:|--------|", + ] + for name in TOOL_NAMES: + row = report["toolchain"][name] + detail = row["detail"] or row["skip_reason"] or "-" + rc = row["returncode"] if row["returncode"] is not None else "-" + lines.append( + f"| {name} | {'yes' if row['available'] else 'no'} | " + f"{row['status']} | {rc} | {detail} |" + ) + lines += [ + "", + "## Hard checks", + "", + ] + for name, ok in report["hard_checks"].items(): + lines.append(f"- [{'x' if ok else ' '}] {name}") + lines += [ + "", + "## Honesty", + "", + report["honesty"], + "", + ] + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--case", type=Path, default=DEFAULT_CASE) + parser.add_argument("--json", type=Path, default=DEFAULT_JSON) + parser.add_argument("--markdown", type=Path, default=DEFAULT_MARKDOWN) + args = parser.parse_args(argv) + if not args.case.is_file(): + parser.error(f"feature case not found: {args.case}") + + report = evaluate(args.case) + args.json.parent.mkdir(parents=True, exist_ok=True) + args.markdown.parent.mkdir(parents=True, exist_ok=True) + args.json.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + args.markdown.write_text(render_markdown(report) + "\n", encoding="utf-8") + print(render_markdown(report)) + if report["hard_failures"]: + print("HARD FAILURES: " + ", ".join(report["hard_failures"])) + return 1 + print(f"reports written: {args.json}, {args.markdown}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git "a/docs/topics/16-LLVM\344\273\243\347\240\201\347\224\237\346\210\220-\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/docs/topics/16-LLVM\344\273\243\347\240\201\347\224\237\346\210\220-\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 0000000..4ad4421 --- /dev/null +++ "b/docs/topics/16-LLVM\344\273\243\347\240\201\347\224\237\346\210\220-\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,724 @@ +# 课题16 LLVM 代码生成后端(库路径)开发文档 + +> 文档版本:v1.0 +> 编写日期:2026-09-14 +> 涉及模块:`scratchv/backend/llvm_codegen.py`、`tests/test_llvm_codegen.py`、`tests/test_llvm_codegen_llvm_tools.py`(新增) +> 配套文档:《设计文档.md》(同目录)——本文件是其实现指南,二者若冲突以设计文档为准 +> 前置环境:Python 3.11+(项目 venv)、`/usr/bin/llvm-as`(LLVM 10,已确认可用)、可选 `/usr/bin/lli`、`/usr/bin/opt` + +--- + +## 一、实施范围与接口契约 + +### 1.1 范围 + +**做**: + +- 修复 `llvm_codegen.py` 的 SSA 唯一性、类型/常量合法性、`_emit_for/_emit_endfor` CFG、`_emit_br_if` 条件类型; +- 为 conv/gemm/matmul/dot/maxpool/softmax 实现真实循环 + GEP + MAC;gelu/sigmoid 改为单定义展开; +- 目标 triple 可配置(构造函数参数,默认省略); +- 扩充/新增测试,含 `llvm-as`、`lli` 集成(缺失时 skip)。 + +**不做**: + +- 不动 `scratchv/standalone/onnx_to_llvm_standalone.py` 及任何 standalone 文件; +- 不动 `scratchv/compiler.py`、`scratchv/main.py`、`scratchv/ir/*`; +- 不引入 llvmlite 或任何新依赖; +- 不做 mem2reg/循环展开/向量化等 IR→IR 优化(只要求正确可汇编)。 + +### 1.2 接口契约(精确名称) + +#### 1.2.1 模块级常量与函数(`scratchv/backend/llvm_codegen.py`) + +| 名称 | 签名 | 说明 | +|------|------|------| +| `_TYPE_MAP` | `dict[DataType, str]` | dtype→LLVM 基类型(保持) | +| `_LLVM_FLOAT` / `_LLVM_DOUBLE` / `_LLVM_I32` / `_LLVM_I64` | `str` 常量 | `"float"`/`"double"`/`"i32"`/`"i64"`(保持) | +| `_llvm_type` | `(dtype: DataType) -> str` | dtype→基类型(保持) | +| `_float_to_llvm_hex` | `(value: float) -> str` | **新增**,复制 standalone 算法:float32 舍入→double 位型→`0x%016X` | +| `_float_literal` | `(value: float) -> str` | **新增**,`0.0/1.0/-1.0` 短写,否则 `_float_to_llvm_hex` | +| `_llvm_const_val` | `(value: float \| int, ty: str) -> str` | 重写:浮点走 `_float_literal`,整型十进制 | +| `_llvm_const` | `(val: Value) -> str` | 按 `val.dtype` 调用 `_llvm_const_val`(保持签名) | +| `_is_pointer_value` | `(val: Value) -> bool` | **新增**,`bool(val.shape)` | + +#### 1.2.2 异常 + +```python +class LLVMCodegenError(Exception): + """Raised for unlowerable IR (missing operand, unmatched endfor, ...).""" +``` + +#### 1.2.3 `SSANamer`(新增类) + +```python +class SSANamer: + @staticmethod + def sanitize(name: str) -> str: ... + def fresh(self, hint: str = "r") -> str: ... # -> "%hint_" + def fresh_label(self, hint: str = "bb") -> str: ... # -> "hint_" + def register_definition(self, reg: str) -> None: ... # 重复注册即抛错 + def registered(self, reg: str) -> bool: ... +``` + +#### 1.2.4 `LoopContext`(新增 dataclass) + +```python +@dataclass +class LoopContext: + ir_name: str | None # IR 循环变量名(DSL for);张量算子内部循环为 None + ptr: str # "%_ptr_": alloca i32* + value: str # "%_ld_": header 中 load 出的 i32 值 + header: str # 标签名(无 %) + body: str + exit: str + limit: int # 循环上界(i32) + step: int = 1 # 步长(i32) +``` + +#### 1.2.5 `LLVMCodegen` 公开 API(兼容保持不变) + +```python +class LLVMCodegen: + def __init__(self, program: Program, target_triple: str | None = None) -> None: ... + def emit(self) -> str: ... + def save(self, path: str) -> None: ... +``` + +- 位置参数 `program` 不变,`LLVMCodegen(program)` 全项目兼容(`compiler.py:390`、examples、benchmarks); +- `target_triple=None` ⇒ 不输出 `target triple` 行;传字符串则原样输出。 + +#### 1.2.6 `LLVMCodegen` 内部 helper(实现契约,供 review/测试引用) + +| 名称 | 签名 | 职责 | +|------|------|------| +| `_fresh` | `(hint: str = "r") -> str` | 委托 `SSANamer.fresh` | +| `_fresh_label` | `(hint: str = "bb") -> str` | 委托 `SSANamer.fresh_label` | +| `_value_ref` | `(val: Value) -> str` | 常量内联;否则查/建 SSA 引用 | +| `_value_type` | `(val: Value) -> str` | 标量/指针的 LLVM 类型 | +| `_bind` | `(name: str, ref: str, llvm_ty: str) -> None` | 写 `_named_values` + `_ref_types` | +| `_dest` | `(instr: Instruction) -> str` | 幂等分配 dst 引用 | +| `_dest_buffer` | `(instr: Instruction, count: int, elem_ty: str) -> str` | dst 缓冲:ALLOCA 复用 / 按 count 分配 | +| `_op` | `(instr: Instruction, idx: int) -> str` | 第 idx 操作数引用;缺失抛 `LLVMCodegenError` | +| `_ptr_of` | `(instr: Instruction, idx: int, ty: str = "float") -> str` | 操作数当指针用;标量 spill 到 `alloca` | +| `_alloc_slot` | `(elem_ty: str, count: int = 1, hint: str = "slot") -> str` | 入口 prologue alloca,返回指针 SSA 名 | +| `_materialize_const` | `(value: float \| int, ty: str, hint: str) -> str` | 常量实体化为 SSA 值 | +| `_coerce_operand` | `(ref: str, from_ty: str, to_ty: str, hint: str) -> str` | 混型算术转换:`sitofp`/`fptosi`;同型原样返回 | +| `_emit_binary` | `(instr, op: str) -> None` | add/sub/mul/div/fadd... 统一发射(内部先 `_coerce_operand`) | +| `_start_block` | `(label: str) -> None` | 结束当前块(必要时补 `br`)并打开新块 | +| `_terminate` | `(line: str) -> None` | 发射终止指令并置位 | +| `_ensure_terminator` | `() -> None` | 当前块无终止符时补 `br` 到合成续块 | +| `_loop_open` | `(limit: int, ir_name: str \| None, hint: str, start: int = 0, step: int = 1) -> LoopContext` | 循环规范形的前半;`ir_name` 非空时绑定 IR 循环变量 | +| `_loop_close` | `(ctx: LoopContext) -> None` | 循环规范形的后半 | +| `_dim_of` | `(instr: Instruction, keys: tuple[str, ...], default: int = 1, operand: int \| None = None, axis: int \| None = None) -> int` | 从 attrs/shape 取维度,键名兼容 | + +#### 1.2.7 CLI(不变) + +```bash +scratchv model.onnx --backend llvm -o out.ll # 既有入口,行为=默认目标无关 IR +python -c "from scratchv.backend.llvm_codegen import LLVMCodegen; \ + open('o.ll','w').write(LLVMCodegen(p, 'riscv64-unknown-elf').emit())" # 显式 triple +``` + +不新增命令行参数;triple 覆盖只走 Python API(范围约束)。 + +--- + +## 二、通用机制实现方案 + +### 2.1 张量表示与 slot 分配 + +**判定**:`_is_pointer_value(val)` 为真 ⇔ `val.shape` 非空;此外 `ALLOCA` 指令的 dest 显式绑定指针类型。 + +**指针类型表**(`_value_type`): + +```python +def _value_type(self, val) -> str: + base = _llvm_type(val.dtype) + if _is_pointer_value(val): + return base + "*" + ref = self._named_values.get(val.name) + if ref is not None and self._ref_types.get(ref, "").endswith("*"): + return self._ref_types[ref] + return base +``` + +**入口 prologue**:`_alloc_slot()` 把 `%p = alloca , i32 ` 追加到 `self._prologue: list[str]`;`_emit_function` 在 `define ... {` 之后、第一个 `_emit_block` 之前输出 prologue(这些指令自动属于 entry 块)。这样循环内的 alloca 不会随迭代增长栈帧(`lli` 数值测试必需)。 + +**标量 spill**(`_ptr_of`): + +```python +def _ptr_of(self, instr, idx, ty="float"): + val = instr.operands[idx] + ref = self._op(instr, idx) + if self._ref_types.get(ref, "").endswith("*"): + return ref + p = self._alloc_slot(ty, 1, "spin") # 退化 1 元素张量 + self._p(f" store {ty} {ref}, {ty}* {p}") + return p +``` + +**结果缓冲**(`_dest_buffer`):`ALLOCA` dest 直接返回其指针;`dest.shape` 非空返回 `_alloc_slot(elem_ty, prod(shape))` 并把 dest 名绑定该指针;否则分配 1 元素缓冲,算子执行完由调用方 `load` 出标量(见 3.2 通用尾巴)。 + +### 2.2 SSA 命名器 + +```python +class SSANamer: + def __init__(self): + self._reg_n = 0 + self._label_n = 0 + self._defs: set[str] = set() + + @staticmethod + def sanitize(name: str) -> str: + s = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in name) + if not s or s[0].isdigit(): + s = "v_" + s + return s + + def fresh(self, hint: str = "r") -> str: + self._reg_n += 1 + return f"%{self.sanitize(hint)}_{self._reg_n}" + + def fresh_label(self, hint: str = "bb") -> str: + self._label_n += 1 + return f"{self.sanitize(hint)}_{self._label_n}" + + def register_definition(self, reg: str) -> None: + if reg in self._defs: + raise LLVMCodegenError(f"duplicate SSA definition: {reg}") + self._defs.add(reg) + + def registered(self, reg: str) -> bool: + return reg in self._defs +``` + +要点: + +- `_dest()` 幂等:若 `instr.dest.name` 已在 `_named_values` 中,直接返回旧引用,**不**调用 `_fresh`; +- 所有 `= ...` 发射点(含 `_materialize_const`、`_emit_*` 内部中间值)必须用 `_fresh` 并通过 `register_definition` 登记; +- 参数名不登记(参数定义不在函数体内),使用前先 `_bind`; +- `gelu/sigmoid` 等展开算子禁止 `_dest` 复用作为多行左值。 + +### 2.3 类型处理与常量 + +**分派规则**: + +| 指令 | 类型来源 | 生成 | +|------|----------|------| +| `fadd/fsub/fmul/fdiv/fneg/fcmp` | `_infer_type(instr)`(dst 优先,其次首操作数) | 浮点指令 | +| `load` | `dest.dtype`(含指针判定) | `%d = load , * %p` | +| `store` | `operands[1].dtype`(值类型) | `store %v, * %p` | +| `load_const` | `dest.dtype` | 浮点:`fadd , 0.0`;整型:`add 0, ` | +| `alloca` | `dest.dtype`,`attrs["size"]` 默认 4 | `%d = alloca , i32 ` | +| `for` | 计数固定 `i32` | 见第五章 | +| `return` | `_ref_types[ref]` 优先,其次操作数类型 | `ret float* %buf` 等 | +| 函数返回类型 | 首个 `RETURN` 操作数的 `_value_type` | 与 `ret` 一致 | + +**常量编码**(P5/P6 修复): + +```python +def _float_to_llvm_hex(value: float) -> str: + f32 = struct.unpack(" str: + if value == 0.0: return "0.0" + if value == 1.0: return "1.0" + if value == -1.0: return "-1.0" + return _float_to_llvm_hex(value) + +def _llvm_const_val(value, ty): + if ty in ("float", "double"): + assert isinstance(value, (int, float)) + return _float_literal(float(value)) + assert isinstance(value, int) or float(value).is_integer() + return str(int(value)) +``` + +**不变量**:浮点指令的参数串中不出现十进制指数形式;整型指令的参数串中不出现小数点。建议在 `_emit_binary`/`_emit_load_const` 后加开发期断言(`if ty in ("float","double"): assert "." in lit or "0x" in lit`)。 + +**混型算术协调**(DSL `for` 循环体常见 `s = add(s, i)`,`s: float`、`i: i32`): + +```python +def _coerce_operand(self, ref: str, from_ty: str, to_ty: str, hint: str) -> str: + if from_ty == to_ty: + return ref + r = self._fresh(hint) + self.namer.register_definition(r) + if from_ty == "i32" and to_ty in ("float", "double"): + self._p(f" {r} = sitofp i32 {ref} to {to_ty}") + elif from_ty in ("float", "double") and to_ty == "i32": + self._p(f" {r} = fptosi {from_ty} {ref} to i32") + else: + raise LLVMCodegenError(f"no coercion {from_ty} -> {to_ty}") + return r + +def _emit_binary(self, instr, fop: str, iop: str | None = None): + ty = self._infer_type(instr) # 结果类型 + lhs = self._coerce_operand(self._op(instr, 0), + self._type_of_operand(instr, 0), ty, "cvt") + rhs = self._coerce_operand(self._op(instr, 1), + self._type_of_operand(instr, 1), ty, "cvt") + dst = self._dest(instr) + op = fop if ty in ("float", "double") else iop + self._p(f" {dst} = {op} {ty} {lhs}, {rhs}") +``` + +`_type_of_operand` 返回操作数引用的 LLVM 类型(常量按 `val.dtype`);`ty` 优先取 `dest.dtype`,dest 缺失时取浮点操作数类型。 + +### 2.4 控制流状态机 + +新增字段:`_terminated: bool`、`_defined_labels: set[str]`、`_prologue: list[str]`、`_loop_stack: list[LoopContext]`。 + +```python +def _start_block(self, label: str) -> None: + if label in self._defined_labels: + label = self._fresh_label(label) # 绝不重复 + if not self._terminated: + self._p(f" br label %{label}") # 显式落空转跳转 + self._p(f"{label}:") + self._defined_labels.add(label) + self._terminated = False + +def _terminate(self, line: str) -> None: + if self._terminated: + raise LLVMCodegenError("terminator emitted twice in one block") + self._p(f" {line}") + self._terminated = True + +def _ensure_terminator(self) -> None: + if not self._terminated: + self._p(f" br label %{self._fresh_label('cont')}") + self._terminated = True +``` + +- `_emit_block` 改为:首块不打印标签(entry 隐式),其余块 `_start_block(block.name)`;块注释保留; +- `_emit_instruction` 若在 `_terminated` 状态下收到非终止指令,先 `_start_block(self._fresh_label("dead"))`,保证不产生“终止符后接指令”的非法文本; +- `_emit_br/_emit_br_if/_emit_return` 全部改调 `_terminate`。 + +### 2.5 `_emit_for` / `_emit_endfor` 修复方案 + +**修复目标**:preheader 跳 header 而非 body;循环上下文栈化;IV 有定义;嵌套标签不重复不悬空(修复 P3/P4)。 + +```python +def _emit_for(self, instr): + self._dest(instr) # 占位登记,值随后绑定 + limit = int(instr.attrs.get("end", 0)) + start = int(instr.attrs.get("start", 0)) + step = int(instr.attrs.get("step", 1)) + ctx = self._loop_open(limit, instr.dest.name, "loop_i", start, step) + self._loop_stack.append(ctx) + +def _loop_open(self, limit, ir_name, hint, start=0, step=1): + ptr = self._alloc_slot("i32", 1, "iv_ptr") + self._p(f" store i32 {start}, i32* {ptr}") + header = self._fresh_label(f"{hint}_hdr") + body = self._fresh_label(f"{hint}_bdy") + exit_ = self._fresh_label(f"{hint}_ext") + self._start_block(header) # 自动补 br label %header + iv = self._fresh(f"{hint}_ld") + self._p(f" {iv} = load i32, i32* {ptr}") + self.namer.register_definition(iv) + cond = self._fresh(f"{hint}_cond") + self._p(f" {cond} = icmp slt i32 {iv}, {limit}") + self._terminate(f"br i1 {cond}, label %{body}, label %{exit_}") + self._start_block(body) + if ir_name is not None: # 仅 DSL for 需要绑定 IR 循环变量 + self._bind(ir_name, iv, "i32") # header load 支配 body+exit + return LoopContext(ir_name, ptr, iv, header, body, exit_, limit, step) + +def _loop_close(self, ctx): + if not self._loop_stack or self._loop_stack[-1] is not ctx: + raise LLVMCodegenError("endfor without matching for") + self._loop_stack.pop() + if not self._terminated: # body 以 ret 结束时为死块,跳过回边 + cur = self._fresh("iv_cur") + self._p(f" {cur} = load i32, i32* {ctx.ptr}") + self.namer.register_definition(cur) + nxt = self._fresh("iv_nxt") + self._p(f" {nxt} = add i32 {cur}, {ctx.step}") + self.namer.register_definition(nxt) + self._p(f" store i32 {nxt}, i32* {ctx.ptr}") + self._terminate(f"br label %{ctx.header}") + self._start_block(ctx.exit) + +def _emit_endfor(self, instr): + if not self._loop_stack: + raise LLVMCodegenError("endfor without matching for") + self._loop_close(self._loop_stack[-1]) +``` + +**边界**: + +- 循环变量在 `endfor` 之后的引用:绑定的是 header 的 load,仍支配 exit,值等于终止时的 IV(合法且语义可解释); +- 空循环体:body 与 header 同名结构仍合法;`_ensure_terminator` 保证 body 有回边; +- `for` 出现在已终止块之后:`_start_block(header)` 会先补 `br`,但这时其实应已由 `_emit_instruction` 开了 `dead` 合成块,无需额外处理。 + +### 2.6 `_emit_br_if` 修复方案(P7) + +```python +_CMP_PRED = {"==": ("oeq", "eq"), "!=": ("one", "ne"), + "<": ("olt", "slt"), "<=": ("ole", "sle"), + ">": ("ogt", "sgt"), ">=": ("oge", "sge")} + +def _emit_br_if(self, instr): + targets = (instr.target or ",").split(",") + true_t = targets[0].strip() + false_t = targets[1].strip() if len(targets) > 1 else true_t + cmp_op = instr.attrs.get("cmp_op") + if cmp_op and len(instr.operands) >= 2: + lhs, rhs = self._op(instr, 0), self._op(instr, 1) + ty = self._infer_type(instr) + fpred, ipred = self._CMP_PRED[str(cmp_op)] + pred = fpred if ty in ("float", "double") else ipred + kind = "fcmp" if ty in ("float", "double") else "icmp" + cond = self._fresh("brc") + self._p(f" {cond} = {kind} {pred} {ty} {lhs}, {rhs}") + self.namer.register_definition(cond) + else: + cond = self._op(instr, 0) # 已是 i1 + self._terminate(f"br i1 {cond}, label %{true_t}, label %{false_t}") +``` + +映射覆盖 `ExtendedDSLParser._parse_condition` 的六种运算符;`IRBuilder.br_if` 的 `operands=[cond]` 形态不受影响。 + +--- + +## 三、逐算子实现方案 + +统一约定:`acc`/`max`/`sum` 等所有 alloca 走 `_alloc_slot`(入口 prologue);循环走 `_loop_open/_loop_close`;`hint` 见设计文档 2.3 表;结果写入 `_dest_buffer` 得到的缓冲,标量 dest 在算子末尾 `load` 首元素。 + +### 3.1 dot + +```python +def _emit_dot(self, instr): + ty = self._infer_type(instr) # "float"/"double" + a = self._ptr_of(instr, 0, ty) + b = self._ptr_of(instr, 1, ty) + n = int(instr.attrs.get("length", instr.attrs.get("len", 1))) + acc = self._alloc_slot(ty, 1, "dot_acc") + self._p(f" store {ty} 0.0, {ty}* {acc}") + ctx = self._loop_open(n, None, "dot_i") + ap = self._gep(ty, a, ctx.value, "dot_ap") + av = self._load(ty, ap, "dot_av") + bp = self._gep(ty, b, ctx.value, "dot_bp") + bv = self._load(ty, bp, "dot_bv") + pr = self._bin("fmul", ty, av, bv, "dot_pr") + old = self._load(ty, acc, "dot_old") + nw = self._bin("fadd", ty, old, pr, "dot_new") + self._p(f" store {ty} {nw}, {ty}* {acc}") + self._loop_close(ctx) + self._finish_scalar_result(instr, acc, ty) # 见 3.2 +``` + +需要的微 helper(也供其余算子复用): + +```python +def _gep(self, ty, base, idx, hint): # %r = getelementptr ty, ty* base, i32 idx +def _load(self, ty, ptr, hint): # %r = load ty, ty* ptr +def _bin(self, op, ty, lhs, rhs, hint): # %r = op ty lhs, rhs +``` + +### 3.2 结果收尾与维度解析 + +```python +def _finish_scalar_result(self, instr, buf_ptr, ty): + if _is_pointer_value(instr.dest): # 张量 dest:绑定缓冲指针 + self._bind(instr.dest.name, buf_ptr, ty + "*") + else: # 标量 dest:load 首元素 + r = self._load(ty, buf_ptr, "res") + self._bind(instr.dest.name, r, ty) + +def _dim_of(self, instr, keys, default=1, operand=None, axis=None): + for k in keys: + v = instr.attrs.get(k) + if isinstance(v, (int, float)) and int(v) > 0: + return int(v) + if operand is not None and operand < len(instr.operands): + shape = instr.operands[operand].shape + if shape: + idx = axis if axis is not None else 0 + if len(shape) > abs(idx): + return int(shape[idx]) + return default +``` + +### 3.3 matmul + +```python +def _emit_matmul(self, instr): + ty = self._infer_type(instr) + a = self._ptr_of(instr, 0, ty) + b = self._ptr_of(instr, 1, ty) + m = self._dim_of(instr, ("m", "rows"), 1, operand=0, axis=0) + k = self._dim_of(instr, ("k", "inner"), 1, operand=0, axis=1) + n = self._dim_of(instr, ("n", "cols"), 1, operand=1, axis=1) + c = self._dest_buffer(instr, m * n, ty) + acc = self._alloc_slot(ty, 1, "mm_acc") + ci = self._loop_open(m, None, "mm_i") + cj = self._loop_open(n, None, "mm_j") + self._p(f" store {ty} 0.0, {ty}* {acc}") + ck = self._loop_open(k, None, "mm_k") + # --- innermost body (i32 offsets) --- + # aoff = add (mul ci.value, k), ck.value + # av = load(gep(a, aoff)) + # boff = add (mul ck.value, n), cj.value + # bv = load(gep(b, boff)) + # pr = fmul av, bv ; acc = fadd load(acc), pr ; store acc + self._loop_close(ck) + # coff = add (mul ci.value, n), cj.value ; store load(acc) -> gep(c, coff) + self._loop_close(cj) + self._loop_close(ci) + self._finish_scalar_result(instr, c, ty) +``` + +(文档中的缩进仅表嵌套层次;实现时按 `_loop_open/_loop_close` 顺序配对。) + +### 3.4 gemm + +维度:`M=_dim_of(attrs ("M",), A.shape[0])`、`K=A.shape[1]`;`trans_b = bool(attrs.get("trans_b", attrs.get("transB", False)))`;`N = W.shape[0] if trans_b else W.shape[1]`,退化 1。循环体: + +```python +acc = bias[j] # load(gep(bias, cj.value)) +for kk: + av = A[i*K + kk] + woff = j*K + kk if trans_b else kk*N + j + wv = W[woff] + acc += av*wv +C[i*N + j] = acc +``` + +`bias` 为标量时 `_ptr_of` spill;`attrs` 中 `trans_a` 暂不支持(如为真,抛 `LLVMCodegenError` 并注释说明)。 + +### 3.5 conv + +维度解析优先级:`attrs` → `operand.shape` → 退化值。 + +```python +x = self._ptr_of(instr, 0, ty); w = self._ptr_of(instr, 1, ty); bias = self._ptr_of(instr, 2, ty) +xs = instr.operands[0].shape # 取后三维 (C,H,W) +cin = xs[-3] if len(xs) >= 3 else 1 +h = xs[-2] if len(xs) >= 3 else 1 +ww = xs[-1] if len(xs) >= 3 else 1 +ws = instr.operands[1].shape +cout = ws[0] if len(ws) >= 4 else _dim_of(instr, ("out_channels",), 1) +k = ws[2] if len(ws) >= 4 else _dim_of(instr, ("kernel_size", "kernel_shape"), 3) +s = _dim_of(instr, ("stride", "strides"), 1) +p = _dim_of(instr, ("padding", "pads"), 0) +ho = (h + 2*p - k)//s + 1; wo = (ww + 2*p - k)//s + 1 +# 若 len(instr.operands) < 3(无 bias),bias 用 0.0 常量替代(_materialize_const),不调用 _ptr_of +``` + +循环体(6 层,oc/oh/ow/ic/kh/kw):每层用 `_loop_open`;kh 内计算 `ih`、`ok_h`;kw 内计算 `iw`、`ok`,并用 `br i1 ok, label %mac, label %skip` 包住 MAC;`mac`/`skip` 用 `_fresh_label("conv_mac"/"conv_skip")` 并 `_start_block`。MAC 地址:输入 `ic*H*W + ih*W + iw`,权重 `oc*Cin*K*K + ic*K*K + kh*K + kw`;kw 退出后写 `out[oc*Ho*Wo + oh*Wo + ow] = acc`。 + +### 3.6 maxpool + +`C/H/W` 来自 `operands[0].shape`(取后三维,退化 1);`k=_dim_of(("kernel","kernel_shape"),2)`、`s=_dim_of(("stride","strides"),2)`;`ho=(h-k)//s+1`、`wo=(w-k)//s+1`。5 层循环;每 (c,oh,ow) 初始化 `m=-3.4e38`(`_float_literal`),内两层 `fcmp ogt + select` 更新,循环结束写回。 + +### 3.7 softmax + +`n = _dim_of(instr, ("length", "n"), 1, operand=0, axis=-1)`;输出缓冲 `_dest_buffer(instr, n, ty)`。 + +- pass1(标签 `sm_max`):`m=-3.4e38`;`m = select(fcmp ogt x[i], m, x[i], m)`; +- pass2(`sm_sum`):`s += expf(x[i] - m)`;`fsub` 后 `call @expf`(f64 用 `@exp`); +- pass3(`sm_div`):`out[i] = expf(x[i] - m) / s`; +- 三趟各自独立 `_loop_open/_loop_close`;结果收尾同 3.2。 + +### 3.8 gelu + +按设计文档 2.5.7 的九行展开实现,全部 `_fresh` + `register_definition`;常量 `0.044715`、`0.7978845608028654` 经 `_float_literal` 内联;`float` 调 `@tanhf`,`double` 调 `@tanh`;最后一行 `_dest` 只定义一次并 `_finish_scalar_result` 绑定。 + +### 3.9 sigmoid + +按设计文档 2.5.8 的四行展开实现;`float` 调 `@expf`,`double` 调 `@exp`。 + +### 3.10 reshape / relu / exp / 算术 + +- `reshape`:同 dtype 直通,`_bind(dest.name, _value_ref(src), _value_type(src))`,不发指令(当前 `fadd x, 0.0` 保留也可,但要求整型安全:按类型选 `fadd`/`add`); +- `relu`:`fcmp ogt + select`(保持); +- `exp`:`call @expf/@exp`(保持); +- `add/sub/mul/div/neg`:`_emit_binary` 统一按 `_infer_type` 分派浮点/整型指令(整型用 `add/sub/mul/sdiv`),避免 int 走 `fadd`。 + +--- + +## 四、测试文件与用例 + +### 4.1 文件清单 + +| 文件 | 内容 | +|------|------| +| `tests/test_llvm_codegen.py`(扩充) | 纯 Python 单元断言:SSA 唯一、标签唯一、常量写法、8 算子结构关键词 | +| `tests/test_llvm_codegen_llvm_tools.py`(新增) | `llvm-as` 可汇编测试 + `lli` 数值测试;工具缺失 skip | + +### 4.2 `tests/test_llvm_codegen.py` 扩充用例(无外部工具依赖) + +```python +def _name_defs(ir: str) -> list[str]: + return re.findall(r"^\s*(%[A-Za-z0-9_.]+)\s*=", ir, re.M) + +def test_no_duplicate_ssa_gelu_sigmoid(...): # set(defs) 数量 == len(defs) +def test_for_labels_unique(...): # 每个 label 形如 ^\s*(\w+):$ 唯一 +def test_float_const_is_hex(...): # 无 "e-0" 指数常量;含 0x +def test_int_const_not_float_op(...): # 无 "fadd i32" +def test_all_ops_emit_loops(...): # 8 算子每个含 "getelementptr"/"icmp slt" +``` + +### 4.3 `llvm-as` 集成测试(环境缺失 skip) + +```python +import shutil, subprocess, pytest + +LLVM_AS = shutil.which("llvm-as") +LLI = shutil.which("lli") +requires_asm = pytest.mark.skipif(LLVM_AS is None, reason="llvm-as not installed") +requires_lli = pytest.mark.skipif(LLI is None, reason="lli not installed") + +def _assemble(ir: str, tmp_path): + ll = tmp_path / "m.ll"; ll.write_text(ir) + return subprocess.run([LLVM_AS, str(ll), "-o", str(tmp_path / "m.bc")], + capture_output=True, text=True) +``` + +用例矩阵(对应设计文档第三章): + +| 用例 | 输入 | 断言 | +|------|------|------| +| `test_asm_gelu_sigmoid` | IRBuilder 展开 | rc==0;无 `multiple definition`;定义次数唯一 | +| `test_asm_for_nested` | DSLParser 双重 for | rc==0;`loop_i_hdr` 先于 body;标签计数为 1 | +| `test_asm_tensor_ops` | dot/matmul/gemm/maxpool/conv 五程序 | rc==0;GEP/MAC/循环结构 | +| `test_asm_softmax` | shape=(2,) 与 (1,) | rc==0;三趟循环标签齐全 | +| `test_asm_onnx_cnn` | `models/graph/cnn.onnx`(存在则跑) | rc==0;无占位注释 | + +### 4.4 `lli` 数值测试(harness 拼接法) + +测试端构造程序:被测函数命名 `kernel`,参数用 `IRBuilder.make_value` 后设 `shape` 得到 `float*`;生成后拼接手写 `@main`: + +```llvm +define i32 @main() { + %a = alloca float, i32 4 + ; store 常量数组 ... + %r = call float @kernel(float* %a, float* %b) + %ten = sitofp i32 10 to float ; 期望值 10.0,避免十进制浮点字面量 + %ok = fcmp oeq float %r, %ten + %rc = select i1 %ok, i32 0, i32 1 + ret i32 %rc +} +``` + +然后用 `LLI m.bc` 执行并断言 `returncode == 0`。用例与期望值: + +| 算子 | 输入 | 期望 | +|------|------|------| +| gelu→sigmoid | x=0.0 | 0.5 | +| 嵌套 for | i,j∈[0,3),`s+=i` | 9.0 | +| dot | a=[1,2,3,4], b=[1,1,1,1] | 10.0 | +| matmul 1×1 | a=2, b=3 | 6.0 | +| gemm 1×1 | a=2, w=3, bias=0.5 | 6.5 | +| maxpool 2×2 | x=[1,2,3,4], K=2,S=1 | 4.0 | +| conv 1×1×1 | x=3, w=2, bias=1, K=1,S=1,P=0 | 7.0 | +| softmax N=2 | x=[0,0] | out=[0.5,0.5] | +| softmax N=1 | x=[7] | out=[1.0] | + +注意:这些期望值全部可被 float32 精确表示,用 `fcmp oeq` 判定不引入容差问题。若 `lli` 存在但符号解析失败(`@expf` 未注册等),把对应激活类用例标记为 `xfail(strict=False)` 并在测试注释中说明;张量类用例不依赖 libm,必须真实执行。 + +### 4.5 回归清单 + +```bash +python -m pytest tests/test_llvm_codegen.py tests/test_llvm_codegen_llvm_tools.py -v +python -m pytest tests/ -q # 全量,无新增失败 +llvm-as /tmp/out.ll -o /dev/null # 手工冒烟(cnn.onnx 输出) +``` + +--- + +## 五、验收标准 + +1. **可汇编(硬门槛)**:8 个算子的样例 IR、DSL `for/if/while` 样例、`models/graph/cnn.onnx` 经 `LLVMCodegen.emit()` 后 `llvm-as` 全部退出码 0;输出中不存在 `; UNSUPPORTED`、`; placeholder`、`; passthrough`。 +2. **结构断言**: + - 每个函数内 SSA 定义名唯一(正则计数 == set 大小); + - 每个标签唯一定义,且 preheader 分支目标为 header; + - 每个基本块以 `br`/`ret` 结尾; + - 张量算子至少含 `getelementptr , *`、`fmul`+`fadd`、`icmp slt i32`、`br i1`;循环层数符合:conv 6、maxpool 5、gemm/matmul 3、dot 1、softmax 3 趟。 +3. **数值断言**:4.4 表格全部通过(`lli` 环境存在时必须真实执行;缺失则 skip,不得以 skip 充当通过)。 +4. **类型断言**:浮点常量全部为 `0x` 十六进制或 `0.0/1.0/-1.0`;整型常量不出现在浮点指令中。 +5. **兼容性**:`tests/test_llvm_codegen.py` 原有用例全绿;`benchmarks/test_benchmark.py::test_codegen_llvm` 通过;`LLVMCodegen(program)` 单参数调用可用。 +6. **范围纪律**:`git diff --stat` 仅包含 `scratchv/backend/llvm_codegen.py` 与两个测试文件。 + +--- + +## 六、风险与回退 + +| 风险 | 概率/影响 | 缓释 | 回退 | +|------|-----------|------|------| +| 标量操作数进入张量算子所需元素数 > 1(如 `dot(a,b,len:8)` 中 a/b 是标量) | 中/中 | 需求元素数 > 1 时抛 `LLVMCodegenError`(`_require_elements`),不再 spill 成 1 元素缓冲后越界读;needed==1 的退化仍支持 | 如评审要求完整向量语义,需扩 IR(超范围,另立课题) | +| DSL 循环/分支变量为静态 SSA(无 phi):累加不生效、while 条件不可变、if/else 合流未定义 | 高/高 | 文档 §5.7 锁定实际行为并补 `lli`/结构回归;while 不可能退出时 fail-loud;正确累加用 IRBuilder alloca/load/store | 前端变量降级为 alloca/load/store 属独立课题(评审 §4.1) | +| ONNX 路径参数变 `float*`、返回 `float*` 改变模块形态 | 中/中 | `benchmarks` 仅断言非空;无其他测试依赖具体签名 | 保持标量签名 + 内部 spill,放弃指针优化(改回点在 `_value_type`) | +| `_start_block` 自动补 `br` 改变 IR 文本,既有结构断言用例误判 | 中/低 | 同步更新 `tests/test_llvm_codegen.py` 中 `": " in ir` 类弱断言 | 保留旧的 `_emit_block` 分支作为开关(不推荐) | +| `lli` 数值测试受 libm 符号/平台影响 | 中/低 | 张量用例零 libm;激活用例 `xfail(strict=False)` | 数值验证降级为“结构 + 常量折叠冒烟” | +| LLVM 版本差异(十进制浮点、attr 语法) | 低/高 | 全部浮点常量走十六进制;外部声明只用 Llvm 10 已有语法 | 以 `/usr/bin/llvm-as` 实测为准,修正编码 | +| 重构范围蔓延到 optimizer/backend 其他文件 | 低/高 | 实施顺序按第七节,单文件提交;scope 检查用 `git diff --stat` | 按文件粒度 `git checkout --` 回退非目标文件 | + +整体回退策略:本课题所有改动集中在 `llvm_codegen.py` 与测试;如集成失败,`git revert` 对应提交即可恢复旧行为(占位实现仍可汇编出错误数值的 IR,不影响 RISC-V 主路径)。 + +--- + +## 七、实施顺序(建议提交粒度) + +1. **命名器 + 类型/常量**:`SSANamer`、`_float_literal`、`_llvm_const_val`、`_emit_load_const`、`_value_type`/`_ref_types`;跑通 `gelu/sigmoid` 的 `llvm-as`(修 P1/P2/P5/P6)。 +2. **控制流状态机**:`_start_block`/`_terminate`/`_loop_open`/`_loop_close`、`_emit_br_if` cmp_op;跑通 `for`(含嵌套)与 `if/while` 的 `llvm-as`(修 P3/P4/P7)。 +3. **基础设施**:`_alloc_slot` prologue、`_ptr_of`、`_dest_buffer`、`_gep/_load/_bin`。 +4. **张量算子**:dot → matmul → gemm → maxpool → conv → softmax,逐个补 `llvm-as` 与 `lli` 用例。 +5. **triple 可选化** + 文档注释清理(删除 placeholder 文本)。 +6. **测试收口**:全量 pytest、`llvm-as` ONNX 冒烟、验收清单逐条勾选。 + +每步结束运行:`python -m pytest tests/test_llvm_codegen.py -q && llvm-as <样例> -o /dev/null`;完成后按项目 Harness 规则执行 self-review 与验证(本任务仅编写文档,不改仓库、不做 git 操作)。 + +--- + +## 实现结果(2026-09-14 集成) + +> **集成 commit**:`bd0db49`(`feat(topic16): implement real NN op lowering and fix invalid LLVM IR`) +> **集成位置**:`Seven_big_summary` 上第 4 个 topic commit(顺序 06 → 07 → 09 → **16** → 17 → …) +> **集成后全量**:`PYTHONPATH=. python3.11 -m pytest tests/ -q` → **1011 passed / 13 xfailed / 20 xpassed / 0 failed** + +### 实现文件与要点 + +| 文件 | 要点 | +|------|------| +| `scratchv/backend/llvm_codegen.py` | 重写(+1121 / −321):SSA 命名、类型/常量、控制流状态机、8 个 NN 算子真实 lowering | +| `tests/test_llvm_codegen_topic16.py` | 47 个新测试(含 `lli` 数值断言;外部工具缺失时 skip) | + +### 测试数字 + +| 口径 | 结果 | +|------|------| +| 定向(`tests/test_llvm_codegen_topic16.py`) | 47 passed(含 lli 数值) | +| 分支全量(cherry-pick 前) | 612 passed | +| 集成后全量 | 1011 passed / 13 xfailed / 20 xpassed / 0 failed | + +### 与本文档的偏差 / 未完成项 + +- 标量变量统一使用 alloca + load/store(不再区分寄存器直出形态)。 +- DSL 嵌套 `for` 的数值断言在修复轮(2026-09-14 阶段 2)已补齐 `lli`:静态 SSA 语义下实际值为 2.0,设计文档 §5.7 与 `test_lli_dsl_nested_for_static_ssa_value` 锁定。 +- DSL `while` 条件变量重赋值被 `_check_unbounded_loops` fail-loud 拒绝(原实现会生成死循环);`if/else` 合流读最后解析分支的行为由结构测试锁定。 +- 多元素 initializer 在无 `shape` 信息时退化为 1 元素。 +- `gemm trans_a=True` 明确抛错(不支持)。 + +### 修复轮补丁要点(2026-09-14 阶段 2,评审 F1–F5/F7/F9) + +| 评审项 | 修复方式 | 代码位置 | +|--------|----------|----------| +| F1(P0) | 设计文档期望值修正 + 已知限制 §5.7;while 不可退出 fail-loud;nested-for/loop-carried/if-else-merge 回归锁定 | `_check_unbounded_loops` | +| F2(P1) | shaped dest 的元素级算子改走逐元素 `_emit_map`(含标量/单元素广播),`_dest` 拒绝张量目标 | `_emit_map`、`_dest` | +| F3(P1) | 张量算子按缓冲真实元素数校验,标量供多元素即抛错 | `_require_elements` | +| F4(P1) | softmax 按 `prod(shape[:-1])` 行循环、缓冲 `prod(shape)`;仅支持 axis=-1 | `_emit_softmax` | +| F5(P1) | 混合 `ret `/`ret void` 与不一致返回类型 fail-loud | `_validate_return_types` | +| F7(P2) | 无 handler 的 opcode(transpose/concat)抛 `LLVMCodegenError`,删除零值伪计算 | `_emit_unsupported` | +| F9(P2) | 补 conv padding/stride、gemm trans_b 非方阵、matmul 非方阵、maxpool stride2、softmax 3 元素容差、for step=2、trans_a 负例等 `lli` 用例 | `tests/test_llvm_codegen_topic16.py` | + +### 已知限制 + +- `llvm-as` / `lli` 环境缺失时相关用例 skip,不得以 skip 充当通过。 +- ONNX 路径 `float*` 签名与返回类型变化仍按文档风险表处理;无其他调用方依赖具体签名。 +- DSL 循环/分支变量语义仍为静态 SSA(根因在 `dsl_parser.py:114` 的 `_vars` 前端,评审 §4.1 列为既有仓库缺陷);本分支仅做守卫/降级与文档锁定,不实现变量身份。 diff --git "a/docs/topics/16-LLVM\344\273\243\347\240\201\347\224\237\346\210\220-\350\256\276\350\256\241\346\226\207\346\241\243.md" "b/docs/topics/16-LLVM\344\273\243\347\240\201\347\224\237\346\210\220-\350\256\276\350\256\241\346\226\207\346\241\243.md" new file mode 100644 index 0000000..3047780 --- /dev/null +++ "b/docs/topics/16-LLVM\344\273\243\347\240\201\347\224\237\346\210\220-\350\256\276\350\256\241\346\226\207\346\241\243.md" @@ -0,0 +1,622 @@ +# 课题16 LLVM 代码生成后端(库路径)技术设计文档 + +> 文档版本:v1.0 +> 编写日期:2026-09-14 +> 涉及模块:`scratchv/backend/llvm_codegen.py`(库路径 LLVM IR 生成器)、`tests/test_llvm_codegen*.py` +> 功能范围:库路径 `--backend llvm` 的正确性修复与算子补全——SSA 唯一命名、类型/常量合法化、`_emit_for` CFG 修复、控制流 `br_if` 修复、目标 triple 可配置、8 个 NN 算子(conv/gemm/matmul/dot/maxpool/softmax/gelu/sigmoid)的真实循环生成;验收工具 `llvm-as` +> 状态说明:本文档描述**目标实现**。当前 `llvm_codegen.py` 存在下述已实测缺陷,文档给出修复设计;实现前代码未修改。 + +--- + +## 一、功能介绍 + +### 1.1 功能概述 + +LLVM 代码生成后端负责把 ScratchV IR(`Program`/`Function`/`BasicBlock`/`Instruction`)翻译为 LLVM IR 文本(`.ll`),供 `llvm-as`、`opt`、`llc`、`lli` 使用。项目有两条 LLVM 路径: + +| | 路径 A(库路径,本课题) | 路径 B(Standalone,不修改) | +|---|---|---| +| 文件 | `scratchv/backend/llvm_codegen.py` | `scratchv/standalone/onnx_to_llvm_standalone.py` | +| 输入 | ScratchV IR `Program` | ONNXModel(手工解析) | +| 算子状态 | conv/softmax/dot 等为占位实现,数值错误 | float32 完整循环生成 | +| 目标 | 正确、可汇编、结构可断言 | 完整可执行 CNN | + +#### 当前缺陷清单(已在 LLVM 10 / `llvm-as` 下实测复现) + +| 编号 | 缺陷 | 触发输入 | 实测结果 | +|------|------|----------|----------| +| P1 | GELU 重复定义同名 SSA | `y = gelu(x)` | `multiple definition of local value named 'x3_2'` | +| P2 | Sigmoid 重复定义同名 SSA | `sigmoid` 算子 | `multiple definition of local value named 'v_1_1'` | +| P3 | for 循环变量从未定义 | `for i = 0, 3` + 循环体引用 `i` | `use of undefined value '%v_1_1'` | +| P4 | 嵌套 for 标签重复/悬空 | 双重 `for` | 标签 `loop_exit_12` 重复定义且 `%loop_exit_4` 未定义;`_loop_context` 单槽被内层覆盖 | +| P5 | 整数常量走浮点指令 | `load_const(3, INT32)` | `%v = fadd i32 3, 0.0` → `floating point constant invalid for type` | +| P6 | 十进制浮点字面量不可表示 | ONNX initializer 如 `-6.987718e-02` | LLVM 10 要求 float 十进制常量**精确可表示**;`fadd float -6.987718e-02, 0.0` 报错 | +| P7 | `br_if` 条件类型错误 | `ExtendedDSLParser` 的 `if/while` | `br i1 %a`(`%a: float`)→ `'%a' defined with type 'float' but expected 'i1'`;`cmp_op` 属性被忽略 | +| P8 | 6 个张量算子占位、数值错误 | conv/gemm/matmul/dot/maxpool/softmax | softmax 只发 `expf(x)`;conv 发 `fadd 0.0, 0.0`;maxpool/reshape 直通;matmul/dot/gemm 发标量乘——**无循环、无 GEP、无 MAC** | +| P9 | 目标 triple 硬编码 | 模块头 | `target triple = "riscv64-unknown-elf"` 对所有用途写死,宿主 `lli`/交叉目标不可配置 | + +#### 修复后期望能力 + +- 任意由 `DSLParser`/`ExtendedDSLParser`/`ONNXParser` 产生的、**被后端接受**的 IR,经 `LLVMCodegen.emit()` 输出后 `llvm-as` 全部通过;明确不支持或语义无法保证的输入(见 2.4.8 与 2.6)抛 `LLVMCodegenError` 而不是输出静默错误或非法 IR。 +- 8 个算子的 IR 具有与 standalone 路径一致的循环嵌套结构、GEP 地址计算、浮点 MAC,可被 `lli`/`opt` 进一步消费。 +- 每个 SSA 名字全函数唯一、每个标签唯一定义、每个基本块以终止指令结束。 +- **DSL 变量语义为静态 SSA**:DSL 前端每次赋值产生一个新的 IR `Value`(`dsl_parser.py:114` 的 `_vars[name] = result`),IR 无 phi;循环携带累加、`while` 条件变量重赋值、`if/else` 合流等跨迭代/跨分支的变量语义**不属于本课题范围**(见 §三 用例 2 与 §5.7)。 + +### 1.2 设计目标 + +- **正确可汇编**:`llvm-as file.ll -o /dev/null` 零错误是硬门槛,优先于性能与可读性。 +- **结构可对齐**:循环结构、索引公式、GEP 形式、浮点常量编码与 `onnx_to_llvm_standalone.py`(下称 standalone)保持一致,便于两条路径对比。 +- **嵌套安全**:for/if/while 任意深度嵌套,标签与 SSA 名无冲突。 +- **数值可验证**:小规模张量场景可用 `lli` 执行并断言数值(整数与 0.5 等可精确表示值)。 +- **改动收敛**:只改库路径与其测试;standalone 不动;不引入 ScratchV IR→LLVM 的高级优化(不做 mem2reg、循环展开、向量化)。 + +--- + +## 二、设计规范 + +### 2.1 总体架构 + +`LLVMCodegen` 采用**单趟文本生成**:遍历 `Program.functions`,逐函数遍历 `BasicBlock`,逐指令分派到 `_emit_`。新增/重构三类基础设施: + +1. **命名器**(`SSANamer`):寄存器与标签分开计数,保证唯一性。 +2. **控制流状态机**:显式跟踪“当前块是否已终止”,所有标签经由 `_start_block()` 打开。 +3. **张量存储约定**:IR `Value` 携带 `shape`,非空即视为指针(`float*` 等);标量参与张量算子时溢写(spill)到 `alloca`,退化为 1 元素张量。 + +不改变 `emit()`/`save()` 的调用契约:`scratchv/compiler.py:388-390` 的 `LLVMCodegen(program).emit()` 继续工作。 + +### 2.2 类型规则 + +| IR dtype | 标量 LLVM 类型 | 指针 LLVM 类型 | 常量写法 | 允许的指令族 | +|----------|----------------|----------------|----------|--------------| +| `FLOAT32` | `float` | `float*` | `0.0`/`1.0`/`-1.0` 或 64 位十六进制(见下) | `fadd/fsub/fmul/fdiv/fneg/fcmp/call @expf @tanhf` | +| `FLOAT64` | `double` | `double*` | 同上 | 同上(`@exp @tanh`) | +| `INT32` | `i32` | `i32*` | 十进制整数(如 `3`、`-1`) | `add/sub/mul/sdiv/srem/icmp` | +| `INT64` | `i64` | `i64*` | 十进制整数 | 同上 | + +约束规则: + +- **不隐式转换**:二元指令两侧类型必须一致;索引、循环计数、维度一律 `i32`。 +- **混型算术显式转换**:当操作数类型与结果类型不一致时插入转换指令——int→float 用 `sitofp v to `,float→int 用 `fptosi v to `(结果类型为整型时);常量直接按结果类型格式化,不生成转换。例:`s = add(s, i)`(`s: float`、`i: i32`)⇒ `%if = sitofp i32 %i to float` + `%r = fadd float %s, %if`。该规则覆盖 DSL `for` 循环体把循环变量(i32)与 float 变量混用的常见写法。 +- **张量指针判定**:`Value.shape != ()` 或该值由 `OpCode.ALLOCA` 定义 ⇒ 类型为“元素类型 + `*`”。函数参数、返回值、算子操作数/结果统一遵循此规则。 +- **整数常量禁止浮点算子**:`load_const` 目标为整型时,生成 `%r = add i32 0, `(或直接把常量作为使用点立即数),绝不生成 `fadd i32 ...`。 +- **浮点常量必须精确可表示**:LLVM IR 十进制浮点常量必须能被目标类型精确表示(LLVM 10 校验严格)。统一使用 standalone 的编码算法: + - 先经 `struct.pack("_`,`n` 在**整个函数内单调递增、永不复用**;函数内与函数间均不依赖 LLVM 自动改名。 +- 寄存器计数器与标签计数器**分离**(现有实现共用 `_block_counter`,是 P1/P2 的根因之一)。 +- 合法字符集:`[A-Za-z0-9_.]`,首字符必须为字母/`_`;`sanitize()` 将非法字符替换为 `_`,空串/数字开头加前缀 `v_`。 +- **一次定义**:同一 SSA 名在函数内只允许一次 `= ...` 定义(参数行除外)。 + - `_dest(instr)` 幂等:同一 IR `Value` 重复出现时返回已绑定引用,不再生成新定义; + - 多指令展开的算子(gelu/sigmoid)**每一步都用 fresh 中间名**,最终结果单独用 fresh 名,禁止“复用 dst 名写三行”(P2 根因)。 +- **循环变量**:IR 循环变量 `Value` 不直接作为 SSA 定义,而是在 header 中 `load` 一次得到 `%_ld_`,该 load 及其结果支配 body 与 exit;循环体与循环后的引用统一绑定到它(P3 根因修复)。 +- 标签名由 `SSANamer.fresh_label(hint)` 产生(无 `%` 前缀),进入 `defined_labels` 集合;重复定义时追加后缀,绝不输出两个同名标签。 +- 中间名 hint 约定(增强可读性): + +| 算子 | 寄存器 hint | 标签 hint | +|------|-------------|-----------| +| gelu | `gelu_t1, gelu_x3, gelu_inner, gelu_tanh, gelu_p1, gelu_hx` | — | +| sigmoid | `sig_neg, sig_exp, sig_den, sig_out` | — | +| dot | `dot_i, dot_acc, dot_prod` | `dot_i` | +| matmul | `mm_i, mm_j, mm_k, mm_acc` | `mm_i/mm_j/mm_k` | +| gemm | `gemm_i, gemm_j, gemm_k, gemm_acc` | `gemm_i/j/k` | +| conv | `conv_oc/oh/ow/ic/kh/kw, conv_acc, conv_mac, conv_skip` | `conv_oc/.../conv_kw` | +| maxpool | `mp_c/oh/ow/kh/kw, mp_max` | `mp_c/.../mp_kw` | +| softmax | `sm_i1/i2/i3, sm_max, sm_sum, sm_e` | `sm_max/sm_sum/sm_div` | + +### 2.4 控制流合法性规则 + +LLVM 基本块要求:**每个块恰好一条终止指令(`ret`/`br`/`br i1`)且在块尾**;标签唯一定义;所有使用被定义支配。对应实现规则: + +1. **块状态机**:字段 `self._terminated: bool`。`_start_block(label)` 的语义是“结束当前块并打开新块”: + - 若 `not self._terminated`,先发射 `br label %