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/.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))" 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/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/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..2b6b13c 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() @@ -387,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", @@ -404,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__": 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." ] } }