@kaminos/webgpu-inference-kit 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/index.js +8 -0
- package/src/kernel-profile.js +1 -1
- package/src/kimodo-route.js +100 -0
- package/src/moge-route.js +80 -0
- package/src/route-boundary.js +54 -0
- package/src/route-receipt-consumer.js +80 -1
- package/src/scheduler-backpressure.js +108 -0
- package/src/scheduler-verification-receipt.js +442 -0
- package/src/sf3d-route.js +55 -0
- package/src/sharp-route.js +55 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
- What route is being invoked, such as MoGE depth/normal or SHARP image-to-splat.
|
|
32
32
|
- Which browser WebGPU adapter, device features, limits, and timestamp capabilities were actually available.
|
|
33
33
|
- Which kernel/profile variant ran, and which stages are required for a useful runtime profile.
|
|
34
|
-
- How a route was scheduled: throughput mode, cooperative/yield posture, phase chunk sizes, submitted-work waits, and unsupported scheduler fields.
|
|
34
|
+
- How a route was scheduled: throughput mode, cooperative/yield posture, breathability spans, yield checkpoints, phase chunk sizes, submitted-work waits, and unsupported scheduler fields.
|
|
35
35
|
- Which artifacts went in and out, so downstream consumers can join routes without losing identity.
|
|
36
36
|
|
|
37
37
|
The immediate goal is practical composition inside Kaminos: MoGE can become a local geometry/depth route, SHARP can emit splat candidates, Kimodo can emit motion clips, SF3D can emit meshes, and pipeline/commoner code can consume those outputs through one route grammar. The longer-term opportunity is a browser-native inference runtime kit that makes future image generators, 3D generators, and possibly language-model routes easier to seat without rebuilding the same WebGPU plumbing from scratch.
|
|
@@ -46,7 +46,7 @@ So the intended stack is:
|
|
|
46
46
|
|
|
47
47
|
1. **Route boundary:** define callable browser-local inference routes with stable input/output roles.
|
|
48
48
|
2. **Runtime profile:** preserve adapter/device/kernel/stage identity for the run that actually happened.
|
|
49
|
-
3. **Scheduler/backpressure profile:** expose whether the route is throughput-oriented, cooperative, furnace-class, warm, cached,
|
|
49
|
+
3. **Scheduler/backpressure profile:** expose whether the route is throughput-oriented, cooperative, furnace-class, warm, cached, frame-tail-sensitive, and where it can honestly yield.
|
|
50
50
|
4. **Receipt and classification:** reject stale, fallback, partial, mismatched, or invalid route output before another system treats it as authoritative.
|
|
51
51
|
|
|
52
52
|
The fourth layer protects the first three. It should not swallow the whole story.
|
|
@@ -63,7 +63,7 @@ The fourth layer protects the first three. It should not swallow the whole story
|
|
|
63
63
|
- `createStagedSubmitProfile(input)`, `addStagedSubmitStage(profile, stage)`, `finishStagedSubmitProfile(profile)`, and `validateStagedSubmitProfile(profile)`: describe staged queue-submit timing in a way that can be compared across routes.
|
|
64
64
|
- `createKernelProfileMetadata(input)` and `createRouteKernelProfileMetadata(input)`: normalize kit version, kernel profile, commit, required stages, and timing-source metadata for route definitions and receipts.
|
|
65
65
|
- `createWebGpuRuntimeProfileInput(input)`, `createWebGpuRuntimeProfile(input)`, and `validateWebGpuRuntimeProfile(profile)`: combine effective backend identity, kernel metadata, staged profile, and route mode into one producer-side runtime profile object.
|
|
66
|
-
- `createWebGpuRouteSchedulerProfile(input)` and `validateWebGpuRouteSchedulerProfile(profile)`: preserve requested versus effective scheduling, including throughput/cooperative mode, route-specific phase chunk sizes, submitted-work waits, yield cadence, and unsupported fields.
|
|
66
|
+
- `createWebGpuRouteSchedulerProfile(input)` and `validateWebGpuRouteSchedulerProfile(profile)`: preserve requested versus effective scheduling, including throughput/cooperative mode, route-specific phase chunk sizes, submitted-work waits, yield cadence, breathability spans, yieldable checkpoints, non-preemptible GPU-submit spans, and unsupported fields.
|
|
67
67
|
- `createWebGpuRouteBackpressureProfile(input)` and `validateWebGpuRouteBackpressureProfile(profile)`: record visible-wait/furnace pressure, warm/cache posture, memory-sharing posture, and frame-tail impact.
|
|
68
68
|
- `defineTensorManifest(input)` and `validateTensorManifest(manifest)`: normalize tensor metadata including dtype sizes and byte lengths.
|
|
69
69
|
- `createWebGpuLocalRouteReceipt(input)`, `createWebGpuRouteReceiptFromArtifacts(input)`, `createRouteReceiptArtifacts(input)`, `finishAndValidateRouteProfile(input)`, `validateRouteReceipt(receipt)`, and `assertAuthoritativeRouteReceipt(receipt)`: shared receipt construction and validation helpers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaminos/webgpu-inference-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Composable browser WebGPU inference route contracts, runtime profiles, and scheduler envelopes.",
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
".": "./src/index.js"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "node tests/receipt-contracts.mjs && node tests/tensor-manifest-contracts.mjs && node tests/gpu-environment-contracts.mjs && node tests/browser-device-context-contracts.mjs && node tests/staged-profile-contracts.mjs && node tests/kernel-profile-contracts.mjs && node tests/runtime-profile-contracts.mjs && node tests/scheduler-backpressure-contracts.mjs && node tests/route-schema-contracts.mjs && node tests/route-receipt-helper-contracts.mjs && node tests/route-receipt-consumer-contracts.mjs && node tests/moge-route-contracts.mjs && node tests/sharp-route-contracts.mjs && node tests/kimodo-route-contracts.mjs && node tests/sf3d-route-contracts.mjs && node tests/route-boundary-contracts.mjs"
|
|
24
|
+
"test": "node tests/receipt-contracts.mjs && node tests/tensor-manifest-contracts.mjs && node tests/gpu-environment-contracts.mjs && node tests/browser-device-context-contracts.mjs && node tests/staged-profile-contracts.mjs && node tests/kernel-profile-contracts.mjs && node tests/runtime-profile-contracts.mjs && node tests/scheduler-backpressure-contracts.mjs && node tests/scheduler-verification-receipt-contracts.mjs && node tests/route-schema-contracts.mjs && node tests/route-receipt-helper-contracts.mjs && node tests/route-receipt-consumer-contracts.mjs && node tests/moge-route-contracts.mjs && node tests/sharp-route-contracts.mjs && node tests/kimodo-route-contracts.mjs && node tests/sf3d-route-contracts.mjs && node tests/route-boundary-contracts.mjs"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/index.js
CHANGED
|
@@ -49,6 +49,14 @@ export {
|
|
|
49
49
|
WEBGPU_ROUTE_SCHEDULER_SCHEMA,
|
|
50
50
|
} from './scheduler-backpressure.js';
|
|
51
51
|
|
|
52
|
+
export {
|
|
53
|
+
SCHEDULER_EVENT_TRACE_SCHEMA,
|
|
54
|
+
SCHEDULER_VERIFICATION_RECEIPT_SCHEMA,
|
|
55
|
+
classifySchedulerVerificationReceipt,
|
|
56
|
+
createSchedulerVerificationReceipt,
|
|
57
|
+
validateSchedulerVerificationReceipt,
|
|
58
|
+
} from './scheduler-verification-receipt.js';
|
|
59
|
+
|
|
52
60
|
export {
|
|
53
61
|
classifyWebGpuRouteReceiptEvidence,
|
|
54
62
|
classifyWebGpuRouteWorkerResultEvidence,
|
package/src/kernel-profile.js
CHANGED
package/src/kimodo-route.js
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
createRouteReceiptInputArtifact,
|
|
9
9
|
createWebGpuRouteReceiptFromArtifacts,
|
|
10
10
|
} from './route-receipt-helper.js';
|
|
11
|
+
import {
|
|
12
|
+
createWebGpuRouteBackpressureProfile,
|
|
13
|
+
createWebGpuRouteSchedulerProfile,
|
|
14
|
+
} from './scheduler-backpressure.js';
|
|
11
15
|
|
|
12
16
|
export const KIMODO_TEXT_TO_MOTION_ROUTE_ID = 'kimodo.text-to-motion.webgpu-local.v0';
|
|
13
17
|
const KIMODO_MODEL_ID = 'NVIDIA/Kimodo-SOMA-RP-v1.1';
|
|
@@ -19,6 +23,100 @@ const OUTPUT_ROLES = [
|
|
|
19
23
|
{ key: 'filmstrip', role: 'filmstrip', required: false },
|
|
20
24
|
];
|
|
21
25
|
|
|
26
|
+
function createDefaultKimodoScheduler() {
|
|
27
|
+
return createWebGpuRouteSchedulerProfile({
|
|
28
|
+
requestedScheduler: {
|
|
29
|
+
mode: 'cooperative',
|
|
30
|
+
yieldMs: 4,
|
|
31
|
+
waitForSubmittedWorkDone: true,
|
|
32
|
+
phaseChunkSize: {
|
|
33
|
+
'text-embedding': 1,
|
|
34
|
+
'ddim-sampling': 1,
|
|
35
|
+
'fk-decode': 1,
|
|
36
|
+
'output-capture': 1,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
effectiveScheduler: {
|
|
40
|
+
mode: 'cooperative',
|
|
41
|
+
yieldMs: 4,
|
|
42
|
+
waitForSubmittedWorkDone: true,
|
|
43
|
+
phaseChunkSize: {
|
|
44
|
+
'text-embedding': 1,
|
|
45
|
+
'ddim-sampling': 1,
|
|
46
|
+
'fk-decode': 1,
|
|
47
|
+
'output-capture': 1,
|
|
48
|
+
},
|
|
49
|
+
unsupportedFields: [],
|
|
50
|
+
},
|
|
51
|
+
verificationState: 'scheduler-unverified',
|
|
52
|
+
breathability: {
|
|
53
|
+
spans: [
|
|
54
|
+
{
|
|
55
|
+
name: 'text-embedding',
|
|
56
|
+
stage: 'text-embedding',
|
|
57
|
+
kind: 'external-bound',
|
|
58
|
+
interruptible: false,
|
|
59
|
+
canYieldBefore: true,
|
|
60
|
+
canYieldAfter: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'ddim-sampling-loop',
|
|
64
|
+
stage: 'ddim-sampling',
|
|
65
|
+
kind: 'gpu-submit-loop',
|
|
66
|
+
interruptible: false,
|
|
67
|
+
canYieldBefore: true,
|
|
68
|
+
canYieldAfter: true,
|
|
69
|
+
nonInterruptibleReason: 'Each diffusion step submit is non-preemptible; cooperative yielding occurs between steps.',
|
|
70
|
+
metadata: { checkpointCadence: 'per-diffusion-step' },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'fk-decode',
|
|
74
|
+
stage: 'fk-decode',
|
|
75
|
+
kind: 'cpu-bound',
|
|
76
|
+
interruptible: true,
|
|
77
|
+
canYieldBefore: true,
|
|
78
|
+
canYieldAfter: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'output-capture',
|
|
82
|
+
stage: 'output-capture',
|
|
83
|
+
kind: 'readback-bound',
|
|
84
|
+
interruptible: false,
|
|
85
|
+
canYieldBefore: true,
|
|
86
|
+
canYieldAfter: true,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
checkpoints: [
|
|
90
|
+
{
|
|
91
|
+
name: 'between-diffusion-steps',
|
|
92
|
+
kind: 'diffusion-step',
|
|
93
|
+
afterStage: 'ddim-sampling',
|
|
94
|
+
yieldable: true,
|
|
95
|
+
waitsForSubmittedWorkDone: true,
|
|
96
|
+
metadata: { cadence: 'per-step' },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'after-output-capture',
|
|
100
|
+
kind: 'readback',
|
|
101
|
+
afterStage: 'output-capture',
|
|
102
|
+
yieldable: true,
|
|
103
|
+
waitsForSubmittedWorkDone: true,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
notes: 'Kimodo can expose useful cooperative pressure between diffusion steps; each submitted step remains non-preemptible.',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createDefaultKimodoBackpressure() {
|
|
112
|
+
return createWebGpuRouteBackpressureProfile({
|
|
113
|
+
requestedBudget: 'visible-wait',
|
|
114
|
+
effectiveBudget: 'visible-wait',
|
|
115
|
+
memoryExclusivity: 'shared',
|
|
116
|
+
warmCacheState: 'unknown',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
22
120
|
export function createKimodoTextToMotionRouteReceipt(input) {
|
|
23
121
|
if (!input || typeof input !== 'object') throw new Error('input must be an object');
|
|
24
122
|
if (!input.input?.artifactId || !input.input?.sha256) {
|
|
@@ -75,6 +173,8 @@ export function createKimodoTextToMotionRouteDefinition(input = {}) {
|
|
|
75
173
|
requiredFeatures: input.requiredFeatures || [],
|
|
76
174
|
requiredStages: routeMetadata.requiredStages,
|
|
77
175
|
timingSource: routeMetadata.timingSource,
|
|
176
|
+
scheduler: input.scheduler || createDefaultKimodoScheduler(),
|
|
177
|
+
backpressure: input.backpressure || createDefaultKimodoBackpressure(),
|
|
78
178
|
worker: input.worker || {
|
|
79
179
|
exportName: 'runKimodoTextToMotionRoute',
|
|
80
180
|
textEmbedding: 'external-llama3-8b',
|
package/src/moge-route.js
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
createRouteReceiptInputArtifact,
|
|
9
9
|
createWebGpuRouteReceiptFromArtifacts,
|
|
10
10
|
} from './route-receipt-helper.js';
|
|
11
|
+
import {
|
|
12
|
+
createWebGpuRouteBackpressureProfile,
|
|
13
|
+
createWebGpuRouteSchedulerProfile,
|
|
14
|
+
} from './scheduler-backpressure.js';
|
|
11
15
|
|
|
12
16
|
export const MOGE_DEPTH_NORMAL_ROUTE_ID = 'moge.depth-normal.webgpu-local.v0';
|
|
13
17
|
const MOGE_MODEL_ID = 'Ruicheng/moge-2-vitl-normal';
|
|
@@ -20,6 +24,80 @@ const OUTPUT_ROLES = [
|
|
|
20
24
|
{ key: 'mask', role: 'mask', required: false },
|
|
21
25
|
];
|
|
22
26
|
|
|
27
|
+
function createDefaultMogeScheduler() {
|
|
28
|
+
return createWebGpuRouteSchedulerProfile({
|
|
29
|
+
requestedScheduler: {
|
|
30
|
+
mode: 'cooperative',
|
|
31
|
+
yieldMs: 4,
|
|
32
|
+
waitForSubmittedWorkDone: true,
|
|
33
|
+
phaseChunkSize: {
|
|
34
|
+
backbone: 1,
|
|
35
|
+
'decoder-heads': 1,
|
|
36
|
+
'output-readback': 1,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
effectiveScheduler: {
|
|
40
|
+
mode: 'cooperative',
|
|
41
|
+
yieldMs: 4,
|
|
42
|
+
waitForSubmittedWorkDone: true,
|
|
43
|
+
phaseChunkSize: {
|
|
44
|
+
backbone: 1,
|
|
45
|
+
'decoder-heads': 1,
|
|
46
|
+
'output-readback': 1,
|
|
47
|
+
},
|
|
48
|
+
unsupportedFields: [],
|
|
49
|
+
},
|
|
50
|
+
verificationState: 'scheduler-unverified',
|
|
51
|
+
breathability: {
|
|
52
|
+
spans: [
|
|
53
|
+
{
|
|
54
|
+
name: 'backbone-submit',
|
|
55
|
+
stage: 'backbone',
|
|
56
|
+
kind: 'gpu-submit-bound',
|
|
57
|
+
interruptible: false,
|
|
58
|
+
canYieldBefore: true,
|
|
59
|
+
canYieldAfter: true,
|
|
60
|
+
nonInterruptibleReason: 'GPU command buffers cannot be preempted after submit',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'decoder-heads-submit',
|
|
64
|
+
stage: 'decoder-heads',
|
|
65
|
+
kind: 'gpu-submit-bound',
|
|
66
|
+
interruptible: false,
|
|
67
|
+
canYieldBefore: true,
|
|
68
|
+
canYieldAfter: true,
|
|
69
|
+
nonInterruptibleReason: 'GPU command buffers cannot be preempted after submit',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'output-readback',
|
|
73
|
+
stage: 'output-readback',
|
|
74
|
+
kind: 'readback-bound',
|
|
75
|
+
interruptible: false,
|
|
76
|
+
canYieldBefore: true,
|
|
77
|
+
canYieldAfter: true,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
checkpoints: REQUIRED_STAGES.map(stage => ({
|
|
81
|
+
name: `after-${stage}`,
|
|
82
|
+
kind: stage === 'output-readback' ? 'readback' : 'stage-boundary',
|
|
83
|
+
afterStage: stage,
|
|
84
|
+
yieldable: true,
|
|
85
|
+
waitsForSubmittedWorkDone: true,
|
|
86
|
+
})),
|
|
87
|
+
notes: 'MoGE can cooperate between staged submits and readback, not inside a submitted GPU pass.',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createDefaultMogeBackpressure() {
|
|
93
|
+
return createWebGpuRouteBackpressureProfile({
|
|
94
|
+
requestedBudget: 'visible-wait',
|
|
95
|
+
effectiveBudget: 'visible-wait',
|
|
96
|
+
memoryExclusivity: 'shared',
|
|
97
|
+
warmCacheState: 'unknown',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
23
101
|
export function createMogeDepthNormalRouteReceipt(input) {
|
|
24
102
|
if (!input || typeof input !== 'object') throw new Error('input must be an object');
|
|
25
103
|
if (!input.input?.artifactId || !input.input?.sha256) {
|
|
@@ -77,6 +155,8 @@ export function createMogeDepthNormalRouteDefinition(input = {}) {
|
|
|
77
155
|
requiredFeatures: input.requiredFeatures || [],
|
|
78
156
|
requiredStages: routeMetadata.requiredStages,
|
|
79
157
|
timingSource: routeMetadata.timingSource,
|
|
158
|
+
scheduler: input.scheduler || createDefaultMogeScheduler(),
|
|
159
|
+
backpressure: input.backpressure || createDefaultMogeBackpressure(),
|
|
80
160
|
worker: input.worker || {
|
|
81
161
|
exportName: 'runMogeDepthNormalRoute',
|
|
82
162
|
},
|
package/src/route-boundary.js
CHANGED
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
assertAuthoritativeRouteReceipt,
|
|
4
4
|
validateRouteReceipt,
|
|
5
5
|
} from './route-receipt.js';
|
|
6
|
+
import {
|
|
7
|
+
validateWebGpuRouteBackpressureProfile,
|
|
8
|
+
validateWebGpuRouteSchedulerProfile,
|
|
9
|
+
} from './scheduler-backpressure.js';
|
|
6
10
|
|
|
7
11
|
export const WEBGPU_ROUTE_DEFINITION_SCHEMA = 'kaminos.webgpu-route-definition.v0';
|
|
8
12
|
export const WEBGPU_ROUTE_REQUEST_SCHEMA = 'kaminos.webgpu-route-request.v0';
|
|
@@ -12,6 +16,18 @@ function clone(value) {
|
|
|
12
16
|
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
13
17
|
}
|
|
14
18
|
|
|
19
|
+
function stableJson(value) {
|
|
20
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
21
|
+
if (value && typeof value === 'object') {
|
|
22
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function deepEqual(a, b) {
|
|
28
|
+
return stableJson(a) === stableJson(b);
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
function isNonEmptyString(value) {
|
|
16
32
|
return typeof value === 'string' && value.trim().length > 0;
|
|
17
33
|
}
|
|
@@ -190,6 +206,8 @@ export function defineWebGpuRoute(input) {
|
|
|
190
206
|
requiredFeatures: Array.isArray(input.requiredFeatures) ? [...input.requiredFeatures].map(String).sort() : [],
|
|
191
207
|
requiredStages: Array.isArray(input.requiredStages) ? [...input.requiredStages] : [],
|
|
192
208
|
timingSource: input.timingSource || 'queue-submit-wait',
|
|
209
|
+
scheduler: clone(input.scheduler || null),
|
|
210
|
+
backpressure: clone(input.backpressure || null),
|
|
193
211
|
worker: clone(input.worker || null),
|
|
194
212
|
};
|
|
195
213
|
}
|
|
@@ -237,6 +255,16 @@ export function validateRouteDefinition(route) {
|
|
|
237
255
|
}
|
|
238
256
|
if (!isNonEmptyString(route.timingSource)) errors.push('timingSource must be a non-empty string');
|
|
239
257
|
|
|
258
|
+
if (route.scheduler != null) {
|
|
259
|
+
const schedulerResult = validateWebGpuRouteSchedulerProfile(route.scheduler);
|
|
260
|
+
if (!schedulerResult.ok) errors.push(...schedulerResult.errors.map(error => `scheduler.${error}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (route.backpressure != null) {
|
|
264
|
+
const backpressureResult = validateWebGpuRouteBackpressureProfile(route.backpressure);
|
|
265
|
+
if (!backpressureResult.ok) errors.push(...backpressureResult.errors.map(error => `backpressure.${error}`));
|
|
266
|
+
}
|
|
267
|
+
|
|
240
268
|
return { ok: errors.length === 0, errors };
|
|
241
269
|
}
|
|
242
270
|
|
|
@@ -293,6 +321,8 @@ export function createRouteInvocationRequest(route, input) {
|
|
|
293
321
|
routeConfig: clone(input.routeConfig || {}),
|
|
294
322
|
model: clone(route.model),
|
|
295
323
|
kernel: clone(route.kernel),
|
|
324
|
+
scheduler: clone(route.scheduler || null),
|
|
325
|
+
backpressure: clone(route.backpressure || null),
|
|
296
326
|
createdAt: input.createdAt || new Date().toISOString(),
|
|
297
327
|
};
|
|
298
328
|
}
|
|
@@ -315,6 +345,30 @@ export function validateRouteInvocationRequest(request, route) {
|
|
|
315
345
|
validateArtifacts(errors, request.outputs, route.outputRoles, 'outputs', { requireHash: false });
|
|
316
346
|
}
|
|
317
347
|
|
|
348
|
+
if (request.scheduler != null) {
|
|
349
|
+
const schedulerResult = validateWebGpuRouteSchedulerProfile(request.scheduler);
|
|
350
|
+
if (!schedulerResult.ok) errors.push(...schedulerResult.errors.map(error => `scheduler.${error}`));
|
|
351
|
+
}
|
|
352
|
+
if (route?.scheduler != null) {
|
|
353
|
+
if (request.scheduler == null) {
|
|
354
|
+
errors.push('scheduler must match route definition');
|
|
355
|
+
} else if (!deepEqual(request.scheduler, route.scheduler)) {
|
|
356
|
+
errors.push('scheduler must match route definition');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (request.backpressure != null) {
|
|
361
|
+
const backpressureResult = validateWebGpuRouteBackpressureProfile(request.backpressure);
|
|
362
|
+
if (!backpressureResult.ok) errors.push(...backpressureResult.errors.map(error => `backpressure.${error}`));
|
|
363
|
+
}
|
|
364
|
+
if (route?.backpressure != null) {
|
|
365
|
+
if (request.backpressure == null) {
|
|
366
|
+
errors.push('backpressure must match route definition');
|
|
367
|
+
} else if (!deepEqual(request.backpressure, route.backpressure)) {
|
|
368
|
+
errors.push('backpressure must match route definition');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
318
372
|
return { ok: errors.length === 0, errors };
|
|
319
373
|
}
|
|
320
374
|
|
|
@@ -20,6 +20,77 @@ function parseTime(value) {
|
|
|
20
20
|
return Number.isFinite(ms) ? ms : null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function clone(value) {
|
|
24
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FALSE_VERIFIED_SCHEDULER_DOWNGRADES = new Set([
|
|
28
|
+
'yield-events-missing',
|
|
29
|
+
'event-trace-missing',
|
|
30
|
+
'queue-wait-events-missing',
|
|
31
|
+
'boundary-assertion-event-mismatch',
|
|
32
|
+
'requested-boundary-assertion-missing',
|
|
33
|
+
'requested-field-dropped-without-unsupported',
|
|
34
|
+
'boundary-assertions-missing',
|
|
35
|
+
'timing-proxy-only',
|
|
36
|
+
'route-identity-missing',
|
|
37
|
+
'scheduler-envelope-missing',
|
|
38
|
+
'backpressure-envelope-missing',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function nestedSchedulerDowngrades(schedulerVerification) {
|
|
42
|
+
return Array.isArray(schedulerVerification?.downgrades)
|
|
43
|
+
? [...schedulerVerification.downgrades]
|
|
44
|
+
: [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasVerifiedBoundaryAssertion(schedulerVerification) {
|
|
48
|
+
return Array.isArray(schedulerVerification?.boundaryAssertions)
|
|
49
|
+
&& schedulerVerification.boundaryAssertions.some(assertion => assertion?.status === 'verified');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasEventTraceEvents(schedulerVerification) {
|
|
53
|
+
return Array.isArray(schedulerVerification?.eventTrace?.events)
|
|
54
|
+
&& schedulerVerification.eventTrace.events.length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeNestedSchedulerVerification(schedulerVerification) {
|
|
58
|
+
if (!schedulerVerification) {
|
|
59
|
+
return {
|
|
60
|
+
receipt: null,
|
|
61
|
+
reportedStatus: null,
|
|
62
|
+
status: null,
|
|
63
|
+
classification: null,
|
|
64
|
+
observationClass: null,
|
|
65
|
+
downgrades: [],
|
|
66
|
+
timingAuthority: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const receipt = clone(schedulerVerification);
|
|
70
|
+
const downgrades = nestedSchedulerDowngrades(schedulerVerification);
|
|
71
|
+
const reportedStatus = schedulerVerification.status || null;
|
|
72
|
+
const falseVerifiedDowngrade = downgrades.some(downgrade => FALSE_VERIFIED_SCHEDULER_DOWNGRADES.has(downgrade));
|
|
73
|
+
const falseVerifiedShape = reportedStatus === 'verified'
|
|
74
|
+
&& (!hasEventTraceEvents(schedulerVerification) || !hasVerifiedBoundaryAssertion(schedulerVerification));
|
|
75
|
+
const status = reportedStatus === 'verified' && (falseVerifiedDowngrade || falseVerifiedShape)
|
|
76
|
+
? 'scheduler-unverified'
|
|
77
|
+
: reportedStatus;
|
|
78
|
+
if (status !== reportedStatus) {
|
|
79
|
+
receipt.reportedStatus = reportedStatus;
|
|
80
|
+
receipt.status = status;
|
|
81
|
+
if (receipt.classification === 'observed-boundary') receipt.classification = 'config-only';
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
receipt,
|
|
85
|
+
reportedStatus,
|
|
86
|
+
status,
|
|
87
|
+
classification: receipt.classification || null,
|
|
88
|
+
observationClass: receipt.observationClass || null,
|
|
89
|
+
downgrades,
|
|
90
|
+
timingAuthority: receipt.eventTrace?.timingAuthority || null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
23
94
|
function staleReason(receipt, options) {
|
|
24
95
|
if (!Number.isFinite(options.maxAgeMs)) return null;
|
|
25
96
|
const createdMs = parseTime(receipt.createdAt);
|
|
@@ -119,6 +190,7 @@ export function classifyWebGpuRouteReceiptEvidence(receipt, options = {}) {
|
|
|
119
190
|
const base = baseClassification(receipt, options);
|
|
120
191
|
const scheduler = receipt?.runtime?.scheduler || null;
|
|
121
192
|
const backpressure = receipt?.runtime?.backpressure || null;
|
|
193
|
+
const schedulerVerification = normalizeNestedSchedulerVerification(receipt?.runtime?.schedulerVerification || null);
|
|
122
194
|
return {
|
|
123
195
|
schema: WEBGPU_ROUTE_EVIDENCE_CLASSIFICATION_SCHEMA,
|
|
124
196
|
classification: base.classification,
|
|
@@ -131,7 +203,14 @@ export function classifyWebGpuRouteReceiptEvidence(receipt, options = {}) {
|
|
|
131
203
|
adapterName: receipt?.backend?.adapterName || null,
|
|
132
204
|
timingSource: receipt?.timings?.source || receipt?.timings?.profile?.timingSource || null,
|
|
133
205
|
totalMs: Number.isFinite(receipt?.timings?.totalMs) ? receipt.timings.totalMs : null,
|
|
134
|
-
|
|
206
|
+
schedulerVerification: schedulerVerification.receipt,
|
|
207
|
+
schedulerVerificationState: schedulerVerification.status || scheduler?.verificationState || null,
|
|
208
|
+
schedulerVerificationStatus: schedulerVerification.status,
|
|
209
|
+
schedulerVerificationReportedStatus: schedulerVerification.reportedStatus,
|
|
210
|
+
schedulerVerificationClassification: schedulerVerification.classification,
|
|
211
|
+
schedulerVerificationObservationClass: schedulerVerification.observationClass,
|
|
212
|
+
schedulerVerificationDowngrades: schedulerVerification.downgrades,
|
|
213
|
+
schedulerVerificationEventTraceTimingAuthority: schedulerVerification.timingAuthority,
|
|
135
214
|
schedulerMode: scheduler?.effectiveScheduler?.mode || scheduler?.requestedScheduler?.mode || null,
|
|
136
215
|
schedulerUnsupportedFields: Array.isArray(scheduler?.effectiveScheduler?.unsupportedFields)
|
|
137
216
|
? [...scheduler.effectiveScheduler.unsupportedFields]
|
|
@@ -6,6 +6,24 @@ const VERIFICATION_STATES = new Set(['verified', 'scheduler-unverified', 'unsupp
|
|
|
6
6
|
const BUDGETS = new Set(['interactive', 'visible-wait', 'furnace', 'batch', 'unknown']);
|
|
7
7
|
const MEMORY_EXCLUSIVITY = new Set(['shared', 'exclusive', 'unknown']);
|
|
8
8
|
const WARM_CACHE_STATES = new Set(['cold', 'warm', 'hot', 'unknown']);
|
|
9
|
+
const BREATHABILITY_SPAN_KINDS = new Set([
|
|
10
|
+
'gpu-submit-bound',
|
|
11
|
+
'gpu-submit-loop',
|
|
12
|
+
'readback-bound',
|
|
13
|
+
'js-yieldable',
|
|
14
|
+
'cpu-bound',
|
|
15
|
+
'external-bound',
|
|
16
|
+
'unknown',
|
|
17
|
+
]);
|
|
18
|
+
const BREATHABILITY_CHECKPOINT_KINDS = new Set([
|
|
19
|
+
'pre-submit',
|
|
20
|
+
'post-submit',
|
|
21
|
+
'stage-boundary',
|
|
22
|
+
'diffusion-step',
|
|
23
|
+
'readback',
|
|
24
|
+
'external-callback',
|
|
25
|
+
'unknown',
|
|
26
|
+
]);
|
|
9
27
|
|
|
10
28
|
function clone(value) {
|
|
11
29
|
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
@@ -53,6 +71,41 @@ function normalizeEffectiveScheduler(input = {}, requestedScheduler) {
|
|
|
53
71
|
};
|
|
54
72
|
}
|
|
55
73
|
|
|
74
|
+
function normalizeBreathabilitySpan(input = {}) {
|
|
75
|
+
return {
|
|
76
|
+
name: input.name,
|
|
77
|
+
stage: input.stage || null,
|
|
78
|
+
kind: input.kind || 'unknown',
|
|
79
|
+
interruptible: Boolean(input.interruptible),
|
|
80
|
+
canYieldBefore: Boolean(input.canYieldBefore),
|
|
81
|
+
canYieldAfter: Boolean(input.canYieldAfter),
|
|
82
|
+
nonInterruptibleReason: input.nonInterruptibleReason || null,
|
|
83
|
+
metadata: isPlainObject(input.metadata) ? clone(input.metadata) : {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeBreathabilityCheckpoint(input = {}) {
|
|
88
|
+
return {
|
|
89
|
+
name: input.name,
|
|
90
|
+
kind: input.kind || 'unknown',
|
|
91
|
+
beforeStage: input.beforeStage || null,
|
|
92
|
+
afterStage: input.afterStage || null,
|
|
93
|
+
yieldable: Boolean(input.yieldable),
|
|
94
|
+
waitsForSubmittedWorkDone: Boolean(input.waitsForSubmittedWorkDone),
|
|
95
|
+
metadata: isPlainObject(input.metadata) ? clone(input.metadata) : {},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeBreathability(input = {}) {
|
|
100
|
+
return {
|
|
101
|
+
spans: Array.isArray(input.spans) ? input.spans.map(normalizeBreathabilitySpan) : [],
|
|
102
|
+
checkpoints: Array.isArray(input.checkpoints)
|
|
103
|
+
? input.checkpoints.map(normalizeBreathabilityCheckpoint)
|
|
104
|
+
: [],
|
|
105
|
+
notes: input.notes || null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
56
109
|
function validateSchedulerShape(errors, scheduler, label) {
|
|
57
110
|
if (!isPlainObject(scheduler)) {
|
|
58
111
|
errors.push(`${label} must be an object`);
|
|
@@ -79,6 +132,59 @@ function validateSchedulerShape(errors, scheduler, label) {
|
|
|
79
132
|
}
|
|
80
133
|
}
|
|
81
134
|
|
|
135
|
+
function validateBreathability(errors, breathability) {
|
|
136
|
+
if (breathability == null) return;
|
|
137
|
+
if (!isPlainObject(breathability)) {
|
|
138
|
+
errors.push('breathability must be an object');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(breathability.spans)) {
|
|
142
|
+
errors.push('breathability.spans must be an array');
|
|
143
|
+
} else {
|
|
144
|
+
breathability.spans.forEach((span, index) => {
|
|
145
|
+
const path = `breathability.spans[${index}]`;
|
|
146
|
+
if (!isNonEmptyString(span?.name)) errors.push(`${path}.name must be a non-empty string`);
|
|
147
|
+
if (span?.stage != null && !isNonEmptyString(span.stage)) errors.push(`${path}.stage must be null or a non-empty string`);
|
|
148
|
+
if (!BREATHABILITY_SPAN_KINDS.has(span?.kind)) errors.push(`${path}.kind has unsupported value`);
|
|
149
|
+
if (typeof span?.interruptible !== 'boolean') errors.push(`${path}.interruptible must be a boolean`);
|
|
150
|
+
if (typeof span?.canYieldBefore !== 'boolean') errors.push(`${path}.canYieldBefore must be a boolean`);
|
|
151
|
+
if (typeof span?.canYieldAfter !== 'boolean') errors.push(`${path}.canYieldAfter must be a boolean`);
|
|
152
|
+
if (span?.nonInterruptibleReason != null && !isNonEmptyString(span.nonInterruptibleReason)) {
|
|
153
|
+
errors.push(`${path}.nonInterruptibleReason must be null or a non-empty string`);
|
|
154
|
+
}
|
|
155
|
+
if (!isPlainObject(span?.metadata)) errors.push(`${path}.metadata must be an object`);
|
|
156
|
+
if ((span?.kind === 'gpu-submit-bound' || span?.kind === 'gpu-submit-loop') && span.interruptible) {
|
|
157
|
+
errors.push(`${path}.${span.kind} cannot be interruptible after GPU submit`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!Array.isArray(breathability.checkpoints)) {
|
|
163
|
+
errors.push('breathability.checkpoints must be an array');
|
|
164
|
+
} else {
|
|
165
|
+
breathability.checkpoints.forEach((checkpoint, index) => {
|
|
166
|
+
const path = `breathability.checkpoints[${index}]`;
|
|
167
|
+
if (!isNonEmptyString(checkpoint?.name)) errors.push(`${path}.name must be a non-empty string`);
|
|
168
|
+
if (!BREATHABILITY_CHECKPOINT_KINDS.has(checkpoint?.kind)) errors.push(`${path}.kind has unsupported value`);
|
|
169
|
+
if (checkpoint?.beforeStage != null && !isNonEmptyString(checkpoint.beforeStage)) {
|
|
170
|
+
errors.push(`${path}.beforeStage must be null or a non-empty string`);
|
|
171
|
+
}
|
|
172
|
+
if (checkpoint?.afterStage != null && !isNonEmptyString(checkpoint.afterStage)) {
|
|
173
|
+
errors.push(`${path}.afterStage must be null or a non-empty string`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof checkpoint?.yieldable !== 'boolean') errors.push(`${path}.yieldable must be a boolean`);
|
|
176
|
+
if (typeof checkpoint?.waitsForSubmittedWorkDone !== 'boolean') {
|
|
177
|
+
errors.push(`${path}.waitsForSubmittedWorkDone must be a boolean`);
|
|
178
|
+
}
|
|
179
|
+
if (!isPlainObject(checkpoint?.metadata)) errors.push(`${path}.metadata must be an object`);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (breathability.notes != null && !isNonEmptyString(breathability.notes)) {
|
|
184
|
+
errors.push('breathability.notes must be null or a non-empty string');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
82
188
|
function missingUnsupportedField(effectiveScheduler, field) {
|
|
83
189
|
return !effectiveScheduler.unsupportedFields.includes(field)
|
|
84
190
|
&& !effectiveScheduler.unsupportedFields.includes('phaseChunkSize');
|
|
@@ -98,6 +204,7 @@ export function createWebGpuRouteSchedulerProfile(input = {}) {
|
|
|
98
204
|
requestedScheduler,
|
|
99
205
|
effectiveScheduler,
|
|
100
206
|
verificationState,
|
|
207
|
+
breathability: normalizeBreathability(input.breathability),
|
|
101
208
|
};
|
|
102
209
|
|
|
103
210
|
const result = validateWebGpuRouteSchedulerProfile(profile);
|
|
@@ -115,6 +222,7 @@ export function validateWebGpuRouteSchedulerProfile(profile) {
|
|
|
115
222
|
}
|
|
116
223
|
validateSchedulerShape(errors, profile.requestedScheduler, 'requestedScheduler');
|
|
117
224
|
validateSchedulerShape(errors, profile.effectiveScheduler, 'effectiveScheduler');
|
|
225
|
+
validateBreathability(errors, profile.breathability);
|
|
118
226
|
|
|
119
227
|
if (!Array.isArray(profile.effectiveScheduler?.unsupportedFields)) {
|
|
120
228
|
errors.push('effectiveScheduler.unsupportedFields must be an array');
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
export const SCHEDULER_VERIFICATION_RECEIPT_SCHEMA = 'kaminos.webgpu-scheduler-verification-receipt.v0';
|
|
2
|
+
export const SCHEDULER_EVENT_TRACE_SCHEMA = 'kaminos.webgpu-scheduler-event-trace.v0';
|
|
3
|
+
|
|
4
|
+
const LEGACY_PHASE_FIELD_ALIASES = {
|
|
5
|
+
spnPatch: ['phaseChunkSize.spnPatch', 'spnPatchChunkSize'],
|
|
6
|
+
vitBlock: ['phaseChunkSize.vitBlock', 'vitBlockChunkSize'],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function cloneJson(value) {
|
|
10
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function uniq(values) {
|
|
14
|
+
return [...new Set(values.filter(Boolean))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asObject(value) {
|
|
18
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function legacyDirectKey(key) {
|
|
22
|
+
if (key === 'spnPatch') return 'spnPatchChunkSize';
|
|
23
|
+
if (key === 'vitBlock') return 'vitBlockChunkSize';
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function canonicalPhaseField(key) {
|
|
28
|
+
return `phaseChunkSize.${key}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function knownPhaseKeys(scheduler = {}) {
|
|
32
|
+
const phaseChunkSize = asObject(scheduler.phaseChunkSize);
|
|
33
|
+
return uniq([
|
|
34
|
+
...Object.keys(phaseChunkSize),
|
|
35
|
+
...Object.keys(LEGACY_PHASE_FIELD_ALIASES).filter(key => Number.isFinite(scheduler[legacyDirectKey(key)])),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function phaseValue(scheduler = {}, key) {
|
|
40
|
+
const phaseChunkSize = asObject(scheduler.phaseChunkSize);
|
|
41
|
+
if (Number.isFinite(phaseChunkSize[key])) return phaseChunkSize[key];
|
|
42
|
+
const directKey = legacyDirectKey(key);
|
|
43
|
+
return directKey && Number.isFinite(scheduler[directKey]) ? scheduler[directKey] : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requestedPhaseFields(requestedScheduler = {}) {
|
|
47
|
+
return knownPhaseKeys(requestedScheduler)
|
|
48
|
+
.map(key => {
|
|
49
|
+
const value = phaseValue(requestedScheduler, key);
|
|
50
|
+
return Number.isFinite(value) && value > 0 ? { key, field: canonicalPhaseField(key), value } : null;
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unsupportedFieldsFrom(input = {}) {
|
|
56
|
+
const scheduler = asObject(input.scheduler);
|
|
57
|
+
const effectiveScheduler = asObject(scheduler.effectiveScheduler);
|
|
58
|
+
return uniq([
|
|
59
|
+
...(Array.isArray(input.unsupportedFields) ? input.unsupportedFields : []),
|
|
60
|
+
...(Array.isArray(scheduler.unsupportedFields) ? scheduler.unsupportedFields : []),
|
|
61
|
+
...(Array.isArray(effectiveScheduler.unsupportedFields) ? effectiveScheduler.unsupportedFields : []),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function aliasesForPhaseKey(key) {
|
|
66
|
+
return uniq([
|
|
67
|
+
canonicalPhaseField(key),
|
|
68
|
+
...(LEGACY_PHASE_FIELD_ALIASES[key] || []),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function unsupportedCovers(fieldKey, unsupportedFields = []) {
|
|
73
|
+
const aliases = aliasesForPhaseKey(fieldKey);
|
|
74
|
+
return unsupportedFields.some(field => aliases.includes(field) || field === fieldKey || field === 'phaseChunkSize');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function eventKindCounts(events = []) {
|
|
78
|
+
return {
|
|
79
|
+
eventCount: events.length,
|
|
80
|
+
queueStartCount: events.filter(event => event?.kind === 'queue-work-done-start').length,
|
|
81
|
+
queueEndCount: events.filter(event => event?.kind === 'queue-work-done-end').length,
|
|
82
|
+
yieldStartCount: events.filter(event => event?.kind === 'js-yield-start').length,
|
|
83
|
+
yieldEndCount: events.filter(event => event?.kind === 'js-yield-end').length,
|
|
84
|
+
chunkCount: events.filter(event => event?.kind === 'chunk-start' || event?.boundary || event?.phase).length,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeEventTrace(eventTrace = {}) {
|
|
89
|
+
const events = Array.isArray(eventTrace.events) ? cloneJson(eventTrace.events) : [];
|
|
90
|
+
return {
|
|
91
|
+
schema: eventTrace.schema || SCHEDULER_EVENT_TRACE_SCHEMA,
|
|
92
|
+
clock: eventTrace.clock || 'performance.now',
|
|
93
|
+
timingAuthority: eventTrace.timingAuthority || 'not-observed',
|
|
94
|
+
events,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeFrameTail(frameTail = {}, eventTrace = {}) {
|
|
99
|
+
const evidenceSource = eventTrace.timingAuthority === 'raf-and-queue-proxy'
|
|
100
|
+
? 'raf-and-queue-proxy'
|
|
101
|
+
: (frameTail.evidenceSource || eventTrace.timingAuthority || 'not-observed');
|
|
102
|
+
return {
|
|
103
|
+
evidenceSource,
|
|
104
|
+
disclaimer: frameTail.disclaimer || 'not-gpu-exclusive-or-present-latency',
|
|
105
|
+
rafFps: frameTail.rafFps ?? null,
|
|
106
|
+
frameP95Ms: frameTail.frameP95Ms ?? null,
|
|
107
|
+
queueDoneP95Ms: frameTail.queueDoneP95Ms ?? null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function legacyBoundaryForPhaseKey(key) {
|
|
112
|
+
if (key === 'spnPatch') return 'spn-patch-chunk';
|
|
113
|
+
if (key === 'vitBlock') return 'vit-block-chunk';
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function eventMatchesBoundary(event = {}, boundary) {
|
|
118
|
+
if (!boundary) return false;
|
|
119
|
+
return event.boundary === boundary
|
|
120
|
+
|| event.phase === boundary
|
|
121
|
+
|| event.stage === boundary
|
|
122
|
+
|| event.name === boundary;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function eventMatchesPhaseKey(event = {}, key) {
|
|
126
|
+
return event.phase === key
|
|
127
|
+
|| event.stage === key
|
|
128
|
+
|| event.boundary === key
|
|
129
|
+
|| event.boundary === `moge-stage:${key}`
|
|
130
|
+
|| event.boundary === `stage:${key}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function observedBoundaryForPhaseKey(key, events = []) {
|
|
134
|
+
const legacyBoundary = legacyBoundaryForPhaseKey(key);
|
|
135
|
+
if (legacyBoundary) return legacyBoundary;
|
|
136
|
+
const event = events.find(candidate => eventMatchesPhaseKey(candidate, key));
|
|
137
|
+
return event?.boundary || event?.phase || key;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function observedCountForPhaseKey(key, observedBoundary, events = []) {
|
|
141
|
+
return events.filter(event => eventMatchesBoundary(event, observedBoundary) || eventMatchesPhaseKey(event, key)).length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function derivedAssertionStatus(key, observedCount, unsupported) {
|
|
145
|
+
if (unsupported) return 'unsupported';
|
|
146
|
+
if (observedCount <= 0) return 'unverified';
|
|
147
|
+
return legacyBoundaryForPhaseKey(key) ? 'verified' : 'observed';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deriveBoundaryAssertions({
|
|
151
|
+
boundaryAssertions,
|
|
152
|
+
requestedScheduler,
|
|
153
|
+
effectiveScheduler,
|
|
154
|
+
unsupportedFields,
|
|
155
|
+
events,
|
|
156
|
+
}) {
|
|
157
|
+
if (Array.isArray(boundaryAssertions) && boundaryAssertions.length) {
|
|
158
|
+
return cloneJson(boundaryAssertions).map(normalizeCallerBoundaryAssertion);
|
|
159
|
+
}
|
|
160
|
+
const counts = eventKindCounts(events);
|
|
161
|
+
return requestedPhaseFields(requestedScheduler).map(({ key, field, value }) => {
|
|
162
|
+
const effective = phaseValue(effectiveScheduler, key);
|
|
163
|
+
const unsupported = unsupportedCovers(key, unsupportedFields);
|
|
164
|
+
const observedBoundary = observedBoundaryForPhaseKey(key, events);
|
|
165
|
+
const observedCount = observedCountForPhaseKey(key, observedBoundary, events);
|
|
166
|
+
return {
|
|
167
|
+
field,
|
|
168
|
+
requested: value,
|
|
169
|
+
effective: Number.isFinite(effective) ? effective : null,
|
|
170
|
+
status: derivedAssertionStatus(key, observedCount, unsupported),
|
|
171
|
+
observedBoundary,
|
|
172
|
+
observedCount,
|
|
173
|
+
expectedMinimumCount: 1,
|
|
174
|
+
observedQueueWaitCount: Math.min(counts.queueStartCount, counts.queueEndCount),
|
|
175
|
+
observedYieldCount: Math.min(counts.yieldStartCount, counts.yieldEndCount),
|
|
176
|
+
unsupportedReason: unsupported ? 'effective scheduler declared this field unsupported' : null,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fieldKeyForAssertion(assertion = {}) {
|
|
182
|
+
const field = assertion.field;
|
|
183
|
+
if (typeof field !== 'string') return null;
|
|
184
|
+
for (const [key, aliases] of Object.entries(LEGACY_PHASE_FIELD_ALIASES)) {
|
|
185
|
+
if (aliases.includes(field)) return key;
|
|
186
|
+
}
|
|
187
|
+
if (field.startsWith('phaseChunkSize.')) return field.slice('phaseChunkSize.'.length);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fieldBoundaryForAssertion(assertion = {}) {
|
|
192
|
+
const key = fieldKeyForAssertion(assertion);
|
|
193
|
+
return key ? legacyBoundaryForPhaseKey(key) : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function expectedBoundaryForAssertion(assertion = {}) {
|
|
197
|
+
return fieldBoundaryForAssertion(assertion) || assertion.observedBoundary || null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertionBoundaryConflictsWithField(assertion = {}) {
|
|
201
|
+
const fieldBoundary = fieldBoundaryForAssertion(assertion);
|
|
202
|
+
return Boolean(fieldBoundary && assertion.observedBoundary && assertion.observedBoundary !== fieldBoundary);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function assertionHasMatchingEvent(assertion = {}, events = []) {
|
|
206
|
+
const expectedBoundary = expectedBoundaryForAssertion(assertion);
|
|
207
|
+
if (!expectedBoundary) return false;
|
|
208
|
+
return events.some(event => eventMatchesBoundary(event, expectedBoundary));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function assertionObservesRequestedKey(assertion = {}, key, events = []) {
|
|
212
|
+
return (assertion?.status === 'verified' || assertion?.status === 'observed')
|
|
213
|
+
&& fieldKeyForAssertion(assertion) === key
|
|
214
|
+
&& !assertionBoundaryConflictsWithField(assertion)
|
|
215
|
+
&& assertionHasMatchingEvent(assertion, events);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function assertionVerifiesRequestedKey(assertion = {}, key, events = []) {
|
|
219
|
+
return assertion?.status === 'verified'
|
|
220
|
+
&& assertionObservesRequestedKey(assertion, key, events);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function normalizeCallerBoundaryAssertion(assertion = {}) {
|
|
224
|
+
const normalized = { ...assertion };
|
|
225
|
+
const key = fieldKeyForAssertion(normalized);
|
|
226
|
+
if (normalized.status === 'verified' && key && !legacyBoundaryForPhaseKey(key)) {
|
|
227
|
+
normalized.reportedStatus = normalized.reportedStatus || normalized.status;
|
|
228
|
+
normalized.status = 'observed';
|
|
229
|
+
}
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function routeIsPresent(route = {}) {
|
|
234
|
+
return Boolean(route.pipelineId || route.requestedRouteId || route.effectiveRouteId || route.adapterReport?.path);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function waitRequested(scheduler = {}) {
|
|
238
|
+
return Boolean(scheduler.waitForSubmittedWorkDone);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function yieldRequested(scheduler = {}) {
|
|
242
|
+
return Number(scheduler.yieldMs || 0) > 0 || Number(scheduler.gaussianPhaseYieldMs || 0) > 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function droppedRequestedFields(requestedScheduler = {}, effectiveScheduler = {}, unsupportedFields = []) {
|
|
246
|
+
return requestedPhaseFields(requestedScheduler)
|
|
247
|
+
.filter(({ key }) => !Number.isFinite(phaseValue(effectiveScheduler, key)) && !unsupportedCovers(key, unsupportedFields))
|
|
248
|
+
.map(({ field }) => field);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeDowngrades(values) {
|
|
252
|
+
return uniq(values);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function deriveObservationClass({
|
|
256
|
+
status,
|
|
257
|
+
boundaryAssertions,
|
|
258
|
+
falseAuthorityChecks,
|
|
259
|
+
events,
|
|
260
|
+
}) {
|
|
261
|
+
if (status === 'verified') return 'observed-scheduler-boundary';
|
|
262
|
+
if (falseAuthorityChecks.timingProxyOnly) return 'proxy-only';
|
|
263
|
+
if (!events.length) return 'config-only';
|
|
264
|
+
if (boundaryAssertions.some(assertion => assertion?.status === 'observed' || assertion?.status === 'verified')) {
|
|
265
|
+
return 'observed-stage-boundary';
|
|
266
|
+
}
|
|
267
|
+
return 'event-trace-only';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createSchedulerVerificationReceipt(input = {}) {
|
|
271
|
+
const route = cloneJson(asObject(input.route));
|
|
272
|
+
const scheduler = cloneJson(asObject(input.scheduler));
|
|
273
|
+
const backpressure = cloneJson(asObject(input.backpressure));
|
|
274
|
+
const requestedScheduler = asObject(scheduler.requestedScheduler);
|
|
275
|
+
const effectiveScheduler = asObject(scheduler.effectiveScheduler);
|
|
276
|
+
const unsupportedFields = unsupportedFieldsFrom({ ...input, scheduler });
|
|
277
|
+
const eventTrace = normalizeEventTrace(input.eventTrace);
|
|
278
|
+
const frameTail = normalizeFrameTail(input.frameTail || {}, eventTrace);
|
|
279
|
+
const events = eventTrace.events;
|
|
280
|
+
const counts = eventKindCounts(events);
|
|
281
|
+
const boundaryAssertions = deriveBoundaryAssertions({
|
|
282
|
+
boundaryAssertions: input.boundaryAssertions,
|
|
283
|
+
requestedScheduler,
|
|
284
|
+
effectiveScheduler,
|
|
285
|
+
unsupportedFields,
|
|
286
|
+
events,
|
|
287
|
+
});
|
|
288
|
+
const downgrades = Array.isArray(input.downgrades) ? [...input.downgrades] : [];
|
|
289
|
+
const falseAuthorityChecks = {
|
|
290
|
+
eventTraceMissing: false,
|
|
291
|
+
verifiedWithoutObservedBoundary: false,
|
|
292
|
+
timingProxyOnly: false,
|
|
293
|
+
queueWaitEventsMissing: false,
|
|
294
|
+
boundaryAssertionEventMismatch: false,
|
|
295
|
+
requestedBoundaryAssertionMissing: false,
|
|
296
|
+
requestedFieldDroppedWithoutUnsupported: false,
|
|
297
|
+
...(asObject(input.falseAuthorityChecks)),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (!routeIsPresent(route)) downgrades.push('route-identity-missing');
|
|
301
|
+
if (!scheduler.schema && !scheduler.requestedScheduler && !scheduler.effectiveScheduler) downgrades.push('scheduler-envelope-missing');
|
|
302
|
+
if (!backpressure.schema && Object.keys(backpressure).length === 0) downgrades.push('backpressure-envelope-missing');
|
|
303
|
+
|
|
304
|
+
const droppedFields = droppedRequestedFields(requestedScheduler, effectiveScheduler, unsupportedFields);
|
|
305
|
+
if (droppedFields.length) {
|
|
306
|
+
downgrades.push('requested-field-dropped-without-unsupported');
|
|
307
|
+
falseAuthorityChecks.requestedFieldDroppedWithoutUnsupported = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!events.length) {
|
|
311
|
+
downgrades.push('event-trace-missing');
|
|
312
|
+
falseAuthorityChecks.eventTraceMissing = true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const verifiedAssertions = boundaryAssertions.filter(assertion => assertion?.status === 'verified');
|
|
316
|
+
const mismatchedVerifiedAssertions = verifiedAssertions.filter(assertion => (
|
|
317
|
+
assertionBoundaryConflictsWithField(assertion) || !assertionHasMatchingEvent(assertion, events)
|
|
318
|
+
));
|
|
319
|
+
if (mismatchedVerifiedAssertions.length) {
|
|
320
|
+
downgrades.push('boundary-assertion-event-mismatch');
|
|
321
|
+
falseAuthorityChecks.boundaryAssertionEventMismatch = true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const requestedBoundaryKeys = requestedPhaseFields(requestedScheduler)
|
|
325
|
+
.map(({ key }) => key)
|
|
326
|
+
.filter(key => !unsupportedCovers(key, unsupportedFields));
|
|
327
|
+
const missingRequestedBoundaryAssertions = requestedBoundaryKeys
|
|
328
|
+
.filter(key => !boundaryAssertions.some(assertion => assertionObservesRequestedKey(assertion, key, events)));
|
|
329
|
+
if (missingRequestedBoundaryAssertions.length) {
|
|
330
|
+
downgrades.push('requested-boundary-assertion-missing');
|
|
331
|
+
falseAuthorityChecks.requestedBoundaryAssertionMissing = true;
|
|
332
|
+
}
|
|
333
|
+
if (scheduler.verificationState === 'verified' && !verifiedAssertions.length) {
|
|
334
|
+
downgrades.push('boundary-assertions-missing');
|
|
335
|
+
falseAuthorityChecks.verifiedWithoutObservedBoundary = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (eventTrace.timingAuthority === 'raf-and-queue-proxy') {
|
|
339
|
+
downgrades.push('timing-proxy-only');
|
|
340
|
+
falseAuthorityChecks.timingProxyOnly = true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (waitRequested(requestedScheduler) || waitRequested(effectiveScheduler)) {
|
|
344
|
+
const hasQueuePair = counts.queueStartCount > 0 && counts.queueEndCount > 0;
|
|
345
|
+
if (!hasQueuePair) {
|
|
346
|
+
downgrades.push('queue-wait-events-missing');
|
|
347
|
+
falseAuthorityChecks.queueWaitEventsMissing = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (yieldRequested(requestedScheduler) || yieldRequested(effectiveScheduler)) {
|
|
352
|
+
const hasYieldPair = counts.yieldStartCount > 0 && counts.yieldEndCount > 0;
|
|
353
|
+
if (!hasYieldPair) downgrades.push('yield-events-missing');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const invalid = droppedFields.length > 0 || !routeIsPresent(route);
|
|
357
|
+
const unsupported = unsupportedFields.length > 0 || boundaryAssertions.some(assertion => assertion?.status === 'unsupported');
|
|
358
|
+
const verified = events.length > 0
|
|
359
|
+
&& requestedBoundaryKeys.length > 0
|
|
360
|
+
&& verifiedAssertions.length > 0
|
|
361
|
+
&& requestedBoundaryKeys.every(key => verifiedAssertions.some(assertion => assertionVerifiesRequestedKey(assertion, key, events)))
|
|
362
|
+
&& !falseAuthorityChecks.timingProxyOnly
|
|
363
|
+
&& !falseAuthorityChecks.queueWaitEventsMissing
|
|
364
|
+
&& !falseAuthorityChecks.boundaryAssertionEventMismatch
|
|
365
|
+
&& !falseAuthorityChecks.requestedBoundaryAssertionMissing
|
|
366
|
+
&& !falseAuthorityChecks.requestedFieldDroppedWithoutUnsupported
|
|
367
|
+
&& !downgrades.includes('yield-events-missing');
|
|
368
|
+
|
|
369
|
+
let status = input.status || 'scheduler-unverified';
|
|
370
|
+
if (invalid) status = 'invalid';
|
|
371
|
+
else if (unsupported) status = 'unsupported';
|
|
372
|
+
else if (verified) status = 'verified';
|
|
373
|
+
else status = 'scheduler-unverified';
|
|
374
|
+
|
|
375
|
+
const classification = status === 'verified'
|
|
376
|
+
? 'observed-boundary'
|
|
377
|
+
: (status === 'unsupported'
|
|
378
|
+
? 'unsupported'
|
|
379
|
+
: (status === 'invalid'
|
|
380
|
+
? 'invalid'
|
|
381
|
+
: (falseAuthorityChecks.timingProxyOnly ? 'damage-only' : 'config-only')));
|
|
382
|
+
const observationClass = deriveObservationClass({
|
|
383
|
+
status,
|
|
384
|
+
boundaryAssertions,
|
|
385
|
+
falseAuthorityChecks,
|
|
386
|
+
events,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
schema: SCHEDULER_VERIFICATION_RECEIPT_SCHEMA,
|
|
391
|
+
status,
|
|
392
|
+
classification,
|
|
393
|
+
observationClass,
|
|
394
|
+
route,
|
|
395
|
+
scheduler: {
|
|
396
|
+
...scheduler,
|
|
397
|
+
unsupportedFields,
|
|
398
|
+
},
|
|
399
|
+
backpressure,
|
|
400
|
+
eventTrace,
|
|
401
|
+
boundaryAssertions,
|
|
402
|
+
frameTail,
|
|
403
|
+
downgrades: normalizeDowngrades(downgrades),
|
|
404
|
+
falseAuthorityChecks,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function validateSchedulerVerificationReceipt(receipt = {}) {
|
|
409
|
+
const errors = [];
|
|
410
|
+
if (receipt.schema !== SCHEDULER_VERIFICATION_RECEIPT_SCHEMA) errors.push('schema-mismatch');
|
|
411
|
+
if (!routeIsPresent(receipt.route || {})) errors.push('route-identity-missing');
|
|
412
|
+
if (!receipt.scheduler || typeof receipt.scheduler !== 'object') errors.push('scheduler-envelope-missing');
|
|
413
|
+
if (!receipt.backpressure || typeof receipt.backpressure !== 'object') errors.push('backpressure-envelope-missing');
|
|
414
|
+
if (receipt.status === 'verified') {
|
|
415
|
+
if (!Array.isArray(receipt.eventTrace?.events) || !receipt.eventTrace.events.length) errors.push('verified-without-event-trace');
|
|
416
|
+
if (!Array.isArray(receipt.boundaryAssertions) || !receipt.boundaryAssertions.some(assertion => assertion?.status === 'verified')) {
|
|
417
|
+
errors.push('verified-without-boundary-assertion');
|
|
418
|
+
}
|
|
419
|
+
if (receipt.falseAuthorityChecks?.timingProxyOnly) errors.push('verified-from-proxy-timing');
|
|
420
|
+
if (receipt.falseAuthorityChecks?.queueWaitEventsMissing) errors.push('verified-without-queue-wait-events');
|
|
421
|
+
if (receipt.falseAuthorityChecks?.boundaryAssertionEventMismatch) errors.push('verified-with-mismatched-boundary-assertion');
|
|
422
|
+
if (receipt.falseAuthorityChecks?.requestedBoundaryAssertionMissing) errors.push('verified-without-requested-boundary-assertion');
|
|
423
|
+
if (receipt.falseAuthorityChecks?.requestedFieldDroppedWithoutUnsupported) errors.push('verified-with-dropped-requested-field');
|
|
424
|
+
if (Array.isArray(receipt.downgrades) && receipt.downgrades.includes('yield-events-missing')) {
|
|
425
|
+
errors.push('verified-without-yield-events');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (receipt.status === 'invalid') errors.push('receipt-invalid');
|
|
429
|
+
return {
|
|
430
|
+
ok: errors.length === 0,
|
|
431
|
+
errors,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function classifySchedulerVerificationReceipt(receipt = {}) {
|
|
436
|
+
return {
|
|
437
|
+
status: receipt.status || 'unknown',
|
|
438
|
+
classification: receipt.classification || 'unknown',
|
|
439
|
+
observationClass: receipt.observationClass || 'unknown',
|
|
440
|
+
downgrades: Array.isArray(receipt.downgrades) ? receipt.downgrades : [],
|
|
441
|
+
};
|
|
442
|
+
}
|
package/src/sf3d-route.js
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
createRouteReceiptInputArtifact,
|
|
9
9
|
createWebGpuRouteReceiptFromArtifacts,
|
|
10
10
|
} from './route-receipt-helper.js';
|
|
11
|
+
import {
|
|
12
|
+
createWebGpuRouteBackpressureProfile,
|
|
13
|
+
createWebGpuRouteSchedulerProfile,
|
|
14
|
+
} from './scheduler-backpressure.js';
|
|
11
15
|
|
|
12
16
|
export const SF3D_IMAGE_TO_MESH_ROUTE_ID = 'sf3d.image-to-mesh.webgpu-local.v0';
|
|
13
17
|
const SF3D_MODEL_ID = 'stabilityai/stable-fast-3d';
|
|
@@ -28,6 +32,55 @@ const OUTPUT_ROLES = [
|
|
|
28
32
|
{ key: 'meshObj', role: 'mesh-obj', required: false },
|
|
29
33
|
];
|
|
30
34
|
|
|
35
|
+
function createDefaultSf3dScheduler() {
|
|
36
|
+
return createWebGpuRouteSchedulerProfile({
|
|
37
|
+
requestedScheduler: {
|
|
38
|
+
mode: 'throughput',
|
|
39
|
+
yieldMs: 0,
|
|
40
|
+
waitForSubmittedWorkDone: true,
|
|
41
|
+
phaseChunkSize: {},
|
|
42
|
+
},
|
|
43
|
+
effectiveScheduler: {
|
|
44
|
+
mode: 'throughput',
|
|
45
|
+
yieldMs: 0,
|
|
46
|
+
waitForSubmittedWorkDone: true,
|
|
47
|
+
phaseChunkSize: {},
|
|
48
|
+
unsupportedFields: [],
|
|
49
|
+
},
|
|
50
|
+
verificationState: 'scheduler-unverified',
|
|
51
|
+
breathability: {
|
|
52
|
+
spans: REQUIRED_STAGES.map(stage => ({
|
|
53
|
+
name: `${stage}-phase`,
|
|
54
|
+
stage,
|
|
55
|
+
kind: stage === 'glb-export' ? 'cpu-bound' : 'gpu-submit-bound',
|
|
56
|
+
interruptible: false,
|
|
57
|
+
canYieldBefore: true,
|
|
58
|
+
canYieldAfter: true,
|
|
59
|
+
nonInterruptibleReason: stage === 'glb-export'
|
|
60
|
+
? null
|
|
61
|
+
: 'SF3D browser phases are treated as non-preemptible GPU work until finer receipts prove step boundaries.',
|
|
62
|
+
})),
|
|
63
|
+
checkpoints: REQUIRED_STAGES.map(stage => ({
|
|
64
|
+
name: `after-${stage}`,
|
|
65
|
+
kind: 'stage-boundary',
|
|
66
|
+
afterStage: stage,
|
|
67
|
+
yieldable: true,
|
|
68
|
+
waitsForSubmittedWorkDone: stage !== 'glb-export',
|
|
69
|
+
})),
|
|
70
|
+
notes: 'SF3D remains furnace-class until attention/triplane/marching phases expose finer cooperative boundaries.',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createDefaultSf3dBackpressure() {
|
|
76
|
+
return createWebGpuRouteBackpressureProfile({
|
|
77
|
+
requestedBudget: 'visible-wait',
|
|
78
|
+
effectiveBudget: 'furnace',
|
|
79
|
+
memoryExclusivity: 'exclusive',
|
|
80
|
+
warmCacheState: 'unknown',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
31
84
|
export function createSf3dImageToMeshRouteReceipt(input) {
|
|
32
85
|
if (!input || typeof input !== 'object') throw new Error('input must be an object');
|
|
33
86
|
if (!input.input?.artifactId || !input.input?.sha256) {
|
|
@@ -86,6 +139,8 @@ export function createSf3dImageToMeshRouteDefinition(input = {}) {
|
|
|
86
139
|
requiredFeatures: input.requiredFeatures || [],
|
|
87
140
|
requiredStages: routeMetadata.requiredStages,
|
|
88
141
|
timingSource: routeMetadata.timingSource,
|
|
142
|
+
scheduler: input.scheduler || createDefaultSf3dScheduler(),
|
|
143
|
+
backpressure: input.backpressure || createDefaultSf3dBackpressure(),
|
|
89
144
|
worker: input.worker || {
|
|
90
145
|
exportName: 'runSf3dImageToMeshRoute',
|
|
91
146
|
meshFormat: 'glb',
|
package/src/sharp-route.js
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
createRouteReceiptInputArtifact,
|
|
9
9
|
createWebGpuRouteReceiptFromArtifacts,
|
|
10
10
|
} from './route-receipt-helper.js';
|
|
11
|
+
import {
|
|
12
|
+
createWebGpuRouteBackpressureProfile,
|
|
13
|
+
createWebGpuRouteSchedulerProfile,
|
|
14
|
+
} from './scheduler-backpressure.js';
|
|
11
15
|
|
|
12
16
|
export const SHARP_IMAGE_TO_SPLAT_ROUTE_ID = 'sharp.image-to-splat.webgpu-local.v0';
|
|
13
17
|
const SHARP_MODEL_ID = 'apple/ml-sharp';
|
|
@@ -20,6 +24,55 @@ const OUTPUT_ROLES = [
|
|
|
20
24
|
{ key: 'autoCropEvidence', role: 'splat-autocrop-evidence', required: false },
|
|
21
25
|
];
|
|
22
26
|
|
|
27
|
+
function createDefaultSharpScheduler() {
|
|
28
|
+
return createWebGpuRouteSchedulerProfile({
|
|
29
|
+
requestedScheduler: {
|
|
30
|
+
mode: 'throughput',
|
|
31
|
+
yieldMs: 0,
|
|
32
|
+
waitForSubmittedWorkDone: true,
|
|
33
|
+
phaseChunkSize: {},
|
|
34
|
+
},
|
|
35
|
+
effectiveScheduler: {
|
|
36
|
+
mode: 'throughput',
|
|
37
|
+
yieldMs: 0,
|
|
38
|
+
waitForSubmittedWorkDone: true,
|
|
39
|
+
phaseChunkSize: {},
|
|
40
|
+
unsupportedFields: [],
|
|
41
|
+
},
|
|
42
|
+
verificationState: 'scheduler-unverified',
|
|
43
|
+
breathability: {
|
|
44
|
+
spans: REQUIRED_STAGES.map(stage => ({
|
|
45
|
+
name: `${stage}-phase`,
|
|
46
|
+
stage,
|
|
47
|
+
kind: stage === 'output-capture' ? 'readback-bound' : 'gpu-submit-bound',
|
|
48
|
+
interruptible: false,
|
|
49
|
+
canYieldBefore: true,
|
|
50
|
+
canYieldAfter: true,
|
|
51
|
+
nonInterruptibleReason: stage === 'output-capture'
|
|
52
|
+
? null
|
|
53
|
+
: 'Browser WebGPU cannot preempt a submitted SHARP adapter phase.',
|
|
54
|
+
})),
|
|
55
|
+
checkpoints: REQUIRED_STAGES.map(stage => ({
|
|
56
|
+
name: `after-${stage}`,
|
|
57
|
+
kind: stage === 'output-capture' ? 'readback' : 'stage-boundary',
|
|
58
|
+
afterStage: stage,
|
|
59
|
+
yieldable: true,
|
|
60
|
+
waitsForSubmittedWorkDone: true,
|
|
61
|
+
})),
|
|
62
|
+
notes: 'SHARP is furnace-class until finer adapter receipts prove smaller cooperative boundaries.',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createDefaultSharpBackpressure() {
|
|
68
|
+
return createWebGpuRouteBackpressureProfile({
|
|
69
|
+
requestedBudget: 'visible-wait',
|
|
70
|
+
effectiveBudget: 'furnace',
|
|
71
|
+
memoryExclusivity: 'exclusive',
|
|
72
|
+
warmCacheState: 'unknown',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
23
76
|
export function createSharpImageToSplatRouteReceipt(input) {
|
|
24
77
|
if (!input || typeof input !== 'object') throw new Error('input must be an object');
|
|
25
78
|
if (!input.input?.artifactId || !input.input?.sha256) {
|
|
@@ -78,6 +131,8 @@ export function createSharpImageToSplatRouteDefinition(input = {}) {
|
|
|
78
131
|
requiredFeatures: input.requiredFeatures || [],
|
|
79
132
|
requiredStages: routeMetadata.requiredStages,
|
|
80
133
|
timingSource: routeMetadata.timingSource,
|
|
134
|
+
scheduler: input.scheduler || createDefaultSharpScheduler(),
|
|
135
|
+
backpressure: input.backpressure || createDefaultSharpBackpressure(),
|
|
81
136
|
worker: input.worker || {
|
|
82
137
|
exportName: 'runSharpImageToSplatRoute',
|
|
83
138
|
adapterReportSchema: 'kaminos.sharp-webgpu-adapter-report.v0',
|