b
This commit is contained in:
parent
b653d77d4e
commit
83b874c2a0
9 changed files with 1831 additions and 183 deletions
579
docs/OPTIMIZATION_PLAN.md
Normal file
579
docs/OPTIMIZATION_PLAN.md
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
# マンデルブロ集合ビューアー最適化計画
|
||||
|
||||
対象baseline: `index.html` v24.2.55 (`24.2.55-no-black-hole-async-recovery`)
|
||||
|
||||
実装版: `index.html` v24.3.0 (`24.3.0-optimized-unknown-pipeline`)
|
||||
|
||||
作成日: 2026-08-25
|
||||
|
||||
## 本番昇格状況(2026-08-25)
|
||||
|
||||
現時点では本番未昇格である。`PIXEL_FRONTIER_PRODUCTION=false` を維持し、候補経路は `?frontier-candidate=1` または自動ゲート実行時だけ有効にする。未検証の active-list engine を Accurate の初期全面描画には使わず、初期描画は bounded strip の既存 Deep kernel、active-list は operation-limit 画素の継続計算だけに限定する。
|
||||
|
||||
| 条件 | 状態 | 根拠 / 残作業 |
|
||||
| --- | --- | --- |
|
||||
| JavaScript 構文、worker 構文、kernel hash | 合格 | `tests/optimization_regression.mjs` |
|
||||
| 1 dispatch の上限 4,000,000 pixel-iterations | 静的合格 | 初期 Deep、sparse DS、frontier の各契約を回帰テストで固定 |
|
||||
| Accurate 初期描画が active-list production path を使わない | 静的合格 | `computeFrame()` は bounded `computePerturbFrameTiled()` を使用 |
|
||||
| UNKNOWN を色で隠さず対象画素を計算する | 静的合格 | operation-limit 全 index を continuation queue に投入。色置換や倍率しきい値は不使用 |
|
||||
| 早すぎる安定判定の防止 | 候補実装済み | 画素幅から連続的に求めた最低反復数へ到達する前は、2 round 安定でも終了しない |
|
||||
| exact escape iteration / class mismatch = 0 | 未確認 | 単一ブラウザーセッションで corpus と高精度 oracle を実行する |
|
||||
| final numeric UNKNOWN = 0 | 未確認 | reset、seahorse、real-axis、報告座標で確認する |
|
||||
| WGSL error / uncaptured error / device loss = 0 | 未確認 | Windows D3D12 実機を含む最終ブラウザー確認が必要 |
|
||||
| p50 / p95 性能基準 | 未確認 | 同一セッション A/B を採取し、特に 10^14 付近の初回表示を確認する |
|
||||
| 通常操作で黒領域が出現・消失しない | 未確認 | 同一近傍の pan / zoom / round-trip hash を確認する |
|
||||
|
||||
`pixelFrontierIterationFloor()` は zoom 倍率で描画方式を切り替えない。`span / canvasWidth` が表す1画素のスケールから最低反復予算を連続的に算出する。これは「少し拡大しただけで未計算領域を黒として確定する」問題を抑えるための下限であり、数学的な集合所属証明ではない。最終的な本番昇格には上表の実機ゲート通過を必須とする。
|
||||
|
||||
本番判定は `tests/production_gate.mjs` に固定した。候補段階では `node tests/production_gate.mjs <artifact> preflight`、本番フラグを有効化した後は同じコーパスを再実行して `node tests/production_gate.mjs <artifact> production` を通す。artifact は同一adapterで採取したbaseline/candidateの対応、異なる2 adapter以上、全12 correctness case、全6 performance caseを各10計測run、3 interaction caseを含まなければ合格しない。10^5と10^8はFAST/Accurateの両方、10^14は800×600でFAST p50 1000 ms・p95 1500 ms未満、Accurate p50 1500 ms・p95 2000 ms未満を追加の絶対条件とする。
|
||||
|
||||
## 追加修正: 10^14付近の速度とWindows GPU停止
|
||||
|
||||
実機ログで、Accurate Deepの長い単一dispatchがWindowsのGPU監視時間を超え、`DXGI_ERROR_DEVICE_HUNG`を発生させていたことを確認した。`powerPreference`の警告は原因ではない。
|
||||
|
||||
- Accurate DeepのActive-list本番化は、正常版との分類経路差になるため撤回する。正確モードはv24.2.55と同じDeep perturbation式へ戻し、1 stripを最大4,000,000 pixel-iterationsへ縮小することでTDRを回避する。
|
||||
- Deep/Direct-DS補修はより重い演算なので、1 dispatch相当を最大4,000,000 pixel-iterationsへ制限する。疎Deep補修がこの上限を超える場合も補修を省略せず、2次元タイルのbounded scanですべての数値UNKNOWNを処理する。
|
||||
- state/二重queueは最大64 MiBのband workspaceとして再利用し、adapterのbuffer/binding上限を超えない。band間で出力座標を保持するためqueue indexはband-local、meta/smooth indexはframe-globalとする。
|
||||
- device loss直後のadapter再作成を1.2秒遅延し、D3D12復旧前の連続した`Device failed at creation`を避ける。
|
||||
- FASTの最終フレームから無条件Deep補修を外す。数値UNKNOWNが実際に残った場合だけ局所reference補修と画素精度BigInt補修を同期実行し、数値UNKNOWNが0になるまで新しいframeを公開しない。通常FASTはDeepを待たず、未計算frameも表示しない。
|
||||
- 履歴のない深部FASTでは、短いreferenceのpreviewがfinalへ再利用できず二重計算になっていたため、最終品質を1回だけ生成する。既存のstable historyがある操作では従来どおり再投影を即時previewとして使う。
|
||||
- 倍率による描画方式の固定閾値は設けない。必要精度を `span / canvasWidth`(複素平面上の1画素幅)から算出し、参照軌道は画素精度+40 guard bit、任意精度補修は同じ精度と+32 bitの二重計算が一致した場合だけ採用する。画素より細かい座標情報は計算しない。数値UNKNOWNは件数上限で打ち切らず、bounded chunkですべて補修してから完成扱いにする。色で未計算を隠す処理や10^100切替は使用しない。
|
||||
|
||||
### 根本解決: pixel-convergent frontier
|
||||
|
||||
黒領域の膨張には、座標精度不足とは別に「反復上限まで脱出しなかった点を、そのまま集合内部と同じ黒で表示する」問題がある。倍率ごとの固定反復数や固定切替では解決しないため、最終形は次の画素収束方式とする。
|
||||
|
||||
1. 初回passは現在のadaptive反復数まで全画素を計算する。
|
||||
2. `operation-limit` 画素のうち、8近傍に `escaped` がある画素だけをGPU上でfrontier queueへcompactする。黒領域の奥を再計算しない。
|
||||
3. frontier画素は保存したperturbation stateからbounded chunkで継続し、新たに脱出した画素だけを更新する。その近傍を次のfrontierへ追加する。
|
||||
4. analytic cardioid/bulbまたは検証済み周期性で内部と証明できた画素はfrontierから除外する。単に反復上限へ達しただけの画素は「証明済み内部」へ昇格しない。
|
||||
5. 変化した境界のHausdorff距離が1画素未満になり、数値UNKNOWNが0になった時だけframeを完成・history commitする。これにより画素以下の境界情報は追跡しない。
|
||||
6. 1 dispatchは既存のpixel-iteration予算内に分割し、操作が入ればgeneration tokenで即時中止する。同一viewの反復延長ではreference/state/queueを再利用し、iteration 0からやり直さない。
|
||||
|
||||
現在の実装では、この前提となる画素幅基準precision、全残存数値UNKNOWNのbounded補修、未完成frameのpublish禁止までを本番経路へ入れた。frontier continuationは既存のshadow active-listを、実機zero-tolerance corpusとWindows TDR試験に通した後で本番へ昇格する。これが反復上限由来の過大な黒を、全画面高反復の速度低下なしに除く本命経路である。
|
||||
- adaptive反復数の64単位上方丸め、BLA既定OFF、同一viewだけのreference再利用は維持する。したがって操作順による黒領域の揺れを抑える決定性は維持し、反復数・誤差guard・分類式は削減しない。
|
||||
|
||||
## 実装状況(2026-08-25)
|
||||
|
||||
この計画のうち、数値品質を維持したまま静的・モデル検証できる項目をv24.3.0へ実装した。
|
||||
|
||||
- generationごとにprimary/final/numeric-quiescent/history-commit、submit/sync差分、UNKNOWN数を最大64件記録し、テストhookへprimary/final/numeric-complete待機とmeta/smooth SHA-256取得を追加する。
|
||||
- Accurate seedを最大24反復で終了し、未採用画素をreason 1〜5のUNKNOWNとしてDSへ渡す。
|
||||
- DSと高精度・高速参照workerで更新後平方を次反復へ引き継ぐ。
|
||||
- Direct、Accurate、FAST、Deep、BLAで共通のUNKNOWN post-statsを使い、理由0、1〜5、6を同じ規則で集計する。
|
||||
- Deep補正をbucket、prefix、scatter、indirect dispatchの疎queueへ接続し、queue不変条件が崩れた時だけbounded full scanへ戻す。
|
||||
- Accurate seed後はUNKNOWN indexだけをcompactし、時間予算に合わせたDirect-DS queue sliceで処理する。selected/enqueued/dispatch/processedまたは範囲不変条件が崩れた場合だけ従来の全画面guard走査へ戻す。
|
||||
- failure mapをGPU上で空間タイル集計し、全画面metadata readbackを局所補修ループから除去する。局所referenceは3 workerで並列準備する。
|
||||
- failure密度によるFAST→Deep直接昇格、同一viewの検証済みreference再利用、final reference prefetch、BLA table cacheを追加する。近傍viewへのreference流用は黒領域の操作順依存を生じ得るため撤回した。
|
||||
- previewの確定済みescape・解析的内点を同一referenceのfinalで再利用し、UNKNOWNだけを再計算する。
|
||||
- GPU先行投入を24 ms(直近入力)/40 ms(idle)の予測時間予算で制限する。
|
||||
- 初期pipelineをDirect、color、present、UNKNOWN stats、実軸対称copyへ縮小し、Accurate、FAST拡張、Deep、補正、AAを遅延コンパイルする。
|
||||
- 最後に残ったreason 1〜5だけをview精度+128 bitと+192 bitで再計算し、両者のescape反復とf32 smoothが一致した結果だけを採用する。
|
||||
- 中心虚部が厳密に0の通常画面では、共役対称性により上半分だけを計算して下半分へmetadata/smoothを複製する。
|
||||
- BLAはoperation削減率だけでなく、直近baseline、table build、probeを含む推定net時間が5%以上改善する場合だけ有効化する。
|
||||
- BLAの不安全分岐は0反復目restartではなく最後の正確checkpointからFAST recurrenceを再開する。
|
||||
- exportはadapter limitに応じた256/512 px tileと2〜3個のstaging workspaceを使い、GPU/readbackを重ねる。PNGのrow順は維持し、UI yieldは24 ms経過時だけ行う。
|
||||
- UNKNOWN/queue statsはframe単位の3-buffer readback ringへ移し、安定buffer/textureのbind groupをcacheする。
|
||||
- stable historyは全画面copyを廃止し、3枚のcolor textureをfront/back/history/spareとして役割回転する。
|
||||
- Direct/Deepのactive-list continuation、indirect compaction、FAST/Deepの証明付き内点判定候補をshadow-onlyで追加した。Deep continuationも品質回帰の切り分けを優先して本番採用を撤回した。内点候補は外向き丸めのf32区間演算を使い、候補indexだけを画素精度+40/+72 bit oracleで確認する。
|
||||
- 操作ごとに黒領域が揺れる問題への安定化として、adaptive反復数を品質を下げない64反復単位の上方丸めにし、近傍view間の参照軌道流用を廃止した。FASTで数値UNKNOWNが発生した場合は、同一viewの局所referenceと画素精度BigInt補修を完了してから表示し、未完成frameをpresent/commitしない。
|
||||
|
||||
`tests/optimization_regression.mjs` は、2本のscript構文、36 string/kernel hash、旧新BigInt軌道2,000例、旧新DS軌道600例、Direct active chunk分割1,000例、任意精度worker構文を検査する。この環境ではブラウザ接続とWebGPU adapterを利用できなかったため、実GPU WGSL compile、画面hash、KPI実測は未実施である。
|
||||
|
||||
`tests/OPTIMIZATION_BENCHMARK.json` のBigInt worker modelでは、240点、700反復、192〜320 bit、7 runの中央値が201.5509 msから115.3558 msになり、42.77%短縮した。この値は参照軌道の平方再利用だけを測るもので、ブラウザや実GPUのend-to-end改善率ではない。
|
||||
|
||||
BLA replayコードは保持しているが、実測時間で数値backendが揺れることを防ぐためproduction既定値をOFFに戻し、明示的なテストopt-inだけを許可する。export ring、readback/bind cache、history role rotationはproduction経路へ接続済みである。Active-listと証明付き内点判定は`productionEligible:false`のshadow-onlyへ戻し、本番Deepは正常版の式を小さなstripで実行する。
|
||||
|
||||
### 要件別の実装証拠
|
||||
|
||||
| 計画項目 | 現在の実装 | 検証証拠 |
|
||||
| --- | --- | --- |
|
||||
| P0-1 seed打切り | `DIRECT_ACCURATE_SEED_WGSL` が24反復でreason 2へ移行 | kernel hash、source invariant |
|
||||
| P0-2 平方再利用 | Direct DS、Deep/Fast reference worker、独立verifyで更新後平方を保持 | BigInt 2,000例、DS 600例の旧新全state一致 |
|
||||
| P0-3 計測 | generation別primary/final/numeric-quiescent/history、submit/sync、UNKNOWN理由を記録 | `waitForPrimary/Final/NumericComplete`、`fieldHashes`、`gpuDiagnostics` |
|
||||
| P1-1 疎queue | Deep bucket/prefix/scatter/indirectとAccurate Direct-DS compact queue | selected/enqueued/dispatch/processed不変条件、異常時full scan |
|
||||
| P1-2 補修routing | failure密度によるFAST→Deep、GPU reason tile map、局所reference | source invariant、reason別post-stats |
|
||||
| P1-3 実体cache | reference prefix/checkpoint、BLA table、同一pending requestを再利用 | key/coalescing/cancel source invariant |
|
||||
| P1-4 preview再利用 | 同一view/referenceのknown pixelを保持しUNKNOWNだけfinal再計算 | `unknownOnly` とreference key guard |
|
||||
| P1-5 時間予算 | 24/40 ms予測budget、adaptive strip/burst、queued DS slice | scheduler source invariant |
|
||||
| P2-1 failure map | GPU tile集計、3 workerのlocal reference並列準備 | recovery経路に全画面meta readbackなし |
|
||||
| P2-2 BLA replay | 最後の正確anchorからbaseline recurrenceを再開。production既定OFF、明示opt-inのみ | 7 fallback分岐、zero restartなし、kernel hash |
|
||||
| P2-3 固定費 | lazy pipeline、3-buffer stats readback、bind cache、history role rotation | 1,000回のtexture role model、source invariant |
|
||||
| P2-4 export | 256/512 tile、2〜3 staging slot、ordered `allSettled`、24 ms yield | export構造回帰、SS1/SS2 smoke hook |
|
||||
| P2-5 任意精度 | reason 1〜5を画素精度+40/+72 bitで評価して一致時のみpatch。全残存画素をbounded chunkで処理 | worker構文、受理条件source invariant |
|
||||
| P3-1 active-list | Direct/Deep state、二重queue、indirect compaction、preview→final resume | Direct chunk 1,000例、GPU zero-tolerance shadow hook |
|
||||
| P3-2 証明付きskip | 厳密実軸対称copy、外向きf32区間によるcardioid/bulb候補 | 画素精度+40/+72 bit oracle shadow、`productionEligible:false` |
|
||||
|
||||
`tests/optimization_webgpu_corpus.mjs` は上表の実GPU証拠を収集するdriverである。7 correctness case、4 performance scenario、SS1/SS2 exportに加え、近接2 viewと同一view再訪からなるinteraction stability sequenceを定義する。WGSL error、uncaptured error、device loss、coverage UNKNOWN、最終numeric UNKNOWN、history commit、field hash、Direct/Deep active mismatch、内点oracle mismatchをzero-toleranceで検査し、同一view再訪時のmeta/smooth hash一致も必須とする。numeric recoveryが完了しないcaseも即失敗とする。
|
||||
|
||||
## 結論
|
||||
|
||||
品質を落とさずに改善できる可能性が高い順序は、次のとおりである。これはprofile前の期待順位であり、Milestone 0のbaseline実測で入れ替える。
|
||||
|
||||
1. Accurate の f32 seed を24反復で終了させる。現在は「24未満の脱出だけを採用」するにもかかわらず、未採用ピクセルを `maxIter` または脱出まで計算し、その後DSで0反復目から再計算している。
|
||||
2. Accurate DSとBigInt参照軌道の反復内で重複計算している更新後の平方を次反復へ引き継ぐ。
|
||||
3. すでに存在する Deep UNKNOWN のbucket/queue/indirect補正を本番経路へ接続し、全画面DS補正走査をやめる。
|
||||
4. previewで確定した脱出・解析的内点と、同一prefixの参照軌道をfinalへ引き継ぎ、同じピクセルを0反復目から二度計算しない。
|
||||
5. 参照軌道とBLA tableを実際の参照点でキャッシュする。同一viewの反復数増加では再利用または延長できるが、近接pan/zoomへの流用は同一view再訪hash gateを通過するまで行わない。
|
||||
6. 数値未確定の位置特定を全画面GPU→CPU readbackからGPU側tile集計へ移し、reason別にDS、軌道延長、局所referenceを選ぶ。
|
||||
7. GPUへ先行投入する仕事を「strip本数」ではなく予測GPU時間で制限し、キャンセル済みビューの無駄計算を抑える。
|
||||
8. 長期的には、一定反復ごとのactive-pixel compactionへ移行し、preview継続、分岐発散削減、細粒度キャンセルを同時に実現する。
|
||||
|
||||
最初の3項目は、最終的に実行する数値式を変えず、明白な重複または空振り計算だけを減らせる。特に1項目は小さな変更で高い効果が見込まれるため最初に検証するが、実効果は解析的内点、24反復未満の脱出、DS対象ピクセルの比率に依存する。
|
||||
|
||||
この文書は実装監査に基づく提案であり、ここに記載した性能効果はまだ実GPUで測定した結果ではない。既存の386.9 ms削減値もworker modelであり、canvasを含む実端末フレーム時間ではない。
|
||||
|
||||
## 目標と非目標
|
||||
|
||||
目標は次の3点である。
|
||||
|
||||
- 最終フレームに残る数値未確定ピクセルを最小化する。
|
||||
- 入力から新規画像、最終品質、数値補修完了までの時間と、PNG生成時間を短縮する。
|
||||
- v24.2.55の解像度、`maxIter`、サンプリング位置、色、数値安全弁を維持し、既知ピクセルをUNKNOWNへ退行させない。
|
||||
|
||||
速度のためにDPR、反復数、AA sample数、誤差判定の安全余裕を下げることは対象外とする。また、本ビューアーのexport metadataは現在も `membershipCertified:false` であるため、本計画の「品質不変」は数学的な集合所属証明ではなく、現行baselineとの同等性と高精度oracleに対する非退行を意味する。
|
||||
|
||||
## 用語と計測単位
|
||||
|
||||
コード上には「計算放棄」という正式な状態名がない。曖昧な集計を避けるため、本計画では次のように分ける。
|
||||
|
||||
| 区分 | metadata reason | 本計画での扱い |
|
||||
| --- | ---: | --- |
|
||||
| 未書込・coverage hole | 0 | バグ検知指標。最終値は必ず0 |
|
||||
| 数値未確定 | 1〜5 | 「計算放棄ピクセル」の主指標 |
|
||||
| 反復上限到達 | 6 | 数値故障ではない。失敗率へ加算しない |
|
||||
| キャンセル済みビューの計算 | metadata外 | GPUへ投入済みだがpublishされない無駄仕事として別集計 |
|
||||
| 重複計算 | metadata外 | preview→final、seed→DS、primary→recoveryで同じピクセルを再反復した仕事 |
|
||||
|
||||
reason 1〜5は `error-bound`、`escape-uncertain`、`reference-end`、`rebase-gap`、`range` であり、現行の `numericalFailureCount()` と同じ定義である(`index.html:2023-2025`)。reason 6は `maxIter` まで脱出しなかったピクセルで、COLOR passでは不透明な黒になる。数値未確定とは区別する。
|
||||
|
||||
ピクセル数だけでは、1反復で失敗したピクセルと12000反復後に捨てたピクセルを区別できない。主な性能指標には必ず「pixel-iterationsまたは実際のkernel steps」も併記する。
|
||||
|
||||
## 現行パイプライン
|
||||
|
||||
```text
|
||||
入力
|
||||
├─ 操作中: stable historyを再投影
|
||||
└─ 操作確定
|
||||
├─ backend選択
|
||||
│ ├─ FAST direct f32 → COLOR → PRESENT
|
||||
│ ├─ FAST perturbation / optional BLA
|
||||
│ │ └─ coverage repair + COLOR → stats readback
|
||||
│ │ └─ background recovery予約 → 一次PRESENT
|
||||
│ │ └─ 60 ms後にlocal refs
|
||||
│ │ └─ 残存時full Deep + DS + local refs → recolor
|
||||
│ ├─ Accurate direct: f32 seed → guarded DS → COLOR → PRESENT
|
||||
│ └─ Accurate Deep: perturbation → stats readback
|
||||
│ └─ 条件成立時full-screen guarded DS → local refs
|
||||
│ └─ COLOR → PRESENT
|
||||
└─ intended contract: 数値未確定が0の時だけstable historyへcommit
|
||||
```
|
||||
|
||||
品質面では、未完成frontをhistoryへ昇格させず、通常のpan/zoomでは透明な数値未確定部分を直前のstable frameで補うことが意図された契約である。ただし現行Accurate hybridには未確定を0件と仮定する例外があり、後述の共通post-statsで修正する。最適化後は全backendでこの契約を満たす。
|
||||
|
||||
## 実装監査で確認した主要ボトルネック
|
||||
|
||||
### 1. Accurate seedが「安価なseed」になっていない
|
||||
|
||||
`DIRECT_ACCURATE_SEED_WGSL` は脱出反復が24未満のピクセルだけを採用するが、loop自体は `maxIter` まで継続する(`index.html:191-200`)。その後、UNKNOWNピクセルはDSで反復0から再計算される(`index.html:1944-1964`)。
|
||||
|
||||
したがって、解析的に証明できない内点は f32で `maxIter` 回、さらにDSで `maxIter` 回計算される。遅く脱出する境界ピクセルも、f32の脱出まで計算した後にDSで再計算される。既存fuzz契約は「f32 escape iteration < 24」のみを採用するため、seedは24反復に達した時点でUNKNOWNとして終了しても最終DS結果は変わらない(`tests/ACCURATE_SEED_FUZZ.json`)。
|
||||
|
||||
### 2. 疎なDS補正用GPU queueが存在するが未使用
|
||||
|
||||
8 bucketのhistogram、prefix、scatter、indirect dispatch、およびqueued DS kernelはすでに実装されている(`index.html:416-800`)。pipelineとencoder helperも起動時に作成される(`index.html:1820-1827`, `1841-1842`, `1863-1867`)。
|
||||
|
||||
一方、本番の `correctUnknownFrame()` は全画面をrow stripでdispatchし、各strip後に同期する(`index.html:1994-2000`)。shader内で既知ピクセルはearly returnするが、全ピクセル分のinvocation、buffer read、分岐とsubmit/waitは残る。「sparse correction」という名前の判定も、実際には全画面走査を呼んでいる。
|
||||
|
||||
### 3. previewとfinalが同じ仕事を繰り返す
|
||||
|
||||
historyがないFAST viewではpreviewとfinalの2段階を使う(`index.html:1445-1453`)。しかしfinalではmeta/smoothをclearし、全ピクセルを0反復目から再計算する(`index.html:1897-1939`)。参照軌道cache keyにも `iter` が入るため、previewとfinalで軌道も再構築される(`index.html:1490`, `1513`)。
|
||||
|
||||
previewで既に脱出したピクセルと解析的に証明された内点は、`maxIter` を増やしても結果が変わらない。同じ参照軌道prefixと同じ数値式を使える場合、これらをfinalで再計算する必要はない。
|
||||
|
||||
### 4. 局所補修が全画面readbackと直列処理を繰り返す
|
||||
|
||||
`recoverNumericalUnknownTiles()` は各roundでmeta全体をGPUからCPUへ読み戻し、main threadで全画素を走査してtileを作る。最後にも再読込する(`index.html:1986`, `2031-2034`)。その後は、1 tileごとにreference build、GPU correction、stats readbackを順番に待つ。
|
||||
|
||||
また「広域failureならfull Deepが安い」とする `shouldPromoteFastToDeep()` が定義されているが、呼ばれていない(`index.html:2026-2028`)。実際のFAST background recoveryはfailure密度にかかわらずlocal recoveryから開始し、残ればfull Deepで画面全体を上書きする(`index.html:2050-2073`)。広域failureでは最初のlocal処理が無駄になる。
|
||||
|
||||
### 5. 参照軌道とBLA tableの再利用範囲が狭い
|
||||
|
||||
Deep/FAST referenceの再利用は同じ `iter` と完全に同じ `span` を要求する(`index.html:2089-2090`)。しかし参照軌道そのものは参照点 `c_ref` に依存し、表示spanには依存しない。参照点が新viewの有効範囲にあり、精度と長さが足りれば、既存の誤差guardを保ったままzoom後にも利用できる。
|
||||
|
||||
BLA tableも参照軌道から作るが、build keyはview座標・寸法込みであり、近接viewでも再構築される(`index.html:1598`, `1674-1679`, `2110-2114`)。さらに `BlaShadowService.cancelPending()` はPromiseを破棄するだけでworkerをterminateしない。同期的な旧viewのtable build中は、新viewの要求がworker queueで待たされる(`index.html:1594-1600`)。
|
||||
|
||||
### 6. キャンセルはpublishを止めるが、投入済みGPU仕事は止めない
|
||||
|
||||
token checkにより古い結果のpublishは防いでいるが、WebGPU queueへsubmit済みのcommand bufferはキャンセルできない。現行はDeep 2本、FAST 4本、BLA 8本をまとめて投入してから待つ(`index.html:1897-1925`)。目標strip時間はDeep 18 ms、FAST 22 msであるため、BLAでは理論上かなり長い旧view仕事がqueueに残り得る。
|
||||
|
||||
`cancelRender()` はJS状態とworker要求を止めるが、GPU queueをpreemptしない(`index.html:2094`)。新viewのcommandは旧viewの仕事の後ろへ並ぶため、無駄計算と入力後latencyの両方に影響する。
|
||||
|
||||
### 7. 初期化、bind group、history copy、exportに固定費がある
|
||||
|
||||
- 初期化では初期FAST表示に不要なAccurate、Deep、queue、AA pipelineまで一括compileする(`index.html:1811-1834`)。
|
||||
- stripごとに同じresource構成のbind groupを作り直す(`index.html:1857-1868`)。
|
||||
- stable history更新はfront texture全体をcopyする(`index.html:1982`)。
|
||||
- exportは単一readback workspaceでtileを完全直列処理し、各row band後に必ず `requestAnimationFrame` を待つ(`index.html:2276-2289`)。
|
||||
|
||||
これらは数値品質に触れずに削減できる固定費である。
|
||||
|
||||
### 8. backend間でUNKNOWN集計が一致していない
|
||||
|
||||
Directはreason 6をmetadataへ書くがstatsを集計しない。より重要なのは、Accurate hybridのDS kernelが `REASON_RANGE` を出し得る一方、orchestrationは完了後に `state.unresolved=0` と無条件設定する点である(`index.html:248`, `2131-2133`)。この場合、数値未確定を見逃してincomplete frameをhistoryへcommitする可能性がある。
|
||||
|
||||
全backendの最終color前に共通post-statsを通し、reason 0、reason 1〜5、reason 6をmetadata実体から集計する。これは最適化効果を測る前提であると同時に、品質契約の穴を塞ぐ変更でもある。
|
||||
|
||||
## 期待優先順位付き最適化案
|
||||
|
||||
| 期待順位 | 案 | 数値未確定 | 生成時間への期待 | キャンセル無駄 | 品質リスク |
|
||||
| ---: | --- | --- | --- | --- | --- |
|
||||
| P0 | Accurate seedを24反復で終了 | 同じ | 大(view依存) | 削減 | 低 |
|
||||
| P0 | DS/BigIntの更新後平方を再利用 | 同じ | 中〜大 | 削減 | 低 |
|
||||
| P0 | 計測契約とbaselineを固定 | 判定可能にする | 判定可能にする | 判定可能にする | 低 |
|
||||
| P1 | UNKNOWN統計をpost-pass化 | 同じ | interior-heavyで削減 | 削減 | 低 |
|
||||
| P1 | 既存の疎queueでDS補正 | 同じか減少 | 大 | 削減 | 低〜中 |
|
||||
| P1 | failure密度・reason別routing | 減少 | 大 | 削減 | 低 |
|
||||
| P1 | reference/BLA cache・延長・同一view再利用 | 減少 | 大 | 削減 | 低〜中 |
|
||||
| P1 | preview確定結果をfinalで再利用 | 同じ | 大 | 削減 | 低〜中 |
|
||||
| P1 | in-flight GPU時間を動的制限 | 同じ | 操作時に改善 | 大きく削減 | 低 |
|
||||
| P2 | GPU failure mapと並列local refs | 減少 | 大 | 削減 | 中 |
|
||||
| P2 | BLA checkpoint replayとnet-time gate | 同じ | BLA viewで改善 | 削減 | 中 |
|
||||
| P2 | pipeline/export/resource固定費削減 | 同じ | cold load/export改善 | 小幅削減 | 低 |
|
||||
| P2 | 残存UNKNOWNだけ任意精度fallback | 最終値を0へ | 少数時のみ有利 | 同じ | 中 |
|
||||
| P3 | active-pixel continuation/compaction | 同じか減少 | hard viewで最大 | 大きく削減 | 高 |
|
||||
| P3 | 証明付き内点判定・対称性 | 同じ | 対象viewで改善 | 削減 | 中〜高 |
|
||||
|
||||
### P0-1. Accurate seedを `lateThreshold` で打ち切る
|
||||
|
||||
`DIRECT_ACCURATE_SEED_WGSL` の各pixelは次のどちらかだけを行う。
|
||||
|
||||
- cardioid/period-2 bulbを解析的に証明できれば確定する。
|
||||
- 最大24回だけf32反復し、24未満で安全に脱出した場合だけ確定する。それ以外は直ちにUNKNOWNとする。
|
||||
|
||||
DS passは現在と同じくUNKNOWNを0反復目から計算する。このため最終meta/smoothは現行と同じで、seed側の24回を超える仕事だけが消える。打切りmetadataは後段queueが選べるreason 1〜5(例えば `REASON_ESCAPE_UNCERTAIN`)にし、reason 6やzero sentinelとは区別する。まず単独feature flagで実装し、既存60,000 fuzz、full-frame meta/smooth hash、strip-order testを通す。
|
||||
|
||||
### P0-2. DSとBigInt軌道で更新後の平方を再利用する
|
||||
|
||||
`DIRECT_DS_CORRECT_WGSL` は、各反復の更新前に `zr²`、`zi²`、`zr*zi` を計算し、更新直後の脱出判定で新しい `zr²` と `zi²` を再計算する。次のloop先頭では同じ新しい平方をもう一度計算する(`index.html:240-246`)。更新後の2平方をstateとして次反復へ渡せば、DS乗算は概ね5回/反復から3回/反復になり、個々の `ds_mul` の入力と演算順は変わらない。
|
||||
|
||||
Deep/Fast reference workerの `orbit()`、`orbitEscape()`、`verify()` にも、座標更新用と直後のmag用で同じBigInt平方を二度作る箇所がある(`index.html:1479-1483`, `1503-1506`)。丸め済みの更新後平方を次反復へ持ち越す。各変更は旧/新の軌道配列、脱出反復、meta/smoothをbit比較する。
|
||||
|
||||
### P0-3. 先に計測を直す
|
||||
|
||||
現行の `setView()` は `dirty || rendering` が終わるまでしか待たず、`refinePending` とbackground numeric recoveryを待たない(`index.html:2303`)。`lastRender` だけを比較すると、preview完了と最終補修完了を取り違える。
|
||||
|
||||
テストhookへ次を追加する。
|
||||
|
||||
- `waitForPrimary()`、`waitForFinal()`、`waitForNumericComplete()`。
|
||||
- generationごとのphase時刻: reference、BLA build/probe、primary、coverage+color、stats readback、DS、local refs、full Deep、recolor、history commit。
|
||||
- reason 0、reason 1〜5の内訳、reason 6をprimary/DS/local/finalの各時点で保存。
|
||||
- 全backendを共通post-statsへ通し、Accurate hybridで `unresolved=0` を仮定しない。
|
||||
- kernel steps: direct、perturbation、DS、BLA、replay、およびキャンセルされたgenerationのsteps。
|
||||
- submit、sync wait、readback回数・bytes・時間、reference/BLA cache hit。
|
||||
- adapter/browser/実ピクセル数/effective DPR/実効 `maxIter`。
|
||||
|
||||
`timestamp-query` が利用可能なadapterではGPU timestampを使い、利用できない場合はwall clockと明記する。累積runtime counterではなく、各generationの差分を記録する。
|
||||
|
||||
### P1-1. Deep/Accurate補正を既存の疎queueへ接続する
|
||||
|
||||
推奨フローは次のとおりである。
|
||||
|
||||
1. primary Deepはpixelごとのglobal atomicを避けるpost-stats variantを使う。
|
||||
2. histogram passでreason 1〜5だけを8個の開始反復bucketへ集計する。
|
||||
3. prefixとscatterでUNKNOWN index queueを作る。
|
||||
4. `dispatchWorkgroupsIndirect` でqueued `correct_pixel()` だけを実行する。
|
||||
5. queue overflow、invalid index、stale entryが1件でもあれば現行full-scanへfallbackする。
|
||||
|
||||
histogram/prefix/scatter基盤はAccurate f32 seed後のDSにも使う。ただし既存queued kernelはDeep referenceを読むperturbation版 `correct_pixel()` であり、絶対座標を使う `DIRECT_DS_CORRECT_WGSL` へそのまま接続してはならない。Direct-DSの数値式をそのまま持つ別のqueued kernelを作る。seedで確定したピクセルはqueueに入らず、DS invocation自体を発生させない。bucket順により、同じ程度の反復で終わるlaneが隣接し、SIMD divergenceも減る。
|
||||
|
||||
queue構築は全画面を軽く走査するため、UNKNOWNが非常に多い場合はfull-scanの方が速い可能性がある。adapter別の実測から密度crossoverを決め、queue/full-scanを選ぶ。各backendで元kernelと同じper-pixel arithmeticとoutput semanticsを保ち、出力差を許容しない。
|
||||
|
||||
同時に、primary hot loopからpixel単位のglobal atomic統計を外す。現在はFAST/Deep/BLAのUNKNOWN 1 pixelにつき最大2回、同じcounterへatomic addする(`index.html:268-270`, `339-347`, `1335-1339`)。特にreason 6が多いinterior-heavy viewでは、長い反復の最後に競合が集中する。Deepにはatomicを除いた `DEEP_PERTURB_POSTSTATS_WGSL` とworkgroup集約histogramが既にあるため、まずこれを接続し、FAST/BLAにも同型のpost-pass集計を用意する。meta/smoothは変更しない。
|
||||
|
||||
### P1-2. failure密度とreasonで補修経路を選ぶ
|
||||
|
||||
一次statsを得た直後に次のroutingを行う。
|
||||
|
||||
- failureが `max(60000, 8% of pixels)` 以上なら、既存 `shouldPromoteFastToDeep()` を使ってlocal-firstを省き、full Deepへ直行する。
|
||||
- `error-bound` / `escape-uncertain` はまずqueued DSを使う。
|
||||
- `reference-end` は「cache prefixが要求iterより短い」場合と「reference自身が脱出して `refLen=escape` になった」場合を分ける。前者だけ同一軌道を延長し、後者は原則として非脱出またはより長く安定する新referenceを選ぶ。脱出後の軌道を無条件に延長しない。
|
||||
- `range` / `rebase-gap` は失敗領域に近いlocal referenceを優先する。
|
||||
- reason 0は数値補修へ混ぜず、coverage契約だけで処理する。
|
||||
|
||||
各経路で `recovered / attempted` とmsを記録し、近接viewでは前回の成功経路をpriorとして使う。ただし結果が残る場合は現在と同じDeep/DS/local fallbackを最後まで保持する。
|
||||
|
||||
### P1-3. 参照軌道とBLA tableを実体ベースで再利用する
|
||||
|
||||
reference cache keyを「要求view」から次の実体へ変更する。
|
||||
|
||||
- 実際に選択された `referenceRe/referenceIm`。
|
||||
- precision bits。
|
||||
- orbitの最長計算済み長さ。
|
||||
- kernel format/version。
|
||||
|
||||
短い `iter` 要求にはprefixを返し、長い要求にはworker内に保持したBigInt状態から延長する。preview→finalで同じ軌道を作り直さない。
|
||||
|
||||
参照点選択でも二重計算を避ける。現在は `orbitEscape()` で候補を評価した後、選ばれた非中心候補を `orbit()` でもう一度0から計算する。候補評価時の最良軌道を保持すれば、選択軌道の再走査を省ける。
|
||||
|
||||
一方、Deepの独立 `verify()` は通常modeでも外さない。新規orbitは従来どおり検証し、検証済みimmutable orbitをcacheから再利用する時だけ検証結果も再利用する。第一pass中のhashだけへ置き換える案は独立guardの弱体化になるため、別実装の同等以上の検証器を用意できるまで採用しない。
|
||||
|
||||
再利用判定では `newSpan === oldSpan` を必須にせず、参照点が新view内または安全margin内にあること、精度が十分なこと、orbit長が足りることを確認する。Deepの誤差bound、reference-end、checkpoint検証は残す。guardが不利と判断した場合だけ新しい参照点を作る。
|
||||
|
||||
BLA tableは `reference buffer/hash + refLen + safety + table format version` でcacheする。viewごとに必要なのはtable再構築ではなく、pixel分布に対する採用probeだけである。旧view buildのキャンセルは、ReferenceServiceと同様のworker terminate/recreate、またはchunk化した協調キャンセルで実際のCPU仕事も止める。
|
||||
|
||||
### P1-4. previewの確定結果をfinalへ引き継ぐ
|
||||
|
||||
最初の段階ではorbit stateを保存せず、次だけを再利用する。
|
||||
|
||||
- `FIELD_ESCAPED`: finalの `maxIter` を増やしても最初の脱出反復とsmooth値は変わらない。
|
||||
- `FIELD_INTERIOR_PROVEN`: classとsmoothは変わらないが、packed iteration部はpreviewの `maxIter` になっている。final前の軽量patch passで `pack_meta(finalIter, FIELD_INTERIOR_PROVEN)` へ更新してから再利用する。
|
||||
- previewのreason 6: finalで再計算が必要。
|
||||
- 数値未確定reason 1〜5: finalまたは補修で再計算が必要。
|
||||
|
||||
Directはfinal kernelへ `unknownOnly` を加えるだけで実現できる。perturbationでは、previewとfinalが同じ最終長referenceのprefix、同じ参照点、同じpixel latticeを使う場合だけ既知pixelを再利用する。条件が1つでも違えば現行full recomputeへ戻す。
|
||||
|
||||
次段階では、未脱出pixelの `z/w/d/scaleExp/reference index/error` をcheckpointとして保存し、preview末尾からfinalを再開する。ただし浮動小数点演算順を変える可能性があるため、これはP3のactive-list方式と同じ厳しい品質gateを通す。
|
||||
|
||||
### P1-5. GPU先行投入量を時間予算で制限する
|
||||
|
||||
現在の固定 `burstLimit` を、直近stripのGPU時間EMAから求める。
|
||||
|
||||
```text
|
||||
burstLimit = clamp(1, maxBurst, floor(maxInFlightMs / predictedStripMs))
|
||||
```
|
||||
|
||||
初期案として、直近に入力があった時は `maxInFlightMs=24`、十分idleなら40程度から測定を始める。値は品質ではなくsubmit/waitとキャンセルlatencyのtrade-offなので、adapter別に自動調整する。
|
||||
|
||||
Directの全画面1 dispatchと高反復exportも同じ時間上限でstrip化する。perturbationだけを細分化しても、Directが長時間queueを占有すれば新viewは待たされる。
|
||||
|
||||
追加で、top-to-bottom固定順ではなく、focus座標と画面中心に近いmacro-tileから処理する。キャンセルされた場合でも、ユーザーが見ている領域に使える仕事の割合が増える。部分表示を行う場合はgeneration stamp付きwork textureを使い、旧generationを絶対にpublishしない。
|
||||
|
||||
### P2-1. failure mapをGPUで作り、local refsを並列化する
|
||||
|
||||
全meta readbackの代わりに、GPUで固定macro-tileごとの次の小さなsummaryを作る。
|
||||
|
||||
- reason別件数。
|
||||
- `sumX/sumY`、min/max座標。
|
||||
- UNKNOWN index rangeまたはcompact queue offset。
|
||||
|
||||
CPUへ戻すのは数KBのsummaryだけにする。必要なlocal referenceは2〜4 workerのpoolで並列構築し、完成したreferenceごとに1 tileずつ待たず、複数queueをまとめてGPUへsubmitする。補正対象も長方形全体ではなく、そのreferenceへ割り当てたUNKNOWN indexだけにする。
|
||||
|
||||
近接pan/zoomでは前frameのfailure mapを再投影してcoarse probeの優先候補に使える。ただし最終判定は新frameのguard結果で行う。
|
||||
|
||||
### P2-2. BLA fallbackを0反復目restartからcheckpoint replayへ変える
|
||||
|
||||
production candidateのBLAは近似chainが不安全になるとbaselineを0反復目から再実行する(`index.html:1320-1323`)。一方、検証用shadow kernelには最後のexact anchorからreplayする実装がある(`index.html:1130-1232`)。このcheckpoint/replayをproductionへ移すと、fallback pixelでBLA以前の仕事を捨てずに済む。
|
||||
|
||||
BLA採用条件も、現在の `fallback <= 12.5%` と `operation reduction >= 50%` だけでなく、table build、upload、probe、replayを含む予測net msで決める。cache miss時の固定費が節約時間を上回るviewではbaseline FASTを選ぶ。
|
||||
|
||||
品質gateではclassとescape iterationだけでなくsmooth値と最終RGBAも比較する。approximationが不確かなpixelは必ず既存FAST recurrenceへreplay/fallbackし、BLA由来の新しいUNKNOWNを許さない。
|
||||
|
||||
### P2-3. 初期化・resource・historyの固定費を減らす
|
||||
|
||||
- cold startではPRESENT、Direct、COLORだけを先にcompileし、FAST extended、Accurate、Deep、queue、AA、診断pipelineは必要時またはidle時にwarm upする。
|
||||
- pipeline/frame/contextごとにbind groupとtexture viewをcacheする。
|
||||
- 小さなstats readback bufferを毎回create/destroyせずringで再利用する。
|
||||
- front/back/historyを3 textureのrole rotationにし、history commit時の全texture copyをなくす。
|
||||
|
||||
これらはshader結果を変えないため、bit-identical testを要求する。
|
||||
|
||||
### P2-4. exportをGPU・readback・PNG圧縮のpipelineにする
|
||||
|
||||
現在の単一workspaceを2〜3個のringへ変え、tile N+1のGPU計算、tile Nのmap/copy、前bandのCompressionStream書込を重ねる。PNGのrow順は維持する。
|
||||
|
||||
各row band後の無条件 `requestAnimationFrame` は、経過時間に応じたyieldへ変える。操作応答性を守る必要がある時だけyieldし、headless/非操作exportでは不要な1 frame待ちを入れない。tile sizeも512固定ではなく、adapter memory limitと目標in-flight時間から選ぶ。
|
||||
|
||||
2x AAは4 sampleを維持する。サンプル削減は、pixel footprint全体が内点であることを証明できる場合以外は行わない。
|
||||
|
||||
### P2-5. 最後に残ったUNKNOWNだけを任意精度で解決する
|
||||
|
||||
local referenceにはmode別の `maxRefs` 上限があり、予算を使い切るとreason 1〜5が残り得る(`index.html:2029-2034`)。final Accurate/strict exportでは、残存indexだけをCPU workerの固定小数点または疎なGPU limb演算で `maxIter` まで直接評価し、escapedまたは各backendの現行maxIter metadataへpatchする。Deep/Fastはreason 6、Accurate Direct DSは `FIELD_INTERIOR_LIKELY` という違いを保持する。これにより、画面やtile全体を再計算せずに最終numeric UNKNOWNを0へ近づけられる。
|
||||
|
||||
これは集合所属の無限反復証明ではない。`maxIter` まで未脱出なら、起点backendの現行分類・metadata policyを変えない。対象が多い場合は先に別reference/full Deepへ戻し、疎な最終手段としてだけ使う。
|
||||
|
||||
### P3-1. active-pixel continuationへ移行する
|
||||
|
||||
最終的な高反復view向けには、1 pixelを1 dispatchで `maxIter` まで回す方式を次へ置き換える。
|
||||
|
||||
1. 64〜256反復のchunkだけ実行する。
|
||||
2. 脱出・証明済み内点を除き、active pixel indexと継続stateをcompactする。
|
||||
3. 次chunkはactive queueだけを実行する。
|
||||
4. preview時点のqueue/stateをfinalへそのまま継続する。
|
||||
|
||||
これにより、早期脱出pixelを後続chunkから除外し、workgroup divergenceを減らし、chunk境界でキャンセルできる。Directでは `z`、Deepでは `w/d/scaleExp/reference index/error` が必要になる。二重queueの論理使用量はactive数に比例するが、通常はworst caseとして全 `P` pixel分のbuffer確保が必要であり、メモリ量もKPIと採用条件に含める。
|
||||
|
||||
最大の注意点は再開前後で演算順、rescale位置、誤差伝播を変えないことである。baselineとbit-identicalにならない場合は高精度oracleに対する誤差非増加を証明し、既存pathをfallbackとして残す。
|
||||
|
||||
### P3-2. 証明できる場合だけ計算を省く
|
||||
|
||||
- Deep/FASTでも `c_ref + dc` をdouble-singleまたは区間で再構成し、main cardioid/period-2 bulbの片側安全判定を追加する。
|
||||
- center imaginary=0かつsample latticeが厳密に鏡映対称なviewでは、実軸対称性を使って片側のmeta/smoothをcopyする。
|
||||
- 周期検出やtile一括fillは、有限精度のepsilon一致だけで内点扱いしない。区間または収縮証明が成立した時だけ確定する。
|
||||
|
||||
素朴なperiodicity checking、境界だけを調べるrectangle fill、色差だけによるadaptive AAは小さい構造を消す可能性があるため採用しない。
|
||||
|
||||
## 品質契約
|
||||
|
||||
最適化を2種類に分ける。
|
||||
|
||||
### 構造だけを変える最適化
|
||||
|
||||
seedの早期終了、平方再利用、queue化、post-stats、bind group再利用、export pipelineなど、最終数値式と採用経路を変えない案は次を必須とする。
|
||||
|
||||
- `fieldMeta`、`fieldSmooth`、最終RGBAがbaselineとbit-identical。
|
||||
- known→UNKNOWNが0。
|
||||
- final reason 0が0。
|
||||
- 数値未確定を含むframeのhistory commitが0。
|
||||
- stale generationのpublishが0。
|
||||
|
||||
### 数値アルゴリズムを変える最適化
|
||||
|
||||
BLA、continuation、証明付き内点判定に加え、選択referenceや補修経路が変わるcache/routingは次を必須とする。同じreference実体をそのまま再利用するだけなら構造変更としてbit比較する。
|
||||
|
||||
- baselineで既知のpixelをUNKNOWNへ戻さない。
|
||||
- baselineとcandidateの双方が既知なら、classはbaselineおよび高精度oracleと一致する。
|
||||
- baseline UNKNOWNをcandidateがescapedまたはreason 6へ解決する差分は改善として許可し、raw classification mismatchへ加算しない。
|
||||
- exact同値を主張する経路ではfirst escape iteration mismatchが0。
|
||||
- BLA derived UNKNOWNとinternal failureが0。
|
||||
- smooth/RGBAのoracle誤差がbaseline以下。単なる画像平均ではなく最大誤差と差分pixel数を保存する。
|
||||
- final numeric UNKNOWNは各caseでbaseline以下。baselineが0ならcandidateも0。
|
||||
- 異常時は同一viewでbaselineへ自動fallbackする。
|
||||
|
||||
既存Accurate hybrid記録にはreset viewで2/1200のescape iteration mismatchがあるため、少なくともこれを悪化させず、目標は0とする。SwiftShaderの時間値はcorrectness用の相対参考であり、実GPU性能gateには使わない。
|
||||
|
||||
## KPI
|
||||
|
||||
画素数を `P = width * height`、reason別件数を `U_r` とする。
|
||||
|
||||
- `coverage_hole_ppm = 1e6 * U_0 / P`。hard targetはfinalで0。
|
||||
- `numeric_unknown_ppm = 1e6 * sum(U_1..U_5) / P`。
|
||||
- `operation_limit_ppm = 1e6 * U_6 / P`。情報指標であり失敗率ではない。
|
||||
- `recovery_rate = (U_primary - U_final) / max(1, U_primary)`。
|
||||
- `recovery_cost_ms_per_1k = (T_recovery_quiescent - T_primary) / recovered_kpixels`。
|
||||
- `cancelled_work_ratio = cancelled_kernel_steps / all_kernel_steps`。
|
||||
- `duplicate_work_ratio = repeated_kernel_steps / all_kernel_steps`。
|
||||
|
||||
latencyは最低でも次の5点を分ける。
|
||||
|
||||
- `T_feedback`: inputから再投影presentまで。
|
||||
- `T_primary`: 同一viewの最初の新規frameまで。
|
||||
- `T_final`: finalIter frameのpresentまで。
|
||||
- `T_recovery_quiescent`: background補修が成功、予算終了、またはfallback完了で停止するまで。数値未確定が残る場合も記録する。
|
||||
- `T_history_commit`: 数値未確定0のstable frameがhistoryへcommitされるまで。未確定が残って到達しないrunは、打切り時刻でcensored dataとして扱う。
|
||||
|
||||
p50、p95、maxを保存し、reference build、BLA build/probe、primary、DS、local refs、readback、color、export compressionの内訳も残す。
|
||||
|
||||
## ベンチマーク行列
|
||||
|
||||
### correctness corpus
|
||||
|
||||
- reset: center `(-0.5, 0)`, span `3.4`。
|
||||
- seahorse-low: `(-0.743643887037151, 0.13182590420533)`, span `0.1`。
|
||||
- cusp-low: `(0.25, 0)`, span `0.85`。
|
||||
- classic Seahorse deep: span `3.4e-13`, 2900 iterations。
|
||||
- BLAが有利なviewと、BLA probeがOFFになるSeahorse/Misiurewicz hard view。
|
||||
- `directPixelRatio` の4直前/直後、32直前/直後。
|
||||
- reason 1〜5をそれぞれ誘発する固定seed。
|
||||
- 実軸対称、interior-heavy、boundary-heavy、reference-end-heavyの各view。
|
||||
|
||||
### performance corpus
|
||||
|
||||
- 320×220 / 750 iter: 既存Accurate比較の継続用。
|
||||
- 654×690 / 2900 iter: 既存BLA prep scenarioとの接続用。
|
||||
- 1920×1080、および対象端末で可能なら4K。
|
||||
- 900 / 1500 / 3000 / 6000 / 12000 iter。
|
||||
- cold load、warm same view、same-span pan、small zoom、distant jump。
|
||||
- FAST/Accurate、BLA forced on/off、連続pan/zoomのcancel storm。
|
||||
- export SS1/SS2、balanced/strict。
|
||||
|
||||
`processMode` はpixel budgetとiteration係数を同時に変えるため、異なるmodeのraw msを直接比較しない。必ず実ピクセル数、DPR、iterを記録する。
|
||||
|
||||
実機は低電力integrated、現行integrated、discrete GPUを最低構成とし、SwiftShaderはcorrectnessだけに使う。各caseはwarmup後にbaseline/candidateを同一sessionで交互に10〜20回実行し、medianとp95を比較する。
|
||||
|
||||
## 合格基準
|
||||
|
||||
### hard quality/safety gate
|
||||
|
||||
- 全caseでfinal reason 0 = 0。
|
||||
- known→UNKNOWN、stale publish、不完全history commit = 0。
|
||||
- baseline既知pixelのknown→UNKNOWN = 0。双方既知pixelのclass mismatch = 0。
|
||||
- baseline UNKNOWNの解決は許可し、その結果が高精度oracleと矛盾しない。
|
||||
- exact経路のescape iteration mismatch = 0。
|
||||
- final numeric UNKNOWNは全caseでbaseline以下。strict/validate golden corpusの目標は0。
|
||||
- reference checkpoint mismatch、strip/full mismatch、WGSL error、uncaptured error、device loss = 0。
|
||||
- operation-limitは別集計し、数値未確定削減として数えない。
|
||||
|
||||
### performance gateの初期値
|
||||
|
||||
以下は実測後に調整する導入基準であり、達成値ではない。
|
||||
|
||||
- 対象hard viewの `T_primary` と `T_recovery_quiescent` のp50を20%以上、p95を15%以上短縮。`T_history_commit` 到達runも別集計で非退行とする。
|
||||
- 非対象viewでp50 +5%、p95 +10%を超える退行を出さない。
|
||||
- failure corpusのprimary numeric UNKNOWN中央値を50%以上、finalを90%以上削減し、各caseでは非増加。
|
||||
- BLA enabled viewはbuild+probe込みでbaselineより10%以上速い。BLA rejected/offの追加費用は5%または8 ms以下。
|
||||
- 1 burstのp95はFAST 25 ms以下、Deep 22 ms以下、max 50 ms以下を初期目標とする。
|
||||
- readback bytes、readback回数、correction sync waitsはbaseline以下。
|
||||
|
||||
## 実装ロードマップ
|
||||
|
||||
### Milestone 0: 観測とbaseline固定
|
||||
|
||||
1. generation単位のmetricsと3種類のUNKNOWN集計を追加する。
|
||||
2. `waitForFinal()` と `waitForNumericComplete()` をテストhookへ追加する。
|
||||
3. meta/smooth/RGBAのgolden artifactと操作traceを作る。
|
||||
4. 実GPUでcold/warm/cancel/export baselineを採る。
|
||||
|
||||
### Milestone 1: 小変更で重複仕事を除く
|
||||
|
||||
1. Accurate seedを24反復で終了する。
|
||||
2. Accurate DSとreference workerで更新後平方を再利用する。
|
||||
3. 全backendのUNKNOWNを共通post-statsで集計し、Deep/FAST/BLA primaryのpixel単位atomicを外す。
|
||||
4. `shouldPromoteFastToDeep()` を実経路へ接続する。
|
||||
5. reference/BLA cache keyを実体ベースへ変更し、旧BLA buildを実際にキャンセルする。
|
||||
6. stats readback bufferとbind groupを再利用する。
|
||||
|
||||
各変更を単独flagでA/Bし、bit-identicalと性能gateを確認する。
|
||||
|
||||
### Milestone 2: 疎queueとpreview再利用
|
||||
|
||||
1. Deep補正を既存bucket/indirect queueへ接続する。
|
||||
2. 同じcompaction基盤と新しいDirect-DS queued kernelをAccurate seed後へ接続する。
|
||||
3. previewのknown pixelと最終長reference prefixをfinalへ引き継ぐ。
|
||||
4. in-flight時間budgetを導入する。
|
||||
|
||||
queue異常時は現行full-scan、reuse条件不成立時は現行full recomputeへ戻す。
|
||||
|
||||
### Milestone 3: recoveryとBLA/exportのpipeline化
|
||||
|
||||
1. GPU failure summaryとUNKNOWN index queueを実装する。
|
||||
2. local reference worker poolとbatched correctionを実装する。
|
||||
3. production BLAへcheckpoint replayとnet-time gateを移す。
|
||||
4. export staging ringとtime-based yieldを実装する。
|
||||
5. strict/export向けの疎な任意精度最終fallbackを実装する。
|
||||
|
||||
### Milestone 4: active-list engine
|
||||
|
||||
chunk continuation、compaction、証明付き内点判定、厳密対称性をshadow-onlyで導入する。golden full-frame比較、複数adapter canary、kill switchを通過したbackendだけを標準へ昇格する。
|
||||
|
||||
## 展開とrollback
|
||||
|
||||
- 新経路はすべてfeature flag既定OFFから開始する。
|
||||
- 数値kernel/BLA変更は最初にshadow-onlyでbaselineと同時計算する。
|
||||
- adapter/browser bucket単位で10%→25%→50%→100%へ段階展開する。
|
||||
- hard quality gateはzero toleranceとし、1件でも違反したbucketは即baselineへ戻す。
|
||||
- default化後も旧path、kill switch、kernel hashを最低1 release残す。
|
||||
- mismatchまたは新しいUNKNOWN reasonを起こしたviewは座標、span、iter、adapter情報と共にgolden corpusへ追加する。
|
||||
|
||||
## 採用しない近道
|
||||
|
||||
次は生成時間を短く見せられるが、品質不変の条件に反するため採用しない。
|
||||
|
||||
- adaptive `maxIter`、DPR、export解像度、AA sample数を下げる。
|
||||
- error boundやBLA safetyを緩め、失敗を内点または脱出扱いする。
|
||||
- reason 6を「計算放棄」と数え、黒のまま補修済みと見なす。
|
||||
- 再投影画像の色が近いという理由だけで、新viewの数値metaを再利用する。
|
||||
- 未証明のperiodicity、rectangle border fill、色差ベースAAで内部sampleを省く。
|
||||
- BLA/近似経路からbaseline fallbackとgeneration tokenを外す。
|
||||
|
||||
この順序なら、まず明白な二重計算と未使用の疎処理を取り除き、その後に参照選択とGPU schedulingを改善できる。画質を守る責務は各最適化のfallbackと自動比較に持たせ、速度と数値未確定率のどちらか一方だけを改善したように見せない。
|
||||
733
index.html
733
index.html
File diff suppressed because it is too large
Load diff
39
tests/KERNEL_HASHES_V24_3_0.json
Normal file
39
tests/KERNEL_HASHES_V24_3_0.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"version": "7d56b1b4e3b54eff64b84e98607abb32eb23ae3ab60e1cd6f66f3223993d8c9f",
|
||||
"DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922",
|
||||
"ACTIVE_PREPARE_WGSL": "27bb3af8310bd0784888f2a648896e1fcf5872b4a9a3ade883ebe95a4868fa6e",
|
||||
"DIRECT_ACTIVE_INIT_WGSL": "a57887e12e91ecdc0272c97e2097268f4b5f05cb467e8a77adfc5263331b2b9b",
|
||||
"DIRECT_ACTIVE_CONTINUE_WGSL": "b0fbfcb9ffccf37b96947ec21c883ad4ce984bbb331c7a3b59a54cdc3e300f93",
|
||||
"ACTIVE_COMPARE_WGSL": "81c37558fd8937fd0560ccb41f73ed66ac5d98d023153ed0b82d8b5a075c3de9",
|
||||
"DIRECT_ACCURATE_SEED_WGSL": "247e1ff1c3d400daf358144b55c6f50c99d337b4e886e0b957e371d39f9c87cf",
|
||||
"DIRECT_DS_CORRECT_WGSL": "e31fa2f8347acc5ea2eb11bbcb3b05a36a3ba72cc1446ded141b1b4207b873e3",
|
||||
"DIRECT_DS_QUEUE_BUILD_WGSL": "95cfcf9cb80e53bd112b0868c035564cb812bdd957de583e2d3ee052bceb1a15",
|
||||
"DIRECT_DS_CORRECT_QUEUE_WGSL": "bb76e1ae09c7494a53e2ae4cad46807f2c671c5afa5bf5649981e1ab60cc2f3e",
|
||||
"FAST_PERTURB_WGSL": "2cc5841740456f10ed3f64005e72ac6ff87d2c38926260a1fcb1521a389013a0",
|
||||
"FAST_PERTURB_POSTSTATS_WGSL": "3680d561d80af6327640d9b5caa127ad67f6a6fc9e499af6dbb98ccdaa261d0c",
|
||||
"DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65",
|
||||
"DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1",
|
||||
"DEEP_ACTIVE_INIT_WGSL": "756373375ad9b21a884ac6906ecf72990612a86ecfde77caf5150fb37cf75604",
|
||||
"DEEP_ACTIVE_RESUME_INIT_WGSL": "a25c0b74bc66b7b91269bc6195ddde8b295f659ea72ec415fcf3fda3d66c126e",
|
||||
"DEEP_ACTIVE_CONTINUE_WGSL": "afe49e748e34d78c01090015c0e2ce5971c0ec6ab7587bba6c25631266acb4f0",
|
||||
"PERTURB_CERTIFIED_INTERIOR_WGSL": "3bd61a7f5cf96e4e52797f5f00a20c848a01fd3ff8eca9a939e8ae87f37568a5",
|
||||
"DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf",
|
||||
"DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824",
|
||||
"DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da",
|
||||
"DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1",
|
||||
"DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88",
|
||||
"DEEP_CORRECT_QUEUE_WGSL": "1580c6bab7d1aa2c4eb2c7e8d6f3cc4d32b90b86b9264561269e1857d2826bae",
|
||||
"UNKNOWN_STATS_WGSL": "df1967bcdacf29831403fa53aadaae17ed138b36d1988cf20f6a6ed3e43e519a",
|
||||
"SYMMETRY_COPY_WGSL": "658e1cf2b9f40d8e759abe2269c041f582a80727cb24e2f48a405cb8676d70e1",
|
||||
"FAILURE_TILE_MAP_WGSL": "8308f57151b119363ad456a7e1255c5445ed5e022ccf53e923a4b13ab2a1ca91",
|
||||
"COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3",
|
||||
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc",
|
||||
"BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f",
|
||||
"BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55",
|
||||
"BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1",
|
||||
"BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019",
|
||||
"BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL": "f55368ed55cda77af550ed2bf38b4027b21e08b6c17d67997e61fcbea17a6c84",
|
||||
"BLA_GPU_FAST_FRAME_WGSL": "1d4f2c2105f0c50a3b5fb1368b262cfe6e0114e20333acdd1b6d8e8093434290",
|
||||
"BLA_GPU_FAST_FRAME_POSTSTATS_WGSL": "6e1d0ac58a1b785075eddb525f565ca8349f50bab2189a537f62bec6a987f6ea"
|
||||
}
|
||||
31
tests/OPTIMIZATION_BENCHMARK.json
Normal file
31
tests/OPTIMIZATION_BENCHMARK.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"date": "2026-08-25",
|
||||
"scope": "BigInt reference-worker recurrence only; not a browser, GPU, canvas, or end-to-end benchmark",
|
||||
"runtime": "Node worker model",
|
||||
"corpus": {
|
||||
"points": 240,
|
||||
"iterations": 700,
|
||||
"precisionBits": "192..320",
|
||||
"runs": 7
|
||||
},
|
||||
"equivalence": {
|
||||
"checksumMismatch": 0,
|
||||
"separateStateRegressionCases": 2000,
|
||||
"separateStateRegressionMismatch": 0
|
||||
},
|
||||
"median": {
|
||||
"oldMs": 201.5509,
|
||||
"newMs": 115.3558,
|
||||
"speedup": 1.7472,
|
||||
"timeReduction": 0.4277
|
||||
},
|
||||
"runs": [
|
||||
{"oldMs": 222.9260, "newMs": 137.0568},
|
||||
{"oldMs": 206.2790, "newMs": 113.4711},
|
||||
{"oldMs": 218.0533, "newMs": 146.6120},
|
||||
{"oldMs": 201.5509, "newMs": 120.1586},
|
||||
{"oldMs": 163.7775, "newMs": 115.3558},
|
||||
{"oldMs": 166.6713, "newMs": 111.5755},
|
||||
{"oldMs": 172.1890, "newMs": 107.0833}
|
||||
]
|
||||
}
|
||||
55
tests/edge_cdp_gate.mjs
Normal file
55
tests/edge_cdp_gate.mjs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import process from 'node:process';
|
||||
|
||||
const port=Number(process.argv[2]||9333);
|
||||
const url=process.argv[3]||'file:///C:/Users/%E3%81%82/Desktop/mandelbrot/index.html?frontier-gate=reset';
|
||||
const timeoutMs=Number(process.argv[4]||300000);
|
||||
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
||||
|
||||
async function endpoint(path,options){
|
||||
const response=await fetch(`http://127.0.0.1:${port}${path}`,options);
|
||||
if(!response.ok)throw new Error(`CDP HTTP ${response.status}: ${await response.text()}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const deadline=Date.now()+timeoutMs;
|
||||
let version;
|
||||
while(Date.now()<deadline){
|
||||
try{version=await endpoint('/json/version');break}catch{await sleep(100)}
|
||||
}
|
||||
if(!version)throw new Error('Edge remote debugging endpoint did not start');
|
||||
|
||||
let target;
|
||||
try{target=await endpoint(`/json/new?${encodeURIComponent(url)}`,{method:'PUT'})}
|
||||
catch{const targets=await endpoint('/json/list');target=targets.find(item=>item.type==='page')}
|
||||
if(!target?.webSocketDebuggerUrl)throw new Error('No debuggable page target');
|
||||
|
||||
const ws=new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve,reject)=>{ws.addEventListener('open',resolve,{once:true});ws.addEventListener('error',reject,{once:true})});
|
||||
let sequence=0;
|
||||
const pending=new Map();
|
||||
ws.addEventListener('message',event=>{
|
||||
const message=JSON.parse(event.data);
|
||||
if(!message.id)return;
|
||||
const waiter=pending.get(message.id);if(!waiter)return;
|
||||
pending.delete(message.id);
|
||||
if(message.error)waiter.reject(new Error(JSON.stringify(message.error)));else waiter.resolve(message.result);
|
||||
});
|
||||
function command(method,params={}){
|
||||
const id=++sequence;
|
||||
return new Promise((resolve,reject)=>{pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}))});
|
||||
}
|
||||
|
||||
await command('Runtime.enable');
|
||||
await command('Page.enable');
|
||||
await command('Page.navigate',{url});
|
||||
let result;
|
||||
while(Date.now()<deadline){
|
||||
const evaluated=await command('Runtime.evaluate',{expression:`JSON.stringify({gate:document.documentElement.getAttribute('data-frontier-gate'),body:document.body?.textContent||'',href:location.href})`,returnByValue:true});
|
||||
const value=JSON.parse(evaluated.result.value);
|
||||
if(value.gate){result=value;break}
|
||||
await sleep(250);
|
||||
}
|
||||
try{await command('Browser.close')}catch{}
|
||||
ws.close();
|
||||
if(!result)throw new Error('Frontier gate timed out');
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
306
tests/optimization_regression.mjs
Normal file
306
tests/optimization_regression.mjs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {correctnessCases,performanceCases,interactionStabilityViews,productionPerformanceThresholds,evaluateProductionPerformance,runCorrectnessCorpus,runPerformanceCorpus,runInteractionStabilityCorpus} from './optimization_webgpu_corpus.mjs';
|
||||
|
||||
const root=fileURLToPath(new URL('../',import.meta.url));
|
||||
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
||||
assert.equal(scripts.length,2);
|
||||
for(const code of scripts)new vm.Script(code);
|
||||
const precisionWorker=scripts[1].match(/function precisionFallbackWorkerSource\(\)\{return String\.raw`([\s\S]*?)`\}/);
|
||||
assert.ok(precisionWorker);
|
||||
new vm.Script(precisionWorker[1]);
|
||||
|
||||
const context={globalThis:null};context.globalThis=context;vm.createContext(context);
|
||||
new vm.Script(scripts[0]).runInContext(context);
|
||||
const kernels=context.MANDEL_WEBGPU_KERNELS;
|
||||
const expected=JSON.parse(await fs.readFile(new URL('./KERNEL_HASHES_V24_3_0.json',import.meta.url),'utf8'));
|
||||
const actual={};
|
||||
for(const [key,value] of Object.entries(kernels)){
|
||||
if(typeof value==='string')actual[key]=crypto.createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
assert.deepEqual(actual,expected);
|
||||
assert.equal(correctnessCases.length,12);
|
||||
assert.equal(performanceCases.length,6);
|
||||
assert.equal(interactionStabilityViews.length,3);
|
||||
assert.ok(correctnessCases.some(x=>x.id==='reported-black-regression'&&x.fixed&&x.frontierCheck));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e5-fast'));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e5-accurate'));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e8-fast'));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e8-accurate'));
|
||||
assert.ok(performanceCases.some(x=>x.id==='fast-1e14-800x600'&&x.absoluteP95Ms<2000));
|
||||
assert.equal(productionPerformanceThresholds.targetP50Reduction,.20);
|
||||
assert.equal(productionPerformanceThresholds.targetP95Reduction,.15);
|
||||
assert.equal(typeof runCorrectnessCorpus,'function');
|
||||
assert.equal(typeof runPerformanceCorpus,'function');
|
||||
assert.equal(typeof runInteractionStabilityCorpus,'function');
|
||||
assert.equal(evaluateProductionPerformance({records:[{id:'target',target:true,summary:{recovery:{p50:79,p95:84}}}]},{records:[{id:'target',summary:{recovery:{p50:100,p95:100}}}]}).pass,true);
|
||||
assert.equal(evaluateProductionPerformance({records:[{id:'target',target:true,summary:{recovery:{p50:90,p95:90}}}]},{records:[{id:'target',summary:{recovery:{p50:100,p95:100}}}]}).pass,false);
|
||||
const corpusSource=await fs.readFile(new URL('./optimization_webgpu_corpus.mjs',import.meta.url),'utf8');
|
||||
assert.match(corpusSource,/assert\.equal\(result\.numericComplete,true/);
|
||||
assert.match(corpusSource,/assert\.equal\(numeric,0,testCase\.id\+' final numeric UNKNOWN'/);
|
||||
assert.match(corpusSource,/assert\.equal\(result\.state\.historyReady,true/);
|
||||
assert.match(corpusSource,/assert\.equal\(compile\.productionEligible,expectProduction\)/);
|
||||
assert.match(corpusSource,/assert\.equal\(compile\.candidateEnabled,true\)/);
|
||||
assert.match(corpusSource,/await t\.waitForNumericComplete\(\{timeout\}\)/);
|
||||
|
||||
assert.match(kernels.DIRECT_ACCURATE_SEED_WGSL,/if\(n>=p\.lateThreshold\)\{fieldMeta\[out\]=pack_unknown/);
|
||||
assert.match(kernels.DIRECT_DS_CORRECT_WGSL,/var zr2=DS\(0\.0,0\.0\);var zi2=DS\(0\.0,0\.0\)/);
|
||||
assert.match(kernels.DIRECT_DS_QUEUE_BUILD_WGSL,/atomicAdd\(&queueStats\[2\],1u\)/);
|
||||
assert.match(kernels.DIRECT_DS_CORRECT_QUEUE_WGSL,/atomicAdd\(&queueStats\[6\],1u\)/);
|
||||
assert.match(kernels.DIRECT_DS_CORRECT_QUEUE_WGSL,/let zri=ds_mul\(zr,zi\)/);
|
||||
assert.match(html,/accurateQueuedPixels/);
|
||||
assert.match(html,/q\.selected===q\.enqueued&&q\.enqueued===q\.dispatchCount&&q\.dispatchCount===q\.processed/);
|
||||
assert.match(html,/accurateQueueFallbacks/);
|
||||
assert.match(html,/encodeDeepBucketHistogram\(e/);
|
||||
assert.match(html,/q\.selected===q\.enqueued&&q\.enqueued===q\.dispatchCount&&q\.dispatchCount===q\.processed/);
|
||||
assert.match(html,/reusePreview/);
|
||||
assert.match(html,/canUseStableReprojection\(\)\|\|fastNeedsExtended\(snapshot\(\),canvas\.width\)\)return\[finalIter\]/);
|
||||
assert.match(html,/numericInFlightBudgetMs/);
|
||||
assert.match(html,/const ADAPTIVE_ITER_QUANTUM=64/);
|
||||
assert.match(html,/function pixelPrecisionBits\(snap=snapshot\(\),w=canvas\.width,guard=32\)/);
|
||||
assert.match(html,/pixelLog2=log2FixedAt\(snap\.span,snap\.bits\)-Math\.log2\(Math\.max\(1,w\)\)/);
|
||||
assert.match(html,/targetBits=pixelPrecisionBits\(snap,width,40\)/);
|
||||
assert.match(html,/function fastReferenceBits\(snap,w=canvas\.width\)\{return pixelPrecisionBits\(snap,w,32\)\}/);
|
||||
assert.match(html,/const bits=baseBits\+attempt\*32,a=pixel\(s,indices\[i\],bits\),b=pixel\(s,indices\[i\],bits\+32\)/);
|
||||
assert.match(html,/for\(let attempt=0;attempt<4&&!accepted\[i\];attempt\+\+\)/);
|
||||
assert.match(html,/for\(let i=0;i<meta\.length;i\+\+\)/);
|
||||
assert.match(html,/for\(let start=0;start<indices\.length;start\+=chunkSize\)/);
|
||||
assert.doesNotMatch(html,/UNRESOLVED_BLACK_MIN_ZOOM_EXP|colorUnresolvedInsteadOfBlack|guardedFastDeep|fast-quality-floor-before-1e100/);
|
||||
assert.match(html,/Math\.ceil\(raw\/quantum\)\*quantum/);
|
||||
assert.match(html,/this\.blaFastDisabled=true;this\.blaFastOptIn=false/);
|
||||
assert.doesNotMatch(html,/deep-stable-bootstrap|forceStableDeep/);
|
||||
assert.match(html,/A FAST frame is not publishable while any numeric UNKNOWN remains/);
|
||||
assert.match(html,/if\(initialFailed>0\)\{const mr=await recoverNumericalUnknownTiles/);
|
||||
assert.match(html,/if\(numericalFailureCount\(stats\)>0\)\{const pf=await recoverResidualPrecision/);
|
||||
assert.match(html,/if\(isFinal\)\{const remaining=numericalFailureCount.*frameを公開しません/);
|
||||
assert.match(html,/viewRe:snap\.re,viewIm:snap\.im/);
|
||||
assert.match(html,/function referenceOriginMatchesView\(/);
|
||||
assert.match(html,/!referenceOriginMatchesView\(src,snap\)/);
|
||||
assert.match(html,/stabilityPolicy:/);
|
||||
assert.match(html,/fastBackgroundDeepOnFailureOnly:true,deepProductionActive:false,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK/);
|
||||
const fastStableStart=html.indexOf('}else if(isFinal&&fastExtended){');
|
||||
const fastStableSource=html.slice(fastStableStart,html.indexOf('}else if(isFinal){',fastStableStart));
|
||||
assert.doesNotMatch(fastStableSource,/scheduleFastNumericRecovery/);
|
||||
assert.match(fastStableSource,/recoverNumericalUnknownTiles/);
|
||||
assert.match(fastStableSource,/recoverResidualPrecision/);
|
||||
assert.match(html,/ensureAccuratePipelines/);
|
||||
assert.match(html,/ensureCorrectionPipelines/);
|
||||
assert.match(html,/precisionFallbackWorkerSource/);
|
||||
assert.match(html,/recoverResidualPrecision/);
|
||||
assert.match(html,/estimatedNetMs/);
|
||||
assert.match(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/fn baseline_from\(s0:S,pre:u32\)->Result/);
|
||||
assert.match(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/var anchor=s;loop/);
|
||||
assert.match(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/anchor=s;\}\}/);
|
||||
assert.doesNotMatch(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/baseline\(g,st\)/);
|
||||
assert.equal((kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL.match(/baseline_from\(anchor,st\)/g)||[]).length,7);
|
||||
assert.ok(kernels.BLA_GPU_FAST_FRAME_WGSL.includes('baseline_from(anchor,st)'));
|
||||
assert.match(kernels.ACTIVE_PREPARE_WGSL,/indirect\[0\]=\(n\+63u\)\/64u/);
|
||||
assert.match(kernels.DIRECT_ACTIVE_CONTINUE_WGSL,/struct DirectState\{zr:f32,zi:f32,n:u32/);
|
||||
assert.match(kernels.DIRECT_ACTIVE_CONTINUE_WGSL,/atomicLoad\(&countIn\[0\]\)/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/struct DeepState\{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32\}/);
|
||||
assert.match(kernels.DEEP_ACTIVE_INIT_WGSL,/states\[local\]=DeepState/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/let out=p\.outputBase\+\(local\/p\.tileW\)\*p\.outputStride/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/if\(s\.m>p\.refLen\)/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/if\(s\.m>=p\.refLen\)/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/struct I\{lo:f32,hi:f32\}/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/fn outward\(/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/lhs\.hi<rhs\.lo/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/bulb\.hi<0\.0625/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/if\(p\.refLen<1u\)\{proven\[i\]=0u;return;\}/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/let ref=refs\[1u\]/);
|
||||
assert.match(html,/runDirectActiveShadow/);
|
||||
assert.match(html,/dispatchWorkgroupsIndirect\(indirect,0\)/);
|
||||
assert.match(html,/direct-active-shadow-resume/);
|
||||
assert.match(html,/resumedFromPreview:previewIter>0/);
|
||||
assert.match(html,/runDeepActiveShadow/);
|
||||
assert.match(html,/computeDeepActiveFrame/);
|
||||
assert.match(html,/workPerDispatch=PIXEL_FRONTIER_WORK,maxPassesPerSubmit=1/);
|
||||
assert.match(html,/const workBudget=deep\?4000000:48000000/);
|
||||
assert.match(html,/const PIXEL_FRONTIER_PRODUCTION=false,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000/);
|
||||
assert.match(html,/PIXEL_FRONTIER_CANDIDATE=.*frontier-gate/);
|
||||
assert.match(html,/PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION\|\|PIXEL_FRONTIER_CANDIDATE/);
|
||||
assert.match(html,/function operationLimitFrontier\(meta,w,h\)/);
|
||||
assert.match(html,/function pixelFrontierIterationFloor\(snap,w,initialIter\)/);
|
||||
assert.match(html,/iter>=minimumIter&&stableRounds>=2/);
|
||||
assert.match(html,/count\*iter/);
|
||||
assert.match(html,/pixel frontier queue invariant failed/);
|
||||
assert.match(html,/compareCurrentToFrontierBaseline/);
|
||||
assert.match(html,/productionEligible:PIXEL_FRONTIER_PRODUCTION/);
|
||||
assert.match(kernels.DEEP_CORRECT_QUEUE_WGSL,/bitcast<u32>\(p\._numeric2\)\+localQi/);
|
||||
assert.match(html,/const maxBatch=Math\.max\(1,Math\.floor\(PIXEL_FRONTIER_WORK\/Math\.max\(1,iter\)\)\);let offset=0/);
|
||||
assert.match(html,/this\.deepParams\(f\.w,f\.h,f\.w,f\.h,0,0,iter,snap,\.5,\.5,0,refX,refY,2,f\.w,0,offset\)/);
|
||||
assert.match(html,/maxPixels=Math\.max\(1,Math\.floor\(4000000\/Math\.max\(1,iter\)\)\),tileW=Math\.min\(f\.w,maxPixels\)/);
|
||||
assert.match(html,/correctUnknownFrame\(snap,iter,token,referencePixel,numericalFailureCount\(stats\)\)/);
|
||||
assert.match(html,/ensureDeepActiveWorkspace/);
|
||||
assert.doesNotMatch(html,/return this\.computeDeepActiveFrame\(snap,iter,token,forceStrict,referencePixel\)/);
|
||||
assert.match(html,/return this\.computePerturbFrameTiled\(snap,iter,true,token,forceStrict,referencePixel,false,colorStyle,colorKey\)/);
|
||||
assert.match(html,/state\.gpuInitFailed=true;markDirty\(false\);setTimeout/);
|
||||
assert.match(html,/deep-active-shadow-resume/);
|
||||
assert.match(html,/deep-active-compare-shadow/);
|
||||
assert.match(html,/runtime\.deepActiveShadowLatest=result/);
|
||||
assert.match(html,/runCertifiedInteriorShadow/);
|
||||
assert.match(html,/precisionFallback\.request\(snap,iter,w,h,gpu\.provenIndices,FIELD_INTERIOR_PROVEN\)/);
|
||||
assert.match(html,/falsePositive===0&&inconclusive===0/);
|
||||
assert.match(html,/oracleBits:\[oracleBits,oracleBits\+32\]/);
|
||||
assert.match(html,/productionEligible:false/);
|
||||
assert.match(html,/waitForPrimary\(/);
|
||||
assert.match(html,/waitForFinal\(/);
|
||||
assert.match(html,/waitForNumericComplete\(/);
|
||||
assert.match(html,/fieldHashes\(\)/);
|
||||
assert.match(html,/readFieldAll\(\)/);
|
||||
assert.match(html,/generationMetrics:\[\]/);
|
||||
assert.match(html,/primaryNumericalFailures/);
|
||||
assert.match(html,/numericQuiescentMs/);
|
||||
assert.match(html,/historyCommitMs/);
|
||||
assert.match(html,/SYMMETRY_COPY_WGSL/);
|
||||
assert.match(html,/canUseRealAxisSymmetry/);
|
||||
assert.match(html,/encodeSymmetryCopy/);
|
||||
assert.match(html,/FAILURE_TILE_MAP_WGSL/);
|
||||
assert.match(html,/readNumericalFailureTiles/);
|
||||
assert.match(html,/tileReferencePool=Array\.from\(\{length:3\}/);
|
||||
assert.match(html,/Promise\.all\(batch\.map/);
|
||||
assert.match(html,/if\(p\.key===key\)return p\.promise/);
|
||||
assert.match(html,/prefetchFinalReference/);
|
||||
assert.match(html,/this\.exportWs=\[\]/);
|
||||
assert.match(html,/exportRingDepth\(tile=this\.exportTileSize\(\)\)/);
|
||||
assert.match(html,/Promise\.allSettled\(batch\.map\(item=>item\.promise\)\)/);
|
||||
assert.match(html,/if\(failed\)throw failed\.reason/);
|
||||
assert.match(html,/performance\.now\(\)-lastYield<24/);
|
||||
assert.doesNotMatch(html,/await png\.rows\(band\);await new Promise\(requestAnimationFrame\)/);
|
||||
assert.match(html,/frameReadback\(kind,size\)/);
|
||||
assert.match(html,/length:3.*kind\+'-readback-'/);
|
||||
const statsReadbackSource=html.slice(html.indexOf('async readUnresolvedStats'),html.indexOf('async readUnresolved(){'));
|
||||
assert.match(statsReadbackSource,/this\.frameReadback\('unknown-stats'/);
|
||||
assert.doesNotMatch(statsReadbackSource,/destroy\(r\)/);
|
||||
assert.match(html,/cachedBindGroup\(pipeline,entries\)/);
|
||||
assert.match(html,/cachedTextureView\(texture\)/);
|
||||
assert.match(html,/this\.cachedBindGroup\(this\.direct/);
|
||||
assert.match(html,/this\.cachedBindGroup\(this\.present/);
|
||||
assert.match(html,/this\.clearBindGroupCache\(\)/);
|
||||
const historyCommitSource=html.slice(html.indexOf('commitHistory(view='),html.indexOf('invalidateHistory(){'));
|
||||
assert.match(historyCommitSource,/f\.history=f\.front/);
|
||||
assert.doesNotMatch(historyCommitSource,/copyTextureToTexture/);
|
||||
assert.match(html,/colorTarget\(f=this\.frame\)/);
|
||||
{
|
||||
const roles={front:'a',back:'b',history:'c',spare:'c'};
|
||||
const commit=()=>{roles.history=roles.front};
|
||||
const render=()=>{if(roles.back===roles.history){const old=roles.history;roles.back=roles.spare;roles.spare=old}assert.notEqual(roles.back,roles.history);assert.notEqual(roles.back,roles.front);[roles.front,roles.back]=[roles.back,roles.front]};
|
||||
for(let i=0;i<1000;i++){if(i%7===0||i%11===0)commit();else render();assert.equal(new Set([roles.front,roles.back,roles.history,roles.spare]).size,3)}
|
||||
}
|
||||
const recoverySource=html.slice(html.indexOf('async function recoverNumericalUnknownTiles'),html.indexOf('async function recoverResidualPrecision'));
|
||||
assert.match(recoverySource,/readNumericalFailureTiles/);
|
||||
assert.match(recoverySource,/const currentMeta=await r\.readMetaAll\(\)/);
|
||||
assert.match(recoverySource,/Uint32Array\.from\(tileIndices\)/);
|
||||
assert.match(html,/p\.unknownOnly==2u && reason!=0u/);
|
||||
assert.match(html,/fast-coverage-repair-and-color/);
|
||||
assert.match(html,/reprojectionSafe\(rawHistory,2\.25,\.45\)/);
|
||||
assert.match(html,/scheduleFastNumericRecovery/);
|
||||
assert.match(html,/runFastNumericRecovery/);
|
||||
assert.match(html,/numericFrameComplete\(\)/);
|
||||
assert.match(html,/requestBuild\(\{ctx,snap,iter,width,height,refPixel,safety=1\/64\}\)/);
|
||||
assert.match(html,/type:'build'/);
|
||||
assert.match(html,/buildOnly=d\.type==='build'/);
|
||||
assert.equal((await fs.readdir(root)).filter(name=>name.startsWith('index.baseline')&&name.endsWith('.html')).length,0);
|
||||
|
||||
let randomState=0x12345678;
|
||||
function randomU32(){randomState=(Math.imul(randomState,1664525)+1013904223)>>>0;return randomState}
|
||||
function roundShift(value,bits){const negative=value<0n,absolute=negative?-value:value,half=1n<<(BigInt(bits)-1n),rounded=(absolute+half)>>BigInt(bits);return negative?-rounded:rounded}
|
||||
function orbitOld(bits,re,im,iterations){const bailout=16n*(1n<<BigInt(bits));let zr=0n,zi=0n;const states=[];for(let n=0;n<iterations;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);states.push([zr,zi,mag]);if(mag>bailout)return{escape:n+1,states}}return{escape:0,states}}
|
||||
function orbitNew(bits,re,im,iterations){const bailout=16n*(1n<<BigInt(bits));let zr=0n,zi=0n,zr2=0n,zi2=0n;const states=[];for(let n=0;n<iterations;n++){zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);const mag=zr2+zi2;states.push([zr,zi,mag]);if(mag>bailout)return{escape:n+1,states}}return{escape:0,states}}
|
||||
|
||||
for(let sample=0;sample<2000;sample++){
|
||||
const bits=80+(randomU32()%177),one=1n<<BigInt(bits);
|
||||
const re=(BigInt(randomU32()%7000001)-3500000n)*one/1000000n;
|
||||
const im=(BigInt(randomU32()%4000001)-2000000n)*one/1000000n;
|
||||
const iterations=20+(randomU32()%180);
|
||||
assert.deepEqual(orbitNew(bits,re,im,iterations),orbitOld(bits,re,im,iterations));
|
||||
}
|
||||
|
||||
const f=Math.fround;
|
||||
function ds(h,l=0){return{h:f(h),l:f(l)}}
|
||||
function quick(a,b){const q=f(a+b),e=f(b-f(q-a));return ds(q,e)}
|
||||
function sum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return ds(q,e)}
|
||||
function product(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return ds(q,e)}
|
||||
function add(a,b){const q=sum(a.h,b.h);return quick(q.h,f(f(q.l+a.l)+b.l))}
|
||||
function neg(a){return ds(-a.h,-a.l)}
|
||||
function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const q=product(a.h,b.h),e=f(f(f(q.l+f(a.h*b.l))+f(a.l*b.h))+f(a.l*b.l));return quick(q.h,e)}
|
||||
function scale(a,b){const q=product(a.h,b);return quick(q.h,f(q.l+f(a.l*b)))}
|
||||
function sameDs(a,b){return Object.is(a.h,b.h)&&Object.is(a.l,b.l)}
|
||||
function dsOrbit(cr,ci,iterations,carrySquares){let zr=ds(0),zi=ds(0),zr2=ds(0),zi2=ds(0);const states=[];for(let n=0;n<iterations;n++){if(!carrySquares){zr2=mul(zr,zr);zi2=mul(zi,zi)}const zri=mul(zr,zi);zr=add(sub(zr2,zi2),cr);zi=add(scale(zri,2),ci);const nextZr2=mul(zr,zr),nextZi2=mul(zi,zi),mag=add(nextZr2,nextZi2);states.push({zr,zi,mag});zr2=nextZr2;zi2=nextZi2}return states}
|
||||
|
||||
for(let sample=0;sample<600;sample++){
|
||||
const cr=ds(f((randomU32()/0xffffffff)*3.5-2.5),f((randomU32()/0xffffffff-.5)*1e-7));
|
||||
const ci=ds(f((randomU32()/0xffffffff)*2.4-1.2),f((randomU32()/0xffffffff-.5)*1e-7));
|
||||
const iterations=8+(randomU32()%64),oldStates=dsOrbit(cr,ci,iterations,false),newStates=dsOrbit(cr,ci,iterations,true);
|
||||
assert.equal(newStates.length,oldStates.length);
|
||||
for(let i=0;i<oldStates.length;i++){
|
||||
assert.ok(sameDs(newStates[i].zr,oldStates[i].zr));
|
||||
assert.ok(sameDs(newStates[i].zi,oldStates[i].zi));
|
||||
assert.ok(sameDs(newStates[i].mag,oldStates[i].mag));
|
||||
}
|
||||
}
|
||||
|
||||
function directChunkOrbit(cr,ci,iterations,chunk){let zr=f(0),zi=f(0),n=0,escape=0,mag=f(0);while(n<iterations&&!escape){const stop=Math.min(iterations,n+chunk);while(n<stop){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(f(2*zr)*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;mag=f(f(zr*zr)+f(zi*zi));if(mag>4){escape=n;break}}}return{zr,zi,n,escape,mag}}
|
||||
for(let sample=0;sample<1000;sample++){
|
||||
const cr=f((randomU32()/0xffffffff)*3.5-2.5),ci=f((randomU32()/0xffffffff)*2.4-1.2),iterations=64+(randomU32()%900),chunk=[64,128,192,256][randomU32()%4];
|
||||
assert.deepEqual(directChunkOrbit(cr,ci,iterations,chunk),directChunkOrbit(cr,ci,iterations,iterations));
|
||||
}
|
||||
|
||||
function deepChunkState(reference,d,scaleExp,iterations,chunk){let w=[f(0),f(0)],n=0,m=0,err=f(Math.pow(2,-24)*Math.max(Math.abs(d[0]),Math.abs(d[1])));while(n<iterations){const stop=Math.min(iterations,n+chunk);while(n<stop){const r=reference[m],delta=[f(w[0]*Math.pow(2,scaleExp)),f(w[1]*Math.pow(2,scaleExp))],sq=[f(f(w[0]*w[0])-f(w[1]*w[1])),f(f(2*w[0])*w[1])],linear=[f(2*f(f(r[0]*w[0])-f(r[1]*w[1]))),f(2*f(f(r[0]*w[1])+f(r[1]*w[0])))],refAbs=Math.max(Math.abs(r[0]),Math.abs(r[1])),wAbs=Math.max(Math.abs(w[0]),Math.abs(w[1])),dAbs=Math.max(Math.abs(d[0]),Math.abs(d[1])),deltaAbs=Math.max(Math.abs(delta[0]),Math.abs(delta[1])),sqAbs=Math.max(Math.abs(f(sq[0]*Math.pow(2,scaleExp))),Math.abs(f(sq[1]*Math.pow(2,scaleExp))));err=f(f(f(2*refAbs)+f(2*deltaAbs))*err+f(Math.pow(2,-24)*f(f(f(2*refAbs)*wAbs)+sqAbs+dAbs+1e-30)));w=[f(f(linear[0]+f(sq[0]*Math.pow(2,scaleExp)))+d[0]),f(f(linear[1]+f(sq[1]*Math.pow(2,scaleExp)))+d[1])];m++;n++;const mm=Math.max(Math.abs(w[0]),Math.abs(w[1]),Math.abs(d[0]),Math.abs(d[1]));if(mm>65536){w=[f(w[0]/65536),f(w[1]/65536)];d=[f(d[0]/65536),f(d[1]/65536)];err=f(err/65536);scaleExp+=16}}}return{w,d,scaleExp,n,m,err}}
|
||||
for(let sample=0;sample<1000;sample++){
|
||||
const iterations=32+(randomU32()%480),reference=Array.from({length:iterations+1},()=>[f((randomU32()/0xffffffff-.5)*2),f((randomU32()/0xffffffff-.5)*2)]),d=[f((randomU32()/0xffffffff-.5)*1e-3),f((randomU32()/0xffffffff-.5)*1e-3)],scaleExp=-20-(randomU32()%60),chunk=1+(randomU32()%256);
|
||||
assert.deepEqual(deepChunkState(reference,[...d],scaleExp,iterations,chunk),deepChunkState(reference,[...d],scaleExp,iterations,iterations));
|
||||
}
|
||||
|
||||
for(let sample=0;sample<500;sample++){
|
||||
const w=64+(randomU32()%512),h=32+(randomU32()%256),iterations=350+(randomU32()%5000),maxPixels=Math.max(1,Math.floor(4000000/iterations)),tileW=Math.min(w,maxPixels),rows=Math.max(1,Math.min(128,Math.floor(maxPixels/tileW))),coverage=new Uint8Array(w*h);
|
||||
for(let y=0;y<h;y+=rows){const th=Math.min(rows,h-y);for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x);assert.ok(tw*th*iterations<=4000000);for(let yy=0;yy<th;yy++)for(let xx=0;xx<tw;xx++)coverage[(y+yy)*w+x+xx]++}}
|
||||
for(const count of coverage)assert.equal(count,1);
|
||||
}
|
||||
|
||||
function adaptiveBudget(screenBits,q,base=350,quantum=64){const raw=48*Math.max(0,q)*Math.max(0,screenBits),bonus=raw>0?Math.ceil(raw/quantum)*quantum:0;return Math.min(12000,Math.max(base,base+bonus))}
|
||||
for(const q of [.72,1,1.25,1.5]){
|
||||
let prior=0;
|
||||
for(let i=0;i<=2000;i++){
|
||||
const screenBits=i/20,value=adaptiveBudget(screenBits,q);
|
||||
assert.ok(value>=350+Math.floor(48*q*screenBits));
|
||||
assert.equal((value-350)%64,0);
|
||||
assert.ok(value>=prior);prior=value;
|
||||
}
|
||||
}
|
||||
assert.equal(adaptiveBudget(Math.log2(800)+12*Math.LOG2E*Math.LN10,1),adaptiveBudget(Math.log2(800)+12.05*Math.LOG2E*Math.LN10,1));
|
||||
|
||||
function frontierModel(meta,w,h){const out=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++){const i=y*w+x,m=meta[i]>>>0;if(((m>>>28)&3)!==0||((m>>>20)&255)!==6)continue;let edge=false;for(let oy=-1;oy<=1&&!edge;oy++)for(let ox=-1;ox<=1;ox++){if(!ox&&!oy)continue;const xx=x+ox,yy=y+oy;if(xx>=0&&xx<w&&yy>=0&&yy<h&&((meta[yy*w+xx]>>>28)&3)===1){edge=true;break}}if(edge)out.push(i)}return out}
|
||||
const opLimit=(6<<20)|350,escaped=(1<<28)|12,interior=(3<<28)|350,frontierFixture=new Uint32Array(25).fill(interior);frontierFixture[12]=escaped;for(const i of [6,7,8,11,13,16,17,18])frontierFixture[i]=opLimit;frontierFixture[0]=opLimit;
|
||||
assert.deepEqual(frontierModel(frontierFixture,5,5),[6,7,8,11,13,16,17,18]);
|
||||
for(let sample=0;sample<500;sample++){const iter=1+(randomU32()%150000),count=1+(randomU32()%200000),chunk=Math.max(1,Math.floor(4000000/iter));for(let start=0;start<count;start+=chunk)assert.ok(Math.min(chunk,count-start)*iter<=4000000)}
|
||||
|
||||
function pixelBitsAt(spanLog2,width,sourceBits=512,guard=32){const pixelLog2=spanLog2-Math.log2(Math.max(1,width)),need=Math.ceil(Math.max(0,-pixelLog2))+Math.max(16,guard|0);return Math.max(96,Math.min(sourceBits+64,need))}
|
||||
assert.equal(pixelBitsAt(Math.log2(3.4)-8*Math.LOG2E*Math.LN10,1920),96);
|
||||
assert.ok(pixelBitsAt(Math.log2(3.4)-100*Math.LOG2E*Math.LN10,1920)>350);
|
||||
assert.ok(pixelBitsAt(-400,3840)>pixelBitsAt(-400,960));
|
||||
assert.equal(pixelBitsAt(-400,1920,256),320);
|
||||
|
||||
export const result={
|
||||
syntaxScripts:scripts.length,
|
||||
precisionWorkerSyntax:true,
|
||||
kernelHashes:Object.keys(actual).length,
|
||||
bigintOrbitCases:2000,
|
||||
dsOrbitCases:600,
|
||||
directActiveChunkCases:1000,
|
||||
deepActiveChunkCases:1000,
|
||||
boundedCorrectionTileCases:500,
|
||||
adaptiveIterationCases:8004,
|
||||
pixelPrecisionCases:4,
|
||||
frontierAdjacencyCases:1,
|
||||
frontierWorkBoundCases:500,
|
||||
webGpuCorrectnessCases:correctnessCases.length,
|
||||
webGpuPerformanceCases:performanceCases.length,
|
||||
webGpuStabilityViews:interactionStabilityViews.length,
|
||||
status:'pass'
|
||||
};
|
||||
122
tests/optimization_webgpu_corpus.mjs
Normal file
122
tests/optimization_webgpu_corpus.mjs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import assert from 'node:assert/strict';
|
||||
|
||||
export const correctnessCases=[
|
||||
{id:'reset',re:'-0.5',im:'0',span:'3.4',bits:256,baseIter:750,adaptive:false,processMode:'validate',renderMode:'fast',activeDirect:true,certified:true,frontierCheck:true},
|
||||
{id:'cusp-low',re:'0.25',im:'0',span:'0.85',bits:256,baseIter:900,adaptive:false,processMode:'validate',renderMode:'accurate',certified:true},
|
||||
{id:'seahorse-low',re:'-0.743643887037151',im:'0.13182590420533',span:'0.1',bits:320,baseIter:1200,adaptive:false,processMode:'validate',renderMode:'accurate'},
|
||||
{id:'moderate-1e5-fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast',frontierCheck:true},
|
||||
{id:'moderate-1e5-accurate',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate',frontierCheck:true},
|
||||
{id:'moderate-1e8-fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast',frontierCheck:true},
|
||||
{id:'moderate-1e8-accurate',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate',frontierCheck:true},
|
||||
{id:'seahorse-deep',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',bits:448,baseIter:2900,adaptive:false,processMode:'validate',renderMode:'accurate',activeDeep:true,frontierCheck:true},
|
||||
{id:'interior-heavy',re:'0',im:'0',span:'0.5',bits:320,baseIter:1500,adaptive:false,processMode:'validate',renderMode:'accurate',certified:true},
|
||||
{id:'real-axis-boundary',re:'-0.75',im:'0',span:'0.02',bits:384,baseIter:3000,adaptive:false,processMode:'validate',renderMode:'accurate',activeDeep:true,certified:true,frontierCheck:true},
|
||||
{id:'misiurewicz-hard',re:'-0.10109636384562',im:'0.95628651080914',span:'0.00000002',bits:384,baseIter:3000,adaptive:false,processMode:'validate',renderMode:'accurate',activeDeep:true,frontierCheck:true},
|
||||
{id:'reported-black-regression',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate',fixed:true,frontierCheck:true}
|
||||
];
|
||||
|
||||
export const performanceCases=[
|
||||
{id:'accurate-320x220-750',viewport:{width:320,height:220},view:correctnessCases[2],iterations:750},
|
||||
{id:'bla-654x690-2900',viewport:{width:654,height:690},view:correctnessCases[7],iterations:2900},
|
||||
{id:'fast-1e14-800x600',viewport:{width:800,height:600},view:{id:'fast-1e14',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'},target:true,absoluteP50Ms:1000,absoluteP95Ms:1500},
|
||||
{id:'accurate-1e14-800x600',viewport:{width:800,height:600},view:{id:'accurate-1e14',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate'},target:true,absoluteP50Ms:1500,absoluteP95Ms:2000},
|
||||
{id:'hd-reset-1500',viewport:{width:1920,height:1080},view:correctnessCases[0],iterations:1500},
|
||||
{id:'hd-boundary-3000',viewport:{width:1920,height:1080},view:correctnessCases[9],iterations:3000}
|
||||
];
|
||||
|
||||
export const productionPerformanceThresholds={targetP50Reduction:.20,targetP95Reduction:.15,nonTargetP50Regression:.05,nonTargetP95Regression:.10};
|
||||
|
||||
export const interactionStabilityViews=[
|
||||
{id:'near-a',re:'-0.743643887037151',im:'0.13182590420533',span:'0.0000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'},
|
||||
{id:'near-b',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000000003030253189',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'},
|
||||
{id:'near-a-repeat',re:'-0.743643887037151',im:'0.13182590420533',span:'0.0000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'}
|
||||
];
|
||||
|
||||
function numericalReasonTotal(reasons={}){return (reasons.errorBound||0)+(reasons.escapeUncertain||0)+(reasons.referenceEnd||0)+(reasons.rebaseGap||0)+(reasons.range||0)}
|
||||
function operationLimit(reasons={}){return reasons.operationLimit||0}
|
||||
|
||||
async function evaluateCase(page,testCase,options){
|
||||
return page.evaluate(async({testCase,options})=>{
|
||||
const t=globalThis.__MANDEL_TEST__;if(!t)throw new Error('__MANDEL_TEST__ is unavailable');
|
||||
await t.setView(testCase);await t.waitForFinal({timeout:options.timeout});
|
||||
let numericComplete=true,numericError='';try{await t.waitForNumericComplete({timeout:options.timeout})}catch(error){numericComplete=false;numericError=String(error&&error.message||error)}
|
||||
const state=t.state(),hashes=await t.fieldHashes(),diagnostics=t.gpuDiagnostics();
|
||||
let activeDirect=null,activeDeep=null,certified=null,frontier=null;
|
||||
if(testCase.activeDirect)activeDirect=await t.runActiveDirectShadow({chunkSize:128});
|
||||
if(testCase.activeDeep)activeDeep=await t.runActiveDeepShadow({width:options.shadowWidth,height:options.shadowHeight,chunkSize:128,strict:true});
|
||||
if(testCase.certified)certified=await t.runCertifiedInteriorShadow({width:options.oracleWidth,height:options.oracleHeight,samples:options.oracleSamples});
|
||||
if(testCase.frontierCheck)frontier=await t.compareCurrentToFrontierBaseline({strict:true});
|
||||
return{state,hashes,diagnostics,numericComplete,numericError,activeDirect,activeDeep,certified,frontier};
|
||||
},{testCase,options});
|
||||
}
|
||||
|
||||
function auditCase(testCase,result,expected){
|
||||
const reasons=result.state.unknownReasons||{},numeric=numericalReasonTotal(reasons),op=operationLimit(reasons),coverage=Math.max(0,(result.state.unresolved||0)-numeric-op),errors=(result.diagnostics?.compilation||[]).flatMap(x=>x.messages||[]).filter(x=>x.type==='error');
|
||||
assert.equal(result.state.kernelVersion,'24.3.0-optimized-unknown-pipeline',testCase.id+' kernel version');
|
||||
assert.equal(coverage,0,testCase.id+' coverage UNKNOWN');
|
||||
assert.equal(errors.length,0,testCase.id+' WGSL compilation errors');
|
||||
assert.equal(result.diagnostics?.uncapturedErrors?.length||0,0,testCase.id+' uncaptured GPU errors');
|
||||
assert.equal(result.diagnostics?.lossReason||'','',testCase.id+' device loss');
|
||||
assert.equal(result.numericComplete,true,testCase.id+' numeric completion: '+result.numericError);
|
||||
assert.equal(numeric,0,testCase.id+' final numeric UNKNOWN');
|
||||
assert.equal(result.state.historyReady,true,testCase.id+' completed history commit');
|
||||
if(result.activeDirect)assert.equal(result.activeDirect.gatePassed,true,testCase.id+' Direct active mismatch');
|
||||
if(result.activeDeep)assert.equal(result.activeDeep.gatePassed,true,testCase.id+' Deep active mismatch');
|
||||
if(result.certified)assert.equal(result.certified.gatePassed,true,testCase.id+' certified interior oracle mismatch');
|
||||
if(result.frontier)assert.equal(result.frontier.gatePassed,true,testCase.id+' production frontier mismatch');
|
||||
assert.equal(result.state.frontierConverged,true,testCase.id+' frontier convergence');
|
||||
assert.ok((result.state.frontierMaxDispatchWork||0)<=4000000,testCase.id+' frontier TDR work bound');
|
||||
if(expected){assert.equal(result.hashes.metaSha256,expected.metaSha256,testCase.id+' meta hash');assert.equal(result.hashes.smoothSha256,expected.smoothSha256,testCase.id+' smooth hash')}
|
||||
return{coverageUnknown:coverage,numericalUnknown:numeric,operationLimit:op};
|
||||
}
|
||||
|
||||
export async function runCorrectnessCorpus(page,{timeout=180000,viewport={width:480,height:320},shadowWidth=160,shadowHeight=112,oracleWidth=320,oracleHeight=220,oracleSamples=289,expectedHashes={},expectProduction=false}={}){
|
||||
if(page.setViewportSize)await page.setViewportSize(viewport);
|
||||
const compile=await page.evaluate(()=>globalThis.__MANDEL_TEST__.compileActiveShadow()),records=[];
|
||||
assert.equal(compile.productionEligible,expectProduction);
|
||||
assert.equal(compile.candidateEnabled,true);
|
||||
assert.equal(compile.pixelFrontier,true);
|
||||
assert.equal(compile.frontierWorkLimit,4000000);
|
||||
for(const testCase of correctnessCases){const started=Date.now(),result=await evaluateCase(page,testCase,{timeout,shadowWidth,shadowHeight,oracleWidth,oracleHeight,oracleSamples}),audit=auditCase(testCase,result,expectedHashes[testCase.id]);records.push({id:testCase.id,elapsedMs:Date.now()-started,audit,...result})}
|
||||
const adapter=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics()?.adapterInfo||{});
|
||||
return{version:'24.3.0',kind:'correctness',date:new Date().toISOString(),expectProduction,compile,adapter,records};
|
||||
}
|
||||
|
||||
function percentile(values,p){const sorted=[...values].sort((a,b)=>a-b);if(!sorted.length)return Infinity;const index=Math.min(sorted.length-1,Math.max(0,Math.ceil(sorted.length*p)-1));return sorted[index]}
|
||||
function summarizeRuns(samples){const primary=samples.map(x=>Number(x.metric?.primaryMs??x.wallMs)),recovery=samples.map(x=>Number(x.metric?.numericQuiescentMs??x.metric?.historyCommitMs??x.metric?.finalMs??x.wallMs));return{runs:samples.length,primary:{p50:percentile(primary,.50),p95:percentile(primary,.95)},recovery:{p50:percentile(recovery,.50),p95:percentile(recovery,.95)}}}
|
||||
export function evaluateProductionPerformance(candidate,baseline,{thresholds=productionPerformanceThresholds}={}){const failures=[],byId=new Map((baseline?.records||[]).map(x=>[x.id,x]));for(const record of candidate?.records||[]){const base=byId.get(record.id),summary=record.summary||summarizeRuns(record.runs||[]);if(record.absoluteP50Ms&&summary.recovery.p50>record.absoluteP50Ms)failures.push(record.id+' recovery p50 '+summary.recovery.p50+' > '+record.absoluteP50Ms);if(record.absoluteP95Ms&&summary.recovery.p95>record.absoluteP95Ms)failures.push(record.id+' recovery p95 '+summary.recovery.p95+' > '+record.absoluteP95Ms);if(!base){failures.push(record.id+' missing baseline');continue}const bs=base.summary||summarizeRuns(base.runs||[]),p50=(summary.recovery.p50-bs.recovery.p50)/Math.max(1,bs.recovery.p50),p95=(summary.recovery.p95-bs.recovery.p95)/Math.max(1,bs.recovery.p95);if(record.target){if(p50>-thresholds.targetP50Reduction)failures.push(record.id+' target p50 reduction '+p50);if(p95>-thresholds.targetP95Reduction)failures.push(record.id+' target p95 reduction '+p95)}else{if(p50>thresholds.nonTargetP50Regression)failures.push(record.id+' non-target p50 regression '+p50);if(p95>thresholds.nonTargetP95Regression)failures.push(record.id+' non-target p95 regression '+p95)}}return{pass:failures.length===0,failures}}
|
||||
|
||||
export async function runPerformanceCorpus(page,{runs=10,timeout=240000,includeLarge=false}={}){
|
||||
const cases=includeLarge?performanceCases:performanceCases.slice(0,2),records=[];
|
||||
for(const scenario of cases){if(page.setViewportSize)await page.setViewportSize(scenario.viewport);const view=scenario.iterations?{...scenario.view,baseIter:scenario.iterations,adaptive:false,processMode:'standard'}:{...scenario.view},samples=[];for(let run=0;run<runs+1;run++){const started=Date.now();await page.evaluate(async({view,timeout})=>{const t=globalThis.__MANDEL_TEST__;await t.setView(view);await t.waitForNumericComplete({timeout})},{view,timeout});const state=await page.evaluate(()=>globalThis.__MANDEL_TEST__.state()),metric=state.generationMetrics.at(-1),diagnostics=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics());assert.equal(state.numericalFailures,0,scenario.id+' numerical failures');assert.equal(diagnostics?.uncapturedErrors?.length||0,0,scenario.id+' uncaptured GPU errors');assert.equal(diagnostics?.lossReason||'','',scenario.id+' device loss');if(run>0)samples.push({wallMs:Date.now()-started,metric})}records.push({id:scenario.id,viewport:scenario.viewport,target:!!scenario.target,absoluteP50Ms:scenario.absoluteP50Ms||0,absoluteP95Ms:scenario.absoluteP95Ms||0,runs:samples,summary:summarizeRuns(samples)})}
|
||||
const exports=await page.evaluate(async()=>({ss1:await globalThis.__MANDEL_TEST__.smokeExportTile({w:128,h:96,strict:true,ss:1}),ss2:await globalThis.__MANDEL_TEST__.smokeExportTile({w:128,h:96,strict:true,ss:2})}));
|
||||
assert.equal(exports.ss1.length,exports.ss1.expected);assert.equal(exports.ss2.length,exports.ss2.expected);
|
||||
const adapter=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics()?.adapterInfo||{});
|
||||
return{version:'24.3.0',kind:'performance',date:new Date().toISOString(),adapter,records,exports};
|
||||
}
|
||||
|
||||
export async function runInteractionStabilityCorpus(page,{timeout=240000,expectProduction=false}={}){
|
||||
if(page.setViewportSize)await page.setViewportSize({width:800,height:600});
|
||||
const policy=await page.evaluate(()=>globalThis.__MANDEL_TEST__.stabilityPolicy());
|
||||
assert.equal(policy.adaptiveIterationQuantum,64);
|
||||
assert.equal(policy.blaFastProductionOptIn,false);
|
||||
assert.equal(policy.blaFastDisabled,true);
|
||||
assert.equal(policy.pixelFrontierProduction,expectProduction);
|
||||
assert.equal(policy.pixelFrontierCandidate,true);
|
||||
assert.equal(policy.pixelFrontierEnabled,true);
|
||||
assert.equal(policy.pixelFrontierWorkLimit,4000000);
|
||||
const records=[];
|
||||
for(const view of interactionStabilityViews){
|
||||
const result=await page.evaluate(async({view,timeout})=>{const t=globalThis.__MANDEL_TEST__;await t.setView(view);await t.waitForNumericComplete({timeout});return{state:t.state(),hashes:await t.fieldHashes(),policy:t.stabilityPolicy()}},{view,timeout});
|
||||
assert.notEqual(result.state.backend,'fast-bla',view.id+' timing-selected backend');
|
||||
assert.equal(result.state.numericalFailures,0,view.id+' numerical failures');
|
||||
records.push({id:view.id,...result});
|
||||
}
|
||||
assert.equal(records[0].state.iter,records[1].state.iter,'nearby zoom iteration envelope');
|
||||
assert.equal(records[0].hashes.metaSha256,records[2].hashes.metaSha256,'repeat meta hash');
|
||||
assert.equal(records[0].hashes.smoothSha256,records[2].hashes.smoothSha256,'repeat smooth hash');
|
||||
const adapter=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics()?.adapterInfo||{});
|
||||
return{version:'24.3.0',kind:'interaction-stability',date:new Date().toISOString(),expectProduction,policy,adapter,records};
|
||||
}
|
||||
|
||||
export async function runWebGpuCorpus(page,options={}){const correctness=await runCorrectnessCorpus(page,options.correctness),stability=await runInteractionStabilityCorpus(page,options.stability),performance=await runPerformanceCorpus(page,options.performance);return{correctness,stability,performance}}
|
||||
65
tests/production_gate.mjs
Normal file
65
tests/production_gate.mjs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {correctnessCases,performanceCases,interactionStabilityViews,evaluateProductionPerformance} from './optimization_webgpu_corpus.mjs';
|
||||
|
||||
const root=fileURLToPath(new URL('../',import.meta.url));
|
||||
const artifactPath=process.argv[2];
|
||||
const phase=process.argv[3]||'preflight';
|
||||
assert.ok(artifactPath,'usage: node tests/production_gate.mjs <gate-bundle.json> [preflight|production]');
|
||||
assert.ok(phase==='preflight'||phase==='production','phase must be preflight or production');
|
||||
|
||||
const bundle=JSON.parse(await fs.readFile(artifactPath,'utf8'));
|
||||
const index=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const production=/const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000/.test(index);
|
||||
assert.equal(production,phase==='production','index production flag does not match gate phase');
|
||||
assert.ok(Array.isArray(bundle.candidateRuns)&&bundle.candidateRuns.length>=2,'at least two candidate adapter runs are required');
|
||||
assert.ok(Array.isArray(bundle.baselineRuns)&&bundle.baselineRuns.length>=2,'matching baseline runs are required');
|
||||
|
||||
function adapterKey(run){const a=run?.correctness?.adapter||run?.performance?.adapter||{};return[a.vendor,a.architecture,a.device,a.description].map(x=>String(x||'')).join('|')}
|
||||
function gpuErrors(diagnostics){return(diagnostics?.compilation||[]).flatMap(x=>x.messages||[]).filter(x=>x.type==='error').length+(diagnostics?.uncapturedErrors?.length||0)+(diagnostics?.lossReason?1:0)}
|
||||
function auditCandidate(run){
|
||||
assert.equal(run?.correctness?.expectProduction,phase==='production','correctness phase mismatch');
|
||||
assert.equal(run?.correctness?.compile?.productionEligible,phase==='production','compile production eligibility mismatch');
|
||||
assert.equal(run?.correctness?.compile?.candidateEnabled,true,'candidate path was not enabled');
|
||||
assert.equal(run?.correctness?.records?.length,correctnessCases.length,'incomplete correctness corpus');
|
||||
for(const record of run.correctness.records){
|
||||
assert.equal(record.audit?.coverageUnknown,0,record.id+' coverage UNKNOWN');
|
||||
assert.equal(record.audit?.numericalUnknown,0,record.id+' numeric UNKNOWN');
|
||||
assert.equal(record.numericComplete,true,record.id+' numeric completion');
|
||||
assert.equal(record.state?.historyReady,true,record.id+' history commit');
|
||||
assert.equal(record.state?.frontierConverged,true,record.id+' frontier convergence');
|
||||
assert.ok((record.state?.frontierMaxDispatchWork||0)<=4000000,record.id+' dispatch work limit');
|
||||
if((record.audit?.operationLimit||0)>0)assert.ok(record.state.frontierIter>=record.state.frontierMinimumIter,record.id+' stopped below pixel-scale iteration floor');
|
||||
assert.equal(gpuErrors(record.diagnostics),0,record.id+' GPU diagnostics');
|
||||
if(record.frontier)assert.equal(record.frontier.gatePassed,true,record.id+' frontier oracle');
|
||||
if(record.activeDirect)assert.equal(record.activeDirect.gatePassed,true,record.id+' Direct active shadow');
|
||||
if(record.activeDeep)assert.equal(record.activeDeep.gatePassed,true,record.id+' Deep active shadow');
|
||||
if(record.certified)assert.equal(record.certified.gatePassed,true,record.id+' certified interior shadow');
|
||||
}
|
||||
assert.equal(run?.stability?.expectProduction,phase==='production','stability phase mismatch');
|
||||
assert.equal(run?.stability?.records?.length,interactionStabilityViews.length,'incomplete interaction corpus');
|
||||
const stability=run.stability.records;
|
||||
assert.equal(stability[0].hashes.metaSha256,stability[2].hashes.metaSha256,'round-trip meta instability');
|
||||
assert.equal(stability[0].hashes.smoothSha256,stability[2].hashes.smoothSha256,'round-trip smooth instability');
|
||||
assert.equal(run?.performance?.records?.length,performanceCases.length,'full performance corpus is required');
|
||||
for(const record of run.performance.records){
|
||||
assert.ok((record.runs?.length||0)>=10,record.id+' requires 10 measured runs');
|
||||
assert.ok(Number.isFinite(record.summary?.recovery?.p50)&&Number.isFinite(record.summary?.recovery?.p95),record.id+' summary missing');
|
||||
}
|
||||
}
|
||||
|
||||
const candidateKeys=new Set();
|
||||
const baselineByAdapter=new Map(bundle.baselineRuns.map(run=>[adapterKey(run),run]));
|
||||
const performance=[];
|
||||
for(const run of bundle.candidateRuns){
|
||||
auditCandidate(run);
|
||||
const key=adapterKey(run);assert.ok(key.replaceAll('|',''),'candidate adapter identity is empty');candidateKeys.add(key);
|
||||
const baseline=baselineByAdapter.get(key);assert.ok(baseline,'missing same-adapter baseline for '+key);
|
||||
const result=evaluateProductionPerformance(run.performance,baseline.performance);assert.equal(result.pass,true,result.failures.join('\n'));
|
||||
performance.push({adapter:key,...result});
|
||||
}
|
||||
assert.ok(candidateKeys.size>=2,'candidate evidence must cover at least two distinct adapters');
|
||||
|
||||
const output={pass:true,phase,adapters:[...candidateKeys],correctnessCases:correctnessCases.length,performanceCases:performanceCases.length,interactionCases:interactionStabilityViews.length,performance};
|
||||
process.stdout.write(JSON.stringify(output,null,2)+'\n');
|
||||
|
|
@ -3,7 +3,8 @@ import re, subprocess, tempfile, json
|
|||
ROOT=Path(__file__).resolve().parents[1]
|
||||
CUR=ROOT/'index.html'
|
||||
OLD=ROOT/'tests'/'KERNEL_HASHES_V24_2_54.json'
|
||||
NEW=ROOT/'tests'/'KERNEL_HASHES_V24_2_55.json'
|
||||
NEW=ROOT/'tests'/'KERNEL_HASHES_V24_3_0.json'
|
||||
CORPUS=ROOT/'tests'/'optimization_webgpu_corpus.mjs'
|
||||
|
||||
def scripts(path): return re.findall(r'<script[^>]*>(.*?)</script>',path.read_text(),re.S)
|
||||
|
||||
|
|
@ -24,9 +25,84 @@ syntax(CUR)
|
|||
old=json.loads(OLD.read_text()); cur=kernels(CUR); expected=json.loads(NEW.read_text())
|
||||
assert cur==expected
|
||||
changed=[k for k,v in old.items() if cur.get(k)!=v]
|
||||
assert changed==['version','FAST_PERTURB_WGSL'], changed
|
||||
assert {'version','DIRECT_ACCURATE_SEED_WGSL','DIRECT_DS_CORRECT_WGSL','FAST_PERTURB_WGSL'}.issubset(changed), changed
|
||||
html=CUR.read_text()
|
||||
assert "version:'24.2.55-no-black-hole-async-recovery'" in html
|
||||
assert CORPUS.exists() and 'runCorrectnessCorpus' in CORPUS.read_text() and 'runPerformanceCorpus' in CORPUS.read_text() and 'runInteractionStabilityCorpus' in CORPUS.read_text()
|
||||
assert "version:'24.3.0-optimized-unknown-pipeline'" in html
|
||||
assert 'if(n>=p.lateThreshold){fieldMeta[out]=pack_unknown' in html
|
||||
assert 'var zr2=DS(0.0,0.0);var zi2=DS(0.0,0.0)' in html
|
||||
assert 'DIRECT_DS_QUEUE_BUILD_WGSL' in html and 'DIRECT_DS_CORRECT_QUEUE_WGSL' in html
|
||||
assert 'accurateQueuedPixels' in html and 'accurateQueueFallbacks' in html
|
||||
assert 'UNKNOWN_STATS_WGSL' in html and 'encodeUnknownStats' in html
|
||||
assert 'encodeDeepBucketHistogram(e' in html and 'encodeQueuedDeepCorrection(e' in html
|
||||
assert 'q.selected===q.enqueued&&q.enqueued===q.dispatchCount&&q.dispatchCount===q.processed' in html
|
||||
assert 'reusePreview' in html and 'numericInFlightBudgetMs' in html
|
||||
assert 'canUseStableReprojection()||fastNeedsExtended(snapshot(),canvas.width))return[finalIter]' in html
|
||||
assert 'const ADAPTIVE_ITER_QUANTUM=64' in html and 'Math.ceil(raw/quantum)*quantum' in html
|
||||
assert 'function pixelPrecisionBits(snap=snapshot(),w=canvas.width,guard=32)' in html
|
||||
assert 'targetBits=pixelPrecisionBits(snap,width,40)' in html and 'fastReferenceBits(snap,w=canvas.width)' in html
|
||||
assert 'const bits=baseBits+attempt*32,a=pixel(s,indices[i],bits),b=pixel(s,indices[i],bits+32)' in html
|
||||
assert 'for(let attempt=0;attempt<4&&!accepted[i];attempt++)' in html
|
||||
assert 'for(let start=0;start<indices.length;start+=chunkSize)' in html
|
||||
assert 'UNRESOLVED_BLACK_MIN_ZOOM_EXP' not in html and 'colorUnresolvedInsteadOfBlack' not in html and 'guardedFastDeep' not in html
|
||||
assert 'this.blaFastDisabled=true;this.blaFastOptIn=false' in html
|
||||
assert 'deep-stable-bootstrap' not in html and 'forceStableDeep' not in html
|
||||
assert 'A FAST frame is not publishable while any numeric UNKNOWN remains' in html
|
||||
assert 'if(initialFailed>0){const mr=await recoverNumericalUnknownTiles' in html
|
||||
assert 'if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision' in html
|
||||
assert "if(isFinal){const remaining=numericalFailureCount" in html and 'frameを公開しません' in html
|
||||
assert 'viewRe:snap.re,viewIm:snap.im' in html and 'referenceOriginMatchesView(src,snap)' in html
|
||||
assert 'fastBackgroundDeepOnFailureOnly:true,deepProductionActive:false,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK' in html
|
||||
assert 'ensureAccuratePipelines' in html and 'ensureCorrectionPipelines' in html
|
||||
assert 'precisionFallbackWorkerSource' in html and 'recoverResidualPrecision' in html
|
||||
assert 'estimatedNetMs' in html and "reason:enabled?'probe-pass':!operationGate?'probe-cost':'probe-net-time'" in html
|
||||
replay=cur['BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL']
|
||||
assert 'fn baseline_from(s0:S,pre:u32)->Result' in replay and 'var anchor=s;loop' in replay
|
||||
assert 'baseline(g,st)' not in replay and replay.count('baseline_from(anchor,st)')==7
|
||||
assert 'anchor=s;}}' in replay and 'baseline_from(anchor,st)' in cur['BLA_GPU_FAST_FRAME_WGSL']
|
||||
assert 'SYMMETRY_COPY_WGSL' in html and 'canUseRealAxisSymmetry' in html and 'encodeSymmetryCopy' in html
|
||||
assert 'FAILURE_TILE_MAP_WGSL' in html and 'readNumericalFailureTiles' in html
|
||||
assert "tileReferencePool=Array.from({length:3}" in html and 'Promise.all(batch.map' in html
|
||||
assert 'if(p.key===key)return p.promise' in html and 'prefetchFinalReference' in html
|
||||
assert 'this.exportWs=[]' in html and 'exportRingDepth(tile=this.exportTileSize())' in html
|
||||
assert 'Promise.allSettled(batch.map(item=>item.promise))' in html and 'if(failed)throw failed.reason' in html and 'performance.now()-lastYield<24' in html
|
||||
assert 'await png.rows(band);await new Promise(requestAnimationFrame)' not in html
|
||||
assert 'frameReadback(kind,size)' in html and "this.frameReadback('unknown-stats'" in html
|
||||
stats_readback=html[html.index('async readUnresolvedStats'):html.index('async readUnresolved(){')]
|
||||
assert 'destroy(r)' not in stats_readback
|
||||
assert 'cachedBindGroup(pipeline,entries)' in html and 'cachedTextureView(texture)' in html
|
||||
assert 'this.cachedBindGroup(this.direct' in html and 'this.cachedBindGroup(this.present' in html
|
||||
history_commit=html[html.index('commitHistory(view='):html.index('invalidateHistory(){')]
|
||||
assert 'f.history=f.front' in history_commit and 'copyTextureToTexture' not in history_commit
|
||||
assert 'colorTarget(f=this.frame)' in html and 'spare:history' in html
|
||||
assert 'ACTIVE_PREPARE_WGSL' in html and 'DIRECT_ACTIVE_CONTINUE_WGSL' in html
|
||||
assert 'DEEP_ACTIVE_CONTINUE_WGSL' in html and 'PERTURB_CERTIFIED_INTERIOR_WGSL' in html
|
||||
assert 'runDirectActiveShadow' in html and 'dispatchWorkgroupsIndirect(indirect,0)' in html
|
||||
assert 'direct-active-shadow-resume' in html and 'resumedFromPreview:previewIter>0' in html
|
||||
assert 'runDeepActiveShadow' in html and 'deep-active-shadow-resume' in html
|
||||
assert 'deep-active-compare-shadow' in html and 'runtime.deepActiveShadowLatest=result' in html
|
||||
assert 'computeDeepActiveFrame' in html and 'workPerDispatch=PIXEL_FRONTIER_WORK,maxPassesPerSubmit=1' in html
|
||||
assert 'const workBudget=deep?4000000:48000000' in html
|
||||
assert 'maxPixels=Math.max(1,Math.floor(4000000/Math.max(1,iter))),tileW=Math.min(f.w,maxPixels)' in html
|
||||
assert 'PIXEL_FRONTIER_PRODUCTION=false,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000' in html
|
||||
assert 'PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION||PIXEL_FRONTIER_CANDIDATE' in html
|
||||
assert 'function operationLimitFrontier(meta,w,h)' in html and 'refineOperationLimitIndices' in html
|
||||
assert 'function pixelFrontierIterationFloor(snap,w,initialIter)' in html and 'iter>=minimumIter&&stableRounds>=2' in html
|
||||
assert 'pixel frontier queue invariant failed' in html and 'compareCurrentToFrontierBaseline' in html
|
||||
assert 'correctUnknownFrame(snap,iter,token,referencePixel,numericalFailureCount(stats))' in html
|
||||
assert 'ensureDeepActiveWorkspace' in html and 'return this.computeDeepActiveFrame(snap,iter,token,forceStrict,referencePixel)' not in html
|
||||
assert 'return this.computePerturbFrameTiled(snap,iter,true,token,forceStrict,referencePixel,false,colorStyle,colorKey)' in html
|
||||
assert 'states[local]=DeepState' in html and 'let out=p.outputBase+(local/p.tileW)*p.outputStride' in html
|
||||
assert 'state.gpuInitFailed=true;markDirty(false);setTimeout' in html
|
||||
assert 'runCertifiedInteriorShadow' in html and 'gpu.provenIndices,FIELD_INTERIOR_PROVEN' in html
|
||||
assert 'falsePositive===0&&inconclusive===0' in html and 'oracleBits:[oracleBits,oracleBits+32]' in html
|
||||
assert 'waitForPrimary(' in html and 'waitForFinal(' in html and 'waitForNumericComplete(' in html
|
||||
assert 'fieldHashes()' in html and 'readFieldAll()' in html
|
||||
assert 'generationMetrics:[]' in html and 'primaryNumericalFailures' in html
|
||||
assert 'numericQuiescentMs' in html and 'historyCommitMs' in html
|
||||
recovery=html[html.index('async function recoverNumericalUnknownTiles'):html.index('async function recoverResidualPrecision')]
|
||||
assert 'readNumericalFailureTiles' in recovery and 'const currentMeta=await r.readMetaAll()' in recovery
|
||||
assert 'Uint32Array.from(tileIndices)' in recovery
|
||||
assert 'p.unknownOnly==2u && reason!=0u' in html
|
||||
assert "fast-coverage-repair-and-color" in html
|
||||
assert 'reprojectionSafe(rawHistory,2.25,.45)' in html
|
||||
|
|
@ -35,4 +111,4 @@ assert 'numericFrameComplete()' in html
|
|||
assert "requestBuild({ctx,snap,iter,width,height,refPixel,safety=1/64})" in html
|
||||
assert "type:'build'" in html and 'buildOnly=d.type===\'build\'' in html
|
||||
assert not list(ROOT.glob('index.baseline*.html'))
|
||||
print('PASS: syntax; only FAST repair gate changed in production kernels; coverage repair, broad temporal fill, async recovery, build-only BLA prep, single HTML')
|
||||
print('PASS: syntax; bounded Accurate seed; DS square reuse; shared UNKNOWN stats; sparse correction invariants; preview/reference reuse; bounded GPU bursts')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue