Skip to content

feat(w4): 完整 Qwen3 外置权重与 RISC-V/QEMU 前向验证 - #104

Draft
yuki-328 wants to merge 52 commits into
ScratchV-Compiler:mainfrom
yuki-328:codex/w4-linux-pr
Draft

yuki-328 wants to merge 52 commits into
ScratchV-Compiler:mainfrom
yuki-328:codex/w4-linux-pr

Conversation

@yuki-328

@yuki-328 yuki-328 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

完整 Qwen3 的权重和激活规模超出小图内嵌常量路径。本 PR 增加外置权重、完整地址布局和 RV64 执行链,使固定模型生成不含大权重的 ELF,并在 QEMU 裸机完成一次全量前向与 logits 校验。

范围:FP32、batch=1、L=256、28 层、无 KV cache;Host 为 Ubuntu 24.04 / Bash / Python 3.12,目标为 RV64GC/LP64D、QEMU virt 裸机。

前置依赖

W2 已由 #106 合入,#94 已关闭。仍依赖未合并的 W3 #96,本 PR 保持草稿;对主干的差异包含 W3,不能全部视为 W4 新增内容。

修改内容

  • 代码与权重分离:外置 bundle、流式写入、名称/dtype/shape/偏移绑定与装载检查。
  • 完整代码生成:Tensor-C 外置 ABI、FP32/FMA 策略、超过 4 GiB 的地址布局、分段装载、输出转储与清理。
  • 数值诊断:真实工具 FMA 预检、全量 logits 比较、前缀检查点及保留数组复核。
  • CI:普通 PR 执行单测与真实小图 QEMU;完整前向须手动选择,Nightly 未启用。

本次同步与修正(2026-10-13)

  • 同步最新主干和 W3 的 Summary、文档及验收记录修正,保留 W4 路由与独立完整前向入口。
  • 外置权重运行器与现有张量运行器保持一致:缓存优先级为 SCRATCHV_ZIG_CACHE、ZIG_GLOBAL_CACHE_DIR、项目默认目录;每次构建的本地缓存不变。
  • 缓存修复定向检查 22 passed / 3 deselected,涵盖默认目录、两种变量及优先级;CI 路由、W2/W3/W4 Summary、工作流和文档定向检查 730 passed;119 份文档、223 个 Bash 块通过。
  • 同步 W3 的陈旧 Summary 断言修复,共享及 W4 报告相关 104 项测试通过;没有删减失败或缺失覆盖检查。
  • 本次未重跑完整模型,未改变数值计算和误差门槛。

已有完整验证

完整 W4 Actions Summary,执行提交 6614ad9f71c28555ab6ee1b3a0f7d43055c8ddc9(2026-10-11):

验收项 实测结果
构建、冒烟、数值 28 层,full_seed_0,guest 退出码 0,三项通过
全量 logits 38,895,616 个 FP32 元素;MaxAbs=8.58306884765625e-5 <1e-3,rtol=0,超限 0
不含权重 ELF 11,379,856 bytes,低于 100 MB
QEMU 墙钟 5912.799 秒,约 98.55 分钟
完整流程 6094.752 秒
FMA 与数组复核 12 项 binary32/FCSR 检查及保存数组复核通过

上述为一个输入的完整前向,属于记录的执行提交和环境;不代表当前 HEAD 重新执行、七组 QEMU 覆盖或团队验收。QEMU 墙钟包含启动、计算和输出转储,不是芯片性能指标。

复现入口、跨阶段修复与待验收事项

完整手动入口为 llm-deploy.yml 的 run_w4_validation=true,用例 w4_case=full_seed_0;可复用工作流的 run_full 默认 false。报告和原始数组分别保存,受 artifact 保留期限制;读取作者数组不替代重新构建执行。

既有跨阶段修复包括命名值绑定、Gemm 参数保留、拒绝未支持的 Softmax/Gelu 语义、QEMU 退出处理、虚拟环境解释器保留及报告/执行绑定检查。Tensor-C GEMM 未在本 PR 实现。历史完整结果保留各自执行身份。

待完成:W3 #96 合并并整理最终基线、E1/E3/E5 独立重新构建执行、真实 schedule Nightly、接口与阶段验收。W5 token 循环/文本生成以及 W6 性能门槛不在本 PR 的完成声明中。

Add bounded external weight bundles, full-model memory layout, Tensor-C lowering, segmented bare-metal QEMU loading and strict retained-array verification. Include Linux QMP shutdown and virtualenv launch fixes, regression coverage, CI entry points and public reproduction contracts.

