From 36822fe211dc670225ebf81b26de4f0859e69f3c Mon Sep 17 00:00:00 2001 From: eric8810 Date: Sun, 19 Jul 2026 19:33:07 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore(release):=20=E5=8F=8C=E5=A2=83?= =?UTF-8?q?=E9=94=81=E5=8D=B0=EF=BC=8C=E4=B8=89=E7=89=88=E5=90=AF=E8=88=AA?= =?UTF-8?q?=20=C2=B7=20prepare=200.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bind the reviewed Linux and Windows WebGPU evidence to the production lock. Build the qualified WebGPU runtime in npm release jobs and keep package smoke tests portable across headless runners. Document the opened release gate and cover pending and production qualification states. --- .github/workflows/npm-release.yml | 28 ++++++- CHANGELOG.md | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- docs/build-and-release.md | 6 +- docs/implementation-status.md | 4 +- docs/linux-device-acceleration.md | 10 +-- docs/npm-packaging.md | 2 +- docs/windows-device-acceleration.md | 2 +- tests/python/test_npm_release.py | 31 ++++++-- tests/python/test_webgpu_report_review.py | 89 +++++++++++++++++++---- tests/python/test_webgpu_runtime.py | 39 ++++++---- tools/npm/smoke.cjs | 54 ++++++++++---- tools/webgpu/README.md | 29 ++++---- tools/webgpu/review_reports.py | 74 +++++++++++++++++-- tools/webgpu/runtime-lock.json | 21 +++--- 16 files changed, 298 insertions(+), 97 deletions(-) diff --git a/.github/workflows/npm-release.yml b/.github/workflows/npm-release.yml index 74be760..4830d3c 100644 --- a/.github/workflows/npm-release.yml +++ b/.github/workflows/npm-release.yml @@ -6,7 +6,7 @@ on: version: description: Lockstep version for all six packages required: true - default: 0.2.1 + default: 0.3.0 type: string publish_to_registry: description: Publish the fully gated candidate to npm after preflight @@ -44,6 +44,9 @@ jobs: test -f contracts/tiled-platform-baselines.json test -f contracts/apple-provider-baselines.json fi + python tools/webgpu/review_reports.py \ + --reports-root reports/webgpu-qualification \ + --output "$RUNNER_TEMP/webgpu-release-evidence.json" build-native: needs: validate @@ -55,15 +58,19 @@ jobs: - id: macos-arm64 runner: macos-15 executable_suffix: "" + runtime_flavor: cpu - id: macos-x64 runner: macos-15-intel executable_suffix: "" + runtime_flavor: cpu - id: linux-x64 runner: ubuntu-24.04 executable_suffix: "" + runtime_flavor: webgpu - id: windows-x64 runner: windows-2022 executable_suffix: ".exe" + runtime_flavor: webgpu runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 @@ -82,6 +89,16 @@ jobs: python tools/bootstrap_dependencies.py --cache-dir .cache/dependencies --offline python tools/bootstrap_models.py --cache-dir .cache/models python tools/package_model_bundle.py + - name: Assemble and revalidate the production WebGPU SDK + if: matrix.runtime_flavor == 'webgpu' + shell: bash + run: | + python tools/webgpu/build_runtime.py \ + --platform "${{ matrix.id }}" \ + --package-cache .cache/webgpu-packages \ + --output-dir "dist/webgpu-sdk/${{ matrix.id }}" + python tools/webgpu/build_runtime.py \ + --validate-sdk "dist/webgpu-sdk/${{ matrix.id }}" - name: Install verified Node development files (Unix) if: runner.os != 'Windows' shell: bash @@ -116,6 +133,8 @@ jobs: cmake -S . -B build-npm -G Ninja -DCMAKE_BUILD_TYPE=Release -DLIGHT_OCR_DEPENDENCY_CACHE_DIR="$PWD/.cache/dependencies" + -DLIGHT_OCR_ONNXRUNTIME_FLAVOR=${{ matrix.runtime_flavor }} + -DLIGHT_OCR_WEBGPU_SDK_DIR="$PWD/dist/webgpu-sdk/${{ matrix.id }}" -DLIGHT_OCR_BUILD_NODE=ON -DLIGHT_OCR_BUILD_TESTS=ON -DLIGHT_OCR_BUILD_TOOLS=ON @@ -129,6 +148,8 @@ jobs: run: >- cmake -S . -B build-npm -G "Visual Studio 17 2022" -A x64 "-DLIGHT_OCR_DEPENDENCY_CACHE_DIR=$env:GITHUB_WORKSPACE/.cache/dependencies" + -DLIGHT_OCR_ONNXRUNTIME_FLAVOR=${{ matrix.runtime_flavor }} + "-DLIGHT_OCR_WEBGPU_SDK_DIR=$env:GITHUB_WORKSPACE/dist/webgpu-sdk/${{ matrix.id }}" -DLIGHT_OCR_BUILD_NODE=ON -DLIGHT_OCR_BUILD_TESTS=ON -DLIGHT_OCR_BUILD_TOOLS=ON @@ -167,7 +188,10 @@ jobs: --build-dir build-npm \ --configuration Release \ --metadata-dir "reports/npm/${{ matrix.id }}" \ - --output-dir "dist/native-input/${{ matrix.id }}" + --output-dir "dist/native-input/${{ matrix.id }}" \ + --runtime-flavor "${{ matrix.runtime_flavor }}" \ + --webgpu-artifact-manifest \ + "dist/webgpu-sdk/${{ matrix.id }}/artifact-manifest.json" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: native-${{ matrix.id }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a4023..720eb2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ This file records user-visible changes to `light-ocr`. Published artifact detail - Heavy model conversion, Compute Plan placement, performance, cache, and lifecycle qualification remain local real-device work. Ordinary CI stays limited to cross-platform builds, contracts, and lightweight tests and does not require paid runners. - The Core ML provider is merged on `main` but is not included in the published `0.2.0` npm packages. The planned `0.3.0` distribution keeps the existing six-package installation shape. - Native WebGPU compatibility and performance are evidenced on the named NVIDIA/Linux and AMD/Windows systems. Other devices may use the open compatibility path but do not inherit these performance numbers. -- The Linux and Windows qualification reports both passed 164/164 mechanical Gates. Production release staging remains closed until the reviewed report and artifact hashes are bound into the runtime lock. +- The Linux and Windows qualification reports both passed 164/164 mechanical Gates. Their reviewed report and artifact-set hashes are bound into the production runtime lock, so ordinary `0.3.0` release staging now accepts the exact qualified payloads. Full evidence and methodology: [Apple device acceleration](docs/apple-device-acceleration.md), [Linux device acceleration](docs/linux-device-acceleration.md), [Windows device acceleration](docs/windows-device-acceleration.md), [implementation status](docs/implementation-status.md), the accepted Apple baseline [`apple-fp16-mixed-20260715.2`](contracts/apple-provider-baselines.json), and the checked-in WebGPU qualification reports. diff --git a/README.md b/README.md index b7a8c08..90186e7 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ The npm distribution installs one facade, one required model package, and the na Direct Core ML acceleration is merged on `main` for the `0.3.0` candidate but is not part of the published `0.2.0` package set. Its release keeps the same six-package installation shape; no extra provider package or runtime download is planned. -PR #11 also carries the Linux x64 and Windows x64 Native WebGPU source candidate. Explicit WebGPU accepts `auto`/`fp32`; Auto also selects FP32. The three required CPU-partition operators are reported and bounded. Both real-device reports passed 164/164 Gates; release packaging remains gated until their immutable hashes are bound into the production lock. Published `0.2.0` packages remain unchanged and CPU-only on those platforms. +PR #11 also carries the Linux x64 and Windows x64 Native WebGPU source candidate. Explicit WebGPU accepts `auto`/`fp32`; Auto also selects FP32. The three required CPU-partition operators are reported and bounded. Both real-device reports passed 164/164 Gates, and their immutable report/artifact hashes are now bound into the production lock for the `0.3.0` release workflow. Published `0.2.0` packages remain unchanged and CPU-only on those platforms. ## Project status diff --git a/README.zh-CN.md b/README.zh-CN.md index 7efad93..af3c145 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -229,7 +229,7 @@ npm 分发会安装一个统一入口、一个必需的模型包,以及与当 Direct Core ML 加速已经合并到 `main`,目标版本为 `0.3.0`,但尚未进入已发布的 `0.2.0` package set。它会继续复用现有六包安装结构,不计划新增 provider package 或运行时下载。 -PR #11 同时包含 Linux x64 与 Windows x64 Native WebGPU 源码候选。显式 WebGPU 接受 `auto/fp32`,Auto 同样选择 FP32;三个必要 CPU partition 算子会被显式报告并限制范围。两份真机报告均已通过 164/164 Gate,release package 仍需先把报告与产物的不可变哈希绑定进 production lock。已发布的 `0.2.0` packages 保持不变,并在这两个平台继续仅使用 CPU。 +PR #11 同时包含 Linux x64 与 Windows x64 Native WebGPU 源码候选。显式 WebGPU 接受 `auto/fp32`,Auto 同样选择 FP32;三个必要 CPU partition 算子会被显式报告并限制范围。两份真机报告均已通过 164/164 Gate,其报告与产物的不可变哈希现已绑定进 production lock,供 `0.3.0` 发布流程使用。已发布的 `0.2.0` packages 保持不变,并在这两个平台继续仅使用 CPU。 ## 项目状态 diff --git a/docs/build-and-release.md b/docs/build-and-release.md index 8391668..9807099 100644 --- a/docs/build-and-release.md +++ b/docs/build-and-release.md @@ -118,7 +118,7 @@ cmake -S . -B build-webgpu -G Ninja \ -DLIGHT_OCR_WEBGPU_QUALIFICATION_BUILD=ON ``` -pending lock 只能用于 qualification build;普通 release configure 要求双平台 Provider Gate 已接受,且 lock 中本平台 `qualifiedArtifactSetSha256` 与 SDK 完全一致。真实 Linux/Windows GPU 的完整构建、npm staging、14-fixture placement/质量/性能/生命周期和报告回收统一执行: +pending lock 只能用于 qualification build;当前 production lock 已接受双平台 Provider Gate,普通 release configure 要求 lock 中本平台 `qualifiedArtifactSetSha256` 与 SDK 完全一致。真实 Linux/Windows GPU 的完整构建、npm staging、14-fixture placement/质量/性能/生命周期和报告回收统一执行: ```bash python3 tools/webgpu/qualify.py @@ -226,11 +226,11 @@ macOS arm64 高分辨率绝对 RSS gates 由 `light_ocr_memory_gate` 独立进 - 六个 tarball 先发布到一次性 Verdaccio registry,只安装 facade 后停止 registry,再执行真实 bounded 与 tiled OCR,证明没有运行时下载依赖。 - 只有以上功能/制品 gates、需要时已经单独完成的受审 baseline,以及 `publish_to_registry=true` 同时满足时,`npm-release` GitHub environment 才能读取 `NPM_TOKEN`;先发布五个依赖到 `next`,通过 registry facade 安装后再发布主包,最终禁网运行并可显式提升到 `latest`。 -首个版本的触发命令为: +`0.3.0` 发布前 dry-run 的触发命令为: ```bash gh workflow run "npm release" --ref main \ - -f version=0.2.0 \ + -f version=0.3.0 \ -f publish_to_registry=false \ -f promote_latest=false ``` diff --git a/docs/implementation-status.md b/docs/implementation-status.md index d27d984..7172565 100644 --- a/docs/implementation-status.md +++ b/docs/implementation-status.md @@ -1,7 +1,7 @@ # C++ Core 与 Node-API 实施状态 更新时间:2026-07-19
-结论:`@arcships/light-ocr@0.2.0` 已发布并提升为 npm `latest`。当前 0.3.0 源码候选已实现 Direct Core ML Apple provider,以及 Linux x64 glibc/Windows x64 official Native WebGPU Plugin EP 的产品 runtime、D112 Auto、自包含 npm payload 与资格工具。Apple M4 已有审阅证据;WebGPU 的 Linux/Windows 真实设备报告均为 164/164 Gate 通过,production lock 仍需在发布前绑定已审阅报告和产物哈希。 +结论:`@arcships/light-ocr@0.2.0` 已发布并提升为 npm `latest`。当前 0.3.0 发布候选已实现 Direct Core ML Apple provider,以及 Linux x64 glibc/Windows x64 official Native WebGPU Plugin EP 的产品 runtime、D112 Auto、自包含 npm payload 与资格工具。Apple M4 已有审阅证据;WebGPU 的 Linux/Windows 真实设备报告均为 164/164 Gate 通过,已审阅报告和产物哈希已绑定 production lock。 状态含义: @@ -26,7 +26,7 @@ | manifest、hash、licenses、SBOM、parity、benchmark | Done | Release commit 已重新生成并保存四平台 metadata、六个 npm tarballs 的 hashes/integrity、parity、quality 与 benchmark 证据。 | | N-API/npm 非本 Core milestone | Done / `0.2.0` published | raw Node-API v8、CJS/ESM、`.d.ts`、内置模型解析、四平台 prebuild、双重背压、AbortSignal 与生命周期均已完成;[npm release run 29340467784](https://github.com/arcships/light-ocr/actions/runs/29340467784) 与 [promotion run 29342178842](https://github.com/arcships/light-ocr/actions/runs/29342178842) 保存六包发布、registry 和禁网证据。 | | Perf-1A / Apple execution | Done locally / open macOS | provider-neutral `InferenceSession` 已加入 Objective-C++ Direct Core ML;公开 union 与 D112 Auto 创建状态机已接线。detector 使用 FP16 range model,recognizer 使用 91-function FP16 MLProgram 和 20 个加权宽度桶;Apple Silicon interactive 为 ANE + 宽文本 GPU,strict 为 GPU,Intel 为 CPU+GPU。schema 1.1 provider contract 使用 `open-macos`、arm64/x86_64、`validatedDeviceFamilies` 和 `deviceValidated`;显式 provider 严格失败,只有 Auto 可按 typed reason 在创建期继续。哈希锁模型、离线编译缓存、跨进程锁、LRU≤20 与 Node 映射均已完成。M4 有正式证据,其他 Mac 直接开放实验兼容。 | -| Perf-2 / Native WebGPU | Implemented / two device Gates passed | Linux x64 glibc/Vulkan 与 Windows x64/D3D12 使用 official ORT Core 1.24.4 + WebGPU Plugin EP 0.1.0。NuGet bytes/SHA-512、headers、runtime/plugin/companions、license 和 session options 已锁定;assembler 支持在线取得、离线复装和 exact SDK 校验。C++/Node plugin registration、D112 `webgpu → cpu`、typed/fatal failure、FP32 allow/strict、真实 provider chain、profiling、schema 2 descriptor、sterile loader、self-contained npm staging、license/SBOM 和双平台 CI 已实现。Linux RTX 5060 Ti/Vulkan 与 Windows Radeon 780M/D3D12 报告均为 164/164 Gate 通过、14/14 FP32 字节级质量对齐;聚合 P50 分别加速 5.698× 与 2.436×。Release configure 继续 fail closed,直到已审阅 report/artifact hashes 绑定进 production lock。 | +| Perf-2 / Native WebGPU | Production-qualified / two device Gates passed | Linux x64 glibc/Vulkan 与 Windows x64/D3D12 使用 official ORT Core 1.24.4 + WebGPU Plugin EP 0.1.0。NuGet bytes/SHA-512、headers、runtime/plugin/companions、license 和 session options 已锁定;assembler 支持在线取得、离线复装和 exact SDK 校验。C++/Node plugin registration、D112 `webgpu → cpu`、typed/fatal failure、FP32 allow/strict、真实 provider chain、profiling、schema 2 descriptor、sterile loader、self-contained npm staging、license/SBOM 和双平台 CI 已实现。Linux RTX 5060 Ti/Vulkan 与 Windows Radeon 780M/D3D12 报告均为 164/164 Gate 通过、14/14 FP32 字节级质量对齐;聚合 P50 分别加速 5.698× 与 2.436×。已审阅 report/artifact hashes 已绑定 production lock,release configure 仅接受精确匹配的 SDK。 | | Node.js JPEG/PNG 内存输入 | Done / `0.2.0` published | `recognizeEncoded(Uint8Array)` 在 engine worker 上使用固定 stb revision 解码,保持 Core raw-pixel 边界;格式、尺寸、pixels、临时内存、queue/snapshot budget、AbortSignal 与 `timingUs.decode` 均有四平台 Node 22/24 package 测试。 | | 高分辨率峰值内存 | Done | Release 原生独立进程本机参考:2048² 空白 `318.8 MiB ≤ 384 MiB`;xfund 密集表单 116 框 `400.5 MiB ≤ 640 MiB`。四平台 release jobs 的真实模型与 RSS gates 均通过。 | | Tiled 高分辨率准确模式 | Done / `0.2.0` published | 1280 tile、2048→4-pass row-major、全局 candidate ceiling、IoU/IOS greedy merge、原图 recognition、C++/Node contract、8-fixture/196-line corpus、独立 oracle、四平台 36-entry accepted baseline 与 package smoke 均已完成。 | diff --git a/docs/linux-device-acceleration.md b/docs/linux-device-acceleration.md index 0847621..db0c20d 100644 --- a/docs/linux-device-acceleration.md +++ b/docs/linux-device-acceleration.md @@ -1,6 +1,6 @@ # Linux Device 加速技术方案 -状态:Linux x64 glibc/Vulkan 与 Windows x64/D3D12 产品实现和真实设备 Provider Gate 已完成;两平台均为 164/164 Gate 通过,production lock 待绑定已审阅报告和产物哈希 +状态:Linux x64 glibc/Vulkan 与 Windows x64/D3D12 产品实现和真实设备 Provider Gate 已完成;两平台均为 164/164 Gate 通过,已审阅报告和产物哈希已绑定 production lock 更新时间:2026-07-19 @@ -18,7 +18,7 @@ Linux 加速可复用当前 ONNX 模型和 backend-neutral `InferenceSession` - **CPU 是稳定最终候选和显式 backend。** WebGPU 不是 NPU API,也不是“任何机器都更快”的全平台兜底。 - **兼容层由上游实现,产品兼容由本项目负责。** ONNX Runtime WebGPU EP 提供算子 kernels,Dawn/Tint 映射 Vulkan/D3D12/Metal;`light-ocr` 仍负责模型覆盖、驱动矩阵、包体、质量、性能、资源和 D112 Auto 契约。 -当前源码已完成 official plugin runtime、C++/Node provider 接线、D112 Auto、schema 2 runtime descriptor、自包含 npm staging、许可证/SBOM、离线复装、跨平台编译测试与双平台真实设备 Gate。显式 FP32 allow 证明 placement 与质量,strict 证明 bounded CPU partition 可以稳定 fail-closed,Auto 与直接 C++ 路径证明产品选择。当前 lock 仍保持 `development-pending-device-validation`,普通 release configure 和 npm release staging 会继续拒绝 WebGPU,直到发布审查把两份报告和对应 artifact set 的不可变哈希写入 production lock。 +当前源码已完成 official plugin runtime、C++/Node provider 接线、D112 Auto、schema 2 runtime descriptor、自包含 npm staging、许可证/SBOM、离线复装、跨平台编译测试与双平台真实设备 Gate。显式 FP32 allow 证明 placement 与质量,strict 证明 bounded CPU partition 可以稳定 fail-closed,Auto 与直接 C++ 路径证明产品选择。当前 lock 为 `production-qualified`,并精确绑定两份已审阅报告及对应 artifact set 的不可变哈希;普通 release configure 和 npm release staging 只接受这些锁定的 WebGPU payload。 ### 1.1 `0.3.0` 真实设备结论 @@ -65,7 +65,7 @@ Linux Native WebGPU 要求宿主提供可访问的 `/dev/dri/renderD*` DRM rende | 状态 | provider/backend | 能力边界 | | --- | --- | --- | | npm `0.2.0` 已发布 | `cpu` | 四个 native platform packages 只携带 CPU runtime;这是当前用户可安装的稳定能力 | -| `0.3.0` 源码候选 | `auto | cpu | apple | webgpu` | `apple` 保持 macOS Direct Core ML;Linux/Windows WebGPU FP32 产品实现、qualification package、CI 与双平台真机 Gate 已完成,production release 只待绑定报告和产物哈希 | +| `0.3.0` 发布候选 | `auto | cpu | apple | webgpu` | `apple` 保持 macOS Direct Core ML;Linux/Windows WebGPU FP32 产品实现、qualification package、CI 与双平台真机 Gate 已完成,报告和产物哈希已绑定 production lock | | 后续技术候选 | `cuda`、`openvino`、`migraphx` 等 | 尚未实现;只有 WebGPU 的真机结论不足或用户加权收益证明值得时才启动独立 Gate | - CPU/macOS release flavor 继续固定 ONNX Runtime `1.22.0`;WebGPU flavor 精确固定 ORT Core `1.24.4` + WebGPU Plugin EP `0.1.0`,不同 flavor 不在同一进程混载。 @@ -251,9 +251,9 @@ Registry 必须: 当前采用 platform runtime flavor:Linux/Windows WebGPU package 使用 ORT 1.24.4 plugin ABI,CPU/macOS flavor 保留 ORT 1.22.0 或 Direct Core ML;facade 一次只加载当前平台 package,进程内不混载两个 ORT ABI。未来统一版本仍需单独评估,不能借本次实现删除 macOS x64 Tier 1。 -真实设备 Gate 后的 release 决策仍可在以下结果中选择: +真实设备 Gate 后的 release 决策已经选择第一项: -1. 接受当前各自精确锁定的 platform runtime flavor,并把 WebGPU artifact/report hashes 写入 production lock; +1. **已接受:**采用当前各自精确锁定的 platform runtime flavor,并把 WebGPU artifact/report hashes 写入 production lock; 2. 缩减 WebGPU 的平台、driver、设备或 partition 范围,保持 qualification-only; 3. 如果质量、严格 placement、性能、内存或维护成本失败,拒绝 WebGPU 发布并保持 CPU/Direct Core ML 路线。 diff --git a/docs/npm-packaging.md b/docs/npm-packaging.md index 9f21e9b..14221a3 100644 --- a/docs/npm-packaging.md +++ b/docs/npm-packaging.md @@ -184,7 +184,7 @@ Facade 只按固定映射加载 package: ### 5.1 硬件加速的分发约束 -当前源码已经为 Linux x64 glibc 与 Windows x64 实现 Native WebGPU qualification payload:Linux package 自带 ORT Core 1.24.4 与 official WebGPU plugin 0.1.0;Windows 还自带 plugin 所需的 `dxcompiler.dll`、`dxil.dll`。schema 2 runtime descriptor 从实际 staging 文件生成,逐文件记录 bytes/SHA-256、provider library、ORT/plugin ABI、qualification identity 与 `webgpu → cpu` Auto policy;共享 loader 拒绝缺失、额外、hash 漂移或 symlink payload,并从 sterile cwd 加载。当前 lock 仍为 `development-pending-device-validation`,因此这些制品只允许 `--qualification-build` staging,不能进入普通 npm release。 +当前源码已经为 Linux x64 glibc 与 Windows x64 实现 production-qualified Native WebGPU payload:Linux package 自带 ORT Core 1.24.4 与 official WebGPU plugin 0.1.0;Windows 还自带 plugin 所需的 `dxcompiler.dll`、`dxil.dll`。schema 2 runtime descriptor 从实际 staging 文件生成,逐文件记录 bytes/SHA-256、provider library、ORT/plugin ABI、qualification identity 与 `webgpu → cpu` Auto policy;共享 loader 拒绝缺失、额外、hash 漂移或 symlink payload,并从 sterile cwd 加载。production lock 已绑定两平台的已审阅 report/artifact hashes,普通 npm release 只允许精确匹配的 payload staging。 后续 CoreML、DirectML、OpenVINO、TensorRT、VitisAI、QNN 或其他 provider 也不得改变本节的用户契约:正常用户仍只运行 `npm install @arcships/light-ocr`,不能被要求另装或配置 ONNX Runtime、Windows ML framework runtime、CUDA、TensorRT、OpenVINO、Ryzen AI/VitisAI、QNN SDK、Python 或编译工具链。正常操作系统和硬件 driver 是唯一允许的系统前置条件;Windows official runtime 依赖的 Microsoft Visual C++ 2015-2022 x64 系统 runtime 需作为平台前置条件明确说明。 diff --git a/docs/windows-device-acceleration.md b/docs/windows-device-acceleration.md index 4045a29..6b78d7a 100644 --- a/docs/windows-device-acceleration.md +++ b/docs/windows-device-acceleration.md @@ -1,6 +1,6 @@ # Windows Device 加速技术方案 -状态:Windows x64/D3D12 产品实现与 AMD Radeon 780M 真实设备 Provider Gate 已完成(164/164);production lock 待绑定已审阅报告和产物哈希 +状态:Windows x64/D3D12 产品实现与 AMD Radeon 780M 真实设备 Provider Gate 已完成(164/164);已审阅报告和产物哈希已绑定 production lock 更新时间:2026-07-19 diff --git a/tests/python/test_npm_release.py b/tests/python/test_npm_release.py index a713d8c..b5000ec 100644 --- a/tests/python/test_npm_release.py +++ b/tests/python/test_npm_release.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import copy import json from pathlib import Path import platform as host_platform @@ -317,7 +318,19 @@ def test_rejects_pending_webgpu_release_but_stages_both_qualification_packages( ) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) - lock = locked() + lock = copy.deepcopy(locked()) + qualification = lock["qualification"] + qualification["status"] = "development-pending-device-validation" + qualification["providerGatePassed"] = False + qualification["productionArtifactQualified"] = False + qualification["qualifiedArtifactSetSha256"] = { + "linux-x64": None, + "windows-x64": None, + } + qualification["qualificationReportSha256"] = { + "linux-x64": None, + "windows-x64": None, + } packages = create_fake_packages(root, lock) build = root / "build" / "bin" build.mkdir(parents=True) @@ -363,14 +376,22 @@ def test_rejects_pending_webgpu_release_but_stages_both_qualification_packages( webgpu_artifact_manifest=manifest_path, qualification_build=False, ) - with self.assertRaisesRegex( - RuntimeError, "accepted Linux and Windows Provider Gates" + with mock.patch( + "tools.npm_release.webgpu_runtime.load_lock", + return_value=lock, ): - npm_release.stage_native(arguments) + with self.assertRaisesRegex( + RuntimeError, "accepted Linux and Windows Provider Gates" + ): + npm_release.stage_native(arguments) self.assertFalse(output.exists()) arguments.qualification_build = True - npm_release.stage_native(arguments) + with mock.patch( + "tools.npm_release.webgpu_runtime.load_lock", + return_value=lock, + ): + npm_release.stage_native(arguments) descriptor = json.loads( (output / "native" / "runtime-descriptor.json").read_text( "utf-8" diff --git a/tests/python/test_webgpu_report_review.py b/tests/python/test_webgpu_report_review.py index 9ac12f0..0b44e85 100644 --- a/tests/python/test_webgpu_report_review.py +++ b/tests/python/test_webgpu_report_review.py @@ -10,6 +10,23 @@ from tools.webgpu import build_runtime, qualify, review_reports +def pending_lock() -> dict[str, object]: + lock = build_runtime.load_lock() + qualification = lock["qualification"] + qualification["status"] = "development-pending-device-validation" + qualification["providerGatePassed"] = False + qualification["productionArtifactQualified"] = False + qualification["qualifiedArtifactSetSha256"] = { + "linux-x64": None, + "windows-x64": None, + } + qualification["qualificationReportSha256"] = { + "linux-x64": None, + "windows-x64": None, + } + return lock + + def line() -> dict[str, object]: return { "text": "HELLO 123", @@ -327,17 +344,20 @@ class WebGpuReportReviewTest(unittest.TestCase): def setUp(self) -> None: self.revision = review_reports.current_revision() - def create_pair(self, root: Path) -> None: - lock = build_runtime.load_lock() + def create_pair(self, root: Path) -> Path: + lock = pending_lock() + lock_path = root / "runtime-lock.json" + write_json(lock_path, lock) for platform_id in review_reports.PLATFORMS: write_platform_report(root, platform_id, lock) + return lock_path def test_collects_intact_pair_as_manual_review_candidate(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - self.create_pair(root) + lock_path = self.create_pair(root) candidate = review_reports.collect_pair( - root, expected_revision=self.revision + root, expected_revision=self.revision, lock_path=lock_path ) self.assertTrue(candidate["mechanicalValidationPassed"]) self.assertEqual(candidate["status"], "manual-review-required") @@ -349,18 +369,20 @@ def test_collects_intact_pair_as_manual_review_candidate(self) -> None: def test_rejects_report_changed_without_sidecar_update(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - self.create_pair(root) + lock_path = self.create_pair(root) report_path = root / "linux-x64" / "qualification-report.json" report = json.loads(report_path.read_text("utf-8")) report["passed"] = False write_json(report_path, report) with self.assertRaisesRegex(RuntimeError, "report hash mismatch"): - review_reports.collect_pair(root, expected_revision=self.revision) + review_reports.collect_pair( + root, expected_revision=self.revision, lock_path=lock_path + ) def test_rejects_rehashed_report_with_missing_gate(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - self.create_pair(root) + lock_path = self.create_pair(root) report_path = root / "linux-x64" / "qualification-report.json" report = json.loads(report_path.read_text("utf-8")) report["gates"].pop() @@ -370,19 +392,23 @@ def test_rejects_rehashed_report_with_missing_gate(self) -> None: "utf-8", ) with self.assertRaisesRegex(RuntimeError, "gate inventory"): - review_reports.collect_pair(root, expected_revision=self.revision) + review_reports.collect_pair( + root, expected_revision=self.revision, lock_path=lock_path + ) def test_rejects_cross_revision_report_pair(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - self.create_pair(root) + lock_path = self.create_pair(root) with self.assertRaisesRegex(RuntimeError, "report identity"): - review_reports.collect_pair(root, expected_revision="a" * 40) + review_reports.collect_pair( + root, expected_revision="a" * 40, lock_path=lock_path + ) def test_collects_staggered_platform_revisions_without_override(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - self.create_pair(root) + lock_path = self.create_pair(root) report_path = root / "windows-x64" / "qualification-report.json" report = json.loads(report_path.read_text("utf-8")) report["sourceRevision"] = "b" * 40 @@ -391,7 +417,7 @@ def test_collects_staggered_platform_revisions_without_override(self) -> None: f"{review_reports.sha256(report_path)} qualification-report.json\n", "utf-8", ) - candidate = review_reports.collect_pair(root) + candidate = review_reports.collect_pair(root, lock_path=lock_path) self.assertEqual( candidate["sourceRevisions"], {"linux-x64": self.revision, "windows-x64": "b" * 40}, @@ -400,7 +426,7 @@ def test_collects_staggered_platform_revisions_without_override(self) -> None: def test_rejects_tampered_copied_descriptor(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - self.create_pair(root) + lock_path = self.create_pair(root) descriptor_path = ( root / "windows-x64" / "artifacts" / "native-runtime-descriptor.json" ) @@ -408,7 +434,42 @@ def test_rejects_tampered_copied_descriptor(self) -> None: descriptor["released"] = True write_json(descriptor_path, descriptor) with self.assertRaisesRegex(RuntimeError, "descriptor policy"): - review_reports.collect_pair(root, expected_revision=self.revision) + review_reports.collect_pair( + root, expected_revision=self.revision, lock_path=lock_path + ) + + def test_production_lock_must_bind_the_reviewed_pair(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.create_pair(root) + lock = pending_lock() + qualification = lock["qualification"] + qualification["status"] = "production-qualified" + qualification["providerGatePassed"] = True + qualification["productionArtifactQualified"] = True + qualification["qualifiedArtifactSetSha256"] = {} + qualification["qualificationReportSha256"] = {} + for platform_id in review_reports.PLATFORMS: + report = json.loads( + (root / platform_id / "qualification-report.json").read_text( + "utf-8" + ) + ) + qualification["qualifiedArtifactSetSha256"][platform_id] = report[ + "sdk" + ]["artifactSetSha256"] + qualification["qualificationReportSha256"][platform_id] = ( + root / platform_id / "qualification-report.sha256" + ).read_text("utf-8").split()[0] + production_lock = root / "production-runtime-lock.json" + write_json(production_lock, lock) + candidate = review_reports.collect_pair(root, lock_path=production_lock) + self.assertEqual(candidate["status"], "production-qualified") + + qualification["qualificationReportSha256"]["windows-x64"] = "0" * 64 + write_json(production_lock, lock) + with self.assertRaisesRegex(RuntimeError, "differs from the reviewed"): + review_reports.collect_pair(root, lock_path=production_lock) if __name__ == "__main__": diff --git a/tests/python/test_webgpu_runtime.py b/tests/python/test_webgpu_runtime.py index f762d75..1f9f273 100644 --- a/tests/python/test_webgpu_runtime.py +++ b/tests/python/test_webgpu_runtime.py @@ -25,6 +25,23 @@ def locked() -> dict[str, object]: ) +def pending_locked() -> dict[str, object]: + lock = locked() + qualification = lock["qualification"] + qualification["status"] = "development-pending-device-validation" + qualification["providerGatePassed"] = False + qualification["productionArtifactQualified"] = False + qualification["qualifiedArtifactSetSha256"] = { + "linux-x64": None, + "windows-x64": None, + } + qualification["qualificationReportSha256"] = { + "linux-x64": None, + "windows-x64": None, + } + return lock + + def package_members(lock: dict[str, object], package_name: str) -> set[str]: members: set[str] = set() for platform_id in ("linux-x64", "windows-x64"): @@ -69,19 +86,11 @@ def test_committed_lock_is_valid(self) -> None: def test_complete_production_qualification_state_is_valid(self) -> None: lock = locked() - qualification = lock["qualification"] - qualification["status"] = "production-qualified" - qualification["providerGatePassed"] = True - qualification["productionArtifactQualified"] = True - qualification["qualifiedArtifactSetSha256"] = { - "linux-x64": "1" * 64, - "windows-x64": "2" * 64, - } - qualification["qualificationReportSha256"] = { - "linux-x64": "3" * 64, - "windows-x64": "4" * 64, - } build_runtime.validate_lock(lock) + self.assertEqual(lock["qualification"]["status"], "production-qualified") + + def test_complete_pending_qualification_state_is_valid(self) -> None: + build_runtime.validate_lock(pending_locked()) def test_production_qualification_requires_both_platform_reports(self) -> None: lock = locked() @@ -145,7 +154,7 @@ def test_lock_rejects_frozen_contract_mutations(self) -> None: ( "provider gate claim", lambda lock: lock["qualification"].__setitem__( - "providerGatePassed", True + "providerGatePassed", False ), "qualification", ), @@ -157,7 +166,7 @@ def test_lock_rejects_frozen_contract_mutations(self) -> None: ( "production claim", lambda lock: lock["qualification"].__setitem__( - "productionArtifactQualified", True + "productionArtifactQualified", False ), "qualification", ), @@ -284,7 +293,7 @@ def test_realistic_fake_packages_stage_and_validate_both_platforms(self) -> None self.assertEqual( len(manifest["artifacts"]["files"]), expected_files ) - self.assertFalse(manifest["qualification"]["providerGatePassed"]) + self.assertTrue(manifest["qualification"]["providerGatePassed"]) def test_sdk_validation_rejects_file_and_manifest_tampering(self) -> None: lock = locked() diff --git a/tools/npm/smoke.cjs b/tools/npm/smoke.cjs index 88a47c0..02fe289 100644 --- a/tools/npm/smoke.cjs +++ b/tools/npm/smoke.cjs @@ -23,6 +23,28 @@ async function main() { assert.equal(engine.info.detectionStrategy, 'bounded'); assert.equal(engine.info.detectionMaxSide, 960); assert.equal(engine.info.defaultRecognitionBatchSize, 1); + if (process.platform === 'darwin') { + assert.deepEqual( + engine.info.execution.selectionTrace.orderedCandidates, + ['apple', 'cpu'], + ); + assert.ok(engine.info.execution.providerCapabilities.some( + (capability) => capability.provider === 'apple' + && capability.packageIncluded, + )); + } else { + assert.deepEqual( + engine.info.execution.selectionTrace.orderedCandidates, + ['webgpu', 'cpu'], + ); + assert.ok(engine.info.execution.providerCapabilities.some( + (capability) => capability.provider === 'webgpu' + && capability.packageIncluded, + )); + assert.ok(['webgpu', 'cpu'].includes( + engine.info.execution.selectionTrace.selectedProvider, + )); + } const result = await engine.recognize({ data: pixels, width: metadata.width, @@ -35,21 +57,23 @@ async function main() { await engine.close(); } - const apple = await cjs.createEngine({ - execution: { - provider: 'apple', - precision: 'fp16', - sessionFallback: 'error', - }, - }); - try { - assert.equal(apple.info.execution.requestedProvider, 'apple'); - const detection = apple.info.execution.sessions.detection; - assert.equal(apple.info.executionProvider, 'CoreML'); - assert.equal(detection.sessionFallback, false); - assert.match(detection.qualificationId, /^apple-/); - } finally { - await apple.close(); + if (process.platform === 'darwin') { + const apple = await cjs.createEngine({ + execution: { + provider: 'apple', + precision: 'fp16', + sessionFallback: 'error', + }, + }); + try { + assert.equal(apple.info.execution.requestedProvider, 'apple'); + const detection = apple.info.execution.sessions.detection; + assert.equal(apple.info.executionProvider, 'CoreML'); + assert.equal(detection.sessionFallback, false); + assert.match(detection.qualificationId, /^apple-/); + } finally { + await apple.close(); + } } const tiledPixels = Buffer.alloc(2048 * 2048 * 3, 255); diff --git a/tools/webgpu/README.md b/tools/webgpu/README.md index c3f3c9b..b033f39 100644 --- a/tools/webgpu/README.md +++ b/tools/webgpu/README.md @@ -2,9 +2,9 @@ This directory owns the reproducible Linux x64 and Windows x64 Native WebGPU runtime, its release gate, and the real-device qualification runner. Product -integration is complete in the current source candidate; production release is -intentionally blocked until both platform reports are reviewed and bound to the -exact artifact sets in `runtime-lock.json`. +integration is complete in the current source candidate. Both platform reports +have been reviewed and bound to the exact artifact sets in `runtime-lock.json`; +ordinary release staging accepts only those production-qualified payloads. Both checked-in real-device reports pass 164/164 Gates. The `0.3.0` public execution profile is FP32-only: Linux/NVIDIA Vulkan measured 5.698x aggregate @@ -195,10 +195,11 @@ when available); absence of a driver identity fails the report. No CUDA, ROCm, OpenVINO, Python inference runtime, or source compiler is a product runtime prerequisite. -Do not edit the pending qualification fields from a successful exit code alone. -Both reports must be reviewed for device identity, placement, quality, -performance, memory, lifecycle, and supported compatibility scope before their -hashes and artifact-set identities can enter the production lock. +Qualification report fields remain immutable historical snapshots; do not edit +them after collection. Both reports were reviewed for device identity, +placement, quality, performance, memory, lifecycle, and supported compatibility +scope before their hashes and artifact-set identities entered the production +lock. ## Report-pair collection @@ -221,9 +222,11 @@ inventory; and checks the copied SDK manifest and schema 2 descriptor against the committed runtime lock, artifact-set hashes, provider inventory, ABI, and payload bytes. Both platforms must pass as one pair. -A successful collector exit writes a hash-protected -`manual-review-required` candidate. This is deliberately not an acceptance or a -production-lock mutation. A maintainer must still inspect device/driver scope, -ORT FP32 placement, bounded CPU partitions, strict rejection, CPU-s, latency distributions, -cold-start, RSS/VRAM evidence, logs, and cross-vendor coverage before choosing a -compatibility or release conclusion. +A successful collector exit writes a hash-protected candidate. With a pending +lock it remains `manual-review-required` and does not mutate the production +lock. With the committed production lock, the collector additionally requires +the recomputed report and artifact-set hashes to match its exact bindings and +marks the reviewed pair `production-qualified`. Device/driver scope, ORT FP32 +placement, bounded CPU partitions, strict rejection, CPU-s, latency +distributions, cold-start, RSS/VRAM evidence, logs, and cross-vendor coverage +remain part of the human review that precedes any future lock change. diff --git a/tools/webgpu/review_reports.py b/tools/webgpu/review_reports.py index a8fadcb..77eba71 100644 --- a/tools/webgpu/review_reports.py +++ b/tools/webgpu/review_reports.py @@ -105,6 +105,35 @@ def list_of_strings(value: object, context: str) -> list[str]: return value +def validate_report_qualification( + value: object, lock_qualification: dict[str, Any], *, platform_id: str +) -> None: + """Validate the pending qualification snapshot captured by a device run.""" + qualification = mapping(value, f"{platform_id} evidence qualification") + if ( + set(qualification) != set(lock_qualification) + or qualification.get("status") + != "development-pending-device-validation" + or qualification.get("evidenceId") != lock_qualification.get("evidenceId") + or qualification.get("providerGatePassed") is not False + or qualification.get("productionArtifactQualified") is not False + or qualification.get("qualifiedArtifactSetSha256") + != {"linux-x64": None, "windows-x64": None} + or qualification.get("qualificationReportSha256") + != {"linux-x64": None, "windows-x64": None} + or qualification.get("requiredPlatforms") + != lock_qualification.get("requiredPlatforms") + or not isinstance(qualification.get("knownLimitations"), list) + or not all( + isinstance(item, str) and item + for item in qualification["knownLimitations"] + ) + ): + raise RuntimeError( + f"{platform_id} evidence qualification snapshot is invalid" + ) + + def current_revision() -> str: revision = subprocess.run( ["git", "rev-parse", "HEAD"], @@ -242,16 +271,21 @@ def validate_manifest( } if "libc" in platform_lock: expected_platform["libc"] = platform_lock["libc"] + lock_qualification = mapping( + lock["qualification"], "runtime lock qualification" + ) if ( manifest.get("contractId") != lock["contractId"] or manifest.get("platform") != expected_platform or manifest.get("runtime") != build_runtime.runtime_identity(lock, platform_id) or manifest.get("sessionOptions") != lock["sessionOptions"] - or manifest.get("qualification") != lock["qualification"] ): raise RuntimeError( f"{platform_id} SDK manifest disagrees with the runtime lock" ) + validate_report_qualification( + manifest.get("qualification"), lock_qualification, platform_id=platform_id + ) artifacts = mapping(manifest.get("artifacts"), f"{platform_id} manifest.artifacts") if set(artifacts) != { @@ -638,12 +672,18 @@ def collect_pair( lock = build_runtime.load_lock(lock_path) build_runtime.validate_lock(lock) qualification = mapping(lock["qualification"], "runtime lock qualification") - if ( - qualification.get("status") != "development-pending-device-validation" - or qualification.get("providerGatePassed") is not False - or qualification.get("productionArtifactQualified") is not False - ): - raise RuntimeError("report collection requires the pending qualification lock") + pending = ( + qualification.get("status") == "development-pending-device-validation" + and qualification.get("providerGatePassed") is False + and qualification.get("productionArtifactQualified") is False + ) + production = ( + qualification.get("status") == "production-qualified" + and qualification.get("providerGatePassed") is True + and qualification.get("productionArtifactQualified") is True + ) + if not pending and not production: + raise RuntimeError("report collection requires a valid qualification lock") root = reports_root.resolve() try: platforms = { @@ -657,9 +697,27 @@ def collect_pair( } except (IndexError, KeyError, TypeError, ValueError) as exception: raise RuntimeError("qualification report structure is invalid") from exception + if production: + artifact_hashes = { + platform_id: platform["artifactSetSha256"] + for platform_id, platform in platforms.items() + } + report_hashes = { + platform_id: platform["reportSha256"] + for platform_id, platform in platforms.items() + } + if ( + qualification.get("qualifiedArtifactSetSha256") != artifact_hashes + or qualification.get("qualificationReportSha256") != report_hashes + ): + raise RuntimeError( + "production qualification lock differs from the reviewed reports" + ) candidate: dict[str, Any] = { "schema": "light-ocr-webgpu-provider-gate-review/1.0", - "status": "manual-review-required", + "status": ( + "production-qualified" if production else "manual-review-required" + ), "mechanicalValidationPassed": True, "evidenceId": qualification["evidenceId"], "sourceRevisions": { diff --git a/tools/webgpu/runtime-lock.json b/tools/webgpu/runtime-lock.json index af686c9..3e8e32d 100644 --- a/tools/webgpu/runtime-lock.json +++ b/tools/webgpu/runtime-lock.json @@ -119,27 +119,28 @@ "deviceIdSupported": false }, "qualification": { - "status": "development-pending-device-validation", + "status": "production-qualified", "evidenceId": "native-webgpu-plugin-0.1.0-ort-1.24.4-dev2", - "providerGatePassed": false, - "productionArtifactQualified": false, + "providerGatePassed": true, + "productionArtifactQualified": true, "qualifiedArtifactSetSha256": { - "linux-x64": null, - "windows-x64": null + "linux-x64": "47caf50e71dad9e7bf6fcf0c15d86df6b97ab8b36b9f7eac3bd25ecaba0e5dd3", + "windows-x64": "6fa272f5d7a9139dd1b1d36e366eae5564201981bb988955fb47bdcc2abcc128" }, "qualificationReportSha256": { - "linux-x64": null, - "windows-x64": null + "linux-x64": "30bfc4d8add1542ed4ae27bf89121951156f643174bf328163c5dd4a544d1df8", + "windows-x64": "f806387b09a50489aefe43e7b5ffe04cc7b701b591b12115ef20402bd1048c3b" }, "requiredPlatforms": [ "linux-x64", "windows-x64" ], "knownLimitations": [ - "Linux and Windows real-device FP32/FP16 placement, quality, performance, memory, and lifecycle reports are pending.", - "The qualified FP16 models use native float16 graph I/O but retain converter-blocked operations at float32; Concat, Gather, and Slice require the bounded CPU partition, so cpuPartition=forbid fails closed.", + "The public WebGPU profile is FP32-only in 0.3.0; the locked FP16 derivations remain internal reproducibility artifacts and are not selectable through the WebGPU API.", + "Concat, Gather, and Slice require the bounded CPU partition, so cpuPartition=forbid fails closed before session creation.", + "Compatibility and performance evidence is limited to the recorded Linux x64/NVIDIA Vulkan and Windows x64/AMD D3D12 systems; other adapters and drivers do not inherit their measured speedups.", "The released WebGPU plugin does not expose a typed driver-compatibility failure for every Dawn initialization error; unknown provider failures remain fatal instead of being inferred from text.", - "No production compatibility range or released Auto policy may be claimed until both platform qualification reports are reviewed." + "Linux arm64, Linux musl, Windows arm64, browser WebGPU, external device/context injection, and zero-copy GPU input are outside the 0.3.0 release scope." ] } } From 51c365bfdc680aa4e7640eeb933b4d8fda406cee Mon Sep 17 00:00:00 2001 From: eric8810 Date: Sun, 19 Jul 2026 19:37:10 +0800 Subject: [PATCH 2/3] =?UTF-8?q?ci(webgpu):=20=E9=94=81=E6=80=81=E9=9A=8F?= =?UTF-8?q?=E6=B5=81=EF=BC=8C=E5=8F=8C=E5=A2=83=E5=90=8C=E9=AA=8C=20=C2=B7?= =?UTF-8?q?=20align=20contract=20CI=20with=20lock=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve pending versus production qualification mode from the runtime lock. Keep contract builds and native staging valid before and after evidence promotion instead of forcing qualification-only mode. --- .github/workflows/webgpu-native.yml | 30 ++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/webgpu-native.yml b/.github/workflows/webgpu-native.yml index aa41d5f..1339169 100644 --- a/.github/workflows/webgpu-native.yml +++ b/.github/workflows/webgpu-native.yml @@ -86,6 +86,22 @@ jobs: --offline \ --package-cache .cache/webgpu-packages \ --output-dir "dist/webgpu-sdk-offline/${{ matrix.id }}" + - name: Resolve the locked WebGPU build mode + shell: bash + run: | + qualification_status="$(python -c 'import json; print(json.load(open("tools/webgpu/runtime-lock.json", encoding="utf-8"))["qualification"]["status"])')" + case "$qualification_status" in + development-pending-device-validation) + echo "WEBGPU_QUALIFICATION_BUILD=ON" >> "$GITHUB_ENV" + ;; + production-qualified) + echo "WEBGPU_QUALIFICATION_BUILD=OFF" >> "$GITHUB_ENV" + ;; + *) + echo "Unsupported WebGPU qualification status: $qualification_status" >&2 + exit 1 + ;; + esac - name: Install Node development files (Linux) if: runner.os != 'Windows' shell: bash @@ -110,7 +126,7 @@ jobs: } "NODE_INCLUDE_DIR=$headers" | Out-File -FilePath $env:GITHUB_ENV -Append "NODE_LIBRARY=$($library.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append - - name: Configure WebGPU qualification build (Linux) + - name: Configure WebGPU contract build (Linux) if: runner.os != 'Windows' shell: bash run: >- @@ -119,13 +135,13 @@ jobs: -DLIGHT_OCR_DEPENDENCY_CACHE_DIR="$PWD/.cache/dependencies" -DLIGHT_OCR_ONNXRUNTIME_FLAVOR=webgpu -DLIGHT_OCR_WEBGPU_SDK_DIR="$PWD/dist/webgpu-sdk/${{ matrix.id }}" - -DLIGHT_OCR_WEBGPU_QUALIFICATION_BUILD=ON + -DLIGHT_OCR_WEBGPU_QUALIFICATION_BUILD="$WEBGPU_QUALIFICATION_BUILD" -DLIGHT_OCR_BUILD_NODE=ON -DLIGHT_OCR_BUILD_TESTS=ON -DLIGHT_OCR_BUILD_TOOLS=ON -DLIGHT_OCR_NODE_INCLUDE_DIR="$NODE_INCLUDE_DIR" -DLIGHT_OCR_NODE_EXECUTABLE="$(command -v node)" - - name: Configure WebGPU qualification build (Windows) + - name: Configure WebGPU contract build (Windows) if: runner.os == 'Windows' shell: pwsh run: >- @@ -133,7 +149,7 @@ jobs: "-DLIGHT_OCR_DEPENDENCY_CACHE_DIR=$env:GITHUB_WORKSPACE/.cache/dependencies" -DLIGHT_OCR_ONNXRUNTIME_FLAVOR=webgpu "-DLIGHT_OCR_WEBGPU_SDK_DIR=$env:GITHUB_WORKSPACE/dist/webgpu-sdk/${{ matrix.id }}" - -DLIGHT_OCR_WEBGPU_QUALIFICATION_BUILD=ON + "-DLIGHT_OCR_WEBGPU_QUALIFICATION_BUILD=$env:WEBGPU_QUALIFICATION_BUILD" -DLIGHT_OCR_BUILD_NODE=ON -DLIGHT_OCR_BUILD_TESTS=ON -DLIGHT_OCR_BUILD_TOOLS=ON @@ -162,6 +178,10 @@ jobs: --configuration Release \ --platform-id "${{ matrix.id }}" \ --output-dir "reports/webgpu/${{ matrix.id }}" + qualification_args=() + if [[ "$WEBGPU_QUALIFICATION_BUILD" == "ON" ]]; then + qualification_args+=(--qualification-build) + fi python tools/npm_release.py stage-native \ --platform-id "${{ matrix.id }}" \ --build-dir build-webgpu \ @@ -170,7 +190,7 @@ jobs: --output-dir "dist/webgpu-native/${{ matrix.id }}" \ --runtime-flavor webgpu \ --webgpu-artifact-manifest "dist/webgpu-sdk/${{ matrix.id }}/artifact-manifest.json" \ - --qualification-build + "${qualification_args[@]}" LIGHT_OCR_NODE_BINARY="$PWD/dist/webgpu-native/${{ matrix.id }}/native/light_ocr_node.node" \ LIGHT_OCR_RUNTIME_DESCRIPTOR="$PWD/dist/webgpu-native/${{ matrix.id }}/native/runtime-descriptor.json" \ node -e "const x=require('./bindings/node/js/load-native.cjs').loadNative(); console.log(JSON.stringify(x.runtimePolicy))" From cc6ea09e0334dc5a5e26df1e51ed1581b2a76d30 Mon Sep 17 00:00:00 2001 From: eric8810 Date: Sun, 19 Jul 2026 19:41:36 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(webgpu):=20=E5=93=88=E5=B8=8C=E5=BD=92?= =?UTF-8?q?=E5=B0=BA=EF=BC=8C=E9=94=81=E5=8D=B0=E5=8F=AF=E9=AA=8C=20=C2=B7?= =?UTF-8?q?=20validate=20production=20hashes=20in=20CMake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CMake's regular expression engine does not accept the counted repetition used by the production gate. Validate lowercase hex and exact length separately so the bound Linux and Windows evidence hashes pass without weakening the release check. --- cmake/WebGpuRuntime.cmake | 17 +++++++++++++---- tests/python/test_webgpu_runtime.py | 10 +++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cmake/WebGpuRuntime.cmake b/cmake/WebGpuRuntime.cmake index d1384d0..1853483 100644 --- a/cmake/WebGpuRuntime.cmake +++ b/cmake/WebGpuRuntime.cmake @@ -227,12 +227,21 @@ function(light_ocr_configure_webgpu_runtime) qualification qualificationReportSha256 windows-x64) string(JSON _qualified_sha GET "${_json}" qualification qualifiedArtifactSetSha256 ${_platform}) + set(_qualification_hashes_valid TRUE) + foreach(_qualification_hash IN ITEMS + _linux_qualified_sha + _windows_qualified_sha + _linux_report_sha + _windows_report_sha) + string(LENGTH "${${_qualification_hash}}" _qualification_hash_length) + if(NOT _qualification_hash_length EQUAL 64 OR + NOT "${${_qualification_hash}}" MATCHES "^[0-9a-f]+$") + set(_qualification_hashes_valid FALSE) + endif() + endforeach() if(NOT _qualification_status STREQUAL "production-qualified" OR NOT _provider_gate OR NOT _artifact_qualified OR - NOT _linux_qualified_sha MATCHES "^[0-9a-f]{64}$" OR - NOT _windows_qualified_sha MATCHES "^[0-9a-f]{64}$" OR - NOT _linux_report_sha MATCHES "^[0-9a-f]{64}$" OR - NOT _windows_report_sha MATCHES "^[0-9a-f]{64}$" OR + NOT _qualification_hashes_valid OR NOT _qualified_sha STREQUAL _artifact_set_sha) message(FATAL_ERROR "WebGPU release SDK requires accepted Linux and Windows Provider Gates bound to this artifact set") diff --git a/tests/python/test_webgpu_runtime.py b/tests/python/test_webgpu_runtime.py index 1f9f273..2b6b13c 100644 --- a/tests/python/test_webgpu_runtime.py +++ b/tests/python/test_webgpu_runtime.py @@ -396,10 +396,11 @@ def test_archive_member_rejects_paths_outside_the_archive_contract(self) -> None build_runtime.archive_member(archive, "../safe", "test") def test_cmake_freezes_plugin_runtime_release_boundary(self) -> None: - dependencies = "\n".join( - (ROOT / "cmake" / name).read_text("utf-8") + sources = { + name: (ROOT / "cmake" / name).read_text("utf-8") for name in ("Dependencies.cmake", "WebGpuRuntime.cmake") - ) + } + dependencies = "\n".join(sources.values()) required = [ "LIGHT_OCR_ONNXRUNTIME_FLAVOR", "LIGHT_OCR_WEBGPU_SDK_DIR", @@ -413,6 +414,9 @@ def test_cmake_freezes_plugin_runtime_release_boundary(self) -> None: for token in required: with self.subTest(token=token): self.assertIn(token, dependencies) + webgpu_runtime = sources["WebGpuRuntime.cmake"] + self.assertIn("_qualification_hash_length EQUAL 64", webgpu_runtime) + self.assertNotIn("[0-9a-f]{64}", webgpu_runtime) if __name__ == "__main__":