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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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"
requires-python = ">=3.11"
dependencies = [
"onnx>=1.14",
"numpy>=1.24",
Expand All @@ -21,9 +21,6 @@ classifiers = [
"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",
Expand Down
15 changes: 15 additions & 0 deletions requirements/ci.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# CI / verification environment
# Target Python: 3.11 - 3.12 (the bundled Python 3.8 .venv is broken; do not use)
# CI and local runs use this file. Install:
# python3.11 -m venv .venv311 && source .venv311/bin/activate
# pip install -r requirements/ci.txt
# pip install -e ".[all]" # optional: tinyfive / llvmlite / onnxruntime

numpy>=1.26,<3
onnx>=1.14,<2
protobuf>=4.21
pytest>=7

# Optional (accelerators / independent verification): uncomment to use
# onnxruntime
# llvmlite
1,691 changes: 1,356 additions & 335 deletions scratchv/standalone/onnx_to_riscv_standalone.py

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions scratchv/standalone/spike_sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- Instruction trace samples
- Spike execution time

Spike binary: /home/kinsomwang/workspace/coralnpu-spike-rv32/bin/spike
Spike binary: $SCRATCHV_SPIKE, else `spike` resolved on PATH (no hardcoded path)
ScratchV binary: output.bin

Usage:
Expand All @@ -25,6 +25,7 @@

import argparse
import os
import shutil
import struct
import subprocess
import sys
Expand All @@ -34,9 +35,16 @@
from dataclasses import dataclass, field

# ── Paths ──────────────────────────────────────────────────────────────────
SPIKE = "/home/kinsomwang/workspace/coralnpu-spike-rv32/bin/spike"
SPIKE_DASM = "/home/kinsomwang/workspace/coralnpu-spike-rv32/bin/spike-dasm"
SPIKE_LOG_PARSER = "/home/kinsomwang/workspace/coralnpu-spike-rv32/bin/spike-log-parser"
# Resolve Spike tools portably: explicit env override first, then PATH.
# No hardcoded machine-specific paths.
def _resolve_tool(name: str, env_var: str) -> str:
"""Resolve a Spike tool path: env override → PATH lookup → bare name."""
return os.environ.get(env_var) or shutil.which(name) or name


SPIKE = _resolve_tool("spike", "SCRATCHV_SPIKE")
SPIKE_DASM = _resolve_tool("spike-dasm", "SCRATCHV_SPIKE_DASM")
SPIKE_LOG_PARSER = _resolve_tool("spike-log-parser", "SCRATCHV_SPIKE_LOG_PARSER")

# ── Constants ──────────────────────────────────────────────────────────────
ELF_BASE = 0x80000000 # RISC-V DRAM base (Spike default)
Expand Down
113 changes: 113 additions & 0 deletions tests/test_platform_asm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""--platform-asm output shape (platform contract; no RISC-V toolchain needed)."""

import contextlib
import io
import tempfile

import onnx
from onnx import TensorProto, helper
import pytest

from scratchv.standalone.onnx_to_riscv_standalone import (
ONNXModel,
CNNRISCVGenerator,
MemoryPlan,
emit_platform_asm,
)


def _conv_model():
x = helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 3, 8, 8])
y = helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 8, 8, 8])
import numpy as np
rng = np.random.default_rng(0)
w = helper.make_tensor("w", TensorProto.FLOAT, [8, 3, 3, 3],
rng.normal(0, 0.3, size=8 * 3 * 9).astype(np.float32).ravel().tolist())
node = helper.make_node("Conv", ["x", "w"], ["y"], pads=[1, 1, 1, 1])
graph = helper.make_graph([node], "c", [x], [y], [w])
return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])


def _build_platform_asm(tmp_path):
path = tmp_path / "m.onnx"
onnx.save(_conv_model(), str(path))
model = ONNXModel.from_file(str(path))
mem = MemoryPlan()
for vi in model.inputs:
n = 1
for d in model.get_shape(vi.name):
n *= d
mem.alloc_workspace(vi.name, n)
weight_data = mem.layout_weights(model.initializers)
gen = CNNRISCVGenerator(model, mem)
with contextlib.redirect_stdout(io.StringIO()):
gen.generate()
return emit_platform_asm(gen, weight_data)


def _fwht_model():
x = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 64])
y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 64])
node = helper.make_node("Fwht", ["X"], ["Y"], domain="org.scratchv")
graph = helper.make_graph([node], "fwht", [x], [y])
return helper.make_model(
graph,
opset_imports=[helper.make_opsetid("org.scratchv", 1),
helper.make_opsetid("", 13)],
)


def test_platform_asm_is_size_independent_for_fwht(tmp_path):
from scratchv.standalone.onnx_to_riscv_standalone import emit_platform_asm

path = tmp_path / "fwht.onnx"
onnx.save(_fwht_model(), str(path))
model = ONNXModel.from_file(str(path))
mem = MemoryPlan()
for vi in model.inputs:
n = 1
for d in model.get_shape(vi.name):
n *= d
mem.alloc_workspace(vi.name, n)
weight_data = mem.layout_weights(model.initializers)
gen = CNNRISCVGenerator(model, mem)
with contextlib.redirect_stdout(io.StringIO()):
gen.generate()

asm = emit_platform_asm(gen, weight_data, model)
assert ".globl cnn_entry" in asm
# runtime-sized: bounds come from a2, not a compile-time constant
assert "mv t2, a2" in asm
assert "blt t3, t2, _p_fwht_copy" in asm # copy uses runtime N
# no hard-coded 64 anywhere as a loop bound
assert "li t2, 64" not in asm


def test_platform_asm_contract(tmp_path):
asm = _build_platform_asm(tmp_path)

# entry symbol is global cnn_entry, and old _start is gone
assert ".globl cnn_entry" in asm
assert "\ncnn_entry:" in asm
assert not any(l.strip() == "_start:" for l in asm.splitlines())

# no forbidden directives
assert ".incbin" not in asm
assert ".include" not in asm

# no unresolved placeholder / symbolic temp labels leak
assert "auipc gp, 0x0" not in asm
assert ".Lsv_" not in asm
assert "la gp, __data_start" in asm

# branch/jump targets use labels, not numeric offsets
import re
for line in asm.splitlines():
code = line.split("#", 1)[0]
if re.match(r"\s*(bne|beq|blt|bge|j|jal)\b", code):
assert not re.search(r",\s*-?\d+\s*$", code), f"numeric target: {line}"

# inline data section with .word, no incbin
assert "__data_start:" in asm
assert ".word" in asm
assert asm.count(".word") > 10
Loading
Loading