Linux full_seed_0 evidence: all 38,895,616 logits compared, max_abs 8.58306884765625e-5 < 1e-3. W4 regression selection: 1017 passed, one platform-specific skip. Preserve the actual execution SHAs and pending remote validation and independent team acceptance.
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

🤖 AI Code Review

共审查 10 个变更文件
⚠️ 另有 20 个文件超过上限(最多 10 个)未审查

📁 .github/workflows/ci.yml

Review: .github/workflows/ci.yml


🔴 Bug: acceptance breaks on non-W4 PRs — w4 has if: github.event_name == 'pull_request' && needs.scope.outputs.w4 == 'true', so it's skipped when w4 != 'true'. Adding w4 to acceptance.needs means needs.w4.result will be 'skipped', not 'success'. If the acceptance step checks for 'success', every non-W4 PR fails CI.

Fix: Either (a) make w4 always run but no-op when not needed, or (b) update the acceptance logic to treat 'skipped' as acceptable for w4.

🔴 Bug: safetensors imported but not explicitly installed — Line python -c "import torch, yaml, safetensors; ...". safetensors isn't in the pip install command in this step. If it's a transitive dep of qwen3-small-probe.txt, it works by accident. If not, this step crashes.

Fix: Add safetensors to the explicit install, or remove from assertion.

🟡 Brittle version assertion — torch.__version__ == '2.7.1+cpu'. A patch bump to 2.7.2 requires a CI update. Consider a minimum version check instead: assert tuple(int(x) for x in torch.__version__.split('+')[0].split('.')) >= (2, 7, 1).

🟡 Thread env vars set to 1 — Correct for avoiding CI oversubscription, but if these tests include matrix-heavy numeric work, single-thread can be a significant perf regression in the PR check. If the test runtime budget is tight, consider 2.

💭 Confusing naming — Reusable workflow w4-full-numeric.yml called with run_full: false. The full in the filename contradicts the false input. Rename to w4-numeric.yml or add run_full semantics to docs.


📁 .github/workflows/llm-deploy.yml

🔴 Bug: W3 summary doesn't enforce failure on explicit request — Lines ~586–610

w3-execution-summary writes "FAIL" to the step summary but exits 0 when W3 was explicitly requested and failed. W4 summary has the correct guard (raise SystemExit(...), ~line 648). A caller enabling run_w3_validation gets a passing workflow with a FAIL label.

Suggestion: Add the same enforcement pattern as W4:

# After the stream.write block
if result in ("failure", "cancelled"):
    # Only if W3 was explicitly requested — needs a W3_REQUESTED env var
    # like W4 has
    raise SystemExit("Explicit W3 validation must actually run and pass")

Also add W3_REQUESTED: ${{ inputs.run_w3_validation }} to the env block to distinguish "not requested" from "requested and failed".


🟡 Redundant fallback — Line ~72: case: ${{ inputs.w4_case || 'full_seed_0' }}

inputs.w4_case already has default: full_seed_0, so || 'full_seed_0' is dead code. Remove for clarity.


💭 Summary jobs don't depend on llm-deploy — w3-execution-summary / w4-execution-summary only needs their respective validation jobs. They can complete before (or report PASS while) llm-deploy is still running or has failed. If summaries are meant to appear only after the full pipeline settles, add llm-deploy to needs.


💭 Nit — The — em-dash in summary text (W3 preparation and full 28-layer IR numeric.) will render fine in GitHub, but confirm your Python heredoc isn't being locale-mangled by any runner LANG setting that strips non-ASCII.


📁 .github/workflows/w3-full-numeric.yml

🟡 Unpinned actions — actions/checkout@v4 and actions/upload-artifact@v4 use floating tags while setup-python is SHA-pinned. A compromised upstream tag or a breaking change in the next minor release of the action would affect this workflow unexpectedly. Pin all three by SHA for consistency.

🟡 No step-level timeouts — The job has timeout-minutes: 60, but individual steps don't. pip install or acquire_model could hang indefinitely, silently consuming the entire budget and masking the actual failure. Add timeout-minutes to the install and download steps at minimum (e.g., 10 for install, 15 for download).

💭 workflow_call input type mismatch — workflow_dispatch defines case as choice, but workflow_call defines it as string. This works because the validation step rejects invalid values, but it gives a misleading API surface to callers. Use string for both (and let the validation step enforce the allowed set) for a consistent contract.

💭 Redundant default in validation — Line case "${SELECTED_CASE:-all}" is safe but redundant since SELECTED_CASE is already guaranteed non-empty by ${{ inputs.case || 'all' }}. Minor, but it obscures the actual invariant (env var is always set).


📁 .github/workflows/w3-nightly.yml

Code Review: .github/workflows/w3-nightly.yml

🟡 workflow_call silently no-ops — When invoked via workflow_call, github.event_name is 'workflow_call', so the if on all three jobs falls through to vars.W3_NIGHTLY_ENABLED == 'true'. If the calling workflow doesn't know about this dependency, every job skips silently with no error signal. Consider adding a dedicated gate job that fails with a clear message when the variable is missing, or documenting the requirement in the workflow_call inputs.

🟡 No timeout-minutes on preparation and full-numeric — Only acceptance has a 5-minute timeout. If a reusable workflow hangs, the entire nightly run is blocked indefinitely. Even a generous upper bound (e.g., timeout-minutes: 120) would prevent zombie runs from consuming runner slots.

💭 cancel-in-progress: false + schedule — Two back-to-back runs on the same branch will queue rather than cancel. Harmless today, but if a future refactor makes runs take >24h, nightly and manual dispatch runs could overlap and race on shared resources (e.g., W3_NIGHTLY_ENABLED mutations, downstream state). Worth a comment if the intent is explicit.

The rest is solid — the always() on acceptance ensures the summary step always executes when the gates are enabled (showing which gate failed), the printf usage is safe from injection ($GITHUB_SHA is a fixed SHA, needs.*.result is an enum), and the cron expression correctly maps to 03:23 Asia/Shanghai.


📁 .github/workflows/w3-preparation.yml

🟡 PROBE_OUTCOME 设为环境变量但未被使用 — 第 95 行:
PROBE_OUTCOME: ${{ steps.probe.outcome }} 设置了环境变量,但 scripts/summarize_w3_ci.py 的调用未传递该值,也未读取该环境变量。要么删掉这个 env 块,要么让 summarizer 消费它。

🟡 SCRATCHV_CC 路径解析脆弱 — 第 56 行:
Path(ziglang.__file__).parent / "zig" 假设 ziglang 包内部布局固定。如果包结构变化,路径会静默失效(写入 $GITHUB_ENV 一个错误的值,后续步骤才会报找不到 zig)。建议加一次 test -x 校验,或者用 shutil.which("zig") / 包官方 API 获取。

🟡 apt-get update 无重试 — 第 47 行:
CI 环境中 APT 偶发超时是已知问题。建议:

for i in 1 2 3; do sudo apt-get update && break || sleep 5; done
sudo apt-get install ...

🟡 无 pip 缓存 — 60 分钟超时窗口下,每次都重装 torch==2.7.1(~200MB)浪费显著。actions/setup-python 支持 cache: pip:

- uses: actions/setup-python@...
  with:
    python-version: "3.12"
    cache: pip

或在步骤前加 actions/cache 缓存 ~/.cache/pip。

💭 测试 glob 无覆盖率守卫 — 第 60 行:
如果某个文件被重命名(如 test_w3_foo.py → test_w3_new.py),glob 静默少跑测试且不报错。可在 pytest 后加一行检查实际收集数:

python -m pytest ... --co -q | tail -1  # 输出 "N tests collected"

或用一个 --ignore-glob 黑名单 + tests/test_w3/ 目录代替通配符列表。


📁 .github/workflows/w4-full-numeric.yml

Code Review: .github/workflows/w4-full-numeric.yml

🔴 No integrity verification of downloaded model assets — Lines ~245–250: acquire_model() downloads ONNX weights, but no hash/signature is checked before execution. The full_gate step verifies source code hashes against the checkout, but the weight data itself is unauthenticated. A compromised CDN/registry could serve tampered weights that pass all subsequent checks. evidence-retention.json acknowledges this gap ("Manifest hashes alone do not authenticate model provenance") but provides no mitigation — consider embedding expected hashes or fetching from a GPG-signed source.

🟡 Brittle hardcoded parameterized test names — Lines ~88–99: The JUnit XML gate asserts exact test IDs like test_blocked_fma_host_and_rv64_are_bitwise_equal[left2-right2]. Any test refactoring (renaming, re-parameterizing) silently breaks this gate with a generic "must execute and pass" message. Extract these names to a shared config or use substring/prefix matching with a clear error showing what was expected vs. found.

🟡 Case whitelist is duplicated with no single source of truth — Lines ~218: allowed = {"full_seed_0", "full_seed_42", ...} is hardcoded in a Python heredoc. Adding a case requires editing this workflow YAML independently of the probe code that defines the cases. Consolidate into a shared constant (e.g., a Python module or JSON config) imported by both the workflow and the probe.

🟡 Inconsistent artifact fallback labels — Line ~130 uses inputs.case || 'pr' for unit artifacts; lines ~370/~390 use inputs.case || 'full_seed_0' for full artifacts. Since inputs.case defaults to full_seed_0, the || fallback never triggers for full artifacts, but the mismatch between 'pr' and 'full_seed_0' is confusing when searching GitHub Actions artifact lists.

💭 Static evidence-retention.json in heredoc — Lines ~340–358: Retention policies (30 days, 3 days) and scope text are hardcoded in a bash heredoc. If the audit or summarize scripts already compute these values, generating the JSON from them would prevent drift. If intentionally static, a brief comment noting why would help future maintainers.


Overall: This is a very thorough workflow — resource preflight, source hash verification against the checkout, git-clean enforcement, FMA conformance gating, and explicit scope disclaimers are all well-designed. The issues above are about hardening and maintainability, not fundamental design flaws.


📁 .github/workflows/w4-nightly.yml

🔴 Bug: Artifact upload guaranteed failure on Python step crash — Lines 76–83

If the Python script exits before writing status.json (e.g. SystemExit, KeyError, or any unexpected exception), the upload step's if: always() still fires, and if-no-files-found: error causes a secondary failure with no artifact.

# Suggestion: guard the upload or pre-create the file
      - name: Upload nightly status even on failure
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: ...
          if-no-files-found: warn          # <-- 'warn' not 'error'
          retention-days: 30
          path: output/w4-nightly/status.json

Or better, pre-create a stub before the Python step:

mkdir -p output/w4-nightly
echo '{"status":"UNKNOWN"}' > output/w4-nightly/status.json   # fallback
python3 - <<'PY'   # overwrites on success

🟡 Suggestion: workflow_call accepts unvalidated case — Lines 14–18

workflow_dispatch constrains case to a choice list, but workflow_call is type: string — any arbitrary value (typos, malicious strings) flows into the downstream workflow with no gate here.

# Suggestion: add an early validation step in nightly-status
      - name: Validate selected case
        run: |
          python3 - <<'PY'
          import os, sys
          valid = {"full_seed_0", "full_seed_42", "one_token", "short_17",
                   "short_255", "changed_future", "changed_padding"}
          case = os.environ["SELECTED_CASE"]
          if case not in valid:
              sys.exit(f"Unknown case '{case}'; valid: {sorted(valid)}")
          PY

This catches typos from calling workflows before hitting the downstream workflow, giving a clear error.


💭 Nit: Failure message undercounts excluded results — Line 72

raise SystemExit("Enabled nightly requires successful full execution; "
                 "missing, cancelled and skipped gates fail")

GitHub Actions also returns "timed_out" as a result value. If a job exceeds timeout-minutes (the reusable workflow likely has its own timeout), this string isn't mentioned. Consider:

raise SystemExit(
    f"Enabled nightly requires successful full execution; "
    f"got '{result}' — cancelled, skipped, timed_out, and other non-success results fail"
)

💭 Nit: Cron timezone comment could mislead — Line 23

# 04:43 Asia/Shanghai. No expensive job runs without repository opt-in.

GitHub Actions cron is always UTC. A reader skimming might think GitHub supports timezone-aware cron. Consider:

# 20:43 UTC = 04:43 Asia/Shanghai (UTC+8). No expensive job runs without repository opt-in.

No other issues — permissions are minimal (contents: read), the Python heredoc avoids shell injection by using env vars, concurrency is correctly scoped, and the disabled-skip logic is sound.


📁 LICENSES/Eigen-MPL-2.0.txt

🟡 License Attribution Completeness — Adding Eigen-MPL-2.0.txt without corresponding NOTICE or attribution in a top-level THIRD_PARTY_NOTICES / NOTICE file may violate MPL 2.0 §3.4 (must not remove license notices). Ensure the project-level third-party notice file references this license and the Eigen source locations.

🟡 File Name Convention — Consider whether the filename should be MPL-2.0 (per SPDX / OSI standard naming) or include the project (Eigen) in a more discoverable location like THIRD_PARTY_LICENSES/ rather than a non-standard LICENSES/ directory. Verify this matches your project's existing convention for other dependency licenses.

🟡 Verify Actual Usage — Confirm Eigen is actually used in the codebase and that this specific file (MPL 2.0) is the correct license for the Eigen files being used. Eigen is historically MPL 2.0 but newer versions (3.4+) moved to MPL 2.0 still — confirm the bundled version matches the license included.

💭 No changelog / commit message context — Adding a license file is low-risk, but ensure the commit message references which dependency and version this covers for auditability.


📁 THIRD_PARTY_NOTICES.txt

🟡 Missing SPDX identifiers — Lines 7, 12: License names are free-text only. Suggestion: Add SPDX IDs (e.g., MPL-2.0, MIT) alongside prose names so tooling like reuse or oss-check can parse them automatically.

🟡 License file existence not verified — Line 8 references LICENSES/Eigen-MPL-2.0.txt. Suggestion: Confirm this file exists in the repo tree; a broken path in a license file is harder to catch than a broken import.

🟡 ONNX Runtime reference is imprecise — Line 11–12: "v1.22.1 (MLAS)" without a commit hash or permalink. Unlike Eigen, there's no reproducibility anchor — if someone wants to verify provenance, they have to guess which tag or branch. Suggestion: Include a commit hash or specific file path in the MLAS source (e.g., cpu/mlas/src/mmla_sse2.c).

💭 Header paragraph assumes reader context — Line 1–3: "Package metadata lists both applicable licenses" — a reader encountering this file standalone won't know which metadata field. Suggestion: Name the field (e.g., project.license in pyproject.toml).

💭 No notice for other potential third-party deps — If fp32_eigen.py or fp32_reference.py pull in anything else transitively (numpy shims, header-only helpers), this file may be incomplete. Worth a quick grep for import / #include in those two files.


📁 benchmarks/bench_onnx_operators.py

🔴 Bug: Bare repository unsupported — If ROOT points to a bare Git repo (e.g., cloned with --bare), the .git subdirectory won't exist (the checkout is the git dir), so this check would falsely reject a valid checkout.

Suggestion: Check for the presence of .git (directory or file, for worktrees/submodules) or a HEAD file at ROOT:

if not ((ROOT / ".git").exists() or (ROOT / "HEAD").exists()):

🟡 Suggestion: Message assumes archive origin — The error message says "an extracted source archive has no Git history," but the real cause could be other (e.g., repo cloned with --depth 1 then .git accidentally removed, or wrong ROOT). Consider wording that states the requirement without speculating on the cause:

"Historical baseline comparison requires a Git checkout at {ROOT}"

(the second half of the current message already says this, so just drop the archive-specific explanation or make it a parenthetical).

💭 Nit: Consider Path.is_dir() vs exists() — a stray file named .git (not a directory) would pass exists() but fail git operations downstream. Minor, but is_dir() is more precise here.



⚠️ 未审查的文件

  • docs/llm-deploy-v1.0/Linux实跑报告-2026-10-09.md
  • docs/llm-deploy-v1.0/W1/W1-报告可靠性修复.md
  • docs/llm-deploy-v1.0/W1/interfaces.md
  • docs/llm-deploy-v1.0/W3/Linux-CI与Nightly.md
  • docs/llm-deploy-v1.0/W3/README.md
  • docs/llm-deploy-v1.0/W3/W3-完整模型执行与数值诊断报告.md
  • docs/llm-deploy-v1.0/W3/W3-工作总结与验收进度.md
  • docs/llm-deploy-v1.0/W3/W3-预备工作本地验收报告.md
  • docs/llm-deploy-v1.0/W3/独立复现与验收清单.md
  • docs/llm-deploy-v1.0/W4/README.md
  • docs/llm-deploy-v1.0/W4/evolution.md
  • docs/llm-deploy-v1.0/W4/reproduction.md
  • docs/llm-deploy-v1.0/W4/runtime-contract.md
  • docs/llm-deploy-v1.0/W4/validation.md
  • docs/llm-deploy-v1.0/evidence/linux-2026-10-09.json
  • docs/llm-deploy-v1.0/开发计划.md
  • probes/w1_qwen3_export/run.py
  • probes/w2_qwen3_parse/README.md
  • probes/w2_qwen3_small/README.md
  • probes/w2_qwen3_small/model.py

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.

1 participant