@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,87 @@
|
|
|
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 SHARP_IMAGE_TO_SPLAT_ROUTE_ID = 'sharp.image-to-splat.webgpu-local.v0';
|
|
13
|
+
const SHARP_MODEL_ID = 'apple/ml-sharp';
|
|
14
|
+
const DEFAULT_KERNEL_PROFILE = 'spn-dinov2l16-monodepth-gaussian-ply';
|
|
15
|
+
const REQUIRED_STAGES = ['spn', 'monodepth', 'gaussian-decoder', 'compose-ply', 'output-capture'];
|
|
16
|
+
const OUTPUT_ROLES = [
|
|
17
|
+
{ key: 'splat', role: 'splat-candidate', required: true },
|
|
18
|
+
{ key: 'depthMap', role: 'depth-map', required: true },
|
|
19
|
+
{ key: 'metadata', role: 'sharp-webgpu-metadata', required: true },
|
|
20
|
+
{ key: 'autoCropEvidence', role: 'splat-autocrop-evidence', required: false },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function createSharpImageToSplatRouteReceipt(input) {
|
|
24
|
+
if (!input || typeof input !== 'object') throw new Error('input must be an object');
|
|
25
|
+
if (!input.input?.artifactId || !input.input?.sha256) {
|
|
26
|
+
throw new Error('input image artifactId and sha256 are required');
|
|
27
|
+
}
|
|
28
|
+
if (!input.outputs?.splat) throw new Error('splat output is required');
|
|
29
|
+
if (!input.outputs?.depthMap) throw new Error('depthMap output is required');
|
|
30
|
+
if (!input.outputs?.metadata) throw new Error('metadata output is required');
|
|
31
|
+
|
|
32
|
+
return createWebGpuRouteReceiptFromArtifacts({
|
|
33
|
+
requestedRouteId: SHARP_IMAGE_TO_SPLAT_ROUTE_ID,
|
|
34
|
+
effectiveRouteId: input.effectiveRouteId || SHARP_IMAGE_TO_SPLAT_ROUTE_ID,
|
|
35
|
+
status: input.status || (input.fallbackReason ? 'fallback' : 'real'),
|
|
36
|
+
fallbackReason: input.fallbackReason || null,
|
|
37
|
+
backend: input.backend,
|
|
38
|
+
model: {
|
|
39
|
+
id: SHARP_MODEL_ID,
|
|
40
|
+
revision: input.model?.revision,
|
|
41
|
+
weightsHash: input.model?.weightsHash,
|
|
42
|
+
dtype: input.model?.dtype || 'fp16',
|
|
43
|
+
},
|
|
44
|
+
kernel: createKernelProfileMetadata(input.kernel, { requireProfile: true }),
|
|
45
|
+
inputs: [
|
|
46
|
+
createRouteReceiptInputArtifact('source-image', input.input),
|
|
47
|
+
],
|
|
48
|
+
outputs: createRouteReceiptArtifacts({ artifacts: input.outputs, roles: OUTPUT_ROLES }),
|
|
49
|
+
profile: input.profile,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createSharpImageToSplatRouteDefinition(input = {}) {
|
|
54
|
+
const routeMetadata = createRouteKernelProfileMetadata(input, {
|
|
55
|
+
defaultProfile: DEFAULT_KERNEL_PROFILE,
|
|
56
|
+
requiredStages: REQUIRED_STAGES,
|
|
57
|
+
timingSource: 'adapter-phase-wall-clock',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return defineWebGpuRoute({
|
|
61
|
+
routeId: SHARP_IMAGE_TO_SPLAT_ROUTE_ID,
|
|
62
|
+
backendKind: 'webgpu-local',
|
|
63
|
+
model: {
|
|
64
|
+
id: SHARP_MODEL_ID,
|
|
65
|
+
revision: input.model?.revision || 'local-sharp-webgpu',
|
|
66
|
+
dtype: input.model?.dtype || 'fp16',
|
|
67
|
+
},
|
|
68
|
+
kernel: routeMetadata.kernel,
|
|
69
|
+
inputs: [
|
|
70
|
+
{ role: 'source-image', required: true, artifactRequired: true, hashRequired: true },
|
|
71
|
+
],
|
|
72
|
+
outputs: [
|
|
73
|
+
{ role: 'splat-candidate', required: true, artifactRequired: true, hashRequired: true, shape: [1179648, 14] },
|
|
74
|
+
{ role: 'depth-map', required: true, artifactRequired: true, hashRequired: true, shape: [768, 768, 4] },
|
|
75
|
+
{ role: 'sharp-webgpu-metadata', required: true, artifactRequired: true, hashRequired: true, shape: [1] },
|
|
76
|
+
{ role: 'splat-autocrop-evidence', required: false, artifactRequired: true, hashRequired: true, shape: [1] },
|
|
77
|
+
],
|
|
78
|
+
requiredFeatures: input.requiredFeatures || [],
|
|
79
|
+
requiredStages: routeMetadata.requiredStages,
|
|
80
|
+
timingSource: routeMetadata.timingSource,
|
|
81
|
+
worker: input.worker || {
|
|
82
|
+
exportName: 'runSharpImageToSplatRoute',
|
|
83
|
+
adapterReportSchema: 'kaminos.sharp-webgpu-adapter-report.v0',
|
|
84
|
+
pipelineRouteId: 'adapter.sharp-image-to-splat-live.v0',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const PROFILE_SCHEMA = 'kaminos.webgpu-staged-profile.v0';
|
|
2
|
+
|
|
3
|
+
function isNonEmptyString(value) {
|
|
4
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function roundMs(value) {
|
|
8
|
+
return Math.round(value * 10) / 10;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createStagedSubmitProfile(input = {}) {
|
|
12
|
+
return {
|
|
13
|
+
schema: PROFILE_SCHEMA,
|
|
14
|
+
route: input.route || 'staged-submits',
|
|
15
|
+
timingSource: input.timingSource || 'queue-submit-wait',
|
|
16
|
+
timestampQueryValidatedAgainstStaged: !!input.timestampQueryValidatedAgainstStaged,
|
|
17
|
+
requiredStages: Array.isArray(input.requiredStages) ? [...input.requiredStages] : [],
|
|
18
|
+
stages: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function addStagedSubmitStage(profile, stage) {
|
|
23
|
+
if (!profile || typeof profile !== 'object') throw new Error('profile must be an object');
|
|
24
|
+
if (!isNonEmptyString(stage?.name)) throw new Error('stage.name must be a non-empty string');
|
|
25
|
+
if (!Number.isFinite(stage.ms) || stage.ms < 0) throw new Error('stage.ms must be a finite non-negative number');
|
|
26
|
+
if (!Array.isArray(profile.stages)) profile.stages = [];
|
|
27
|
+
|
|
28
|
+
profile.stages.push({
|
|
29
|
+
name: stage.name,
|
|
30
|
+
ms: roundMs(stage.ms),
|
|
31
|
+
shape: Array.isArray(stage.shape) ? [...stage.shape] : undefined,
|
|
32
|
+
metadata: stage.metadata ? JSON.parse(JSON.stringify(stage.metadata)) : undefined,
|
|
33
|
+
});
|
|
34
|
+
return profile;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function finishStagedSubmitProfile(profile) {
|
|
38
|
+
if (!profile || typeof profile !== 'object') throw new Error('profile must be an object');
|
|
39
|
+
const stages = Array.isArray(profile.stages) ? profile.stages : [];
|
|
40
|
+
const totalMs = roundMs(stages.reduce((sum, stage) => sum + (Number.isFinite(stage.ms) ? stage.ms : 0), 0));
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...profile,
|
|
44
|
+
schema: profile.schema || PROFILE_SCHEMA,
|
|
45
|
+
stages,
|
|
46
|
+
stageNames: stages.map(stage => stage.name),
|
|
47
|
+
totalMs,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validateStagedSubmitProfile(profile) {
|
|
52
|
+
const errors = [];
|
|
53
|
+
|
|
54
|
+
if (!profile || typeof profile !== 'object') {
|
|
55
|
+
return { ok: false, errors: ['profile must be an object'] };
|
|
56
|
+
}
|
|
57
|
+
if (profile.schema !== PROFILE_SCHEMA) errors.push(`schema must be ${PROFILE_SCHEMA}`);
|
|
58
|
+
if (!isNonEmptyString(profile.route)) errors.push('route must be a non-empty string');
|
|
59
|
+
if (!isNonEmptyString(profile.timingSource)) errors.push('timingSource must be a non-empty string');
|
|
60
|
+
if (!Array.isArray(profile.stages) || profile.stages.length === 0) {
|
|
61
|
+
errors.push('stages must be a non-empty array');
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(profile.totalMs) || profile.totalMs < 0) {
|
|
64
|
+
errors.push('totalMs must be a finite non-negative number');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const stageNames = new Set();
|
|
68
|
+
if (Array.isArray(profile.stages)) {
|
|
69
|
+
profile.stages.forEach((stage, index) => {
|
|
70
|
+
if (!isNonEmptyString(stage.name)) errors.push(`stages[${index}].name must be a non-empty string`);
|
|
71
|
+
if (!Number.isFinite(stage.ms) || stage.ms < 0) errors.push(`stages[${index}].ms must be a finite non-negative number`);
|
|
72
|
+
if (isNonEmptyString(stage.name)) stageNames.add(stage.name);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(profile.requiredStages)) {
|
|
77
|
+
for (const required of profile.requiredStages) {
|
|
78
|
+
if (!stageNames.has(required)) errors.push(`missing required stage ${required}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (profile.timingSource === 'timestamp-query' && profile.timestampQueryValidatedAgainstStaged !== true) {
|
|
83
|
+
errors.push('timestamp-query profile must be validated against staged-submit timings');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { ok: errors.length === 0, errors };
|
|
87
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const MANIFEST_SCHEMA = 'kaminos.tensor-manifest.v0';
|
|
2
|
+
|
|
3
|
+
const BYTES_PER_ELEMENT = new Map([
|
|
4
|
+
['fp32', 4],
|
|
5
|
+
['f32', 4],
|
|
6
|
+
['fp16', 2],
|
|
7
|
+
['f16', 2],
|
|
8
|
+
['u32', 4],
|
|
9
|
+
['i32', 4],
|
|
10
|
+
['u8', 1],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function isNonEmptyString(value) {
|
|
14
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function tensorElements(shape) {
|
|
18
|
+
if (!Array.isArray(shape) || shape.length === 0 || !shape.every(Number.isInteger)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (shape.some(dim => dim <= 0)) return null;
|
|
22
|
+
return shape.reduce((product, dim) => product * dim, 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function defineTensorManifest(input) {
|
|
26
|
+
const tensors = (input.tensors || []).map(tensor => {
|
|
27
|
+
const elements = tensorElements(tensor.shape);
|
|
28
|
+
const bytesPerElement = BYTES_PER_ELEMENT.get(tensor.dtype) || null;
|
|
29
|
+
return {
|
|
30
|
+
...tensor,
|
|
31
|
+
elements,
|
|
32
|
+
bytesPerElement,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
schema: MANIFEST_SCHEMA,
|
|
38
|
+
modelId: input.modelId,
|
|
39
|
+
revision: input.revision,
|
|
40
|
+
weightFormat: input.weightFormat || MANIFEST_SCHEMA,
|
|
41
|
+
tensors,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateTensorManifest(manifest) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
|
|
48
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
49
|
+
return { ok: false, errors: ['manifest must be an object'] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (manifest.schema !== MANIFEST_SCHEMA) {
|
|
53
|
+
errors.push(`schema must be ${MANIFEST_SCHEMA}`);
|
|
54
|
+
}
|
|
55
|
+
if (!isNonEmptyString(manifest.modelId)) errors.push('modelId must be a non-empty string');
|
|
56
|
+
if (!isNonEmptyString(manifest.revision)) errors.push('revision must be a non-empty string');
|
|
57
|
+
if (!Array.isArray(manifest.tensors) || manifest.tensors.length === 0) {
|
|
58
|
+
errors.push('tensors must be a non-empty array');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(manifest.tensors)) {
|
|
62
|
+
manifest.tensors.forEach((tensor, index) => {
|
|
63
|
+
if (!isNonEmptyString(tensor.name)) errors.push(`tensors[${index}].name must be a non-empty string`);
|
|
64
|
+
if (!BYTES_PER_ELEMENT.has(tensor.dtype)) errors.push(`tensors[${index}] unsupported dtype ${tensor.dtype}`);
|
|
65
|
+
const elements = tensorElements(tensor.shape);
|
|
66
|
+
if (elements == null) errors.push(`tensors[${index}].shape must contain positive integer dimensions`);
|
|
67
|
+
if (!Number.isInteger(tensor.byteOffset) || tensor.byteOffset < 0) {
|
|
68
|
+
errors.push(`tensors[${index}].byteOffset must be a non-negative integer`);
|
|
69
|
+
}
|
|
70
|
+
if (!Number.isInteger(tensor.byteLength) || tensor.byteLength < 0) {
|
|
71
|
+
errors.push(`tensors[${index}].byteLength must be a non-negative integer`);
|
|
72
|
+
}
|
|
73
|
+
const bytesPerElement = BYTES_PER_ELEMENT.get(tensor.dtype);
|
|
74
|
+
if (elements != null && bytesPerElement != null && tensor.byteLength !== elements * bytesPerElement) {
|
|
75
|
+
errors.push(`tensors[${index}].byteLength must equal shape elements * dtype size`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { ok: errors.length === 0, errors };
|
|
81
|
+
}
|