Skip to content

fix(standalone): correct CNN address generation and loop targets - #73

Open
Mastttttter wants to merge 2 commits into
ScratchV-Compiler:mainfrom
Mastttttter:fix/standalone-cnn-codegen
Open

Mastttttter wants to merge 2 commits into
ScratchV-Compiler:mainfrom
Mastttttter:fix/standalone-cnn-codegen

Conversation

@Mastttttter

@Mastttttter Mastttttter commented Sep 21, 2026

Copy link
Copy Markdown

仓库 cnn.onnx 经 standalone 编译后,在 QEMU 中执行全零输入会触发 SIGSEGV;默认配置和开启常量合并时
均可复现。

本次修复:

  • 隔离各算子的循环标签,并校验标签定义和跳转范围。
  • 修复卷积地址寄存器复用、步长、权重偏移及跨通道步进,每个输出位置重新初始化 bias。
  • 修复 Sigmoid 的 Q16.16 常量 32768 被立即数编码截断的问题。
  • 增加独立 RV32 执行回归,接入现有 test job。

验证:
11 项新增回归在原始 origin/main 上全部失败;修复后与现有常量合并测试合计 14 项通过,无跳过。完整
CNN 在常量合并开、关两种配置下均通过三组固定输入,正常返回、缓冲区边界完整,工作区和输出一致。

整网定点结果与 ONNX 浮点参考的精度对照另行验证。

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown

🤖 AI Code Review

共审查 2 个变更文件

📁 .github/workflows/ci.yml

🟡 **Hard CI dependency on RISC-V** — The `SCRATCHV_REQUIRE_RISCV_EXECUTION: "1"` env var is set unconditionally for the entire test step. This converts what may have been an optional/skippable feature test into a hard requirement. If any CI runner can't install `qemu-user` (non-Ubuntu runners, restricted networks, etc.), every test run fails. Verify this is intentional — or gate it with an `if:` on the OS/arch so other runners still pass.

🟡 **Package install is always run** — `command -v` check guards the install, but `apt-get update` (if reached) still costs ~30s on cache-miss runs. Consider a separate cached install or pinning the Docker image layer that includes these tools, so the step is essentially a no-op check rather than a network operation.

🟡 **Comment accuracy** — Old comment mentioned "parser, CLI and benchmark regression tests"; new comment only mentions "diagnostics and standalone CNN execution". If the test selection command hasn't changed (still `pytest tests/`), the comment is now narrower than what actually runs. Either restore the fuller list or confirm the test suite itself was narrowed.

💭 **`PYTHONHASHSEED: "0"` placement** — This is a good practice for deterministic test ordering, but it's set only for this step. If other test steps exist (or are added later), they won't inherit it. Consider moving to job-level `env:` for consistency across all pytest invocations.

💭 **No verification of installed versions** — `command -v` confirms presence but not version. If a specific `clang`/`qemu` feature is required (e.g., RISC-V 32-bit user mode was added in qemu 4.x), a version check (`clang --version | head -1`) would catch regressions from base-image drift faster than a cryptic compile/runtime failure.

📁 scratchv/standalone/onnx_to_riscv_standalone.py

🔴 Bug fix confirmed: sigmoid rv_addi(dst, dst, 32768) — RV32I addi takes 12-bit signed imm (max 2047); 32768 lower-12-bits = 0, so old code added nothing. emit_li + add is correct.

🔴 Bug fix confirmed: ic_advance_bytes — Old (H*W - K - (K-1)*(W-K)) = 18 for K=3,W=5,H=5; correct is H*W - (K-1)*W - K = 12 (pointer ends at offset (K-1)*W + (K-1), next-ic start is H*W). New formula matches the comment.

🟡 Bias load perf regression — Moved from once/oc to once per (oh,ow): H_out×W_out × 3 extra instrs per oc. Consider loading bias[oc] into a free saved reg at oc-loop top, then rv_mv(ACC_REG, BIAS_VAL_REG) per element (saves 2 instr/element). Verify a saved reg is available (s5-s9 likely free given s0/s1=saved a0/a1, s2/s3/s4=base ptrs, s10/s11=kh/kw→ic_adv/row_adv).

🟡 Removed stride_h_W_times_4 / stride_w_times_4 — Confirm no remaining references (padding path? other kernels? _gen_conv_transpose?). Dead-code removal is fine if truly unused.

🟡 IH_BASE_REG=_R_A0, IW_BASE_REG=_R_A1 — Reusing caller-saved arg regs as scratch. Safe if _gen_conv is only called from top-level (entry saves a0/a1 in s0/s1 per comment), but fragile: if _gen_conv ever becomes a callable subroutine, a0/a1 clobbering violates RV ABI. Worth a comment asserting the invariant.

🟡 label_prefix = f"_node_{len(self.emit.code)}_" — Two distinct nodes that emit zero instructions would collide on the same prefix, and the duplicate-label check would then spuriously raise. Also breaks any cross-node label references (shared epilogues, jump tables). A monotonic counter (self._node_id += 1) is more robust. Verify no handler relies on referencing a label defined in a sibling node.

🟡 Range check off-by-onenot -limit <= byte_offset < limit permits branch offset 4095 and jal 1048575; actual encodable max even offsets are 4094 / 1048574. Tighten to < limit - 2 (or assert byte_offset % 4 == 0 since instr-aligned). Encoder will likely fault/mis-encode on the boundary, so catching here is better.

💭 General-path weight addressing via WT_PTR base — Saves 2-3 instrs/load, but correctness depends on the ic-loop advancing WT_PTR by K*K*4 each iteration (not visible in diff). Please confirm.

💭 raise on duplicate/undefined labels — Strict but appropriate since resolve_fixups runs after all emit. Good change.

💭 Prefix applied in label/emit_branch/emit_jump/emit_jal — Consistent. If any code touches self.emit.labels[name] directly (bypassing these helpers), it won't get the prefix; grep for direct dict access to be sure.


@jizhenjun

Copy link
Copy Markdown
Collaborator

unit case的回归加入ci pipeline

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants