@kaminos/webgpu-inference-kit 0.1.0
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 +156 -0
- package/package.json +26 -0
- package/src/gpu-environment.js +139 -0
- package/src/index.js +106 -0
- package/src/kernel-profile.js +101 -0
- package/src/kimodo-route.js +84 -0
- package/src/moge-route.js +84 -0
- package/src/route-boundary.js +347 -0
- package/src/route-receipt-consumer.js +160 -0
- package/src/route-receipt-helper.js +86 -0
- package/src/route-receipt.js +147 -0
- package/src/route-schema-contract.js +29 -0
- package/src/runtime-profile.js +139 -0
- package/src/scheduler-backpressure.js +221 -0
- package/src/sf3d-route.js +96 -0
- package/src/sharp-route.js +87 -0
- package/src/staged-profile.js +87 -0
- package/src/tensor-manifest.js +81 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export const WEBGPU_ROUTE_RECEIPT_SCHEMA = 'kaminos.webgpu-route-receipt.v0';
|
|
2
|
+
|
|
3
|
+
function clone(value) {
|
|
4
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isNonEmptyString(value) {
|
|
8
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isFiniteNonNegative(value) {
|
|
12
|
+
return Number.isFinite(value) && value >= 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireString(errors, value, path) {
|
|
16
|
+
if (!isNonEmptyString(value)) errors.push(`${path} must be a non-empty string`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireArray(errors, value, path) {
|
|
20
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
21
|
+
errors.push(`${path} must be a non-empty array`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createWebGpuLocalRouteReceipt(input) {
|
|
26
|
+
const status = input.status || (input.fallbackReason ? 'fallback' : 'real');
|
|
27
|
+
return {
|
|
28
|
+
schema: WEBGPU_ROUTE_RECEIPT_SCHEMA,
|
|
29
|
+
requestedRouteId: input.requestedRouteId,
|
|
30
|
+
effectiveRouteId: input.effectiveRouteId,
|
|
31
|
+
status,
|
|
32
|
+
fallbackReason: input.fallbackReason || null,
|
|
33
|
+
backend: clone(input.backend),
|
|
34
|
+
model: clone(input.model),
|
|
35
|
+
kernel: clone(input.kernel),
|
|
36
|
+
inputs: clone(input.inputs || []),
|
|
37
|
+
outputs: clone(input.outputs || []),
|
|
38
|
+
timings: clone(input.timings),
|
|
39
|
+
createdAt: input.createdAt || new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateRouteReceipt(receipt) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
|
|
46
|
+
if (!receipt || typeof receipt !== 'object') {
|
|
47
|
+
return { ok: false, errors: ['receipt must be an object'] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (receipt.schema !== WEBGPU_ROUTE_RECEIPT_SCHEMA) {
|
|
51
|
+
errors.push(`schema must be ${WEBGPU_ROUTE_RECEIPT_SCHEMA}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
requireString(errors, receipt.requestedRouteId, 'requestedRouteId');
|
|
55
|
+
requireString(errors, receipt.effectiveRouteId, 'effectiveRouteId');
|
|
56
|
+
requireString(errors, receipt.status, 'status');
|
|
57
|
+
|
|
58
|
+
if (!receipt.backend || typeof receipt.backend !== 'object') {
|
|
59
|
+
errors.push('backend must be an object');
|
|
60
|
+
} else {
|
|
61
|
+
if (receipt.backend.kind !== 'webgpu-local') {
|
|
62
|
+
errors.push('backend.kind must be webgpu-local');
|
|
63
|
+
}
|
|
64
|
+
requireString(errors, receipt.backend.runtime, 'backend.runtime');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!receipt.model || typeof receipt.model !== 'object') {
|
|
68
|
+
errors.push('model must be an object');
|
|
69
|
+
} else {
|
|
70
|
+
requireString(errors, receipt.model.id, 'model.id');
|
|
71
|
+
requireString(errors, receipt.model.revision, 'model.revision');
|
|
72
|
+
requireString(errors, receipt.model.weightsHash, 'model.weightsHash');
|
|
73
|
+
requireString(errors, receipt.model.dtype, 'model.dtype');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!receipt.kernel || typeof receipt.kernel !== 'object') {
|
|
77
|
+
errors.push('kernel must be an object');
|
|
78
|
+
} else {
|
|
79
|
+
requireString(errors, receipt.kernel.kitVersion, 'kernel.kitVersion');
|
|
80
|
+
requireString(errors, receipt.kernel.profile, 'kernel.profile');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
requireArray(errors, receipt.inputs, 'inputs');
|
|
84
|
+
if (Array.isArray(receipt.inputs)) {
|
|
85
|
+
receipt.inputs.forEach((input, index) => {
|
|
86
|
+
requireString(errors, input?.role, `inputs[${index}].role`);
|
|
87
|
+
requireString(errors, input?.artifactId, `inputs[${index}].artifactId`);
|
|
88
|
+
requireString(errors, input?.sha256, `inputs[${index}].sha256`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
requireArray(errors, receipt.outputs, 'outputs');
|
|
93
|
+
if (Array.isArray(receipt.outputs)) {
|
|
94
|
+
receipt.outputs.forEach((output, index) => {
|
|
95
|
+
requireString(errors, output?.role, `outputs[${index}].role`);
|
|
96
|
+
requireString(errors, output?.artifactId, `outputs[${index}].artifactId`);
|
|
97
|
+
requireString(errors, output?.sha256, `outputs[${index}].sha256`);
|
|
98
|
+
requireString(errors, output?.status, `outputs[${index}].status`);
|
|
99
|
+
if (!Array.isArray(output?.shape) || output.shape.length === 0 || !output.shape.every(Number.isInteger)) {
|
|
100
|
+
errors.push(`outputs[${index}].shape must be a non-empty integer array`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!receipt.timings || typeof receipt.timings !== 'object') {
|
|
106
|
+
errors.push('timings must be an object');
|
|
107
|
+
} else {
|
|
108
|
+
requireString(errors, receipt.timings.source, 'timings.source');
|
|
109
|
+
if (!isFiniteNonNegative(receipt.timings.totalMs)) {
|
|
110
|
+
errors.push('timings.totalMs must be a finite non-negative number');
|
|
111
|
+
}
|
|
112
|
+
if (receipt.timings.stages != null && !Array.isArray(receipt.timings.stages)) {
|
|
113
|
+
errors.push('timings.stages must be an array when present');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (receipt.status === 'fallback' && !isNonEmptyString(receipt.fallbackReason)) {
|
|
118
|
+
errors.push('fallback receipts must include fallbackReason');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { ok: errors.length === 0, errors };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function assertAuthoritativeRouteReceipt(receipt) {
|
|
125
|
+
const result = validateRouteReceipt(receipt);
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
throw new Error(`invalid route receipt: ${result.errors.join('; ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (receipt.status !== 'real') {
|
|
131
|
+
throw new Error(`not authoritative: receipt status is ${receipt.status}`);
|
|
132
|
+
}
|
|
133
|
+
if (receipt.fallbackReason) {
|
|
134
|
+
throw new Error(`not authoritative: fallback reason present (${receipt.fallbackReason})`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const output of receipt.outputs) {
|
|
138
|
+
if (output.status === 'partial') {
|
|
139
|
+
throw new Error(`partial output is not authoritative: ${output.role}`);
|
|
140
|
+
}
|
|
141
|
+
if (output.status !== 'real') {
|
|
142
|
+
throw new Error(`output is not authoritative: ${output.role} status=${output.status}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return receipt;
|
|
147
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WEBGPU_ROUTE_RECEIPT_SCHEMA } from './route-receipt.js';
|
|
2
|
+
import { WEBGPU_ROUTE_EVIDENCE_CLASSIFICATION_SCHEMA } from './route-receipt-consumer.js';
|
|
3
|
+
import { WEBGPU_RUNTIME_PROFILE_SCHEMA } from './runtime-profile.js';
|
|
4
|
+
import {
|
|
5
|
+
WEBGPU_ROUTE_BACKPRESSURE_SCHEMA,
|
|
6
|
+
WEBGPU_ROUTE_SCHEDULER_SCHEMA,
|
|
7
|
+
} from './scheduler-backpressure.js';
|
|
8
|
+
import {
|
|
9
|
+
WEBGPU_ROUTE_DEFINITION_SCHEMA,
|
|
10
|
+
WEBGPU_ROUTE_REQUEST_SCHEMA,
|
|
11
|
+
WEBGPU_ROUTE_RESULT_SCHEMA,
|
|
12
|
+
} from './route-boundary.js';
|
|
13
|
+
|
|
14
|
+
export function createWebGpuRouteSchemaContract(input = {}) {
|
|
15
|
+
return {
|
|
16
|
+
schema: 'kaminos.webgpu-route-schema-contract.v0',
|
|
17
|
+
kitVersion: input.kitVersion || '0.0.0',
|
|
18
|
+
definitionSchema: WEBGPU_ROUTE_DEFINITION_SCHEMA,
|
|
19
|
+
requestSchema: WEBGPU_ROUTE_REQUEST_SCHEMA,
|
|
20
|
+
resultSchema: WEBGPU_ROUTE_RESULT_SCHEMA,
|
|
21
|
+
receiptSchema: WEBGPU_ROUTE_RECEIPT_SCHEMA,
|
|
22
|
+
runtimeProfileSchema: WEBGPU_RUNTIME_PROFILE_SCHEMA,
|
|
23
|
+
evidenceClassificationSchema: WEBGPU_ROUTE_EVIDENCE_CLASSIFICATION_SCHEMA,
|
|
24
|
+
schedulerSchema: WEBGPU_ROUTE_SCHEDULER_SCHEMA,
|
|
25
|
+
backpressureSchema: WEBGPU_ROUTE_BACKPRESSURE_SCHEMA,
|
|
26
|
+
authoritativeReceiptStatuses: ['real'],
|
|
27
|
+
nonAuthoritativeReceiptStatuses: ['fallback', 'partial', 'cached'],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createWebGpuBackendIdentity,
|
|
3
|
+
validateWebGpuBackendIdentity,
|
|
4
|
+
} from './gpu-environment.js';
|
|
5
|
+
import {
|
|
6
|
+
createKernelProfileMetadata,
|
|
7
|
+
createRouteTimingMetadata,
|
|
8
|
+
validateKernelProfileMetadata,
|
|
9
|
+
validateRouteTimingMetadata,
|
|
10
|
+
} from './kernel-profile.js';
|
|
11
|
+
import {
|
|
12
|
+
finishAndValidateRouteProfile,
|
|
13
|
+
} from './route-receipt-helper.js';
|
|
14
|
+
import {
|
|
15
|
+
validateStagedSubmitProfile,
|
|
16
|
+
} from './staged-profile.js';
|
|
17
|
+
|
|
18
|
+
export const WEBGPU_RUNTIME_PROFILE_SCHEMA = 'kaminos.webgpu-runtime-profile.v0';
|
|
19
|
+
|
|
20
|
+
const EVIDENCE_MODES = new Set(['live', 'fallback', 'cache', 'demo', 'partial', 'unknown']);
|
|
21
|
+
|
|
22
|
+
function clone(value) {
|
|
23
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isNonEmptyString(value) {
|
|
27
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createBackendIdentity(input) {
|
|
31
|
+
if (input?.kind === 'webgpu-local') return clone(input);
|
|
32
|
+
return createWebGpuBackendIdentity(input || {});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createEvidence(input = {}) {
|
|
36
|
+
const mode = input.mode || 'live';
|
|
37
|
+
return {
|
|
38
|
+
mode,
|
|
39
|
+
source: input.source || 'browser-webgpu-route',
|
|
40
|
+
fallbackReason: input.fallbackReason || null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateEvidence(errors, evidence) {
|
|
45
|
+
if (!evidence || typeof evidence !== 'object') {
|
|
46
|
+
errors.push('evidence must be an object');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!EVIDENCE_MODES.has(evidence.mode)) errors.push('evidence.mode has unsupported state');
|
|
50
|
+
if (!isNonEmptyString(evidence.source)) errors.push('evidence.source must be a non-empty string');
|
|
51
|
+
if (evidence.mode === 'fallback' && !isNonEmptyString(evidence.fallbackReason)) {
|
|
52
|
+
errors.push('fallback evidence must include fallbackReason');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createWebGpuRuntimeProfileInput(input = {}) {
|
|
57
|
+
if (!input || typeof input !== 'object') throw new Error('runtime profile input must be an object');
|
|
58
|
+
if (!isNonEmptyString(input.routeId)) throw new Error('routeId must be a non-empty string');
|
|
59
|
+
|
|
60
|
+
const backend = createBackendIdentity(input.backend);
|
|
61
|
+
const backendResult = validateWebGpuBackendIdentity(backend);
|
|
62
|
+
if (!backendResult.ok) throw new Error(`invalid WebGPU backend identity: ${backendResult.errors.join('; ')}`);
|
|
63
|
+
|
|
64
|
+
const kernel = createKernelProfileMetadata(input.kernel, { requireProfile: true });
|
|
65
|
+
const profile = finishAndValidateRouteProfile(input.profile);
|
|
66
|
+
const evidence = createEvidence(input.evidence);
|
|
67
|
+
|
|
68
|
+
const errors = [];
|
|
69
|
+
validateEvidence(errors, evidence);
|
|
70
|
+
if (errors.length > 0) throw new Error(errors.join('; '));
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
routeId: input.routeId,
|
|
74
|
+
runtimeLabel: input.runtimeLabel || 'browser-webgpu',
|
|
75
|
+
backend,
|
|
76
|
+
kernel,
|
|
77
|
+
profile,
|
|
78
|
+
evidence,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createWebGpuRuntimeProfile(input = {}) {
|
|
83
|
+
const runtimeInput = createWebGpuRuntimeProfileInput(input);
|
|
84
|
+
const timing = createRouteTimingMetadata({
|
|
85
|
+
requiredStages: Array.isArray(input.requiredStages) ? input.requiredStages : runtimeInput.profile.requiredStages,
|
|
86
|
+
timingSource: input.timingSource || runtimeInput.profile.timingSource,
|
|
87
|
+
}, { validate: true });
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
schema: WEBGPU_RUNTIME_PROFILE_SCHEMA,
|
|
91
|
+
routeId: runtimeInput.routeId,
|
|
92
|
+
runtimeLabel: runtimeInput.runtimeLabel,
|
|
93
|
+
backend: runtimeInput.backend,
|
|
94
|
+
kernel: runtimeInput.kernel,
|
|
95
|
+
profile: runtimeInput.profile,
|
|
96
|
+
evidence: runtimeInput.evidence,
|
|
97
|
+
requiredStages: timing.requiredStages,
|
|
98
|
+
timingSource: timing.timingSource,
|
|
99
|
+
createdAt: input.createdAt || new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function validateWebGpuRuntimeProfile(runtimeProfile) {
|
|
104
|
+
const errors = [];
|
|
105
|
+
|
|
106
|
+
if (!runtimeProfile || typeof runtimeProfile !== 'object') {
|
|
107
|
+
return { ok: false, errors: ['runtimeProfile must be an object'] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (runtimeProfile.schema !== WEBGPU_RUNTIME_PROFILE_SCHEMA) {
|
|
111
|
+
errors.push(`schema must be ${WEBGPU_RUNTIME_PROFILE_SCHEMA}`);
|
|
112
|
+
}
|
|
113
|
+
if (!isNonEmptyString(runtimeProfile.routeId)) errors.push('routeId must be a non-empty string');
|
|
114
|
+
if (!isNonEmptyString(runtimeProfile.runtimeLabel)) errors.push('runtimeLabel must be a non-empty string');
|
|
115
|
+
|
|
116
|
+
const backendResult = validateWebGpuBackendIdentity(runtimeProfile.backend);
|
|
117
|
+
if (!backendResult.ok) errors.push(...backendResult.errors.map(error => `backend.${error}`));
|
|
118
|
+
|
|
119
|
+
const kernelResult = validateKernelProfileMetadata(runtimeProfile.kernel);
|
|
120
|
+
if (!kernelResult.ok) errors.push(...kernelResult.errors.map(error => `kernel.${error}`));
|
|
121
|
+
|
|
122
|
+
const profileResult = validateStagedSubmitProfile(runtimeProfile.profile);
|
|
123
|
+
if (!profileResult.ok) errors.push(...profileResult.errors.map(error => `profile.${error}`));
|
|
124
|
+
|
|
125
|
+
const timingResult = validateRouteTimingMetadata(runtimeProfile);
|
|
126
|
+
if (!timingResult.ok) errors.push(...timingResult.errors);
|
|
127
|
+
|
|
128
|
+
validateEvidence(errors, runtimeProfile.evidence);
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
isNonEmptyString(runtimeProfile.timingSource)
|
|
132
|
+
&& isNonEmptyString(runtimeProfile.profile?.timingSource)
|
|
133
|
+
&& runtimeProfile.timingSource !== runtimeProfile.profile.timingSource
|
|
134
|
+
) {
|
|
135
|
+
errors.push('timingSource must match profile.timingSource');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { ok: errors.length === 0, errors };
|
|
139
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
export const WEBGPU_ROUTE_SCHEDULER_SCHEMA = 'kaminos.webgpu-route-scheduler.v0';
|
|
2
|
+
export const WEBGPU_ROUTE_BACKPRESSURE_SCHEMA = 'kaminos.webgpu-route-backpressure.v0';
|
|
3
|
+
|
|
4
|
+
const SCHEDULER_MODES = new Set(['throughput', 'cooperative']);
|
|
5
|
+
const VERIFICATION_STATES = new Set(['verified', 'scheduler-unverified', 'unsupported']);
|
|
6
|
+
const BUDGETS = new Set(['interactive', 'visible-wait', 'furnace', 'batch', 'unknown']);
|
|
7
|
+
const MEMORY_EXCLUSIVITY = new Set(['shared', 'exclusive', 'unknown']);
|
|
8
|
+
const WARM_CACHE_STATES = new Set(['cold', 'warm', 'hot', 'unknown']);
|
|
9
|
+
|
|
10
|
+
function clone(value) {
|
|
11
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value) {
|
|
15
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isNonEmptyString(value) {
|
|
19
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isNonNegativeNumber(value) {
|
|
23
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizePhaseChunkSize(input = {}) {
|
|
27
|
+
const out = {};
|
|
28
|
+
if (!isPlainObject(input)) return out;
|
|
29
|
+
for (const [phase, chunkSize] of Object.entries(input)) {
|
|
30
|
+
out[phase] = chunkSize;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeScheduler(input = {}) {
|
|
36
|
+
return {
|
|
37
|
+
mode: input.mode || 'throughput',
|
|
38
|
+
yieldMs: input.yieldMs ?? 0,
|
|
39
|
+
waitForSubmittedWorkDone: Boolean(input.waitForSubmittedWorkDone),
|
|
40
|
+
phaseChunkSize: normalizePhaseChunkSize(input.phaseChunkSize),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeEffectiveScheduler(input = {}, requestedScheduler) {
|
|
45
|
+
const base = normalizeScheduler({
|
|
46
|
+
...requestedScheduler,
|
|
47
|
+
...input,
|
|
48
|
+
phaseChunkSize: input.phaseChunkSize ?? requestedScheduler.phaseChunkSize,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
unsupportedFields: Array.isArray(input.unsupportedFields) ? [...input.unsupportedFields] : [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateSchedulerShape(errors, scheduler, label) {
|
|
57
|
+
if (!isPlainObject(scheduler)) {
|
|
58
|
+
errors.push(`${label} must be an object`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!SCHEDULER_MODES.has(scheduler.mode)) {
|
|
62
|
+
errors.push(`${label}.mode must be throughput or cooperative`);
|
|
63
|
+
}
|
|
64
|
+
if (!isNonNegativeNumber(scheduler.yieldMs)) {
|
|
65
|
+
errors.push(`${label}.yieldMs must be a non-negative number`);
|
|
66
|
+
}
|
|
67
|
+
if (typeof scheduler.waitForSubmittedWorkDone !== 'boolean') {
|
|
68
|
+
errors.push(`${label}.waitForSubmittedWorkDone must be a boolean`);
|
|
69
|
+
}
|
|
70
|
+
if (!isPlainObject(scheduler.phaseChunkSize)) {
|
|
71
|
+
errors.push(`${label}.phaseChunkSize must be an object`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const [phase, chunkSize] of Object.entries(scheduler.phaseChunkSize)) {
|
|
75
|
+
if (!isNonEmptyString(phase)) errors.push(`${label}.phaseChunkSize contains an empty phase name`);
|
|
76
|
+
if (!Number.isInteger(chunkSize) || chunkSize < 1) {
|
|
77
|
+
errors.push(`${label}.phaseChunkSize.${phase} must be a positive integer`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function missingUnsupportedField(effectiveScheduler, field) {
|
|
83
|
+
return !effectiveScheduler.unsupportedFields.includes(field)
|
|
84
|
+
&& !effectiveScheduler.unsupportedFields.includes('phaseChunkSize');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createWebGpuRouteSchedulerProfile(input = {}) {
|
|
88
|
+
const requestedScheduler = normalizeScheduler(input.requestedScheduler || input);
|
|
89
|
+
const effectiveScheduler = normalizeEffectiveScheduler(
|
|
90
|
+
input.effectiveScheduler || {},
|
|
91
|
+
requestedScheduler,
|
|
92
|
+
);
|
|
93
|
+
const verificationState = input.verificationState
|
|
94
|
+
|| (requestedScheduler.mode === 'cooperative' ? 'scheduler-unverified' : 'unsupported');
|
|
95
|
+
|
|
96
|
+
const profile = {
|
|
97
|
+
schema: WEBGPU_ROUTE_SCHEDULER_SCHEMA,
|
|
98
|
+
requestedScheduler,
|
|
99
|
+
effectiveScheduler,
|
|
100
|
+
verificationState,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const result = validateWebGpuRouteSchedulerProfile(profile);
|
|
104
|
+
if (!result.ok) throw new Error(result.errors.join('; '));
|
|
105
|
+
return profile;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function validateWebGpuRouteSchedulerProfile(profile) {
|
|
109
|
+
const errors = [];
|
|
110
|
+
if (!isPlainObject(profile)) {
|
|
111
|
+
return { ok: false, errors: ['scheduler profile must be an object'] };
|
|
112
|
+
}
|
|
113
|
+
if (profile.schema !== WEBGPU_ROUTE_SCHEDULER_SCHEMA) {
|
|
114
|
+
errors.push(`schema must be ${WEBGPU_ROUTE_SCHEDULER_SCHEMA}`);
|
|
115
|
+
}
|
|
116
|
+
validateSchedulerShape(errors, profile.requestedScheduler, 'requestedScheduler');
|
|
117
|
+
validateSchedulerShape(errors, profile.effectiveScheduler, 'effectiveScheduler');
|
|
118
|
+
|
|
119
|
+
if (!Array.isArray(profile.effectiveScheduler?.unsupportedFields)) {
|
|
120
|
+
errors.push('effectiveScheduler.unsupportedFields must be an array');
|
|
121
|
+
} else {
|
|
122
|
+
for (const field of profile.effectiveScheduler.unsupportedFields) {
|
|
123
|
+
if (!isNonEmptyString(field)) errors.push('effectiveScheduler.unsupportedFields entries must be non-empty strings');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!VERIFICATION_STATES.has(profile.verificationState)) {
|
|
128
|
+
errors.push('verificationState must be verified, scheduler-unverified, or unsupported');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isPlainObject(profile.requestedScheduler) && isPlainObject(profile.effectiveScheduler)) {
|
|
132
|
+
for (const [phase, requestedChunk] of Object.entries(profile.requestedScheduler.phaseChunkSize || {})) {
|
|
133
|
+
const effectiveChunk = profile.effectiveScheduler.phaseChunkSize?.[phase];
|
|
134
|
+
const field = `phaseChunkSize.${phase}`;
|
|
135
|
+
if (effectiveChunk !== requestedChunk && missingUnsupportedField(profile.effectiveScheduler, field)) {
|
|
136
|
+
if (profile.verificationState === 'verified') {
|
|
137
|
+
errors.push(`verified scheduler cannot drop requested ${field}`);
|
|
138
|
+
} else {
|
|
139
|
+
errors.push(`effectiveScheduler must list unsupported ${field}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (profile.verificationState === 'verified') {
|
|
146
|
+
if (profile.effectiveScheduler?.unsupportedFields?.length > 0) {
|
|
147
|
+
errors.push('verified scheduler cannot include unsupportedFields');
|
|
148
|
+
}
|
|
149
|
+
if (profile.requestedScheduler?.mode === 'cooperative' && profile.effectiveScheduler?.mode !== 'cooperative') {
|
|
150
|
+
errors.push('verified cooperative scheduler must have effectiveScheduler.mode cooperative');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ok: errors.length === 0, errors };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeFrameTail(input = {}) {
|
|
158
|
+
return {
|
|
159
|
+
sampleWindowMs: input.sampleWindowMs ?? 0,
|
|
160
|
+
longFrameCount: input.longFrameCount ?? 0,
|
|
161
|
+
maxFrameGapMs: input.maxFrameGapMs ?? 0,
|
|
162
|
+
p95FrameGapMs: input.p95FrameGapMs ?? null,
|
|
163
|
+
p99FrameGapMs: input.p99FrameGapMs ?? null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createWebGpuRouteBackpressureProfile(input = {}) {
|
|
168
|
+
const profile = {
|
|
169
|
+
schema: WEBGPU_ROUTE_BACKPRESSURE_SCHEMA,
|
|
170
|
+
requestedBudget: input.requestedBudget || 'unknown',
|
|
171
|
+
effectiveBudget: input.effectiveBudget || input.requestedBudget || 'unknown',
|
|
172
|
+
memoryExclusivity: input.memoryExclusivity || 'unknown',
|
|
173
|
+
warmCacheState: input.warmCacheState || 'unknown',
|
|
174
|
+
frameTail: normalizeFrameTail(input.frameTail),
|
|
175
|
+
};
|
|
176
|
+
const result = validateWebGpuRouteBackpressureProfile(profile);
|
|
177
|
+
if (!result.ok) throw new Error(result.errors.join('; '));
|
|
178
|
+
return profile;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateOptionalFrameMs(errors, frameTail, field) {
|
|
182
|
+
if (frameTail[field] != null && !isNonNegativeNumber(frameTail[field])) {
|
|
183
|
+
errors.push(`frameTail.${field} must be null or a non-negative number`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function validateWebGpuRouteBackpressureProfile(profile) {
|
|
188
|
+
const errors = [];
|
|
189
|
+
if (!isPlainObject(profile)) {
|
|
190
|
+
return { ok: false, errors: ['backpressure profile must be an object'] };
|
|
191
|
+
}
|
|
192
|
+
if (profile.schema !== WEBGPU_ROUTE_BACKPRESSURE_SCHEMA) {
|
|
193
|
+
errors.push(`schema must be ${WEBGPU_ROUTE_BACKPRESSURE_SCHEMA}`);
|
|
194
|
+
}
|
|
195
|
+
if (!BUDGETS.has(profile.requestedBudget)) errors.push('requestedBudget has unsupported value');
|
|
196
|
+
if (!BUDGETS.has(profile.effectiveBudget)) errors.push('effectiveBudget has unsupported value');
|
|
197
|
+
if (!MEMORY_EXCLUSIVITY.has(profile.memoryExclusivity)) errors.push('memoryExclusivity has unsupported value');
|
|
198
|
+
if (!WARM_CACHE_STATES.has(profile.warmCacheState)) errors.push('warmCacheState has unsupported value');
|
|
199
|
+
|
|
200
|
+
if (!isPlainObject(profile.frameTail)) {
|
|
201
|
+
errors.push('frameTail must be an object');
|
|
202
|
+
} else {
|
|
203
|
+
if (!isNonNegativeNumber(profile.frameTail.sampleWindowMs)) {
|
|
204
|
+
errors.push('frameTail.sampleWindowMs must be a non-negative number');
|
|
205
|
+
}
|
|
206
|
+
if (!Number.isInteger(profile.frameTail.longFrameCount) || profile.frameTail.longFrameCount < 0) {
|
|
207
|
+
errors.push('frameTail.longFrameCount must be a non-negative integer');
|
|
208
|
+
}
|
|
209
|
+
if (!isNonNegativeNumber(profile.frameTail.maxFrameGapMs)) {
|
|
210
|
+
errors.push('frameTail.maxFrameGapMs must be a non-negative number');
|
|
211
|
+
}
|
|
212
|
+
validateOptionalFrameMs(errors, profile.frameTail, 'p95FrameGapMs');
|
|
213
|
+
validateOptionalFrameMs(errors, profile.frameTail, 'p99FrameGapMs');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { ok: errors.length === 0, errors };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function cloneWebGpuRouteSchedulerProfile(profile) {
|
|
220
|
+
return clone(profile);
|
|
221
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineWebGpuRoute } from './route-boundary.js';
|
|
2
|
+
import {
|
|
3
|
+
createKernelProfileMetadata,
|
|
4
|
+
createRouteKernelProfileMetadata,
|
|
5
|
+
} from './kernel-profile.js';
|
|
6
|
+
import {
|
|
7
|
+
createRouteReceiptArtifacts,
|
|
8
|
+
createRouteReceiptInputArtifact,
|
|
9
|
+
createWebGpuRouteReceiptFromArtifacts,
|
|
10
|
+
} from './route-receipt-helper.js';
|
|
11
|
+
|
|
12
|
+
export const SF3D_IMAGE_TO_MESH_ROUTE_ID = 'sf3d.image-to-mesh.webgpu-local.v0';
|
|
13
|
+
const SF3D_MODEL_ID = 'stabilityai/stable-fast-3d';
|
|
14
|
+
const DEFAULT_KERNEL_PROFILE = 'dinov2-two-stream-triplane-marching-tet-texture-bake';
|
|
15
|
+
const REQUIRED_STAGES = [
|
|
16
|
+
'image-preprocess',
|
|
17
|
+
'dinov2-tokenizer',
|
|
18
|
+
'two-stream-backbone',
|
|
19
|
+
'triplane-decode',
|
|
20
|
+
'marching-tet',
|
|
21
|
+
'texture-bake',
|
|
22
|
+
'glb-export',
|
|
23
|
+
];
|
|
24
|
+
const OUTPUT_ROLES = [
|
|
25
|
+
{ key: 'meshGlb', role: 'mesh-glb', required: true },
|
|
26
|
+
{ key: 'albedoTexture', role: 'albedo-texture', required: true },
|
|
27
|
+
{ key: 'normalMap', role: 'normal-map', required: true },
|
|
28
|
+
{ key: 'meshObj', role: 'mesh-obj', required: false },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function createSf3dImageToMeshRouteReceipt(input) {
|
|
32
|
+
if (!input || typeof input !== 'object') throw new Error('input must be an object');
|
|
33
|
+
if (!input.input?.artifactId || !input.input?.sha256) {
|
|
34
|
+
throw new Error('input image artifactId and sha256 are required');
|
|
35
|
+
}
|
|
36
|
+
if (!input.outputs?.meshGlb) throw new Error('meshGlb output is required');
|
|
37
|
+
if (!input.outputs?.albedoTexture) throw new Error('albedoTexture output is required');
|
|
38
|
+
if (!input.outputs?.normalMap) throw new Error('normalMap output is required');
|
|
39
|
+
|
|
40
|
+
return createWebGpuRouteReceiptFromArtifacts({
|
|
41
|
+
requestedRouteId: SF3D_IMAGE_TO_MESH_ROUTE_ID,
|
|
42
|
+
effectiveRouteId: input.effectiveRouteId || SF3D_IMAGE_TO_MESH_ROUTE_ID,
|
|
43
|
+
status: input.status || (input.fallbackReason ? 'fallback' : 'real'),
|
|
44
|
+
fallbackReason: input.fallbackReason || null,
|
|
45
|
+
backend: input.backend,
|
|
46
|
+
model: {
|
|
47
|
+
id: SF3D_MODEL_ID,
|
|
48
|
+
revision: input.model?.revision,
|
|
49
|
+
weightsHash: input.model?.weightsHash,
|
|
50
|
+
dtype: input.model?.dtype || 'fp16',
|
|
51
|
+
},
|
|
52
|
+
kernel: createKernelProfileMetadata(input.kernel, { requireProfile: true }),
|
|
53
|
+
inputs: [
|
|
54
|
+
createRouteReceiptInputArtifact('source-image', input.input),
|
|
55
|
+
],
|
|
56
|
+
outputs: createRouteReceiptArtifacts({ artifacts: input.outputs, roles: OUTPUT_ROLES }),
|
|
57
|
+
profile: input.profile,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createSf3dImageToMeshRouteDefinition(input = {}) {
|
|
62
|
+
const routeMetadata = createRouteKernelProfileMetadata(input, {
|
|
63
|
+
defaultProfile: DEFAULT_KERNEL_PROFILE,
|
|
64
|
+
requiredStages: REQUIRED_STAGES,
|
|
65
|
+
timingSource: 'adapter-phase-wall-clock',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return defineWebGpuRoute({
|
|
69
|
+
routeId: SF3D_IMAGE_TO_MESH_ROUTE_ID,
|
|
70
|
+
backendKind: 'webgpu-local',
|
|
71
|
+
model: {
|
|
72
|
+
id: SF3D_MODEL_ID,
|
|
73
|
+
revision: input.model?.revision || 'stable-fast-3d',
|
|
74
|
+
dtype: input.model?.dtype || 'fp16',
|
|
75
|
+
},
|
|
76
|
+
kernel: routeMetadata.kernel,
|
|
77
|
+
inputs: [
|
|
78
|
+
{ role: 'source-image', required: true, artifactRequired: true, hashRequired: true },
|
|
79
|
+
],
|
|
80
|
+
outputs: [
|
|
81
|
+
{ role: 'mesh-glb', required: true, artifactRequired: true, hashRequired: true, shape: [1] },
|
|
82
|
+
{ role: 'albedo-texture', required: true, artifactRequired: true, hashRequired: true },
|
|
83
|
+
{ role: 'normal-map', required: true, artifactRequired: true, hashRequired: true },
|
|
84
|
+
{ role: 'mesh-obj', required: false, artifactRequired: true, hashRequired: true, shape: [1] },
|
|
85
|
+
],
|
|
86
|
+
requiredFeatures: input.requiredFeatures || [],
|
|
87
|
+
requiredStages: routeMetadata.requiredStages,
|
|
88
|
+
timingSource: routeMetadata.timingSource,
|
|
89
|
+
worker: input.worker || {
|
|
90
|
+
exportName: 'runSf3dImageToMeshRoute',
|
|
91
|
+
meshFormat: 'glb',
|
|
92
|
+
textureBake: true,
|
|
93
|
+
normalMap: true,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|