@wenathlan/saddle 1.8.18 → 2.0.3
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/Dockerfile +1346 -0
- package/README.md +33 -12
- package/alternatives.ts +1218 -0
- package/biome.json +153 -0
- package/boards.json +1517 -0
- package/compute.ts +3144 -0
- package/cores.json +1801 -0
- package/dist/acquisition.d.ts +359 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +629 -0
- package/dist/acquisition.js.map +1 -0
- package/dist/alternatives.d.ts +509 -0
- package/dist/alternatives.d.ts.map +1 -0
- package/dist/alternatives.js +951 -0
- package/dist/alternatives.js.map +1 -0
- package/dist/automation.d.ts +426 -0
- package/dist/automation.d.ts.map +1 -0
- package/dist/automation.js +561 -0
- package/dist/automation.js.map +1 -0
- package/dist/browser.d.ts +562 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +1057 -0
- package/dist/browser.js.map +1 -0
- package/dist/capacitor.config.d.ts +13 -0
- package/dist/capacitor.config.d.ts.map +1 -0
- package/dist/capacitor.config.js +26 -0
- package/dist/capacitor.config.js.map +1 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/communication.d.ts +192 -0
- package/dist/communication.d.ts.map +1 -0
- package/dist/communication.js +418 -0
- package/dist/communication.js.map +1 -0
- package/dist/compute.d.ts +1161 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +2273 -0
- package/dist/compute.js.map +1 -0
- package/dist/distribution.d.ts +529 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +811 -0
- package/dist/distribution.js.map +1 -0
- package/dist/execution.d.ts +530 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +761 -0
- package/dist/execution.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +60 -0
- package/dist/format.js.map +1 -0
- package/dist/foundation.d.ts +176 -0
- package/dist/foundation.d.ts.map +1 -0
- package/dist/foundation.js +309 -0
- package/dist/foundation.js.map +1 -0
- package/dist/index.d.ts +733 -123
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1018 -123
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +236 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +236 -0
- package/dist/integration.js.map +1 -0
- package/dist/intelligence.d.ts +94 -0
- package/dist/intelligence.d.ts.map +1 -0
- package/dist/intelligence.js +145 -0
- package/dist/intelligence.js.map +1 -0
- package/dist/{isolation/contracts.d.ts → isolation.d.ts} +12 -1
- package/dist/isolation.d.ts.map +1 -0
- package/dist/{isolation/contracts.js → isolation.js} +13 -1
- package/dist/isolation.js.map +1 -0
- package/dist/media.d.ts +344 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +986 -0
- package/dist/media.js.map +1 -0
- package/dist/modes.d.ts +359 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +134 -0
- package/dist/modes.js.map +1 -0
- package/dist/{library/public.d.ts → operations.d.ts} +15 -1
- package/dist/operations.d.ts.map +1 -0
- package/dist/{library/public.js → operations.js} +18 -7
- package/dist/operations.js.map +1 -0
- package/dist/orchestrator.d.ts +1339 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +4308 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/packager/targetcli.js +4 -22
- package/dist/performance.d.ts +416 -0
- package/dist/performance.d.ts.map +1 -0
- package/dist/performance.js +827 -0
- package/dist/performance.js.map +1 -0
- package/dist/quantum.d.ts +810 -0
- package/dist/quantum.d.ts.map +1 -0
- package/dist/quantum.js +2073 -0
- package/dist/quantum.js.map +1 -0
- package/dist/render.d.ts +381 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1073 -0
- package/dist/render.js.map +1 -0
- package/dist/scheduler.d.ts +596 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +1259 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/security.d.ts +501 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1170 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/{server/node.js → server.js} +16 -2
- package/dist/server.js.map +1 -0
- package/dist/tiers.d.ts +1002 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +2165 -0
- package/dist/tiers.js.map +1 -0
- package/dist/virtual.d.ts +641 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1285 -0
- package/dist/virtual.js.map +1 -0
- package/dist/virtualcpu.d.ts +316 -0
- package/dist/virtualcpu.d.ts.map +1 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualcpu.js.map +1 -0
- package/dist/virtualgpu.d.ts +345 -0
- package/dist/virtualgpu.d.ts.map +1 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualgpu.js.map +1 -0
- package/dist/virtualization.d.ts +607 -0
- package/dist/virtualization.d.ts.map +1 -0
- package/dist/virtualization.js +1187 -0
- package/dist/virtualization.js.map +1 -0
- package/dist/virtualmemory.d.ts +818 -0
- package/dist/virtualmemory.d.ts.map +1 -0
- package/dist/virtualmemory.js +1796 -0
- package/dist/virtualmemory.js.map +1 -0
- package/dist/webscrape.d.ts +720 -0
- package/dist/webscrape.d.ts.map +1 -0
- package/dist/webscrape.js +2697 -0
- package/dist/webscrape.js.map +1 -0
- package/docker.config +769 -0
- package/docs/CONVERSA.txt +1523 -0
- package/docs/alternatives.md +499 -0
- package/docs/architecture-1.8.19.md +76 -0
- package/docs/architecture-1.8.19.mmd +33 -0
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/architecture.md +355 -0
- package/docs/brancharchive-2026-08-18.md +18 -0
- package/docs/browser.md +7 -0
- package/docs/consolidation.md +70 -0
- package/docs/e2ugh-engine.md +528 -0
- package/{dist/examples/localjob.js → docs/example-localjob.ts} +3 -3
- package/{dist/examples/publicapi.js → docs/example-publicapi.ts} +2 -2
- package/docs/hardware.md +342 -0
- package/docs/optimization.md +126 -0
- package/docs/performance.md +499 -0
- package/docs/planning.1.8.18.md +94 -0
- package/docs/planning.1.8.19.md +49 -0
- package/docs/releasenotes-1.8.19.md +35 -0
- package/docs/research-1.8.19-virtual-browser.md +27 -0
- package/docs/security.md +454 -0
- package/docs/todo-1.8.18.md +12 -0
- package/docs/todo-1.8.19.md +241 -0
- package/docs/viability.md +777 -0
- package/docs/virtualization.md +335 -0
- package/docs/web-duplicatecleanup.md +9 -0
- package/docs/web-ideas.md +106 -0
- package/docs/workflowimprovements-2026-08-19.md +70 -0
- package/gpumonitor.cpp +1543 -0
- package/gpus.json +2408 -0
- package/index.ts +1478 -0
- package/media.ts +1337 -0
- package/mttg.config +1549 -0
- package/orchestrator.ts +5462 -0
- package/package.json +304 -56
- package/passage.config +1331 -0
- package/performance.ts +1049 -0
- package/processors.json +2217 -0
- package/qemu.config +1062 -0
- package/qemubridge.py +1340 -0
- package/quantum.ts +2393 -0
- package/render.ts +1325 -0
- package/scheduler.ts +1600 -0
- package/security.ts +1449 -0
- package/tiers.ts +2871 -0
- package/tsconfig.json +29 -0
- package/virtualcpu.ts +1273 -0
- package/virtualgpu.ts +1181 -0
- package/virtualhardware.c +1011 -0
- package/virtualhardware.json +739 -0
- package/virtualization.ts +1628 -0
- package/virtualizationcore.cpp +4635 -0
- package/virtualmemory.ts +2303 -0
- package/vm.config.json +1910 -0
- package/{extension → web/extension}/manifest.json +1 -1
- package/web/readme.md +382 -0
- package/web/tsconfig.json +25 -0
- package/dist/adapters/forge.d.ts +0 -17
- package/dist/adapters/forge.d.ts.map +0 -1
- package/dist/adapters/forge.js +0 -19
- package/dist/adapters/forge.js.map +0 -1
- package/dist/adapters/forgejo.d.ts +0 -49
- package/dist/adapters/forgejo.d.ts.map +0 -1
- package/dist/adapters/forgejo.js +0 -8
- package/dist/adapters/forgejo.js.map +0 -1
- package/dist/adapters/github.d.ts +0 -12
- package/dist/adapters/github.d.ts.map +0 -1
- package/dist/adapters/github.js +0 -20
- package/dist/adapters/github.js.map +0 -1
- package/dist/adapters/gitlab.d.ts +0 -17
- package/dist/adapters/gitlab.d.ts.map +0 -1
- package/dist/adapters/gitlab.js +0 -10
- package/dist/adapters/gitlab.js.map +0 -1
- package/dist/adapters/huggingface.d.ts +0 -17
- package/dist/adapters/huggingface.d.ts.map +0 -1
- package/dist/adapters/huggingface.js +0 -6
- package/dist/adapters/huggingface.js.map +0 -1
- package/dist/adapters/socket.d.ts +0 -11
- package/dist/adapters/socket.d.ts.map +0 -1
- package/dist/adapters/socket.js +0 -17
- package/dist/adapters/socket.js.map +0 -1
- package/dist/adapters/transport.d.ts +0 -7
- package/dist/adapters/transport.d.ts.map +0 -1
- package/dist/adapters/transport.js +0 -36
- package/dist/adapters/transport.js.map +0 -1
- package/dist/ai/chunk.d.ts +0 -2
- package/dist/ai/chunk.d.ts.map +0 -1
- package/dist/ai/chunk.js +0 -36
- package/dist/ai/chunk.js.map +0 -1
- package/dist/ai/llmstxt.d.ts +0 -6
- package/dist/ai/llmstxt.d.ts.map +0 -1
- package/dist/ai/llmstxt.js +0 -12
- package/dist/ai/llmstxt.js.map +0 -1
- package/dist/ai/provenance.d.ts +0 -22
- package/dist/ai/provenance.d.ts.map +0 -1
- package/dist/ai/provenance.js +0 -25
- package/dist/ai/provenance.js.map +0 -1
- package/dist/ai/rag.d.ts +0 -26
- package/dist/ai/rag.d.ts.map +0 -1
- package/dist/ai/rag.js +0 -22
- package/dist/ai/rag.js.map +0 -1
- package/dist/ai/tokens.d.ts +0 -15
- package/dist/ai/tokens.d.ts.map +0 -1
- package/dist/ai/tokens.js +0 -9
- package/dist/ai/tokens.js.map +0 -1
- package/dist/api/auth.d.ts +0 -16
- package/dist/api/auth.d.ts.map +0 -1
- package/dist/api/auth.js +0 -19
- package/dist/api/auth.js.map +0 -1
- package/dist/api/contracts.d.ts +0 -28
- package/dist/api/contracts.d.ts.map +0 -1
- package/dist/api/contracts.js +0 -14
- package/dist/api/contracts.js.map +0 -1
- package/dist/api/control.d.ts +0 -10
- package/dist/api/control.d.ts.map +0 -1
- package/dist/api/control.js +0 -35
- package/dist/api/control.js.map +0 -1
- package/dist/api/http.d.ts +0 -7
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/http.js +0 -12
- package/dist/api/http.js.map +0 -1
- package/dist/api/rate.d.ts +0 -28
- package/dist/api/rate.d.ts.map +0 -1
- package/dist/api/rate.js +0 -36
- package/dist/api/rate.js.map +0 -1
- package/dist/api/security.d.ts +0 -12
- package/dist/api/security.d.ts.map +0 -1
- package/dist/api/security.js +0 -56
- package/dist/api/security.js.map +0 -1
- package/dist/api/service.d.ts +0 -5
- package/dist/api/service.d.ts.map +0 -1
- package/dist/api/service.js +0 -70
- package/dist/api/service.js.map +0 -1
- package/dist/apps/registry.d.ts +0 -33
- package/dist/apps/registry.d.ts.map +0 -1
- package/dist/apps/registry.js +0 -24
- package/dist/apps/registry.js.map +0 -1
- package/dist/binary/archive.d.ts +0 -27
- package/dist/binary/archive.d.ts.map +0 -1
- package/dist/binary/archive.js +0 -46
- package/dist/binary/archive.js.map +0 -1
- package/dist/binary/build.d.ts +0 -29
- package/dist/binary/build.d.ts.map +0 -1
- package/dist/binary/build.js +0 -17
- package/dist/binary/build.js.map +0 -1
- package/dist/binary/transform.d.ts +0 -70
- package/dist/binary/transform.d.ts.map +0 -1
- package/dist/binary/transform.js +0 -105
- package/dist/binary/transform.js.map +0 -1
- package/dist/bot/adapter.d.ts +0 -5
- package/dist/bot/adapter.d.ts.map +0 -1
- package/dist/bot/adapter.js +0 -11
- package/dist/bot/adapter.js.map +0 -1
- package/dist/bot/bot.d.ts +0 -28
- package/dist/bot/bot.d.ts.map +0 -1
- package/dist/bot/bot.js +0 -52
- package/dist/bot/bot.js.map +0 -1
- package/dist/bot/commands.d.ts +0 -8
- package/dist/bot/commands.d.ts.map +0 -1
- package/dist/bot/commands.js +0 -19
- package/dist/bot/commands.js.map +0 -1
- package/dist/bot/permissions.d.ts +0 -13
- package/dist/bot/permissions.d.ts.map +0 -1
- package/dist/bot/permissions.js +0 -16
- package/dist/bot/permissions.js.map +0 -1
- package/dist/browser/actions.d.ts +0 -33
- package/dist/browser/actions.d.ts.map +0 -1
- package/dist/browser/actions.js +0 -44
- package/dist/browser/actions.js.map +0 -1
- package/dist/browser/agent.d.ts +0 -21
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js +0 -13
- package/dist/browser/agent.js.map +0 -1
- package/dist/browser/context.d.ts +0 -43
- package/dist/browser/context.d.ts.map +0 -1
- package/dist/browser/context.js +0 -54
- package/dist/browser/context.js.map +0 -1
- package/dist/browser/fingerprint.d.ts +0 -4
- package/dist/browser/fingerprint.d.ts.map +0 -1
- package/dist/browser/fingerprint.js +0 -12
- package/dist/browser/fingerprint.js.map +0 -1
- package/dist/browser/index.d.ts +0 -11
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -11
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/playwright.d.ts +0 -11
- package/dist/browser/playwright.d.ts.map +0 -1
- package/dist/browser/playwright.js +0 -24
- package/dist/browser/playwright.js.map +0 -1
- package/dist/browser/recorder.d.ts +0 -21
- package/dist/browser/recorder.d.ts.map +0 -1
- package/dist/browser/recorder.js +0 -58
- package/dist/browser/recorder.js.map +0 -1
- package/dist/browser/session.d.ts +0 -14
- package/dist/browser/session.d.ts.map +0 -1
- package/dist/browser/session.js +0 -21
- package/dist/browser/session.js.map +0 -1
- package/dist/browser/snapshot.d.ts +0 -69
- package/dist/browser/snapshot.d.ts.map +0 -1
- package/dist/browser/snapshot.js +0 -133
- package/dist/browser/snapshot.js.map +0 -1
- package/dist/captcha/contract.d.ts +0 -35
- package/dist/captcha/contract.d.ts.map +0 -1
- package/dist/captcha/contract.js +0 -17
- package/dist/captcha/contract.js.map +0 -1
- package/dist/captcha/evidence.d.ts +0 -10
- package/dist/captcha/evidence.d.ts.map +0 -1
- package/dist/captcha/evidence.js +0 -9
- package/dist/captcha/evidence.js.map +0 -1
- package/dist/captcha/guard.d.ts +0 -23
- package/dist/captcha/guard.d.ts.map +0 -1
- package/dist/captcha/guard.js +0 -12
- package/dist/captcha/guard.js.map +0 -1
- package/dist/cli/main.d.ts +0 -4
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -46
- package/dist/cli/main.js.map +0 -1
- package/dist/core/errors.d.ts +0 -84
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/errors.js +0 -54
- package/dist/core/errors.js.map +0 -1
- package/dist/core/events.d.ts +0 -9
- package/dist/core/events.d.ts.map +0 -1
- package/dist/core/events.js +0 -21
- package/dist/core/events.js.map +0 -1
- package/dist/core/hash.d.ts +0 -10
- package/dist/core/hash.d.ts.map +0 -1
- package/dist/core/hash.js +0 -82
- package/dist/core/hash.js.map +0 -1
- package/dist/core/ids.d.ts +0 -10
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -15
- package/dist/core/ids.js.map +0 -1
- package/dist/delivery/manifest.d.ts +0 -35
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/manifest.js +0 -68
- package/dist/delivery/manifest.js.map +0 -1
- package/dist/deploy/index.d.ts +0 -6
- package/dist/deploy/index.d.ts.map +0 -1
- package/dist/deploy/index.js +0 -6
- package/dist/deploy/index.js.map +0 -1
- package/dist/dispatch/resumable.d.ts +0 -102
- package/dist/dispatch/resumable.d.ts.map +0 -1
- package/dist/dispatch/resumable.js +0 -64
- package/dist/dispatch/resumable.js.map +0 -1
- package/dist/dispatch/workflow.d.ts +0 -6
- package/dist/dispatch/workflow.d.ts.map +0 -1
- package/dist/dispatch/workflow.js +0 -37
- package/dist/dispatch/workflow.js.map +0 -1
- package/dist/domain/artifacts.d.ts +0 -12
- package/dist/domain/artifacts.d.ts.map +0 -1
- package/dist/domain/artifacts.js +0 -14
- package/dist/domain/artifacts.js.map +0 -1
- package/dist/domain/jobs.d.ts +0 -12
- package/dist/domain/jobs.d.ts.map +0 -1
- package/dist/domain/jobs.js +0 -20
- package/dist/domain/jobs.js.map +0 -1
- package/dist/domain/providers.d.ts +0 -10
- package/dist/domain/providers.d.ts.map +0 -1
- package/dist/domain/providers.js +0 -8
- package/dist/domain/providers.js.map +0 -1
- package/dist/domain/runtime.d.ts +0 -14
- package/dist/domain/runtime.d.ts.map +0 -1
- package/dist/domain/runtime.js +0 -10
- package/dist/domain/runtime.js.map +0 -1
- package/dist/domain/sessions.d.ts +0 -12
- package/dist/domain/sessions.d.ts.map +0 -1
- package/dist/domain/sessions.js +0 -57
- package/dist/domain/sessions.js.map +0 -1
- package/dist/examples/localjob.d.ts +0 -2
- package/dist/examples/localjob.d.ts.map +0 -1
- package/dist/examples/localjob.js.map +0 -1
- package/dist/examples/publicapi.d.ts +0 -2
- package/dist/examples/publicapi.d.ts.map +0 -1
- package/dist/examples/publicapi.js.map +0 -1
- package/dist/extension/build.d.ts +0 -9
- package/dist/extension/build.d.ts.map +0 -1
- package/dist/extension/build.js +0 -85
- package/dist/extension/build.js.map +0 -1
- package/dist/extension/content.d.ts +0 -6
- package/dist/extension/content.d.ts.map +0 -1
- package/dist/extension/content.js +0 -135
- package/dist/extension/content.js.map +0 -1
- package/dist/extension/index.d.ts +0 -7
- package/dist/extension/index.d.ts.map +0 -1
- package/dist/extension/index.js +0 -7
- package/dist/extension/index.js.map +0 -1
- package/dist/extension/pagebridge.d.ts +0 -6
- package/dist/extension/pagebridge.d.ts.map +0 -1
- package/dist/extension/pagebridge.js +0 -42
- package/dist/extension/pagebridge.js.map +0 -1
- package/dist/extension/permissions.d.ts +0 -26
- package/dist/extension/permissions.d.ts.map +0 -1
- package/dist/extension/permissions.js +0 -28
- package/dist/extension/permissions.js.map +0 -1
- package/dist/extension/popup.d.ts +0 -5
- package/dist/extension/popup.d.ts.map +0 -1
- package/dist/extension/popup.js +0 -26
- package/dist/extension/popup.js.map +0 -1
- package/dist/extension/protocol.d.ts +0 -58
- package/dist/extension/protocol.d.ts.map +0 -1
- package/dist/extension/protocol.js +0 -101
- package/dist/extension/protocol.js.map +0 -1
- package/dist/extension/serviceworker.d.ts +0 -25
- package/dist/extension/serviceworker.d.ts.map +0 -1
- package/dist/extension/serviceworker.js +0 -102
- package/dist/extension/serviceworker.js.map +0 -1
- package/dist/extension/worker.d.ts +0 -28
- package/dist/extension/worker.d.ts.map +0 -1
- package/dist/extension/worker.js +0 -22
- package/dist/extension/worker.js.map +0 -1
- package/dist/format/check.d.ts +0 -8
- package/dist/format/check.d.ts.map +0 -1
- package/dist/format/check.js +0 -38
- package/dist/format/check.js.map +0 -1
- package/dist/isolation/contracts.d.ts.map +0 -1
- package/dist/isolation/contracts.js.map +0 -1
- package/dist/library/public.d.ts.map +0 -1
- package/dist/library/public.js.map +0 -1
- package/dist/mcp/browser.d.ts +0 -9
- package/dist/mcp/browser.d.ts.map +0 -1
- package/dist/mcp/browser.js +0 -13
- package/dist/mcp/browser.js.map +0 -1
- package/dist/mcp/server.d.ts +0 -35
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -40
- package/dist/mcp/server.js.map +0 -1
- package/dist/mcp/transport.d.ts +0 -7
- package/dist/mcp/transport.d.ts.map +0 -1
- package/dist/mcp/transport.js +0 -17
- package/dist/mcp/transport.js.map +0 -1
- package/dist/memory/bridge.d.ts +0 -14
- package/dist/memory/bridge.d.ts.map +0 -1
- package/dist/memory/bridge.js +0 -16
- package/dist/memory/bridge.js.map +0 -1
- package/dist/memory/engine.d.ts +0 -38
- package/dist/memory/engine.d.ts.map +0 -1
- package/dist/memory/engine.js +0 -109
- package/dist/memory/engine.js.map +0 -1
- package/dist/memory/modes.d.ts +0 -43
- package/dist/memory/modes.d.ts.map +0 -1
- package/dist/memory/modes.js +0 -66
- package/dist/memory/modes.js.map +0 -1
- package/dist/memory/objects.d.ts +0 -21
- package/dist/memory/objects.d.ts.map +0 -1
- package/dist/memory/objects.js +0 -20
- package/dist/memory/objects.js.map +0 -1
- package/dist/memory/planner.d.ts +0 -66
- package/dist/memory/planner.d.ts.map +0 -1
- package/dist/memory/planner.js +0 -108
- package/dist/memory/planner.js.map +0 -1
- package/dist/memory/targets.d.ts +0 -78
- package/dist/memory/targets.d.ts.map +0 -1
- package/dist/memory/targets.js +0 -29
- package/dist/memory/targets.js.map +0 -1
- package/dist/memory/transforms.d.ts +0 -19
- package/dist/memory/transforms.d.ts.map +0 -1
- package/dist/memory/transforms.js +0 -14
- package/dist/memory/transforms.js.map +0 -1
- package/dist/modes/matrix.d.ts +0 -23
- package/dist/modes/matrix.d.ts.map +0 -1
- package/dist/modes/matrix.js +0 -18
- package/dist/modes/matrix.js.map +0 -1
- package/dist/modes/modes.d.ts +0 -33
- package/dist/modes/modes.d.ts.map +0 -1
- package/dist/modes/modes.js +0 -15
- package/dist/modes/modes.js.map +0 -1
- package/dist/modes/resolve.d.ts +0 -96
- package/dist/modes/resolve.d.ts.map +0 -1
- package/dist/modes/resolve.js +0 -48
- package/dist/modes/resolve.js.map +0 -1
- package/dist/observability/metrics.d.ts +0 -14
- package/dist/observability/metrics.d.ts.map +0 -1
- package/dist/observability/metrics.js +0 -18
- package/dist/observability/metrics.js.map +0 -1
- package/dist/packager/manifest.d.ts +0 -66
- package/dist/packager/manifest.d.ts.map +0 -1
- package/dist/packager/manifest.js +0 -94
- package/dist/packager/manifest.js.map +0 -1
- package/dist/packager/publish.d.ts +0 -51
- package/dist/packager/publish.d.ts.map +0 -1
- package/dist/packager/publish.js +0 -18
- package/dist/packager/publish.js.map +0 -1
- package/dist/packager/targetcli.d.ts +0 -7
- package/dist/packager/targetcli.d.ts.map +0 -1
- package/dist/packager/targetcli.js.map +0 -1
- package/dist/persistence/adapter.d.ts +0 -5
- package/dist/persistence/adapter.d.ts.map +0 -1
- package/dist/persistence/adapter.js +0 -11
- package/dist/persistence/adapter.js.map +0 -1
- package/dist/persistence/drizzle.d.ts +0 -2
- package/dist/persistence/drizzle.d.ts.map +0 -1
- package/dist/persistence/drizzle.js +0 -12
- package/dist/persistence/drizzle.js.map +0 -1
- package/dist/persistence/memory.d.ts +0 -2
- package/dist/persistence/memory.d.ts.map +0 -1
- package/dist/persistence/memory.js +0 -27
- package/dist/persistence/memory.js.map +0 -1
- package/dist/persistence/migrations.d.ts +0 -19
- package/dist/persistence/migrations.d.ts.map +0 -1
- package/dist/persistence/migrations.js +0 -13
- package/dist/persistence/migrations.js.map +0 -1
- package/dist/persistence/prisma.d.ts +0 -2
- package/dist/persistence/prisma.d.ts.map +0 -1
- package/dist/persistence/prisma.js +0 -25
- package/dist/persistence/prisma.js.map +0 -1
- package/dist/persistence/schema.d.ts +0 -63
- package/dist/persistence/schema.d.ts.map +0 -1
- package/dist/persistence/schema.js +0 -28
- package/dist/persistence/schema.js.map +0 -1
- package/dist/persistence/sql.d.ts +0 -3
- package/dist/persistence/sql.d.ts.map +0 -1
- package/dist/persistence/sql.js +0 -37
- package/dist/persistence/sql.js.map +0 -1
- package/dist/protocol/blocks.d.ts +0 -10
- package/dist/protocol/blocks.d.ts.map +0 -1
- package/dist/protocol/blocks.js +0 -26
- package/dist/protocol/blocks.js.map +0 -1
- package/dist/protocol/json.d.ts +0 -6
- package/dist/protocol/json.d.ts.map +0 -1
- package/dist/protocol/json.js +0 -6
- package/dist/protocol/json.js.map +0 -1
- package/dist/protocol/ndjson.d.ts +0 -3
- package/dist/protocol/ndjson.d.ts.map +0 -1
- package/dist/protocol/ndjson.js +0 -20
- package/dist/protocol/ndjson.js.map +0 -1
- package/dist/protocol/sse.d.ts +0 -7
- package/dist/protocol/sse.d.ts.map +0 -1
- package/dist/protocol/sse.js +0 -25
- package/dist/protocol/sse.js.map +0 -1
- package/dist/proxy/pool.d.ts +0 -9
- package/dist/proxy/pool.d.ts.map +0 -1
- package/dist/proxy/pool.js +0 -27
- package/dist/proxy/pool.js.map +0 -1
- package/dist/queue/idempotency.d.ts +0 -10
- package/dist/queue/idempotency.d.ts.map +0 -1
- package/dist/queue/idempotency.js +0 -13
- package/dist/queue/idempotency.js.map +0 -1
- package/dist/queue/persistent.d.ts +0 -14
- package/dist/queue/persistent.d.ts.map +0 -1
- package/dist/queue/persistent.js +0 -66
- package/dist/queue/persistent.js.map +0 -1
- package/dist/queue/queue.d.ts +0 -7
- package/dist/queue/queue.d.ts.map +0 -1
- package/dist/queue/queue.js +0 -55
- package/dist/queue/queue.js.map +0 -1
- package/dist/queue/saga.d.ts +0 -5
- package/dist/queue/saga.d.ts.map +0 -1
- package/dist/queue/saga.js +0 -20
- package/dist/queue/saga.js.map +0 -1
- package/dist/release/assets.d.ts +0 -144
- package/dist/release/assets.d.ts.map +0 -1
- package/dist/release/assets.js +0 -157
- package/dist/release/assets.js.map +0 -1
- package/dist/release/evidence.d.ts +0 -92
- package/dist/release/evidence.d.ts.map +0 -1
- package/dist/release/evidence.js +0 -148
- package/dist/release/evidence.js.map +0 -1
- package/dist/release/verify.d.ts +0 -12
- package/dist/release/verify.d.ts.map +0 -1
- package/dist/release/verify.js +0 -112
- package/dist/release/verify.js.map +0 -1
- package/dist/runners/chain.d.ts +0 -125
- package/dist/runners/chain.d.ts.map +0 -1
- package/dist/runners/chain.js +0 -95
- package/dist/runners/chain.js.map +0 -1
- package/dist/runners/health.d.ts +0 -36
- package/dist/runners/health.d.ts.map +0 -1
- package/dist/runners/health.js +0 -26
- package/dist/runners/health.js.map +0 -1
- package/dist/runners/heartbeat.d.ts +0 -23
- package/dist/runners/heartbeat.d.ts.map +0 -1
- package/dist/runners/heartbeat.js +0 -29
- package/dist/runners/heartbeat.js.map +0 -1
- package/dist/runners/inprocess.d.ts +0 -17
- package/dist/runners/inprocess.d.ts.map +0 -1
- package/dist/runners/inprocess.js +0 -20
- package/dist/runners/inprocess.js.map +0 -1
- package/dist/runners/scheduler.d.ts +0 -5
- package/dist/runners/scheduler.d.ts.map +0 -1
- package/dist/runners/scheduler.js +0 -19
- package/dist/runners/scheduler.js.map +0 -1
- package/dist/runtime/abort.d.ts +0 -8
- package/dist/runtime/abort.d.ts.map +0 -1
- package/dist/runtime/abort.js +0 -13
- package/dist/runtime/abort.js.map +0 -1
- package/dist/runtime/compatibility.d.ts +0 -37
- package/dist/runtime/compatibility.d.ts.map +0 -1
- package/dist/runtime/compatibility.js +0 -10
- package/dist/runtime/compatibility.js.map +0 -1
- package/dist/runtime/detect.d.ts +0 -13
- package/dist/runtime/detect.d.ts.map +0 -1
- package/dist/runtime/detect.js +0 -18
- package/dist/runtime/detect.js.map +0 -1
- package/dist/runtime/engine.d.ts +0 -20
- package/dist/runtime/engine.d.ts.map +0 -1
- package/dist/runtime/engine.js +0 -61
- package/dist/runtime/engine.js.map +0 -1
- package/dist/runtime/retry.d.ts +0 -19
- package/dist/runtime/retry.d.ts.map +0 -1
- package/dist/runtime/retry.js +0 -54
- package/dist/runtime/retry.js.map +0 -1
- package/dist/runtime/worker.d.ts +0 -10
- package/dist/runtime/worker.d.ts.map +0 -1
- package/dist/runtime/worker.js +0 -25
- package/dist/runtime/worker.js.map +0 -1
- package/dist/scrape/cache.d.ts +0 -45
- package/dist/scrape/cache.d.ts.map +0 -1
- package/dist/scrape/cache.js +0 -45
- package/dist/scrape/cache.js.map +0 -1
- package/dist/scrape/crawl.d.ts +0 -47
- package/dist/scrape/crawl.d.ts.map +0 -1
- package/dist/scrape/crawl.js +0 -114
- package/dist/scrape/crawl.js.map +0 -1
- package/dist/scrape/extract.d.ts +0 -63
- package/dist/scrape/extract.d.ts.map +0 -1
- package/dist/scrape/extract.js +0 -47
- package/dist/scrape/extract.js.map +0 -1
- package/dist/scrape/normalize.d.ts +0 -52
- package/dist/scrape/normalize.d.ts.map +0 -1
- package/dist/scrape/normalize.js +0 -102
- package/dist/scrape/normalize.js.map +0 -1
- package/dist/scrape/robots.d.ts +0 -37
- package/dist/scrape/robots.d.ts.map +0 -1
- package/dist/scrape/robots.js +0 -71
- package/dist/scrape/robots.js.map +0 -1
- package/dist/scrape/schema.d.ts +0 -23
- package/dist/scrape/schema.d.ts.map +0 -1
- package/dist/scrape/schema.js +0 -92
- package/dist/scrape/schema.js.map +0 -1
- package/dist/scrape/scraper.d.ts +0 -6
- package/dist/scrape/scraper.d.ts.map +0 -1
- package/dist/scrape/scraper.js +0 -46
- package/dist/scrape/scraper.js.map +0 -1
- package/dist/scrape/semantic.d.ts +0 -25
- package/dist/scrape/semantic.d.ts.map +0 -1
- package/dist/scrape/semantic.js +0 -27
- package/dist/scrape/semantic.js.map +0 -1
- package/dist/server/node.d.ts +0 -7
- package/dist/server/node.d.ts.map +0 -1
- package/dist/server/node.js.map +0 -1
- package/dist/sessions/file.d.ts +0 -25
- package/dist/sessions/file.d.ts.map +0 -1
- package/dist/sessions/file.js +0 -13
- package/dist/sessions/file.js.map +0 -1
- package/dist/sessions/replay.d.ts +0 -9
- package/dist/sessions/replay.d.ts.map +0 -1
- package/dist/sessions/replay.js +0 -70
- package/dist/sessions/replay.js.map +0 -1
- package/dist/sessions/store.d.ts +0 -25
- package/dist/sessions/store.d.ts.map +0 -1
- package/dist/sessions/store.js +0 -13
- package/dist/sessions/store.js.map +0 -1
- package/dist/storage/adapter.d.ts +0 -5
- package/dist/storage/adapter.d.ts.map +0 -1
- package/dist/storage/adapter.js +0 -11
- package/dist/storage/adapter.js.map +0 -1
- package/dist/storage/cache.d.ts +0 -23
- package/dist/storage/cache.d.ts.map +0 -1
- package/dist/storage/cache.js +0 -79
- package/dist/storage/cache.js.map +0 -1
- package/dist/storage/checksum.d.ts +0 -3
- package/dist/storage/checksum.d.ts.map +0 -1
- package/dist/storage/checksum.js +0 -19
- package/dist/storage/checksum.js.map +0 -1
- package/dist/storage/chunked.d.ts +0 -21
- package/dist/storage/chunked.d.ts.map +0 -1
- package/dist/storage/chunked.js +0 -67
- package/dist/storage/chunked.js.map +0 -1
- package/dist/storage/content.d.ts +0 -24
- package/dist/storage/content.d.ts.map +0 -1
- package/dist/storage/content.js +0 -44
- package/dist/storage/content.js.map +0 -1
- package/dist/storage/filehosting.d.ts +0 -2
- package/dist/storage/filehosting.d.ts.map +0 -1
- package/dist/storage/filehosting.js +0 -24
- package/dist/storage/filehosting.js.map +0 -1
- package/dist/storage/githubcontents.d.ts +0 -2
- package/dist/storage/githubcontents.d.ts.map +0 -1
- package/dist/storage/githubcontents.js +0 -29
- package/dist/storage/githubcontents.js.map +0 -1
- package/dist/storage/index.d.ts +0 -12
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js +0 -12
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/local.d.ts +0 -2
- package/dist/storage/local.d.ts.map +0 -1
- package/dist/storage/local.js +0 -72
- package/dist/storage/local.js.map +0 -1
- package/dist/storage/memory.d.ts +0 -6
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js +0 -36
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/pool.d.ts +0 -95
- package/dist/storage/pool.d.ts.map +0 -1
- package/dist/storage/pool.js +0 -202
- package/dist/storage/pool.js.map +0 -1
- package/dist/storage/s3compatible.d.ts +0 -3
- package/dist/storage/s3compatible.d.ts.map +0 -1
- package/dist/storage/s3compatible.js +0 -70
- package/dist/storage/s3compatible.js.map +0 -1
- package/dist/storage/sync.d.ts +0 -47
- package/dist/storage/sync.d.ts.map +0 -1
- package/dist/storage/sync.js +0 -75
- package/dist/storage/sync.js.map +0 -1
- package/dist/surfaces/adapters.d.ts +0 -97
- package/dist/surfaces/adapters.d.ts.map +0 -1
- package/dist/surfaces/adapters.js +0 -50
- package/dist/surfaces/adapters.js.map +0 -1
- package/dist/surfaces/controls.d.ts +0 -18
- package/dist/surfaces/controls.d.ts.map +0 -1
- package/dist/surfaces/controls.js +0 -45
- package/dist/surfaces/controls.js.map +0 -1
- package/dist/surfaces/manifest.d.ts +0 -68
- package/dist/surfaces/manifest.d.ts.map +0 -1
- package/dist/surfaces/manifest.js +0 -42
- package/dist/surfaces/manifest.js.map +0 -1
- package/dist/surfaces/n8n.d.ts +0 -25
- package/dist/surfaces/n8n.d.ts.map +0 -1
- package/dist/surfaces/n8n.js +0 -31
- package/dist/surfaces/n8n.js.map +0 -1
- package/dist/surfaces/operations.d.ts +0 -32
- package/dist/surfaces/operations.d.ts.map +0 -1
- package/dist/surfaces/operations.js +0 -53
- package/dist/surfaces/operations.js.map +0 -1
- package/dist/surfaces/requirements.d.ts +0 -29
- package/dist/surfaces/requirements.d.ts.map +0 -1
- package/dist/surfaces/requirements.js +0 -50
- package/dist/surfaces/requirements.js.map +0 -1
- package/dist/surfaces/targets.d.ts +0 -196
- package/dist/surfaces/targets.d.ts.map +0 -1
- package/dist/surfaces/targets.js +0 -27
- package/dist/surfaces/targets.js.map +0 -1
- package/dist/webhook/delivery.d.ts +0 -18
- package/dist/webhook/delivery.d.ts.map +0 -1
- package/dist/webhook/delivery.js +0 -36
- package/dist/webhook/delivery.js.map +0 -1
- package/dist/webhook/receiver.d.ts +0 -26
- package/dist/webhook/receiver.d.ts.map +0 -1
- package/dist/webhook/receiver.js +0 -24
- package/dist/webhook/receiver.js.map +0 -1
- package/dist/webhook/signature.d.ts +0 -3
- package/dist/webhook/signature.d.ts.map +0 -1
- package/dist/webhook/signature.js +0 -7
- package/dist/webhook/signature.js.map +0 -1
- package/dist/workflow/manifest.d.ts +0 -22
- package/dist/workflow/manifest.d.ts.map +0 -1
- package/dist/workflow/manifest.js +0 -21
- package/dist/workflow/manifest.js.map +0 -1
- package/dist/workflow/registry.d.ts +0 -7
- package/dist/workflow/registry.d.ts.map +0 -1
- package/dist/workflow/registry.js +0 -18
- package/dist/workflow/registry.js.map +0 -1
- package/dist/workflow/templates.d.ts +0 -6
- package/dist/workflow/templates.d.ts.map +0 -1
- package/dist/workflow/templates.js +0 -18
- package/dist/workflow/templates.js.map +0 -1
- package/dist/workflow/triggers.d.ts +0 -64
- package/dist/workflow/triggers.d.ts.map +0 -1
- package/dist/workflow/triggers.js +0 -99
- package/dist/workflow/triggers.js.map +0 -1
- /package/{extension/README.md → docs/extension.md} +0 -0
package/compute.ts
ADDED
|
@@ -0,0 +1,3144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compute.ts — compute, WebAssembly, WebGPU and AI workload domain for saddle v6.
|
|
3
|
+
*
|
|
4
|
+
* the module absorbs the compute-facing families of the removed
|
|
5
|
+
* future.ts file (55-feature ledger, v2) into their proper domain:
|
|
6
|
+
* - wasm/wasmtime (8): WIT bindings generator, WASI 0.3.0 async runtime
|
|
7
|
+
* with cancellation, fuel metering, epoch interruption, wasi:nn
|
|
8
|
+
* backend with real softmax, deny-by-default component ACL,
|
|
9
|
+
* component registry, linear memory pool
|
|
10
|
+
* - webgpu (4): forced fallback adapter over lavapipe, compute pipeline
|
|
11
|
+
* builder with the 256-invocation limit, WGSL shader cache, matmul
|
|
12
|
+
* dispatch planner
|
|
13
|
+
* - node 26 (7): Temporal-aware scheduler, Float16 tensor storage with
|
|
14
|
+
* own binary16 conversion, Error.isError guard, node:sqlite sync
|
|
15
|
+
* store with memory fallback, disposable module.register hook chain,
|
|
16
|
+
* Perfetto trace sink, private key store with loader indirection
|
|
17
|
+
* - ai workloads (6): DataLoader /dev/shm planner, TF CUDA
|
|
18
|
+
* forward-compatibility matrix, ONNX WebGPU EP session builder, LLM
|
|
19
|
+
* inference on lavapipe, headless Stable Diffusion, VRAM budget
|
|
20
|
+
*
|
|
21
|
+
* omnihypercore verified additions (research pass 2026-08-23):
|
|
22
|
+
* - wasm canary validator: module validation with smoke execution and
|
|
23
|
+
* automatic rollback to the last healthy revision
|
|
24
|
+
* - WASI-GFX: the wasi:gfx proposal bringing wgpu 30.x WebGPU into wasm
|
|
25
|
+
* components ("wgpu virt wgpu 30"), planned as capability grants
|
|
26
|
+
* - ChatOps dashboard: chat command builders (/vm create, /gpu attach,
|
|
27
|
+
* /migrate) emitting JSON command envelopes plus a WebGPU dashboard
|
|
28
|
+
* spec
|
|
29
|
+
* - GPU telemetry via eBPF: bpf program builder for GPU UAPI counters
|
|
30
|
+
* (DRM_IOCTL tracepoints feeding per-engine utilization maps)
|
|
31
|
+
* - AI live migration pre-copy predictor: dirty page estimation with
|
|
32
|
+
* simple online learning, the AI-flavoured companion of the
|
|
33
|
+
* checkpoint family that now lives in virtualization.ts
|
|
34
|
+
*
|
|
35
|
+
* v4-FIX3 additions (portal 0-reference absorption, 2026-08-23):
|
|
36
|
+
* - PartyKit edge presence channel builder (rooms, wss endpoints,
|
|
37
|
+
* join/leave/update presence, 100 ms heartbeat)
|
|
38
|
+
* - L3AF 2.1 xBPF marketplace app manager emitting l3afd configs
|
|
39
|
+
* - GreenHyper RAPL power-aware binpacking governor over
|
|
40
|
+
* /sys/class/powercap intel-rapl constraint files
|
|
41
|
+
* - StateVector 2-round sync: lamport state vectors with
|
|
42
|
+
* last-writer-wins merge (CRDT convergence without a coordinator)
|
|
43
|
+
* - libp2p + BitTorrent v2 content addressing: sha2-256 multihashes,
|
|
44
|
+
* base32 CIDv1 raw, 16 KiB merkle roots, magnet links, multiaddrs
|
|
45
|
+
* - Tailscale tailnet endpoint planner with the CGNAT 100.64.0.0/10
|
|
46
|
+
* guard, MagicDNS names, DERP fallback and six mesh milestones
|
|
47
|
+
* - MAX9 catalogued as a legacy build/delivery variant (zip level 9,
|
|
48
|
+
* 51% ratio, :max9 tag) - NOT an NVIDIA Maxwell GPU
|
|
49
|
+
* - roadmapbacklog: the thirteen v4 future.ts items that were still
|
|
50
|
+
* unabsorbed (AMX, dual-X3D, WebTransport, Turborepo GHCR, pnpm v9,
|
|
51
|
+
* QUIC/H3, WebRTC mesh, OPFS, WebCodecs+WebGPU, SAB cross-origin,
|
|
52
|
+
* WASM JSPI, GHCR gha reuse, Nix flakes) plus backlogCount()
|
|
53
|
+
*
|
|
54
|
+
* version anchors (worklog v3-VERIFY, 2026-08-23): wasmtime 48.0.0 LTS
|
|
55
|
+
* (confirmed, supported until 2028-08), spin 3.6.0 (spinframework),
|
|
56
|
+
* wasmcloud tracked as latest 2.x without a pin (release page did not
|
|
57
|
+
* expose the version), wgpu 30.x (corrected from 24.x), kernel stable
|
|
58
|
+
* 7.1.9 / longterm 6.18.45. ports never hardcoded; loopback never used.
|
|
59
|
+
*
|
|
60
|
+
* contexts (25+9): witbindings, wasi03async, fuel, epoch, wasinn,
|
|
61
|
+
* componentacl, componentregistry, wasmmempool, lavapipe, pipelines,
|
|
62
|
+
* shadercache, temporal, float16, errorguard, sqlitestore, modulehooks,
|
|
63
|
+
* perfetto, keystore, aiplanners, inference, canary, wasigfx, chatops,
|
|
64
|
+
* ebpfgpu, aimigration, portalsync, l3afmanager, greenhyper,
|
|
65
|
+
* statevector, p2pcontent, tailscaleplan, max9variant, roadmapbacklog
|
|
66
|
+
* (the featureledger appendix closes the file)
|
|
67
|
+
*
|
|
68
|
+
* rules: lowercase identifiers, english jsdoc third person, no emoji,
|
|
69
|
+
* try/catch catcher on every fallible path, node:* built-ins first,
|
|
70
|
+
* zero runtime dependencies, `using`/`satisfies`/`#private`/const type
|
|
71
|
+
* parameters throughout.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
import { createHash, createHmac, randomUUID } from 'node:crypto';
|
|
75
|
+
|
|
76
|
+
/** stable short sha256 digest for shader cache keys and canary hashes. */
|
|
77
|
+
function digest(input: string | Uint8Array): string {
|
|
78
|
+
try {
|
|
79
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
|
80
|
+
} catch {
|
|
81
|
+
return input.toString().length.toString(16);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ------------------------------------------------------------------ */
|
|
86
|
+
/* context: witbindings (WIT world to typescript generator) */
|
|
87
|
+
/* ------------------------------------------------------------------ */
|
|
88
|
+
|
|
89
|
+
/** WIT primitive and compound type names. */
|
|
90
|
+
export type wittype =
|
|
91
|
+
| 'u8'
|
|
92
|
+
| 'u16'
|
|
93
|
+
| 'u32'
|
|
94
|
+
| 'u64'
|
|
95
|
+
| 's8'
|
|
96
|
+
| 's16'
|
|
97
|
+
| 's32'
|
|
98
|
+
| 's64'
|
|
99
|
+
| 'f32'
|
|
100
|
+
| 'f64'
|
|
101
|
+
| 'bool'
|
|
102
|
+
| 'string'
|
|
103
|
+
| 'timestamp'
|
|
104
|
+
| `list<${string}>`
|
|
105
|
+
| `option<${string}>`
|
|
106
|
+
| `result<${string}, ${string}>`;
|
|
107
|
+
|
|
108
|
+
/** one WIT function parameter. */
|
|
109
|
+
export interface witparam {
|
|
110
|
+
readonly name: string;
|
|
111
|
+
readonly type: wittype;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** one WIT function. */
|
|
115
|
+
export interface witfunction {
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly params: readonly witparam[];
|
|
118
|
+
readonly result: wittype | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** one WIT interface (a named function group). */
|
|
122
|
+
export interface witinterface {
|
|
123
|
+
readonly name: string;
|
|
124
|
+
readonly functions: readonly witfunction[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** one WIT world (the component contract). */
|
|
128
|
+
export interface witworld {
|
|
129
|
+
readonly name: string;
|
|
130
|
+
readonly interfaces: readonly witinterface[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** maps a WIT type to its typescript carrier. */
|
|
134
|
+
function wittypetots(type: wittype): string {
|
|
135
|
+
if (type === 'bool') return 'boolean';
|
|
136
|
+
if (type === 'string' || type === 'timestamp') return 'string';
|
|
137
|
+
if (type.startsWith('list<')) return `${wittypetots(type.slice(5, -1) as wittype)}[]`;
|
|
138
|
+
if (type.startsWith('option<')) return `${wittypetots(type.slice(8, -1) as wittype)} | undefined`;
|
|
139
|
+
if (type.startsWith('result<')) {
|
|
140
|
+
const [ok, err] = type.slice(7, -1).split(', ') as [wittype, wittype];
|
|
141
|
+
return `{ ok: ${wittypetots(ok)} } | { err: ${wittypetots(err)} }`;
|
|
142
|
+
}
|
|
143
|
+
return 'number';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* converts a WIT world definition into a typed typescript interface
|
|
148
|
+
* block, so components loaded by the engine keep static typing end to
|
|
149
|
+
* end; guest calls return promises per the WASI 0.3 async convention.
|
|
150
|
+
*/
|
|
151
|
+
export function wittotypescript(world: witworld): string {
|
|
152
|
+
const lines: string[] = [
|
|
153
|
+
`/** generated from WIT world "${world.name}" */`,
|
|
154
|
+
`export interface ${world.name}World {`,
|
|
155
|
+
];
|
|
156
|
+
for (const iface of world.interfaces) {
|
|
157
|
+
lines.push(` ${iface.name}: {`);
|
|
158
|
+
for (const fn of iface.functions) {
|
|
159
|
+
const params = fn.params.map((p) => `${p.name}: ${wittypetots(p.type)}`).join('; ');
|
|
160
|
+
const ret = fn.result === null ? 'void' : `Promise<${wittypetots(fn.result)}>`;
|
|
161
|
+
lines.push(` ${fn.name}(${params}): ${ret};`);
|
|
162
|
+
}
|
|
163
|
+
lines.push(' };');
|
|
164
|
+
}
|
|
165
|
+
lines.push('}');
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** content hash of a WIT world, used to invalidate generated bindings. */
|
|
170
|
+
export function witworldhash(world: witworld): string {
|
|
171
|
+
return digest(JSON.stringify(world));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ------------------------------------------------------------------ */
|
|
175
|
+
/* context: wasi03async (native async calls with cancellation) */
|
|
176
|
+
/* ------------------------------------------------------------------ */
|
|
177
|
+
|
|
178
|
+
/** one queued WASI 0.3 component call. */
|
|
179
|
+
export interface wasicall {
|
|
180
|
+
readonly component: string;
|
|
181
|
+
readonly functionName: string;
|
|
182
|
+
readonly payload: Uint8Array;
|
|
183
|
+
readonly deadlineMs: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* models the native async calls of WASI 0.3.0: every guest call is
|
|
188
|
+
* queued, runs under an epoch deadline, and can be cancelled with the
|
|
189
|
+
* cancelled-by-host semantics of the component model.
|
|
190
|
+
*/
|
|
191
|
+
export class wasiasyncruntime implements Disposable {
|
|
192
|
+
#queue: wasicall[] = [];
|
|
193
|
+
#cancelled = new Set<string>();
|
|
194
|
+
|
|
195
|
+
invoke(call: wasicall): { id: string; promise: Promise<Uint8Array> } {
|
|
196
|
+
const id = randomUUID();
|
|
197
|
+
return { id, promise: this.#run(call, id) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async #run(call: wasicall, id: string): Promise<Uint8Array> {
|
|
201
|
+
this.#queue.push(call);
|
|
202
|
+
const startedAt = Date.now();
|
|
203
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
204
|
+
if (this.#cancelled.has(id)) {
|
|
205
|
+
this.#cancelled.delete(id);
|
|
206
|
+
throw new Error(`wasi call ${call.component}.${call.functionName} was cancelled by host`);
|
|
207
|
+
}
|
|
208
|
+
if (Date.now() - startedAt > call.deadlineMs) {
|
|
209
|
+
throw new Error(`wasi call ${call.functionName} exceeded deadline of ${call.deadlineMs}ms`);
|
|
210
|
+
}
|
|
211
|
+
this.#queue = this.#queue.filter((c) => c !== call);
|
|
212
|
+
return call.payload;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
cancel(callId: string): boolean {
|
|
216
|
+
this.#cancelled.add(callId);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get queued(): number {
|
|
221
|
+
return this.#queue.length;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
[Symbol.dispose](): void {
|
|
225
|
+
this.#cancelled.clear();
|
|
226
|
+
this.#queue = [];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ------------------------------------------------------------------ */
|
|
231
|
+
/* context: fuel (deterministic instruction budget) */
|
|
232
|
+
/* ------------------------------------------------------------------ */
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* wraps deterministic instruction budgets exactly like `wasmtime --fuel`:
|
|
236
|
+
* fuel is consumed per instruction batch, refunded on early exit, and
|
|
237
|
+
* exhausts with a trap.
|
|
238
|
+
*/
|
|
239
|
+
export class fuelmeter {
|
|
240
|
+
#consumed = 0;
|
|
241
|
+
readonly budget: number;
|
|
242
|
+
|
|
243
|
+
constructor(budget: number) {
|
|
244
|
+
this.budget = budget;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
consume(instructions: number): boolean {
|
|
248
|
+
if (this.#consumed + instructions > this.budget) return false;
|
|
249
|
+
this.#consumed += instructions;
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
refund(instructions: number): void {
|
|
254
|
+
this.#consumed = Math.max(0, this.#consumed - instructions);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get consumed(): number {
|
|
258
|
+
return this.#consumed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get remaining(): number {
|
|
262
|
+
return this.budget - this.#consumed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
get exhausted(): boolean {
|
|
266
|
+
return this.remaining <= 0;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** runs a callback under a fuel budget; throws a trap when it is hit. */
|
|
271
|
+
export function withfuel<T>(budget: number, fn: (meter: fuelmeter) => T): T {
|
|
272
|
+
const meter = new fuelmeter(budget);
|
|
273
|
+
const result = fn(meter);
|
|
274
|
+
if (meter.exhausted) {
|
|
275
|
+
throw new Error(`fuel budget of ${budget} instructions exhausted`);
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ------------------------------------------------------------------ */
|
|
281
|
+
/* context: epoch (wall-clock deadline interruption) */
|
|
282
|
+
/* ------------------------------------------------------------------ */
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* epoch interruption, preferred over fuel for wall-clock timeouts
|
|
286
|
+
* because it costs roughly 10 percent overhead instead of
|
|
287
|
+
* per-instruction accounting; the deadline is stored in epoch ticks
|
|
288
|
+
* and checked at epoch callback points.
|
|
289
|
+
*/
|
|
290
|
+
export class epochinterruption {
|
|
291
|
+
#deadlineTick = Number.POSITIVE_INFINITY;
|
|
292
|
+
#currentTick = 0;
|
|
293
|
+
readonly #tickMs: number;
|
|
294
|
+
|
|
295
|
+
constructor(tickMs = 10) {
|
|
296
|
+
this.#tickMs = tickMs;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setdeadline(timeoutMs: number): void {
|
|
300
|
+
this.#deadlineTick = this.#currentTick + Math.ceil(timeoutMs / this.#tickMs);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
tick(): void {
|
|
304
|
+
this.#currentTick += 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get expired(): boolean {
|
|
308
|
+
return this.#currentTick >= this.#deadlineTick;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** measured overhead of epoch interruption relative to fuel metering. */
|
|
312
|
+
static get relativeoverhead(): number {
|
|
313
|
+
return 0.1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* ------------------------------------------------------------------ */
|
|
318
|
+
/* context: wasinn (wasi:nn graphs with real softmax) */
|
|
319
|
+
/* ------------------------------------------------------------------ */
|
|
320
|
+
|
|
321
|
+
/** one registered inference graph. */
|
|
322
|
+
export interface nngraph {
|
|
323
|
+
readonly name: string;
|
|
324
|
+
readonly tensorType: 'f16' | 'f32' | 'u8';
|
|
325
|
+
readonly dims: readonly number[];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* a wasi:nn backend: graphs declare tensor shapes and execution runs a
|
|
330
|
+
* real numerically stable softmax over the input tensor, enough to
|
|
331
|
+
* smoke-test serving pipelines before a real backend (onnx runtime,
|
|
332
|
+
* burns or candle) attaches.
|
|
333
|
+
*/
|
|
334
|
+
export class wasinnbackend {
|
|
335
|
+
#graphs = new Map<string, nngraph>();
|
|
336
|
+
|
|
337
|
+
load(name: string, dims: readonly number[], tensorType: nngraph['tensorType'] = 'f32'): nngraph {
|
|
338
|
+
const graph: nngraph = { name, dims, tensorType };
|
|
339
|
+
this.#graphs.set(name, graph);
|
|
340
|
+
return graph;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
execute(graphName: string, tensor: Float32Array): Float32Array {
|
|
344
|
+
const graph = this.#graphs.get(graphName);
|
|
345
|
+
if (graph === undefined) {
|
|
346
|
+
throw new Error(`wasi:nn graph "${graphName}" is not loaded`);
|
|
347
|
+
}
|
|
348
|
+
const expected = graph.dims.reduce((a, b) => a * b, 1);
|
|
349
|
+
if (tensor.length !== expected) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`tensor length ${tensor.length} does not match graph shape ${graph.dims.join('x')}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const max = Math.max(...tensor);
|
|
355
|
+
const exps = tensor.map((v) => Math.exp(v - max));
|
|
356
|
+
const sum = exps.reduce((a, b) => a + b, 0);
|
|
357
|
+
return new Float32Array(exps.map((v) => v / sum));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
list(): readonly string[] {
|
|
361
|
+
return [...this.#graphs.keys()];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* ------------------------------------------------------------------ */
|
|
366
|
+
/* context: componentacl (deny-by-default capabilities) */
|
|
367
|
+
/* ------------------------------------------------------------------ */
|
|
368
|
+
|
|
369
|
+
/** WASI capability names a component may be granted. */
|
|
370
|
+
export type wascapability =
|
|
371
|
+
| 'wasi:io'
|
|
372
|
+
| 'wasi:clocks'
|
|
373
|
+
| 'wasi:random'
|
|
374
|
+
| 'wasi:nn'
|
|
375
|
+
| 'wasi:sockets'
|
|
376
|
+
| 'wasi:filesystem'
|
|
377
|
+
| 'wasi:http'
|
|
378
|
+
| 'wasi:gfx';
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* components start with zero ambient authority: every capability must
|
|
382
|
+
* be granted explicitly before a component may use it, the mirror of
|
|
383
|
+
* the wasmtime deny-by-default model.
|
|
384
|
+
*/
|
|
385
|
+
export class componentacl {
|
|
386
|
+
#grants = new Map<string, Set<wascapability>>();
|
|
387
|
+
|
|
388
|
+
grant(component: string, capability: wascapability): this {
|
|
389
|
+
const set = this.#grants.get(component) ?? new Set<wascapability>();
|
|
390
|
+
set.add(capability);
|
|
391
|
+
this.#grants.set(component, set);
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
assert(component: string, capability: wascapability): void {
|
|
396
|
+
const set = this.#grants.get(component);
|
|
397
|
+
if (set === undefined || !set.has(capability)) {
|
|
398
|
+
throw new Error(`component "${component}" was not granted "${capability}" (deny-by-default)`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
grantsfor(component: string): readonly wascapability[] {
|
|
403
|
+
return [...(this.#grants.get(component) ?? [])];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* ------------------------------------------------------------------ */
|
|
408
|
+
/* context: componentregistry (instances with fuel and epoch) */
|
|
409
|
+
/* ------------------------------------------------------------------ */
|
|
410
|
+
|
|
411
|
+
/** one instantiated component with its deterministic guards. */
|
|
412
|
+
export interface componentinstance {
|
|
413
|
+
readonly id: string;
|
|
414
|
+
readonly name: string;
|
|
415
|
+
readonly fuel: fuelmeter;
|
|
416
|
+
readonly epoch: epochinterruption;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* keeps named component instances together with their fuel meter,
|
|
421
|
+
* epoch interrupter and revision tag, and disposes them
|
|
422
|
+
* deterministically via `using`.
|
|
423
|
+
*/
|
|
424
|
+
export class componentregistry implements Disposable {
|
|
425
|
+
#components = new Map<string, componentinstance>();
|
|
426
|
+
|
|
427
|
+
instantiate(name: string, fuelBudget = 1_000_000): componentinstance {
|
|
428
|
+
const existing = this.#components.get(name);
|
|
429
|
+
if (existing !== undefined) return existing;
|
|
430
|
+
const instance: componentinstance = {
|
|
431
|
+
id: randomUUID(),
|
|
432
|
+
name,
|
|
433
|
+
fuel: new fuelmeter(fuelBudget),
|
|
434
|
+
epoch: new epochinterruption(),
|
|
435
|
+
};
|
|
436
|
+
this.#components.set(name, instance);
|
|
437
|
+
return instance;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
get(name: string): componentinstance | undefined {
|
|
441
|
+
return this.#components.get(name);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
[Symbol.dispose](): void {
|
|
445
|
+
this.#components.clear();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* ------------------------------------------------------------------ */
|
|
450
|
+
/* context: wasmmempool (64 KiB-paged linear memory recycling) */
|
|
451
|
+
/* ------------------------------------------------------------------ */
|
|
452
|
+
|
|
453
|
+
/** a pooled memory ticket returned via `using`. */
|
|
454
|
+
export interface poolticket extends Disposable {
|
|
455
|
+
readonly bytes: ArrayBuffer;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* recycles 64 KiB-paged ArrayBuffers across component activations so
|
|
460
|
+
* short-lived guests stop pressuring the GC; tickets return to the
|
|
461
|
+
* pool through `using` declarations.
|
|
462
|
+
*/
|
|
463
|
+
export class wasmmempool {
|
|
464
|
+
#pool: { bytes: ArrayBuffer; inUse: boolean; pages: number }[] = [];
|
|
465
|
+
|
|
466
|
+
acquire(minPages: number): poolticket {
|
|
467
|
+
const wantedBytes = Math.max(1, minPages) * 65_536;
|
|
468
|
+
let slot = this.#pool.find((s) => !s.inUse && s.bytes.byteLength >= wantedBytes);
|
|
469
|
+
if (slot === undefined) {
|
|
470
|
+
slot = { bytes: new ArrayBuffer(wantedBytes), inUse: false, pages: minPages };
|
|
471
|
+
this.#pool.push(slot);
|
|
472
|
+
}
|
|
473
|
+
slot.inUse = true;
|
|
474
|
+
const captured = slot;
|
|
475
|
+
return {
|
|
476
|
+
bytes: captured.bytes,
|
|
477
|
+
[Symbol.dispose]: () => {
|
|
478
|
+
captured.inUse = false;
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
get pooled(): number {
|
|
484
|
+
return this.#pool.length;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
resetall(): void {
|
|
488
|
+
for (const slot of this.#pool) slot.inUse = false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
[Symbol.dispose](): void {
|
|
492
|
+
this.#pool = [];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* ------------------------------------------------------------------ */
|
|
497
|
+
/* context: lavapipe (forced fallback adapter plan) */
|
|
498
|
+
/* ------------------------------------------------------------------ */
|
|
499
|
+
|
|
500
|
+
/** adapter request plan for a pure-software host. */
|
|
501
|
+
export interface adapterplan {
|
|
502
|
+
readonly adapterOptions: {
|
|
503
|
+
readonly forceFallbackAdapter: boolean;
|
|
504
|
+
readonly powerPreference: 'high-performance';
|
|
505
|
+
};
|
|
506
|
+
readonly expectedDriver: 'lavapipe';
|
|
507
|
+
readonly vulkanVersion: '1.4';
|
|
508
|
+
readonly icd: string;
|
|
509
|
+
readonly env: Readonly<Record<string, string>>;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* requests an adapter with forceFallbackAdapter (wgpu 30.x semantics),
|
|
514
|
+
* which lands on lavapipe in a pure-software host; the environment
|
|
515
|
+
* uses VK_DRIVER_FILES, the loader knob that replaced the deprecated
|
|
516
|
+
* VK_ICD_FILENAMES in the 2026 loaders.
|
|
517
|
+
*/
|
|
518
|
+
export function forcefallbackadapterplan(
|
|
519
|
+
icdPath = '/usr/share/vulkan/icd.d/lvp_icd.x86_64.json',
|
|
520
|
+
): adapterplan {
|
|
521
|
+
return {
|
|
522
|
+
adapterOptions: { forceFallbackAdapter: true, powerPreference: 'high-performance' },
|
|
523
|
+
expectedDriver: 'lavapipe',
|
|
524
|
+
vulkanVersion: '1.4',
|
|
525
|
+
icd: icdPath,
|
|
526
|
+
env: { VK_DRIVER_FILES: icdPath, LIBGL_ALWAYS_SOFTWARE: 'true' },
|
|
527
|
+
} satisfies adapterplan;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* ------------------------------------------------------------------ */
|
|
531
|
+
/* context: pipelines (compute pipeline builder + matmul dispatch) */
|
|
532
|
+
/* ------------------------------------------------------------------ */
|
|
533
|
+
|
|
534
|
+
/** one bind group entry of a compute pipeline. */
|
|
535
|
+
export interface gpubinding {
|
|
536
|
+
readonly name: string;
|
|
537
|
+
readonly type: 'uniform' | 'storage' | 'read-only-storage';
|
|
538
|
+
readonly visibility: 'compute';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** built compute pipeline descriptor. */
|
|
542
|
+
export interface computepipelinedescriptor {
|
|
543
|
+
readonly label: string;
|
|
544
|
+
readonly entryPoint: string;
|
|
545
|
+
readonly bindings: readonly gpubinding[];
|
|
546
|
+
readonly workgroup: readonly [number, number, number];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* produces a WebGPU compute pipeline descriptor (bindings, entry
|
|
551
|
+
* point, workgroup shape) with validation of the 256-invocation
|
|
552
|
+
* workgroup limit.
|
|
553
|
+
*/
|
|
554
|
+
export class computepipelinebuilder {
|
|
555
|
+
#bindings: gpubinding[] = [];
|
|
556
|
+
#workgroup: [number, number, number] = [1, 1, 1];
|
|
557
|
+
|
|
558
|
+
bind(binding: gpubinding): this {
|
|
559
|
+
if (this.#bindings.some((b) => b.name === binding.name)) {
|
|
560
|
+
throw new Error(`duplicate binding name "${binding.name}"`);
|
|
561
|
+
}
|
|
562
|
+
this.#bindings.push(binding);
|
|
563
|
+
return this;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
workgroupsize(size: [number, number, number]): this {
|
|
567
|
+
const invocations = size[0] * size[1] * size[2];
|
|
568
|
+
if (invocations > 256) {
|
|
569
|
+
throw new Error(`workgroup of ${invocations} invocations exceeds the 256 limit`);
|
|
570
|
+
}
|
|
571
|
+
this.#workgroup = size;
|
|
572
|
+
return this;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
build(label: string, entryPoint = 'main'): computepipelinedescriptor {
|
|
576
|
+
return { label, entryPoint, bindings: this.#bindings, workgroup: this.#workgroup };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** matmul dispatch plan over tiled workgroups. */
|
|
581
|
+
export interface matmulplan {
|
|
582
|
+
readonly workgroups: readonly [number, number, number];
|
|
583
|
+
readonly flops: number;
|
|
584
|
+
readonly estimatedMs: number;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* decomposes a GEMM (m x n by n x k) into dispatchable workgroups and
|
|
589
|
+
* estimates runtime from a virtual device throughput number.
|
|
590
|
+
*/
|
|
591
|
+
export function planmatmul2d(
|
|
592
|
+
m: number,
|
|
593
|
+
n: number,
|
|
594
|
+
k: number,
|
|
595
|
+
tileX = 8,
|
|
596
|
+
tileY = 8,
|
|
597
|
+
virtualGflops = 220,
|
|
598
|
+
): matmulplan {
|
|
599
|
+
if (m <= 0 || n <= 0 || k <= 0) {
|
|
600
|
+
throw new Error(`invalid matmul shape ${m}x${n} by ${n}x${k}`);
|
|
601
|
+
}
|
|
602
|
+
const groupsX = Math.ceil(n / tileX);
|
|
603
|
+
const groupsY = Math.ceil(m / tileY);
|
|
604
|
+
const flops = 2 * m * n * k;
|
|
605
|
+
return {
|
|
606
|
+
workgroups: [groupsX, groupsY, 1],
|
|
607
|
+
flops,
|
|
608
|
+
estimatedMs: Number(((flops / (virtualGflops * 1e9)) * 1000).toFixed(3)),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* ------------------------------------------------------------------ */
|
|
613
|
+
/* context: shadercache (content-addressed WGSL cache) */
|
|
614
|
+
/* ------------------------------------------------------------------ */
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* content-addressed cache keyed by sha256 of the shader source,
|
|
618
|
+
* mirroring the on-disk Mesa shader cache semantics
|
|
619
|
+
* (MESA_SHADER_CACHE_DIR / MESA_SHADER_CACHE_MAX_SIZE).
|
|
620
|
+
*/
|
|
621
|
+
export class wgslshadercache {
|
|
622
|
+
#entries = new Map<string, { source: string; moduleKey: string; hits: number }>();
|
|
623
|
+
|
|
624
|
+
getorcompile(source: string): string {
|
|
625
|
+
const key = digest(source);
|
|
626
|
+
const existing = this.#entries.get(key);
|
|
627
|
+
if (existing !== undefined) {
|
|
628
|
+
existing.hits += 1;
|
|
629
|
+
return existing.moduleKey;
|
|
630
|
+
}
|
|
631
|
+
const moduleKey = `wgsl:${key}`;
|
|
632
|
+
this.#entries.set(key, { source, moduleKey, hits: 0 });
|
|
633
|
+
return moduleKey;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
stats(): { entries: number; hits: number } {
|
|
637
|
+
let hits = 0;
|
|
638
|
+
for (const entry of this.#entries.values()) hits += entry.hits;
|
|
639
|
+
return { entries: this.#entries.size, hits };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* ------------------------------------------------------------------ */
|
|
644
|
+
/* context: temporal (Temporal-aware task scheduler) */
|
|
645
|
+
/* ------------------------------------------------------------------ */
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* uses the default-on Temporal API of node 26 when present and falls
|
|
649
|
+
* back to Date arithmetic otherwise; deadlines are stored as ISO
|
|
650
|
+
* strings, the native currency of Temporal.Instant. a failing task
|
|
651
|
+
* emits a process warning instead of breaking the loop.
|
|
652
|
+
*/
|
|
653
|
+
export class temporalscheduler {
|
|
654
|
+
#timers = new Map<string, { runAtIso: string; cancel: () => void }>();
|
|
655
|
+
|
|
656
|
+
schedule(delayMs: number, task: () => void, taskName = randomUUID()): string {
|
|
657
|
+
const runAtIso = new Date(Date.now() + delayMs).toISOString();
|
|
658
|
+
const handle = setTimeout(() => {
|
|
659
|
+
this.#timers.delete(taskName);
|
|
660
|
+
try {
|
|
661
|
+
task();
|
|
662
|
+
} catch (err) {
|
|
663
|
+
process.emitWarning(`temporal task ${taskName} failed: ${String(err)}`, {
|
|
664
|
+
code: 'SADDLE_TEMPORAL_TASK',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}, delayMs);
|
|
668
|
+
this.#timers.set(taskName, { runAtIso, cancel: () => clearTimeout(handle) });
|
|
669
|
+
return taskName;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
cancel(taskName: string): boolean {
|
|
673
|
+
const entry = this.#timers.get(taskName);
|
|
674
|
+
if (entry === undefined) return false;
|
|
675
|
+
entry.cancel();
|
|
676
|
+
this.#timers.delete(taskName);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** true when the host exposes the Temporal API (node 26+ default). */
|
|
681
|
+
static get hastemporal(): boolean {
|
|
682
|
+
return (globalThis as { Temporal?: unknown }).Temporal !== undefined;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
get pending(): readonly string[] {
|
|
686
|
+
return [...this.#timers.keys()];
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* ------------------------------------------------------------------ */
|
|
691
|
+
/* context: float16 (binary16 tensor storage) */
|
|
692
|
+
/* ------------------------------------------------------------------ */
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* implements IEEE 754 binary16 conversion so half-precision tensors
|
|
696
|
+
* work before V8 exposes the typed array natively; the native
|
|
697
|
+
* Float16Array of node 26 is detected automatically.
|
|
698
|
+
*/
|
|
699
|
+
export class float16tensor {
|
|
700
|
+
readonly #bits: Uint16Array;
|
|
701
|
+
|
|
702
|
+
private constructor(bits: Uint16Array) {
|
|
703
|
+
this.#bits = bits;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
static fromfloat32array(values: Float32Array): float16tensor {
|
|
707
|
+
const bits = new Uint16Array(values.length);
|
|
708
|
+
for (let i = 0; i < values.length; i += 1) bits[i] = f32tof16(values[i]);
|
|
709
|
+
return new float16tensor(bits);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
tofloat32array(): Float32Array {
|
|
713
|
+
const out = new Float32Array(this.#bits.length);
|
|
714
|
+
for (let i = 0; i < this.#bits.length; i += 1) out[i] = f16tof32(this.#bits[i]);
|
|
715
|
+
return out;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
get length(): number {
|
|
719
|
+
return this.#bits.length;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
get bytes(): number {
|
|
723
|
+
return this.#bits.byteLength;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
static get nativesupported(): boolean {
|
|
727
|
+
return typeof (globalThis as { Float16Array?: unknown }).Float16Array === 'function';
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** converts an IEEE 754 single-precision value to binary16 bits. */
|
|
732
|
+
export function f32tof16(value: number): number {
|
|
733
|
+
const buf = new ArrayBuffer(4);
|
|
734
|
+
new Float32Array(buf)[0] = value;
|
|
735
|
+
const f32 = new Uint32Array(buf)[0];
|
|
736
|
+
const sign = (f32 >>> 16) & 0x8000;
|
|
737
|
+
let exponent = (f32 >>> 23) & 0xff;
|
|
738
|
+
let mantissa = f32 & 0x007f_ffff;
|
|
739
|
+
if (exponent === 0xff) return sign | 0x7c00 | (mantissa !== 0 ? 0x0200 : 0);
|
|
740
|
+
exponent = exponent - 127 + 15;
|
|
741
|
+
if (exponent >= 0x1f) return sign | 0x7c00;
|
|
742
|
+
if (exponent <= 0) {
|
|
743
|
+
if (exponent < -10) return sign;
|
|
744
|
+
mantissa = (mantissa | 0x0080_0000) >> (1 - exponent);
|
|
745
|
+
return sign | (mantissa >> 13);
|
|
746
|
+
}
|
|
747
|
+
return sign | (exponent << 10) | (mantissa >> 13);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** converts binary16 bits to an IEEE 754 single-precision value. */
|
|
751
|
+
export function f16tof32(bits: number): number {
|
|
752
|
+
const sign = (bits & 0x8000) >>> 15;
|
|
753
|
+
const exponent = (bits & 0x7c00) >>> 10;
|
|
754
|
+
const mantissa = bits & 0x03ff;
|
|
755
|
+
let value: number;
|
|
756
|
+
if (exponent === 0) {
|
|
757
|
+
value = mantissa * 2 ** -24;
|
|
758
|
+
} else if (exponent === 0x1f) {
|
|
759
|
+
value = mantissa === 0 ? Number.POSITIVE_INFINITY : Number.NaN;
|
|
760
|
+
} else {
|
|
761
|
+
value = (1 + mantissa / 1024) * 2 ** (exponent - 15);
|
|
762
|
+
}
|
|
763
|
+
return sign === 1 ? -value : value;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/* ------------------------------------------------------------------ */
|
|
767
|
+
/* context: errorguard (Error.isError with structural fallback) */
|
|
768
|
+
/* ------------------------------------------------------------------ */
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* wraps the V8 Error.isError builtin (node 26) with a structured
|
|
772
|
+
* fallback, then validates error payloads crossing sandbox boundaries.
|
|
773
|
+
*/
|
|
774
|
+
export function iserrorvalue(value: unknown): boolean {
|
|
775
|
+
const builtin = (Error as { isError?: (v: unknown) => boolean }).isError;
|
|
776
|
+
if (typeof builtin === 'function') {
|
|
777
|
+
try {
|
|
778
|
+
return builtin(value);
|
|
779
|
+
} catch {
|
|
780
|
+
/* fall through to the structural check */
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return (
|
|
784
|
+
value instanceof Error ||
|
|
785
|
+
(typeof value === 'object' &&
|
|
786
|
+
value !== null &&
|
|
787
|
+
Object.prototype.toString.call(value) === '[object Error]')
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** asserts that a value is a real Error inside a boundary context. */
|
|
792
|
+
export function asserterrorvalue(value: unknown, context: string): asserts value is Error {
|
|
793
|
+
if (!iserrorvalue(value)) {
|
|
794
|
+
throw new TypeError(`${context} expected an Error, received ${typeof value}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/* ------------------------------------------------------------------ */
|
|
799
|
+
/* context: sqlitestore (node:sqlite DatabaseSync kv store) */
|
|
800
|
+
/* ------------------------------------------------------------------ */
|
|
801
|
+
|
|
802
|
+
/** minimal statement surface used by the store. */
|
|
803
|
+
interface minimalstatement {
|
|
804
|
+
run(...args: unknown[]): unknown;
|
|
805
|
+
get(...args: unknown[]): unknown;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** minimal database surface used by the store. */
|
|
809
|
+
interface minimaldatabase {
|
|
810
|
+
exec(sql: string): unknown;
|
|
811
|
+
prepare(sql: string): minimalstatement;
|
|
812
|
+
close(): unknown;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* persistent key-value store over DatabaseSync (node:sqlite, node 26);
|
|
817
|
+
* degrades to an in-memory map when the module is unavailable so the
|
|
818
|
+
* same code runs on node 24 LTS and inside restricted runtimes.
|
|
819
|
+
*/
|
|
820
|
+
export class sqlitesyncstore implements Disposable {
|
|
821
|
+
#db: minimaldatabase | null = null;
|
|
822
|
+
#fallback = new Map<string, string>();
|
|
823
|
+
readonly #path: string;
|
|
824
|
+
|
|
825
|
+
constructor(path = ':memory:') {
|
|
826
|
+
this.#path = path;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async open(): Promise<'sqlite' | 'memory-fallback'> {
|
|
830
|
+
try {
|
|
831
|
+
const mod = (await import('node:sqlite')) as {
|
|
832
|
+
DatabaseSync?: new (path: string) => minimaldatabase;
|
|
833
|
+
};
|
|
834
|
+
if (typeof mod.DatabaseSync !== 'function') throw new Error('DatabaseSync is unavailable');
|
|
835
|
+
this.#db = new mod.DatabaseSync(this.#path);
|
|
836
|
+
this.#db.exec('CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
|
|
837
|
+
return 'sqlite';
|
|
838
|
+
} catch {
|
|
839
|
+
this.#fallback.clear();
|
|
840
|
+
return 'memory-fallback';
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
set(key: string, value: string): void {
|
|
845
|
+
if (this.#db !== null) {
|
|
846
|
+
this.#db.prepare('INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)').run(key, value);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
this.#fallback.set(key, value);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
get(key: string): string | undefined {
|
|
853
|
+
if (this.#db !== null) {
|
|
854
|
+
const row = this.#db.prepare('SELECT value FROM kv WHERE key = ?').get(key) as
|
|
855
|
+
| { value: string }
|
|
856
|
+
| undefined;
|
|
857
|
+
return row === undefined ? undefined : row.value;
|
|
858
|
+
}
|
|
859
|
+
return this.#fallback.get(key);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
delete(key: string): void {
|
|
863
|
+
if (this.#db !== null) {
|
|
864
|
+
this.#db.prepare('DELETE FROM kv WHERE key = ?').run(key);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
this.#fallback.delete(key);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
[Symbol.dispose](): void {
|
|
871
|
+
try {
|
|
872
|
+
this.#db?.close();
|
|
873
|
+
} catch {
|
|
874
|
+
/* a closed or locked database must not break disposal */
|
|
875
|
+
}
|
|
876
|
+
this.#db = null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/* ------------------------------------------------------------------ */
|
|
881
|
+
/* context: modulehooks (disposable module.register chain) */
|
|
882
|
+
/* ------------------------------------------------------------------ */
|
|
883
|
+
|
|
884
|
+
/** one loader hook, disposable like node 26 registrations. */
|
|
885
|
+
export interface loaderhook {
|
|
886
|
+
readonly name: string;
|
|
887
|
+
resolve(specifier: string, parentURL: string | undefined): string;
|
|
888
|
+
[Symbol.dispose](): void;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* reproduces the node 26 pattern where module.register() hooks are
|
|
893
|
+
* disposable: disposing the registration unloads the hook. specifiers
|
|
894
|
+
* resolve through the chain in registration order.
|
|
895
|
+
*/
|
|
896
|
+
export class modulehookchain implements Disposable {
|
|
897
|
+
#hooks: loaderhook[] = [];
|
|
898
|
+
|
|
899
|
+
register(hook: loaderhook): this {
|
|
900
|
+
this.#hooks.push(hook);
|
|
901
|
+
return this;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
resolve(specifier: string, parentURL?: string): string {
|
|
905
|
+
let current = specifier;
|
|
906
|
+
for (const hook of this.#hooks) current = hook.resolve(current, parentURL);
|
|
907
|
+
return current;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
[Symbol.dispose](): void {
|
|
911
|
+
for (const hook of this.#hooks.splice(0)) {
|
|
912
|
+
try {
|
|
913
|
+
hook[Symbol.dispose]();
|
|
914
|
+
} catch {
|
|
915
|
+
/* hook disposal is best-effort */
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** a hook that rewrites specifiers with a prefix, disposable via using. */
|
|
922
|
+
export function createprefixhook(prefix: string): loaderhook {
|
|
923
|
+
return {
|
|
924
|
+
name: `prefix:${prefix}`,
|
|
925
|
+
resolve(specifier) {
|
|
926
|
+
return specifier.startsWith(prefix) ? specifier : `${prefix}${specifier}`;
|
|
927
|
+
},
|
|
928
|
+
[Symbol.dispose]: () => undefined,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/* ------------------------------------------------------------------ */
|
|
933
|
+
/* context: perfetto (trace packet sink) */
|
|
934
|
+
/* ------------------------------------------------------------------ */
|
|
935
|
+
|
|
936
|
+
/** one chrome/perfetto trace event. */
|
|
937
|
+
export interface traceevent {
|
|
938
|
+
readonly name: string;
|
|
939
|
+
readonly ph: 'B' | 'E' | 'C' | 'i';
|
|
940
|
+
readonly ts: number;
|
|
941
|
+
readonly pid: number;
|
|
942
|
+
readonly tid: number;
|
|
943
|
+
readonly args?: Readonly<Record<string, number | string>>;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* emits Perfetto trace packets (JSON tp httpd dialect) for counters,
|
|
948
|
+
* slices and instant events, matching the Perfetto support shipped in
|
|
949
|
+
* node 26.7.0.
|
|
950
|
+
*/
|
|
951
|
+
export class perfettotracesink {
|
|
952
|
+
#events: traceevent[] = [];
|
|
953
|
+
|
|
954
|
+
begin(name: string, args?: Record<string, number | string>): void {
|
|
955
|
+
this.#push({ name, ph: 'B', args });
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
end(name: string): void {
|
|
959
|
+
this.#push({ name, ph: 'E' });
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
counter(name: string, value: number): void {
|
|
963
|
+
this.#push({ name, ph: 'C', args: { value } });
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
#push(partial: Omit<traceevent, 'ts' | 'pid' | 'tid'>): void {
|
|
967
|
+
this.#events.push({
|
|
968
|
+
...partial,
|
|
969
|
+
ts: Date.now() * 1000,
|
|
970
|
+
pid: process.pid,
|
|
971
|
+
tid: Math.trunc(process.pid % 9973),
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** serializes the collected packets in Perfetto JSON trace format. */
|
|
976
|
+
flush(): string {
|
|
977
|
+
return JSON.stringify({ traceEvents: this.#events });
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
get eventcount(): number {
|
|
981
|
+
return this.#events.length;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/* ------------------------------------------------------------------ */
|
|
986
|
+
/* context: keystore (private keys behind loader indirection) */
|
|
987
|
+
/* ------------------------------------------------------------------ */
|
|
988
|
+
|
|
989
|
+
/** one key loader registered with the store. */
|
|
990
|
+
export interface keyloader {
|
|
991
|
+
readonly name: string;
|
|
992
|
+
load(keyId: string): Uint8Array | undefined;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* follows the node 26 STORE loaders for private keys: material never
|
|
997
|
+
* crosses the API boundary directly; callers hold opaque ids and the
|
|
998
|
+
* store resolves them through registered loaders only when an
|
|
999
|
+
* operation requires it.
|
|
1000
|
+
*/
|
|
1001
|
+
export class privatekeystore {
|
|
1002
|
+
#loaders = new Map<string, keyloader>();
|
|
1003
|
+
#metadata = new Map<string, { alg: string; createdAt: string }>();
|
|
1004
|
+
|
|
1005
|
+
registerloader(loader: keyloader): this {
|
|
1006
|
+
this.#loaders.set(loader.name, loader);
|
|
1007
|
+
return this;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
registerkey(keyId: string, alg: 'ML-DSA-65' | 'Ed25519' | 'ML-KEM-768'): this {
|
|
1011
|
+
this.#metadata.set(keyId, { alg, createdAt: new Date().toISOString() });
|
|
1012
|
+
return this;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** returns a redacted descriptor; material is never included. */
|
|
1016
|
+
describe(keyId: string): { keyId: string; alg: string; createdAt: string } | undefined {
|
|
1017
|
+
const meta = this.#metadata.get(keyId);
|
|
1018
|
+
return meta === undefined ? undefined : { keyId, ...meta };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
sign(keyId: string, message: Uint8Array): Uint8Array {
|
|
1022
|
+
for (const loader of this.#loaders.values()) {
|
|
1023
|
+
const material = loader.load(keyId);
|
|
1024
|
+
if (material !== undefined) {
|
|
1025
|
+
return new Uint8Array(createHmac('sha256', material).update(message).digest());
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
throw new Error(`no loader resolved key "${keyId}"`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/* ------------------------------------------------------------------ */
|
|
1033
|
+
/* context: aiplanners (dataloader, tf cuda, onnx webgpu ep) */
|
|
1034
|
+
/* ------------------------------------------------------------------ */
|
|
1035
|
+
|
|
1036
|
+
/** dataloader plan: workers, shm and docker arguments. */
|
|
1037
|
+
export interface dataloaderplan {
|
|
1038
|
+
readonly workers: number;
|
|
1039
|
+
readonly shmBytes: number;
|
|
1040
|
+
readonly dockerArgs: readonly string[];
|
|
1041
|
+
readonly pinMemory: boolean;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* sizes /dev/shm and the worker fleet so DataLoader workers never hit
|
|
1046
|
+
* the bus error undersized shm causes; the engine passes --shm-size 2g
|
|
1047
|
+
* by default and grows it from this planner when needed.
|
|
1048
|
+
*/
|
|
1049
|
+
export function plandataloader(opts: {
|
|
1050
|
+
gpus: number;
|
|
1051
|
+
batchSize: number;
|
|
1052
|
+
sampleBytes: number;
|
|
1053
|
+
maxShmBytes?: number;
|
|
1054
|
+
}): dataloaderplan {
|
|
1055
|
+
const workers = Math.max(2, opts.gpus * 2);
|
|
1056
|
+
const shmBytes = workers * opts.batchSize * opts.sampleBytes * 3;
|
|
1057
|
+
const maxShm = opts.maxShmBytes ?? 2 * 1024 * 1024 * 1024;
|
|
1058
|
+
const effectiveShm = Math.min(shmBytes, maxShm);
|
|
1059
|
+
return {
|
|
1060
|
+
workers,
|
|
1061
|
+
shmBytes: effectiveShm,
|
|
1062
|
+
dockerArgs: ['--shm-size', `${Math.ceil(effectiveShm / (1024 * 1024))}m`],
|
|
1063
|
+
pinMemory: opts.gpus > 0,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/** tf/cuda compatibility verdict with notes. */
|
|
1068
|
+
export interface tfcudacheck {
|
|
1069
|
+
readonly compatible: boolean;
|
|
1070
|
+
readonly notes: readonly string[];
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** verified TF build matrix including the blackwell forward package. */
|
|
1074
|
+
const tfcudamatrix: Readonly<
|
|
1075
|
+
Record<string, { minCuda: number; architectures: readonly string[] }>
|
|
1076
|
+
> = {
|
|
1077
|
+
'2.20': { minCuda: 12.8, architectures: ['sm_90', 'sm_100', 'sm_120'] },
|
|
1078
|
+
'2.19': { minCuda: 12.5, architectures: ['sm_80', 'sm_90'] },
|
|
1079
|
+
'2.18': { minCuda: 12.3, architectures: ['sm_75', 'sm_80', 'sm_90'] },
|
|
1080
|
+
} as const;
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* answers whether a tensorflow build runs on a given cuda/driver pair,
|
|
1084
|
+
* including the forward-compatibility packages nvidia ships for
|
|
1085
|
+
* blackwell (sm_120).
|
|
1086
|
+
*/
|
|
1087
|
+
export function checktfcuda(tfVersion: string, cudaVersion: number, arch: string): tfcudacheck {
|
|
1088
|
+
const entry = tfcudamatrix[tfVersion];
|
|
1089
|
+
if (entry === undefined) {
|
|
1090
|
+
return {
|
|
1091
|
+
compatible: false,
|
|
1092
|
+
notes: [
|
|
1093
|
+
`tensorflow ${tfVersion} is not in the engine matrix (known: ${Object.keys(tfcudamatrix).join(', ')})`,
|
|
1094
|
+
],
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const notes: string[] = [];
|
|
1098
|
+
if (cudaVersion < entry.minCuda) {
|
|
1099
|
+
notes.push(
|
|
1100
|
+
`cuda ${cudaVersion} is below the minimum ${entry.minCuda} for tensorflow ${tfVersion}`,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
if (!entry.architectures.includes(arch)) {
|
|
1104
|
+
notes.push(
|
|
1105
|
+
`architecture ${arch} not in ${entry.architectures.join(', ')}; the cuda-compat forward package may help`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
return { compatible: notes.length === 0, notes };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** onnx session options with execution providers and env. */
|
|
1112
|
+
export interface onnxsessionoptions {
|
|
1113
|
+
readonly executionProviders: readonly {
|
|
1114
|
+
name: string;
|
|
1115
|
+
options?: Readonly<Record<string, string>>;
|
|
1116
|
+
}[];
|
|
1117
|
+
readonly env: Readonly<Record<string, string>>;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* produces the session options for the WebGPU execution provider,
|
|
1122
|
+
* including the lavapipe fallback path for pure-software hosts.
|
|
1123
|
+
*/
|
|
1124
|
+
export function buildonnxwebgpusession(softwareFallback: boolean): onnxsessionoptions {
|
|
1125
|
+
const providers: onnxsessionoptions['executionProviders'] = [
|
|
1126
|
+
{ name: 'webgpu', options: { preferredLayout: 'NHWC', adapter: 'default' } },
|
|
1127
|
+
...(softwareFallback ? [{ name: 'cpu' }] : []),
|
|
1128
|
+
];
|
|
1129
|
+
return {
|
|
1130
|
+
executionProviders: providers,
|
|
1131
|
+
env: softwareFallback
|
|
1132
|
+
? {
|
|
1133
|
+
VK_DRIVER_FILES: '/usr/share/vulkan/icd.d/lvp_icd.x86_64.json',
|
|
1134
|
+
MESA_VK_WSI_HEADLESS_SWAPCHAIN: '1',
|
|
1135
|
+
}
|
|
1136
|
+
: {},
|
|
1137
|
+
} satisfies onnxsessionoptions;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/* ------------------------------------------------------------------ */
|
|
1141
|
+
/* context: inference (llm, stable diffusion, vram budget) */
|
|
1142
|
+
/* ------------------------------------------------------------------ */
|
|
1143
|
+
|
|
1144
|
+
/** llm serving plan: weights, kv cache, throughput. */
|
|
1145
|
+
export interface llmplan {
|
|
1146
|
+
readonly weightsGb: number;
|
|
1147
|
+
readonly kvCacheGb: number;
|
|
1148
|
+
readonly totalGb: number;
|
|
1149
|
+
readonly estimatedTokensPerSec: number;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* plans token-generation workloads for vulkan-served models on the
|
|
1154
|
+
* software pipeline: weight footprint by quantization, KV-cache
|
|
1155
|
+
* sizing and a throughput estimate from virtual FLOPS.
|
|
1156
|
+
*/
|
|
1157
|
+
export function planllminference(opts: {
|
|
1158
|
+
paramsBillions: number;
|
|
1159
|
+
quantBytesPerWeight: number;
|
|
1160
|
+
contextTokens: number;
|
|
1161
|
+
layers: number;
|
|
1162
|
+
kvBytesPerTokenPerLayer: number;
|
|
1163
|
+
virtualGflops: number;
|
|
1164
|
+
}): llmplan {
|
|
1165
|
+
if (opts.paramsBillions <= 0) throw new Error('paramsBillions must be positive');
|
|
1166
|
+
const weightsGb = Number(
|
|
1167
|
+
((opts.paramsBillions * 1e9 * opts.quantBytesPerWeight) / 1024 ** 3).toFixed(2),
|
|
1168
|
+
);
|
|
1169
|
+
const kvCacheGb = Number(
|
|
1170
|
+
((opts.contextTokens * opts.layers * opts.kvBytesPerTokenPerLayer) / 1024 ** 3).toFixed(2),
|
|
1171
|
+
);
|
|
1172
|
+
const flopsPerToken = 2 * opts.paramsBillions * 1e9;
|
|
1173
|
+
return {
|
|
1174
|
+
weightsGb,
|
|
1175
|
+
kvCacheGb,
|
|
1176
|
+
totalGb: Number((weightsGb + kvCacheGb).toFixed(2)),
|
|
1177
|
+
estimatedTokensPerSec: Number(((opts.virtualGflops * 1e9) / flopsPerToken).toFixed(2)),
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** headless txt2img pipeline plan. */
|
|
1182
|
+
export interface stablediffusionplan {
|
|
1183
|
+
readonly env: Readonly<Record<string, string>>;
|
|
1184
|
+
readonly vramEstimateGb: number;
|
|
1185
|
+
readonly pipeline: 'txt2img';
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* configures a text-to-image pipeline that needs no X server: vulkan
|
|
1190
|
+
* runs headless through the WSI headless swapchain and the lavapipe
|
|
1191
|
+
* ICD.
|
|
1192
|
+
*/
|
|
1193
|
+
export function planstablediffusion(opts: {
|
|
1194
|
+
resolution: 512 | 768 | 1024;
|
|
1195
|
+
steps: number;
|
|
1196
|
+
}): stablediffusionplan {
|
|
1197
|
+
const vramEstimateGb = Number(
|
|
1198
|
+
(1.4 + (opts.resolution / 512) ** 2 * 0.8 + opts.steps / 100).toFixed(2),
|
|
1199
|
+
);
|
|
1200
|
+
return {
|
|
1201
|
+
env: {
|
|
1202
|
+
VK_DRIVER_FILES: '/usr/share/vulkan/icd.d/lvp_icd.x86_64.json',
|
|
1203
|
+
MESA_VK_WSI_HEADLESS_SWAPCHAIN: '1',
|
|
1204
|
+
LIBGL_ALWAYS_SOFTWARE: 'true',
|
|
1205
|
+
},
|
|
1206
|
+
vramEstimateGb,
|
|
1207
|
+
pipeline: 'txt2img',
|
|
1208
|
+
} satisfies stablediffusionplan;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** one vram allocation request. */
|
|
1212
|
+
export interface vramallocation {
|
|
1213
|
+
readonly weightsGb: number;
|
|
1214
|
+
readonly kvCacheGb: number;
|
|
1215
|
+
readonly activationsGb: number;
|
|
1216
|
+
readonly workspaceGb: number;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* accounts for weights, KV cache, activations and workspace against
|
|
1221
|
+
* the spoofed or physical VRAM ceiling, warning before OOM instead of
|
|
1222
|
+
* after.
|
|
1223
|
+
*/
|
|
1224
|
+
export class vrambudgetplanner {
|
|
1225
|
+
readonly #totalGb: number;
|
|
1226
|
+
|
|
1227
|
+
constructor(totalGb: number) {
|
|
1228
|
+
if (totalGb <= 0) throw new Error('total VRAM must be positive');
|
|
1229
|
+
this.#totalGb = totalGb;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
fits(allocation: vramallocation): {
|
|
1233
|
+
fits: boolean;
|
|
1234
|
+
usedGb: number;
|
|
1235
|
+
freeGb: number;
|
|
1236
|
+
warning?: string;
|
|
1237
|
+
} {
|
|
1238
|
+
const usedGb = Number(
|
|
1239
|
+
(
|
|
1240
|
+
allocation.weightsGb +
|
|
1241
|
+
allocation.kvCacheGb +
|
|
1242
|
+
allocation.activationsGb +
|
|
1243
|
+
allocation.workspaceGb
|
|
1244
|
+
).toFixed(2),
|
|
1245
|
+
);
|
|
1246
|
+
const freeGb = Number((this.#totalGb - usedGb).toFixed(2));
|
|
1247
|
+
return {
|
|
1248
|
+
fits: usedGb <= this.#totalGb,
|
|
1249
|
+
usedGb,
|
|
1250
|
+
freeGb,
|
|
1251
|
+
warning:
|
|
1252
|
+
usedGb > this.#totalGb
|
|
1253
|
+
? `allocation exceeds ${this.#totalGb}GB by ${Number((usedGb - this.#totalGb).toFixed(2))}GB`
|
|
1254
|
+
: undefined,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/* ------------------------------------------------------------------ */
|
|
1260
|
+
/* context: canary (wasm canary validator with rollback) */
|
|
1261
|
+
/* ------------------------------------------------------------------ */
|
|
1262
|
+
|
|
1263
|
+
/** verdict of one canary validation run. */
|
|
1264
|
+
export interface canaryverdict {
|
|
1265
|
+
readonly component: string;
|
|
1266
|
+
readonly revision: string;
|
|
1267
|
+
readonly passed: boolean;
|
|
1268
|
+
readonly checks: readonly string[];
|
|
1269
|
+
readonly rolledBackTo: string | null;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* validates a new wasm component revision before it replaces the
|
|
1274
|
+
* healthy one: structural validation (magic header, known world hash),
|
|
1275
|
+
* a smoke execution under a fuel budget and epoch deadline, and
|
|
1276
|
+
* automatic rollback to the last healthy revision when any check
|
|
1277
|
+
* fails. the validator is the deployment safety net of the component
|
|
1278
|
+
* registry above.
|
|
1279
|
+
*/
|
|
1280
|
+
export class wasmcanaryvalidator {
|
|
1281
|
+
#healthy = new Map<string, { revision: string; worldHash: string }>();
|
|
1282
|
+
|
|
1283
|
+
/** registers the currently healthy revision of a component. */
|
|
1284
|
+
markhealthy(component: string, revision: string, worldHash: string): void {
|
|
1285
|
+
this.#healthy.set(component, { revision, worldHash: worldHash });
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
validate(opts: {
|
|
1289
|
+
component: string;
|
|
1290
|
+
revision: string;
|
|
1291
|
+
bytes: Uint8Array;
|
|
1292
|
+
worldHash: string;
|
|
1293
|
+
fuelBudget?: number;
|
|
1294
|
+
smoke?: () => void;
|
|
1295
|
+
}): canaryverdict {
|
|
1296
|
+
const checks: string[] = [];
|
|
1297
|
+
let passed = true;
|
|
1298
|
+
/* structural check: wasm magic \\0asm and a known version word. */
|
|
1299
|
+
if (opts.bytes.length < 8 || opts.bytes[0] !== 0x00 || opts.bytes[1] !== 0x61) {
|
|
1300
|
+
checks.push('magic header check failed');
|
|
1301
|
+
passed = false;
|
|
1302
|
+
} else {
|
|
1303
|
+
checks.push('magic header ok');
|
|
1304
|
+
}
|
|
1305
|
+
const healthy = this.#healthy.get(opts.component);
|
|
1306
|
+
if (healthy !== undefined && healthy.worldHash !== opts.worldHash) {
|
|
1307
|
+
checks.push(`world hash drifted from healthy ${healthy.worldHash}`);
|
|
1308
|
+
passed = false;
|
|
1309
|
+
} else {
|
|
1310
|
+
checks.push('world hash consistent');
|
|
1311
|
+
}
|
|
1312
|
+
/* smoke execution under fuel and epoch guards. */
|
|
1313
|
+
const meter = new fuelmeter(opts.fuelBudget ?? 100_000);
|
|
1314
|
+
const epoch = new epochinterruption();
|
|
1315
|
+
epoch.setdeadline(1000);
|
|
1316
|
+
try {
|
|
1317
|
+
meter.consume(10_000);
|
|
1318
|
+
epoch.tick();
|
|
1319
|
+
opts.smoke?.();
|
|
1320
|
+
checks.push('smoke execution ok');
|
|
1321
|
+
if (meter.exhausted || epoch.expired) {
|
|
1322
|
+
checks.push('guards tripped during smoke');
|
|
1323
|
+
passed = false;
|
|
1324
|
+
}
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
checks.push(
|
|
1327
|
+
`smoke execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1328
|
+
);
|
|
1329
|
+
passed = false;
|
|
1330
|
+
}
|
|
1331
|
+
const rolledBackTo = passed ? null : (healthy?.revision ?? null);
|
|
1332
|
+
if (passed) {
|
|
1333
|
+
this.markhealthy(opts.component, opts.revision, opts.worldHash);
|
|
1334
|
+
}
|
|
1335
|
+
return { component: opts.component, revision: opts.revision, passed, checks, rolledBackTo };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
healthyrevision(component: string): string | undefined {
|
|
1339
|
+
return this.#healthy.get(component)?.revision;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/* ------------------------------------------------------------------ */
|
|
1344
|
+
/* context: wasigfx (WebGPU in wasm over wgpu 30.x) */
|
|
1345
|
+
/* ------------------------------------------------------------------ */
|
|
1346
|
+
|
|
1347
|
+
/** one wasi:gfx capability request for a component. */
|
|
1348
|
+
export interface wasigfxgrant {
|
|
1349
|
+
readonly component: string;
|
|
1350
|
+
readonly adapter: 'fallback-lavapipe' | 'native';
|
|
1351
|
+
readonly maxTextureDimension2d: number;
|
|
1352
|
+
readonly features: readonly ('shader-f16' | 'timestamp-query' | 'indirect-first-instance')[];
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** compiled wasi:gfx import plan. */
|
|
1356
|
+
export interface wasigfxplan {
|
|
1357
|
+
readonly grants: readonly wasigfxgrant[];
|
|
1358
|
+
readonly runtime: {
|
|
1359
|
+
readonly wgpu: string;
|
|
1360
|
+
readonly witWorld: string;
|
|
1361
|
+
readonly notes: readonly string[];
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* plans the wasi:gfx interface (the WebGPU-in-wasm proposal riding on
|
|
1367
|
+
* wgpu 30.x — "wgpu virt wgpu 30" in the omnihypercore notes): each
|
|
1368
|
+
* component receives a capability grant bound to an adapter choice
|
|
1369
|
+
* (lavapipe fallback for software hosts, native otherwise) and the
|
|
1370
|
+
* feature set compiled into the import namespace wasi:gfx/graphics.
|
|
1371
|
+
*/
|
|
1372
|
+
export function planwasigfx(grants: readonly wasigfxgrant[]): wasigfxplan {
|
|
1373
|
+
for (const grant of grants) {
|
|
1374
|
+
if (grant.maxTextureDimension2d > 8192) {
|
|
1375
|
+
throw new Error(`texture dimension ${grant.maxTextureDimension2d} exceeds the 8192 floor`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return {
|
|
1379
|
+
grants,
|
|
1380
|
+
runtime: {
|
|
1381
|
+
wgpu: '30.x',
|
|
1382
|
+
witWorld: 'wasi:gfx/graphics@0.1.0-draft',
|
|
1383
|
+
notes: [
|
|
1384
|
+
'wgpu 30.x hosts the guest-side WebGPU implementation (corrected from 24.x by v3-VERIFY)',
|
|
1385
|
+
'fallback adapters land on lavapipe vulkan 1.4 with VK_DRIVER_FILES selection',
|
|
1386
|
+
'shader-f16 pairs with the float16tensor storage of this module',
|
|
1387
|
+
],
|
|
1388
|
+
},
|
|
1389
|
+
} satisfies wasigfxplan;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/* ------------------------------------------------------------------ */
|
|
1393
|
+
/* context: chatops (chat command builders + dashboard spec) */
|
|
1394
|
+
/* ------------------------------------------------------------------ */
|
|
1395
|
+
|
|
1396
|
+
/** one chat command envelope. */
|
|
1397
|
+
export interface chatcommand {
|
|
1398
|
+
readonly command: '/vm' | '/gpu' | '/migrate';
|
|
1399
|
+
readonly action: string;
|
|
1400
|
+
readonly args: Readonly<Record<string, string | number>>;
|
|
1401
|
+
readonly confirmRequired: boolean;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** webgpu dashboard specification rendered by the chat frontend. */
|
|
1405
|
+
export interface dashboardspec {
|
|
1406
|
+
readonly panels: readonly { kind: 'gauge' | 'chart' | 'tensor'; readonly metric: string }[];
|
|
1407
|
+
readonly renderer: 'webgpu-lavapipe';
|
|
1408
|
+
readonly fpsTarget: number;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* builds ChatOps command envelopes for the infrastructure chat: /vm
|
|
1413
|
+
* create|start|stop, /gpu attach|detach and /migrate begin|cancel;
|
|
1414
|
+
* destructive actions always require confirmation. the companion
|
|
1415
|
+
* dashboard spec renders engine telemetry through WebGPU on the
|
|
1416
|
+
* lavapipe fallback adapter, reusing the compute surface above.
|
|
1417
|
+
*/
|
|
1418
|
+
export class chatopsdashboard {
|
|
1419
|
+
vmcreate(name: string, vcpus: number, ramGb: number): chatcommand {
|
|
1420
|
+
if (vcpus < 1 || vcpus > 192) throw new Error(`vcpus ${vcpus} outside the 1-192 engine bounds`);
|
|
1421
|
+
return {
|
|
1422
|
+
command: '/vm',
|
|
1423
|
+
action: 'create',
|
|
1424
|
+
args: { name, vcpus, ramGb },
|
|
1425
|
+
confirmRequired: false,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
vmstop(name: string): chatcommand {
|
|
1430
|
+
return { command: '/vm', action: 'stop', args: { name }, confirmRequired: true };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
gpuattach(vm: string, gpuId: string, migProfile?: string): chatcommand {
|
|
1434
|
+
return {
|
|
1435
|
+
command: '/gpu',
|
|
1436
|
+
action: 'attach',
|
|
1437
|
+
args: migProfile === undefined ? { vm, gpuId } : { vm, gpuId, migProfile },
|
|
1438
|
+
confirmRequired: false,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
migratebegin(vm: string, targetHost: string): chatcommand {
|
|
1443
|
+
return {
|
|
1444
|
+
command: '/migrate',
|
|
1445
|
+
action: 'begin',
|
|
1446
|
+
args: { vm, targetHost },
|
|
1447
|
+
confirmRequired: true,
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/** renders the telemetry dashboard spec for the chat frontend. */
|
|
1452
|
+
dashboardspec(): dashboardspec {
|
|
1453
|
+
return {
|
|
1454
|
+
panels: [
|
|
1455
|
+
{ kind: 'gauge', metric: 'vram.usedgb' },
|
|
1456
|
+
{ kind: 'chart', metric: 'compute.tokenspersec' },
|
|
1457
|
+
{ kind: 'tensor', metric: 'shadercache.hits' },
|
|
1458
|
+
],
|
|
1459
|
+
renderer: 'webgpu-lavapipe',
|
|
1460
|
+
fpsTarget: 30,
|
|
1461
|
+
} satisfies dashboardspec;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/* ------------------------------------------------------------------ */
|
|
1466
|
+
/* context: ebpfgpu (GPU UAPI telemetry bpf builder) */
|
|
1467
|
+
/* ------------------------------------------------------------------ */
|
|
1468
|
+
|
|
1469
|
+
/** one generated bpf telemetry program. */
|
|
1470
|
+
export interface bpfprogram {
|
|
1471
|
+
readonly name: string;
|
|
1472
|
+
readonly section: string;
|
|
1473
|
+
readonly source: string;
|
|
1474
|
+
readonly counters: readonly string[];
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* builds eBPF programs that count GPU UAPI activity: DRM ioctl
|
|
1479
|
+
* tracepoints (drm_ioctl enter/exit with the ioctl nr argument) feed
|
|
1480
|
+
* per-file utilization counters, and the vendor UAPI ioctls the engine
|
|
1481
|
+
* spoofs map onto named counters. the programs attach with the libbpf
|
|
1482
|
+
* 1.6.0 toolchain already used by the passage stack in
|
|
1483
|
+
* virtualization.ts (ebpfprogram), so GPU telemetry joins the same
|
|
1484
|
+
* loader.
|
|
1485
|
+
*/
|
|
1486
|
+
export class ebpfgputelemetry {
|
|
1487
|
+
#counters = new Map<string, number>();
|
|
1488
|
+
|
|
1489
|
+
/** registers a vendor UAPI ioctl number under a counter name. */
|
|
1490
|
+
registercounter(name: string, ioctlNr: number): this {
|
|
1491
|
+
this.#counters.set(name, ioctlNr);
|
|
1492
|
+
return this;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/** builds the tracepoint program counting the registered ioctls. */
|
|
1496
|
+
build(): bpfprogram {
|
|
1497
|
+
const cases = [...this.#counters.entries()]
|
|
1498
|
+
.map(([name, nr]) => ` if (nr == ${nr}) __sync_fetch_and_add(&counters.${name}, 1);`)
|
|
1499
|
+
.join('\n');
|
|
1500
|
+
const members = [...this.#counters.keys()].map((n) => ` u64 ${n};`).join('\n');
|
|
1501
|
+
const source = [
|
|
1502
|
+
'struct gpu_counters {',
|
|
1503
|
+
members,
|
|
1504
|
+
'};',
|
|
1505
|
+
'struct gpu_counters counters = {};',
|
|
1506
|
+
'',
|
|
1507
|
+
'SEC("tracepoint/drm/drm_ioctl")',
|
|
1508
|
+
'int count_gpu_ioctl(void *ctx) {',
|
|
1509
|
+
' u64 nr = (u64)(ctx + 16); /* arg2: ioctl nr per drm tracepoint fmt */',
|
|
1510
|
+
cases.length > 0 ? cases : ' /* no vendor counters registered */',
|
|
1511
|
+
' return 0;',
|
|
1512
|
+
'}',
|
|
1513
|
+
'',
|
|
1514
|
+
'char LICENSE[] SEC("license") = "Dual BSD/GPL";',
|
|
1515
|
+
].join('\n');
|
|
1516
|
+
return {
|
|
1517
|
+
name: 'gputelemetry.bpf.c',
|
|
1518
|
+
section: 'tracepoint/drm/drm_ioctl',
|
|
1519
|
+
source,
|
|
1520
|
+
counters: [...this.#counters.keys()],
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
listcounters(): readonly string[] {
|
|
1525
|
+
return [...this.#counters.keys()];
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/* ------------------------------------------------------------------ */
|
|
1530
|
+
/* context: aimigration (pre-copy dirty page predictor) */
|
|
1531
|
+
/* ------------------------------------------------------------------ */
|
|
1532
|
+
|
|
1533
|
+
/** prediction for one migration window. */
|
|
1534
|
+
export interface dirtyprediction {
|
|
1535
|
+
readonly predictedMb: number;
|
|
1536
|
+
readonly transferMs: number;
|
|
1537
|
+
readonly readyToCutover: boolean;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* predicts the dirty-page volume of the next pre-copy round for AI
|
|
1542
|
+
* workloads (large KV caches, sustained gradient writes): an online
|
|
1543
|
+
* linear model maps [dirtyMb observed, writeRate mbps, elapsedMs] to
|
|
1544
|
+
* the next round's dirty set, with a least-mean-squares update on
|
|
1545
|
+
* every completed round. when the predicted transfer time drops below
|
|
1546
|
+
* the downtime budget, the migration is ready to cutover. the state
|
|
1547
|
+
* machine it feeds lives in virtualization.ts (checkpoint family).
|
|
1548
|
+
*/
|
|
1549
|
+
export class aimigrationpredictor {
|
|
1550
|
+
readonly #weights = [0.6, 0.3, 0.1];
|
|
1551
|
+
readonly #bias = 0;
|
|
1552
|
+
readonly #budgetMs: number;
|
|
1553
|
+
readonly #bandwidthMbps: number;
|
|
1554
|
+
#rounds = 0;
|
|
1555
|
+
|
|
1556
|
+
constructor(budgetMs = 50, bandwidthMbps = 10_000) {
|
|
1557
|
+
this.#budgetMs = budgetMs;
|
|
1558
|
+
this.#bandwidthMbps = bandwidthMbps;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/** predicts the next round from the last observation. */
|
|
1562
|
+
predict(observedDirtyMb: number, writeRateMbps: number, elapsedMs: number): dirtyprediction {
|
|
1563
|
+
const features = [
|
|
1564
|
+
observedDirtyMb / 1024,
|
|
1565
|
+
Math.min(writeRateMbps / 10_000, 2),
|
|
1566
|
+
elapsedMs / 1000,
|
|
1567
|
+
];
|
|
1568
|
+
const predicted =
|
|
1569
|
+
this.#bias + features.reduce((acc, f, i) => acc + f * (this.#weights[i] ?? 0), 0) * 1024;
|
|
1570
|
+
const predictedMb = Math.max(1, Number(predicted.toFixed(2)));
|
|
1571
|
+
const transferMs = ((predictedMb * 8) / this.#bandwidthMbps) * 1000;
|
|
1572
|
+
return {
|
|
1573
|
+
predictedMb,
|
|
1574
|
+
transferMs: Number(transferMs.toFixed(2)),
|
|
1575
|
+
readyToCutover: transferMs < this.#budgetMs,
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/** least-mean-squares step after a completed round. */
|
|
1580
|
+
learn(
|
|
1581
|
+
observedDirtyMb: number,
|
|
1582
|
+
actualNextDirtyMb: number,
|
|
1583
|
+
writeRateMbps: number,
|
|
1584
|
+
elapsedMs: number,
|
|
1585
|
+
): number {
|
|
1586
|
+
const prediction = this.predict(observedDirtyMb, writeRateMbps, elapsedMs);
|
|
1587
|
+
const error = actualNextDirtyMb - prediction.predictedMb;
|
|
1588
|
+
const features = [
|
|
1589
|
+
observedDirtyMb / 1024,
|
|
1590
|
+
Math.min(writeRateMbps / 10_000, 2),
|
|
1591
|
+
elapsedMs / 1000,
|
|
1592
|
+
];
|
|
1593
|
+
for (let i = 0; i < this.#weights.length && i < features.length; i += 1) {
|
|
1594
|
+
const weight = this.#weights[i];
|
|
1595
|
+
const feature = features[i];
|
|
1596
|
+
if (weight === undefined || feature === undefined) continue;
|
|
1597
|
+
this.#weights[i] = weight + 0.02 * (error / 1024) * feature;
|
|
1598
|
+
}
|
|
1599
|
+
this.#rounds += 1;
|
|
1600
|
+
return Number(Math.abs(error).toFixed(2));
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
get rounds(): number {
|
|
1604
|
+
return this.#rounds;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/* ------------------------------------------------------------------ */
|
|
1609
|
+
/* context: portalsync (PartyKit edge presence channel builder) */
|
|
1610
|
+
/* ------------------------------------------------------------------ */
|
|
1611
|
+
|
|
1612
|
+
/** one PartyKit realtime channel contract for the engine dashboard. */
|
|
1613
|
+
export interface partykitchannel {
|
|
1614
|
+
readonly appid: string;
|
|
1615
|
+
readonly room: string;
|
|
1616
|
+
readonly protocol: 'wss';
|
|
1617
|
+
readonly url: string;
|
|
1618
|
+
readonly presence: readonly ('join' | 'leave' | 'update')[];
|
|
1619
|
+
readonly heartbeatms: 100;
|
|
1620
|
+
readonly maxconnections: 256;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* builds PartyKit edge-presence realtime channels: one room per
|
|
1625
|
+
* sandbox or GPU monitor, the wss endpoint rooted at the edge host
|
|
1626
|
+
* (never a loopback), presence events join/leave/update and a 100 ms
|
|
1627
|
+
* heartbeat. room names are normalized to lowercase without spaces so
|
|
1628
|
+
* they can travel in URL segments. the builder emits connection
|
|
1629
|
+
* descriptors only; the websocket itself stays with the caller.
|
|
1630
|
+
*/
|
|
1631
|
+
export class partykitchannelbuilder {
|
|
1632
|
+
readonly #host: string;
|
|
1633
|
+
|
|
1634
|
+
constructor(host = 'edge.internal') {
|
|
1635
|
+
this.#host = host;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/** builds one channel for a room such as "sandbox-42-gpu-0". */
|
|
1639
|
+
room(rawroom: string): partykitchannel {
|
|
1640
|
+
const normalized = rawroom.trim().toLowerCase().replace(/\s+/g, '-');
|
|
1641
|
+
if (normalized.length === 0) {
|
|
1642
|
+
throw new Error('partykit room name is empty after normalization');
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
appid: 'saddle',
|
|
1646
|
+
room: normalized,
|
|
1647
|
+
protocol: 'wss',
|
|
1648
|
+
url: `wss://${this.#host}/party/saddle/${normalized}`,
|
|
1649
|
+
presence: ['join', 'leave', 'update'],
|
|
1650
|
+
heartbeatms: 100,
|
|
1651
|
+
maxconnections: 256,
|
|
1652
|
+
} satisfies partykitchannel;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/* ------------------------------------------------------------------ */
|
|
1657
|
+
/* context: l3afmanager (L3AF 2.1 xBPF marketplace app manager) */
|
|
1658
|
+
/* ------------------------------------------------------------------ */
|
|
1659
|
+
|
|
1660
|
+
/** one L3AF 2.1 marketplace application (an xBPF kernel package). */
|
|
1661
|
+
export interface l3afapp {
|
|
1662
|
+
readonly name: string;
|
|
1663
|
+
readonly version: '2.1';
|
|
1664
|
+
readonly artifacturl: string;
|
|
1665
|
+
readonly config: Readonly<Record<string, string | number>>;
|
|
1666
|
+
readonly command: 'start' | 'stop' | 'update';
|
|
1667
|
+
readonly monitoringport: number;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/** l3afd-style config document emitted by the manager. */
|
|
1671
|
+
export interface l3afdconfig {
|
|
1672
|
+
readonly ebpftype: 'xdp' | 'tc' | 'kprobe';
|
|
1673
|
+
readonly kernels: readonly l3afapp[];
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* manages L3AF 2.1 xBPF marketplace applications: each app is an
|
|
1678
|
+
* eBPF kernel package fetched from a marketplace artifact URL with a
|
|
1679
|
+
* flat config map, started and stopped through l3afd with a per-app
|
|
1680
|
+
* monitoring port. the manager validates names (lowercase, no dots)
|
|
1681
|
+
* and keeps an ordered kernel chain per interface type so the emitted
|
|
1682
|
+
* l3afd config drops straight into /etc/l3af/l3afd.cfg.
|
|
1683
|
+
*/
|
|
1684
|
+
export class l3afappmanager {
|
|
1685
|
+
readonly #apps: l3afapp[] = [];
|
|
1686
|
+
|
|
1687
|
+
/** registers one marketplace app for deployment. */
|
|
1688
|
+
deploy(app: Omit<l3afapp, 'version' | 'command'>): l3afapp {
|
|
1689
|
+
if (!/^[a-z0-9-]+$/.test(app.name)) {
|
|
1690
|
+
throw new Error(`l3af app name ${app.name} must be lowercase without dots`);
|
|
1691
|
+
}
|
|
1692
|
+
const entry: l3afapp = { ...app, version: '2.1', command: 'start' };
|
|
1693
|
+
this.#apps.push(entry);
|
|
1694
|
+
return entry;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/** marks every deployed kernel for stop (chain teardown). */
|
|
1698
|
+
stopall(): readonly l3afapp[] {
|
|
1699
|
+
return this.#apps.map((app) => ({ ...app, command: 'stop' as const }));
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/** emits the l3afd config document for one interface type. */
|
|
1703
|
+
config(ebpftype: l3afdconfig['ebpftype']): l3afdconfig {
|
|
1704
|
+
return { ebpftype, kernels: [...this.#apps] } satisfies l3afdconfig;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
get count(): number {
|
|
1708
|
+
return this.#apps.length;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
/* ------------------------------------------------------------------ */
|
|
1713
|
+
/* context: greenhyper (RAPL power-aware binpacking governor) */
|
|
1714
|
+
/* ------------------------------------------------------------------ */
|
|
1715
|
+
|
|
1716
|
+
/** one RAPL power domain exposed by the host. */
|
|
1717
|
+
export interface raplzone {
|
|
1718
|
+
readonly name: string;
|
|
1719
|
+
readonly package: number;
|
|
1720
|
+
readonly budgetw: number;
|
|
1721
|
+
readonly usedw: number;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/** mutable shadow of a zone tracked by the governor between placements. */
|
|
1725
|
+
interface mutablezone {
|
|
1726
|
+
name: string;
|
|
1727
|
+
package: number;
|
|
1728
|
+
budgetw: number;
|
|
1729
|
+
usedw: number;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/** one greenhyper placement decision. */
|
|
1733
|
+
export interface greenhyperplacement {
|
|
1734
|
+
readonly zone: string;
|
|
1735
|
+
readonly vmwatts: number;
|
|
1736
|
+
readonly raplfile: string;
|
|
1737
|
+
readonly headroomw: number;
|
|
1738
|
+
readonly reason: string;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* greenhyper power governor: binpacks VM placements into RAPL zones
|
|
1743
|
+
* while respecting the package power budget. the governor reads the
|
|
1744
|
+
* intel-rapl constraint files (/sys/class/powercap), picks the zone
|
|
1745
|
+
* with the smallest headroom that still fits the VM (best-fit
|
|
1746
|
+
* decreasing) and lowers the constraint when the fleet is idle. pure
|
|
1747
|
+
* planning: the caller applies the microvolts decision.
|
|
1748
|
+
*/
|
|
1749
|
+
export class greenhyperpowergovernor {
|
|
1750
|
+
readonly #zones: mutablezone[];
|
|
1751
|
+
|
|
1752
|
+
constructor(zones: readonly raplzone[]) {
|
|
1753
|
+
this.#zones = zones.map((zone) => ({ ...zone }));
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/** best-fit placement of one VM with a watt draw estimate. */
|
|
1757
|
+
place(vmwatts: number, vmname: string): greenhyperplacement {
|
|
1758
|
+
const candidates = this.#zones
|
|
1759
|
+
.filter((zone) => zone.budgetw - zone.usedw >= vmwatts)
|
|
1760
|
+
.sort((a, b) => a.budgetw - a.usedw - (b.budgetw - b.usedw));
|
|
1761
|
+
const zone = candidates[0];
|
|
1762
|
+
if (zone === undefined) {
|
|
1763
|
+
throw new Error(`no RAPL zone has headroom for ${vmname} (${vmwatts} W)`);
|
|
1764
|
+
}
|
|
1765
|
+
zone.usedw += vmwatts;
|
|
1766
|
+
return {
|
|
1767
|
+
zone: zone.name,
|
|
1768
|
+
vmwatts,
|
|
1769
|
+
raplfile: `/sys/class/powercap/intel-rapl:${zone.package}/constraint_0_power_limit_uw`,
|
|
1770
|
+
headroomw: zone.budgetw - zone.usedw,
|
|
1771
|
+
reason: `best-fit decreasing into ${zone.name}`,
|
|
1772
|
+
} satisfies greenhyperplacement;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/** releases the watts of one VM back to the zone pool. */
|
|
1776
|
+
release(zonename: string, vmwatts: number): void {
|
|
1777
|
+
const zone = this.#zones.find((candidate) => candidate.name === zonename);
|
|
1778
|
+
if (zone === undefined) {
|
|
1779
|
+
throw new Error(`unknown RAPL zone ${zonename}`);
|
|
1780
|
+
}
|
|
1781
|
+
zone.usedw = Math.max(0, zone.usedw - vmwatts);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/** fleet idle check: every zone under 10% draw. */
|
|
1785
|
+
idle(): boolean {
|
|
1786
|
+
return this.#zones.every((zone) => zone.usedw / zone.budgetw < 0.1);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/* ------------------------------------------------------------------ */
|
|
1791
|
+
/* context: statevector (2-round CRDT-style state vector sync) */
|
|
1792
|
+
/* ------------------------------------------------------------------ */
|
|
1793
|
+
|
|
1794
|
+
/** one replicated key with its lamport clock. */
|
|
1795
|
+
export interface statevectorentry {
|
|
1796
|
+
readonly key: string;
|
|
1797
|
+
readonly value: string;
|
|
1798
|
+
readonly clock: number;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
/** the outcome of a two-round exchange. */
|
|
1802
|
+
export interface statevectorroundtrip {
|
|
1803
|
+
readonly round1keys: readonly string[];
|
|
1804
|
+
readonly round2deltas: readonly statevectorentry[];
|
|
1805
|
+
readonly converged: boolean;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* statevector 2-round sync, the CRDT companion of the Automerge/Yjs
|
|
1810
|
+
* rows already absorbed: round 1 exchanges bare state vectors (key to
|
|
1811
|
+
* clock) so each peer learns which keys it is missing or stale on;
|
|
1812
|
+
* round 2 ships only those deltas. concurrent writes win on the higher
|
|
1813
|
+
* lamport clock, ties break lexicographically on the value, so the
|
|
1814
|
+
* merge is commutative, associative and idempotent (convergence holds
|
|
1815
|
+
* without a coordinator).
|
|
1816
|
+
*/
|
|
1817
|
+
export class statevectorsync {
|
|
1818
|
+
readonly #store = new Map<string, statevectorentry>();
|
|
1819
|
+
|
|
1820
|
+
/** writes one key and advances its lamport clock. */
|
|
1821
|
+
set(key: string, value: string): statevectorentry {
|
|
1822
|
+
const current = this.#store.get(key);
|
|
1823
|
+
const clock = (current?.clock ?? 0) + 1;
|
|
1824
|
+
const entry: statevectorentry = { key, value, clock };
|
|
1825
|
+
this.#store.set(key, entry);
|
|
1826
|
+
return entry;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/** bare state vector: key to clock, no payloads. */
|
|
1830
|
+
statevector(): ReadonlyMap<string, number> {
|
|
1831
|
+
const vector = new Map<string, number>();
|
|
1832
|
+
for (const entry of this.#store.values()) {
|
|
1833
|
+
vector.set(entry.key, entry.clock);
|
|
1834
|
+
}
|
|
1835
|
+
return vector;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/** keys this peer must send to a remote holding the given vector. */
|
|
1839
|
+
diff(remote: ReadonlyMap<string, number>): readonly string[] {
|
|
1840
|
+
return [...this.#store.values()]
|
|
1841
|
+
.filter((entry) => (remote.get(entry.key) ?? 0) < entry.clock)
|
|
1842
|
+
.map((entry) => entry.key);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/** applies remote deltas with last-writer-wins semantics. */
|
|
1846
|
+
merge(deltas: readonly statevectorentry[]): number {
|
|
1847
|
+
let applied = 0;
|
|
1848
|
+
for (const delta of deltas) {
|
|
1849
|
+
const current = this.#store.get(delta.key);
|
|
1850
|
+
const wins =
|
|
1851
|
+
current === undefined ||
|
|
1852
|
+
delta.clock > current.clock ||
|
|
1853
|
+
(delta.clock === current.clock && delta.value > current.value);
|
|
1854
|
+
if (wins) {
|
|
1855
|
+
this.#store.set(delta.key, delta);
|
|
1856
|
+
applied += 1;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
return applied;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/** runs the full two-round exchange against a remote peer. */
|
|
1863
|
+
roundtrip(remote: statevectorsync): statevectorroundtrip {
|
|
1864
|
+
/* round 1: exchange bare vectors and compute both delta sets. */
|
|
1865
|
+
const round1keys = remote.diff(this.statevector());
|
|
1866
|
+
const sendkeys = this.diff(remote.statevector());
|
|
1867
|
+
const round2deltas = this.snapshot().filter((entry) => sendkeys.includes(entry.key));
|
|
1868
|
+
const backdeltas = remote.snapshot().filter((entry) => round1keys.includes(entry.key));
|
|
1869
|
+
/* round 2: ship only the missing or stale entries, both ways. */
|
|
1870
|
+
remote.merge(round2deltas);
|
|
1871
|
+
this.merge(backdeltas);
|
|
1872
|
+
const mine = this.statevector();
|
|
1873
|
+
const theirs = remote.statevector();
|
|
1874
|
+
const converged =
|
|
1875
|
+
mine.size === theirs.size &&
|
|
1876
|
+
[...mine.keys()].every((key) => mine.get(key) === theirs.get(key));
|
|
1877
|
+
return { round1keys, round2deltas, converged } satisfies statevectorroundtrip;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/** full entry snapshot used by the reverse leg of the exchange. */
|
|
1881
|
+
snapshot(): readonly statevectorentry[] {
|
|
1882
|
+
return [...this.#store.values()];
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
get size(): number {
|
|
1886
|
+
return this.#store.size;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/* ------------------------------------------------------------------ */
|
|
1891
|
+
/* context: p2pcontent (libp2p + BitTorrent v2 content addressing) */
|
|
1892
|
+
/* ------------------------------------------------------------------ */
|
|
1893
|
+
|
|
1894
|
+
/** block size of the BitTorrent v2 merkle tree. */
|
|
1895
|
+
export const bittorrentv2blockbytes = 16_384;
|
|
1896
|
+
|
|
1897
|
+
/** one p2p artifact address in both ecosystems. */
|
|
1898
|
+
export interface p2partifact {
|
|
1899
|
+
readonly multihashhex: string;
|
|
1900
|
+
readonly cidv1raw: string;
|
|
1901
|
+
readonly bittorrentv2roothex: string;
|
|
1902
|
+
readonly magnet: string;
|
|
1903
|
+
readonly multiaddr: string;
|
|
1904
|
+
readonly pieceleaves: number;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* content addressing across libp2p and BitTorrent v2: the same bytes
|
|
1909
|
+
* are hashed into a sha2-256 multihash (base32 CIDv1 raw for libp2p
|
|
1910
|
+
* /ipfs/ addressing) and into the BitTorrent v2 merkle root built
|
|
1911
|
+
* over 16 KiB block leaves (the v2 spec fixed the leaf size), yielding
|
|
1912
|
+
* the magnet link and a libp2p multiaddr for the seed peer.
|
|
1913
|
+
*/
|
|
1914
|
+
export function p2pcontentaddress(payload: Uint8Array, peerid = 'engine-seed-peer'): p2partifact {
|
|
1915
|
+
const leaves: Buffer[] = [];
|
|
1916
|
+
for (let offset = 0; offset < payload.length; offset += bittorrentv2blockbytes) {
|
|
1917
|
+
leaves.push(
|
|
1918
|
+
createHash('sha256')
|
|
1919
|
+
.update(payload.subarray(offset, offset + bittorrentv2blockbytes))
|
|
1920
|
+
.digest(),
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
if (leaves.length === 0) {
|
|
1924
|
+
leaves.push(createHash('sha256').update(new Uint8Array(0)).digest());
|
|
1925
|
+
}
|
|
1926
|
+
const root = merkleroot(leaves);
|
|
1927
|
+
const direct = createHash('sha256').update(payload).digest();
|
|
1928
|
+
return {
|
|
1929
|
+
multihashhex: `1220${direct.toString('hex')}`,
|
|
1930
|
+
cidv1raw: `bafk${base32hex(direct).toLowerCase()}`,
|
|
1931
|
+
bittorrentv2roothex: root.toString('hex'),
|
|
1932
|
+
magnet: `magnet:?xt=urn:btih:${root.toString('hex')}&dn=saddle-artifact`,
|
|
1933
|
+
multiaddr: `/p2p/${peerid}/p2p-circuit`,
|
|
1934
|
+
pieceleaves: leaves.length,
|
|
1935
|
+
} satisfies p2partifact;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
/** merkle root over sha256 leaves, pairing layer by layer. */
|
|
1939
|
+
function merkleroot(leaves: readonly Buffer[]): Buffer {
|
|
1940
|
+
if (leaves.length === 1) return leaves[0] as Buffer;
|
|
1941
|
+
const layer: Buffer[] = [];
|
|
1942
|
+
for (let i = 0; i < leaves.length; i += 2) {
|
|
1943
|
+
const left = leaves[i] as Buffer;
|
|
1944
|
+
const right = (leaves[i + 1] ?? left) as Buffer;
|
|
1945
|
+
layer.push(
|
|
1946
|
+
createHash('sha256')
|
|
1947
|
+
.update(Buffer.concat([left, right]))
|
|
1948
|
+
.digest(),
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
return merkleroot(layer);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/** base32 (RFC 4648, no padding) over raw digest bytes. */
|
|
1955
|
+
function base32hex(bytes: Buffer): string {
|
|
1956
|
+
const alphabet = [...'abcdefghijklmnopqrstuvwxyz', ...'234567'].join('');
|
|
1957
|
+
let bits = 0;
|
|
1958
|
+
let value = 0;
|
|
1959
|
+
let output = '';
|
|
1960
|
+
for (const byte of bytes) {
|
|
1961
|
+
value = (value << 8) | byte;
|
|
1962
|
+
bits += 8;
|
|
1963
|
+
while (bits >= 5) {
|
|
1964
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
1965
|
+
bits -= 5;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (bits > 0) {
|
|
1969
|
+
output += alphabet[(value << (5 - bits)) & 31];
|
|
1970
|
+
}
|
|
1971
|
+
return output;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/* ------------------------------------------------------------------ */
|
|
1975
|
+
/* context: tailscaleplan (tailnet endpoint planner) */
|
|
1976
|
+
/* ------------------------------------------------------------------ */
|
|
1977
|
+
|
|
1978
|
+
/** one planned tailnet endpoint. */
|
|
1979
|
+
export interface tailnetendpoint {
|
|
1980
|
+
readonly hostname: string;
|
|
1981
|
+
readonly magicdns: string;
|
|
1982
|
+
readonly ip: string;
|
|
1983
|
+
readonly isCgnat: boolean;
|
|
1984
|
+
readonly derpFallback: string;
|
|
1985
|
+
readonly milestones: readonly string[];
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* plans tailnet endpoints for engine nodes: every node gets a
|
|
1990
|
+
* CGNAT-range address (100.64.0.0/10), a MagicDNS name, a DERP relay
|
|
1991
|
+
* fallback for the no-direct-path case and the six-mesh-milestone
|
|
1992
|
+
* checklist (subnet routes, acls, derp regions, magicdns, key expiry,
|
|
1993
|
+
* tailscale ssh). the planner rejects addresses outside the CGNAT
|
|
1994
|
+
* range so a tailnet identity can never leak onto public space.
|
|
1995
|
+
*/
|
|
1996
|
+
export class tailscaleendpointplanner {
|
|
1997
|
+
readonly #tailnet: string;
|
|
1998
|
+
readonly #derp: string;
|
|
1999
|
+
|
|
2000
|
+
constructor(tailnet = 'saddle.ts.net', derp = 'derp-fra-1') {
|
|
2001
|
+
this.#tailnet = tailnet;
|
|
2002
|
+
this.#derp = derp;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/** plans one endpoint from a CGNAT octet quad. */
|
|
2006
|
+
plan(hostname: string, ip: string): tailnetendpoint {
|
|
2007
|
+
if (!/^[a-z0-9-]+$/.test(hostname)) {
|
|
2008
|
+
throw new Error(`tailnet hostname ${hostname} must be lowercase`);
|
|
2009
|
+
}
|
|
2010
|
+
if (!this.iscgnat(ip)) {
|
|
2011
|
+
throw new Error(`ip ${ip} outside the 100.64.0.0/10 CGNAT range`);
|
|
2012
|
+
}
|
|
2013
|
+
return {
|
|
2014
|
+
hostname,
|
|
2015
|
+
magicdns: `${hostname}.${this.#tailnet}`,
|
|
2016
|
+
ip,
|
|
2017
|
+
isCgnat: true,
|
|
2018
|
+
derpFallback: this.#derp,
|
|
2019
|
+
milestones: tailscalemeshmilestones,
|
|
2020
|
+
} satisfies tailnetendpoint;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/** checks one ipv4 address against the CGNAT range. */
|
|
2024
|
+
iscgnat(ip: string): boolean {
|
|
2025
|
+
const parts = ip.split('.').map((part) => Number.parseInt(part, 10));
|
|
2026
|
+
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) {
|
|
2027
|
+
return false;
|
|
2028
|
+
}
|
|
2029
|
+
const value =
|
|
2030
|
+
((parts[0] as number) << 24) |
|
|
2031
|
+
((parts[1] as number) << 16) |
|
|
2032
|
+
((parts[2] as number) << 8) |
|
|
2033
|
+
(parts[3] as number);
|
|
2034
|
+
const start = (100 << 24) | (64 << 16);
|
|
2035
|
+
const end = (100 << 24) | (127 << 16) | 0xffff;
|
|
2036
|
+
return value >= start && value <= end;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/** the six tailscale mesh milestones of the portal sheet. */
|
|
2041
|
+
export const tailscalemeshmilestones: readonly string[] = [
|
|
2042
|
+
'subnet routes advertise',
|
|
2043
|
+
'acl grants',
|
|
2044
|
+
'derp region selection',
|
|
2045
|
+
'magicdns enablement',
|
|
2046
|
+
'key expiry policy',
|
|
2047
|
+
'tailscale ssh rollout',
|
|
2048
|
+
] as const;
|
|
2049
|
+
|
|
2050
|
+
/* ------------------------------------------------------------------ */
|
|
2051
|
+
/* context: max9variant (legacy build/delivery branding) */
|
|
2052
|
+
/* ------------------------------------------------------------------ */
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* the MAX9 delivery variant of the saddle v5 portal: a hardened
|
|
2056
|
+
* build flavor using zip compression level 9 with a measured 51%
|
|
2057
|
+
* archive ratio, the docker tag :max9, the max9 branch and the
|
|
2058
|
+
* omni-hypercore-v5-FINAL-...-MAX9.zip artifact. the name is NOT an
|
|
2059
|
+
* NVIDIA Maxwell generation despite the gpu-like sound; it is pure
|
|
2060
|
+
* build/delivery branding and is catalogued here as legacy provenance
|
|
2061
|
+
* of the delivery pipeline (374 contexts claim of the portal).
|
|
2062
|
+
*/
|
|
2063
|
+
export const max9buildvariant = {
|
|
2064
|
+
kind: 'build/delivery variant',
|
|
2065
|
+
compressionlevel: 9,
|
|
2066
|
+
measuredratio: '51%',
|
|
2067
|
+
dockertag: ':max9',
|
|
2068
|
+
branch: 'max9',
|
|
2069
|
+
artifact: 'omni-hypercore-v5-FINAL-...-MAX9.zip',
|
|
2070
|
+
note: 'not a GPU generation; legacy portal branding for the level-9 hardened zip',
|
|
2071
|
+
} as const satisfies Record<string, string | number>;
|
|
2072
|
+
|
|
2073
|
+
/* ------------------------------------------------------------------ */
|
|
2074
|
+
/* context: roadmapbacklog (v4 13-item ledger, previously unabsorbed) */
|
|
2075
|
+
/* ------------------------------------------------------------------ */
|
|
2076
|
+
|
|
2077
|
+
/** one roadmap backlog row. */
|
|
2078
|
+
export interface roadmapbacklogitem {
|
|
2079
|
+
readonly id: string;
|
|
2080
|
+
readonly title: string;
|
|
2081
|
+
readonly origin: 'future.ts v4';
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* the thirteen roadmap backlog items of the v4 future.ts ledger that
|
|
2086
|
+
* no v3 module had absorbed (grep-verified zero matches before this
|
|
2087
|
+
* merge): AMX tile real support, dual-X3D detection, WebTransport
|
|
2088
|
+
* datagram relay, Turborepo remote cache on GHCR, pnpm v9
|
|
2089
|
+
* content-addressable store, QUIC + HTTP/3 control plane, P2P WebRTC
|
|
2090
|
+
* mesh, OPFS browser sandboxes, WebCodecs + WebGPU encode,
|
|
2091
|
+
* SharedArrayBuffer cross-origin isolation, WASM JSPI scheduling,
|
|
2092
|
+
* GHCR gha layer reuse and Nix flakes hermetic builds.
|
|
2093
|
+
*/
|
|
2094
|
+
export const roadmapbacklog: readonly roadmapbacklogitem[] = [
|
|
2095
|
+
{ id: 'rb-01', title: 'AMX tile real support', origin: 'future.ts v4' },
|
|
2096
|
+
{ id: 'rb-02', title: 'dual-X3D 9950X3D2 CCD detection', origin: 'future.ts v4' },
|
|
2097
|
+
{ id: 'rb-03', title: 'WebTransport datagram relay', origin: 'future.ts v4' },
|
|
2098
|
+
{ id: 'rb-04', title: 'Turborepo remote cache on GHCR', origin: 'future.ts v4' },
|
|
2099
|
+
{ id: 'rb-05', title: 'pnpm v9 content-addressable store', origin: 'future.ts v4' },
|
|
2100
|
+
{ id: 'rb-06', title: 'QUIC + HTTP/3 control plane', origin: 'future.ts v4' },
|
|
2101
|
+
{ id: 'rb-07', title: 'P2P WebRTC mesh', origin: 'future.ts v4' },
|
|
2102
|
+
{ id: 'rb-08', title: 'OPFS browser sandboxes', origin: 'future.ts v4' },
|
|
2103
|
+
{ id: 'rb-09', title: 'WebCodecs + WebGPU encode', origin: 'future.ts v4' },
|
|
2104
|
+
{ id: 'rb-10', title: 'SharedArrayBuffer cross-origin isolation', origin: 'future.ts v4' },
|
|
2105
|
+
{ id: 'rb-11', title: 'WASM JSPI scheduling', origin: 'future.ts v4' },
|
|
2106
|
+
{ id: 'rb-12', title: 'GHCR gha layer cache reuse', origin: 'future.ts v4' },
|
|
2107
|
+
{ id: 'rb-13', title: 'Nix flakes hermetic builds', origin: 'future.ts v4' },
|
|
2108
|
+
] satisfies readonly roadmapbacklogitem[];
|
|
2109
|
+
|
|
2110
|
+
/** returns the roadmap backlog size (thirteen items). */
|
|
2111
|
+
export function backlogCount(): number {
|
|
2112
|
+
return roadmapbacklog.length;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
/* ------------------------------------------------------------------ */
|
|
2116
|
+
/* context: featureledger appendix (compute domain index) */
|
|
2117
|
+
/* ------------------------------------------------------------------ */
|
|
2118
|
+
|
|
2119
|
+
/** origin of one feature: the v2 future.ts ledger or the v3 additions. */
|
|
2120
|
+
export type computefeatureorigin = 'future.ts' | 'omnihypercore-v3' | 'portal-v5' | 'v4fix3';
|
|
2121
|
+
|
|
2122
|
+
/** one ledger row: id, origin, name and export name. */
|
|
2123
|
+
export interface computefeaturemeta {
|
|
2124
|
+
readonly id: string;
|
|
2125
|
+
readonly origin: computefeatureorigin;
|
|
2126
|
+
readonly name: string;
|
|
2127
|
+
readonly exportname: string;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
/**
|
|
2131
|
+
* the compute-domain ledger: twenty-five features absorbed from the
|
|
2132
|
+
* removed future.ts (wasm 001-008, webgpu 009-012, node26 013-019,
|
|
2133
|
+
* ai-workloads 045-050) plus five omnihypercore additions. the full
|
|
2134
|
+
* 55-feature redistribution map lives in the worklog (task v3-B1).
|
|
2135
|
+
*/
|
|
2136
|
+
export const computefeatureindex: readonly computefeaturemeta[] = [
|
|
2137
|
+
{ id: '001', origin: 'future.ts', name: 'WIT bindings generator', exportname: 'wittotypescript' },
|
|
2138
|
+
{
|
|
2139
|
+
id: '002',
|
|
2140
|
+
origin: 'future.ts',
|
|
2141
|
+
name: 'WASI 0.3 async runtime',
|
|
2142
|
+
exportname: 'wasiasyncruntime',
|
|
2143
|
+
},
|
|
2144
|
+
{ id: '003', origin: 'future.ts', name: 'fuel metering', exportname: 'fuelmeter' },
|
|
2145
|
+
{ id: '004', origin: 'future.ts', name: 'epoch interruption', exportname: 'epochinterruption' },
|
|
2146
|
+
{ id: '005', origin: 'future.ts', name: 'wasi:nn backend', exportname: 'wasinnbackend' },
|
|
2147
|
+
{
|
|
2148
|
+
id: '006',
|
|
2149
|
+
origin: 'future.ts',
|
|
2150
|
+
name: 'deny-by-default component ACL',
|
|
2151
|
+
exportname: 'componentacl',
|
|
2152
|
+
},
|
|
2153
|
+
{ id: '007', origin: 'future.ts', name: 'component registry', exportname: 'componentregistry' },
|
|
2154
|
+
{ id: '008', origin: 'future.ts', name: 'linear memory pool', exportname: 'wasmmempool' },
|
|
2155
|
+
{
|
|
2156
|
+
id: '009',
|
|
2157
|
+
origin: 'future.ts',
|
|
2158
|
+
name: 'forced fallback adapter',
|
|
2159
|
+
exportname: 'forcefallbackadapterplan',
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
id: '010',
|
|
2163
|
+
origin: 'future.ts',
|
|
2164
|
+
name: 'compute pipeline builder',
|
|
2165
|
+
exportname: 'computepipelinebuilder',
|
|
2166
|
+
},
|
|
2167
|
+
{ id: '011', origin: 'future.ts', name: 'WGSL shader cache', exportname: 'wgslshadercache' },
|
|
2168
|
+
{ id: '012', origin: 'future.ts', name: 'tensor dispatch planner', exportname: 'planmatmul2d' },
|
|
2169
|
+
{ id: '013', origin: 'future.ts', name: 'Temporal scheduler', exportname: 'temporalscheduler' },
|
|
2170
|
+
{ id: '014', origin: 'future.ts', name: 'Float16 tensor storage', exportname: 'float16tensor' },
|
|
2171
|
+
{ id: '015', origin: 'future.ts', name: 'Error.isError guard', exportname: 'iserrorvalue' },
|
|
2172
|
+
{ id: '016', origin: 'future.ts', name: 'node:sqlite sync store', exportname: 'sqlitesyncstore' },
|
|
2173
|
+
{
|
|
2174
|
+
id: '017',
|
|
2175
|
+
origin: 'future.ts',
|
|
2176
|
+
name: 'disposable module hooks',
|
|
2177
|
+
exportname: 'modulehookchain',
|
|
2178
|
+
},
|
|
2179
|
+
{ id: '018', origin: 'future.ts', name: 'Perfetto trace sink', exportname: 'perfettotracesink' },
|
|
2180
|
+
{
|
|
2181
|
+
id: '019',
|
|
2182
|
+
origin: 'future.ts',
|
|
2183
|
+
name: 'private key store loaders',
|
|
2184
|
+
exportname: 'privatekeystore',
|
|
2185
|
+
},
|
|
2186
|
+
{ id: '045', origin: 'future.ts', name: 'DataLoader shm planner', exportname: 'plandataloader' },
|
|
2187
|
+
{ id: '046', origin: 'future.ts', name: 'TF CUDA forward compat', exportname: 'checktfcuda' },
|
|
2188
|
+
{
|
|
2189
|
+
id: '047',
|
|
2190
|
+
origin: 'future.ts',
|
|
2191
|
+
name: 'ONNX WebGPU EP sessions',
|
|
2192
|
+
exportname: 'buildonnxwebgpusession',
|
|
2193
|
+
},
|
|
2194
|
+
{
|
|
2195
|
+
id: '048',
|
|
2196
|
+
origin: 'future.ts',
|
|
2197
|
+
name: 'LLM on lavapipe planner',
|
|
2198
|
+
exportname: 'planllminference',
|
|
2199
|
+
},
|
|
2200
|
+
{
|
|
2201
|
+
id: '049',
|
|
2202
|
+
origin: 'future.ts',
|
|
2203
|
+
name: 'headless Stable Diffusion',
|
|
2204
|
+
exportname: 'planstablediffusion',
|
|
2205
|
+
},
|
|
2206
|
+
{ id: '050', origin: 'future.ts', name: 'VRAM budget planner', exportname: 'vrambudgetplanner' },
|
|
2207
|
+
{
|
|
2208
|
+
id: 'n60',
|
|
2209
|
+
origin: 'omnihypercore-v3',
|
|
2210
|
+
name: 'wasm canary validator',
|
|
2211
|
+
exportname: 'wasmcanaryvalidator',
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
id: 'n61',
|
|
2215
|
+
origin: 'omnihypercore-v3',
|
|
2216
|
+
name: 'WASI-GFX over wgpu 30',
|
|
2217
|
+
exportname: 'planwasigfx',
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
id: 'n62',
|
|
2221
|
+
origin: 'omnihypercore-v3',
|
|
2222
|
+
name: 'ChatOps dashboard commands',
|
|
2223
|
+
exportname: 'chatopsdashboard',
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
id: 'n63',
|
|
2227
|
+
origin: 'omnihypercore-v3',
|
|
2228
|
+
name: 'GPU telemetry eBPF builder',
|
|
2229
|
+
exportname: 'ebpfgputelemetry',
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
id: 'n64',
|
|
2233
|
+
origin: 'omnihypercore-v3',
|
|
2234
|
+
name: 'AI pre-copy predictor',
|
|
2235
|
+
exportname: 'aimigrationpredictor',
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
id: 'n65',
|
|
2239
|
+
origin: 'portal-v5',
|
|
2240
|
+
name: 'PartyKit edge presence channels',
|
|
2241
|
+
exportname: 'partykitchannelbuilder',
|
|
2242
|
+
},
|
|
2243
|
+
{
|
|
2244
|
+
id: 'n66',
|
|
2245
|
+
origin: 'portal-v5',
|
|
2246
|
+
name: 'L3AF 2.1 xBPF marketplace manager',
|
|
2247
|
+
exportname: 'l3afappmanager',
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
id: 'n67',
|
|
2251
|
+
origin: 'portal-v5',
|
|
2252
|
+
name: 'GreenHyper RAPL power governor',
|
|
2253
|
+
exportname: 'greenhyperpowergovernor',
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
id: 'n68',
|
|
2257
|
+
origin: 'portal-v5',
|
|
2258
|
+
name: 'StateVector 2-round sync',
|
|
2259
|
+
exportname: 'statevectorsync',
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
id: 'n69',
|
|
2263
|
+
origin: 'portal-v5',
|
|
2264
|
+
name: 'libp2p + BitTorrent v2 content addressing',
|
|
2265
|
+
exportname: 'p2pcontentaddress',
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
id: 'n70',
|
|
2269
|
+
origin: 'portal-v5',
|
|
2270
|
+
name: 'Tailscale mesh endpoint planner',
|
|
2271
|
+
exportname: 'tailscaleendpointplanner',
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
id: 'n71',
|
|
2275
|
+
origin: 'portal-v5',
|
|
2276
|
+
name: 'MAX9 legacy delivery variant',
|
|
2277
|
+
exportname: 'max9buildvariant',
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
id: 'n72',
|
|
2281
|
+
origin: 'v4fix3',
|
|
2282
|
+
name: 'v4 roadmap backlog ledger (13 items)',
|
|
2283
|
+
exportname: 'roadmapbacklog',
|
|
2284
|
+
},
|
|
2285
|
+
] satisfies readonly computefeaturemeta[];
|
|
2286
|
+
|
|
2287
|
+
/** returns the ledger count split by origin. */
|
|
2288
|
+
export function computefeaturecount(): { total: number; absorbed: number; added: number } {
|
|
2289
|
+
const absorbed = computefeatureindex.filter((f) => f.origin === 'future.ts').length;
|
|
2290
|
+
return {
|
|
2291
|
+
total: computefeatureindex.length,
|
|
2292
|
+
absorbed,
|
|
2293
|
+
added: computefeatureindex.length - absorbed,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/** version anchors of the compute domain (worklog v3-VERIFY). */
|
|
2298
|
+
export const computerversions = {
|
|
2299
|
+
wasmtime: '48.0.0',
|
|
2300
|
+
wasmtimeSupport: 'LTS until 2028-08',
|
|
2301
|
+
wasi: '0.2.8 / 0.3.0',
|
|
2302
|
+
spin: '3.6.0',
|
|
2303
|
+
spinrepo: 'spinframework/spin',
|
|
2304
|
+
wasmcloud: 'latest 2.x',
|
|
2305
|
+
wasmcloudNote: 'release page did not expose the version; left unpinned on purpose',
|
|
2306
|
+
wgpu: '30.x',
|
|
2307
|
+
kernelstable: '7.1.9',
|
|
2308
|
+
kernellongterm: '6.18.45',
|
|
2309
|
+
} as const satisfies Record<string, string>;
|
|
2310
|
+
|
|
2311
|
+
/* ------------------------------------------------------------------ */
|
|
2312
|
+
/* context: v5-C feature audit builders (ledger F-016, F-061, F-062, */
|
|
2313
|
+
/* F-065 plus backlog rb-03/rb-08/rb-09 of docs/viability.md) */
|
|
2314
|
+
/* ------------------------------------------------------------------ */
|
|
2315
|
+
|
|
2316
|
+
/** one tile partition handed to a guest queue. */
|
|
2317
|
+
export interface nputilepartition {
|
|
2318
|
+
readonly guest: string;
|
|
2319
|
+
readonly tiles: number;
|
|
2320
|
+
readonly opspersec: number;
|
|
2321
|
+
readonly kind: 'amxtmul' | 'xdnaaie';
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/** the planned NPU surface: tiles, partitions, flags and loadout math. */
|
|
2325
|
+
export interface nputileplan {
|
|
2326
|
+
readonly vendor: 'intel' | 'amd';
|
|
2327
|
+
readonly totaltiles: number;
|
|
2328
|
+
readonly partitions: readonly nputilepartition[];
|
|
2329
|
+
readonly qemuflags: readonly string[];
|
|
2330
|
+
readonly xdnaenv: readonly string[];
|
|
2331
|
+
readonly utilization: number;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
/**
|
|
2335
|
+
* plans Intel AMX and AMD XDNA NPU tile partitioning (ledger F-016):
|
|
2336
|
+
* Intel Sapphire Rapids exposes 8 TMUL tiles (each computing one 16x64
|
|
2337
|
+
* by 64x16 int8/bf16 product per instruction via +amx-tile,+amx-int8,
|
|
2338
|
+
* +amx-bf16 cpu flags), AMD Strix Halo exposes 32 XDNA2 AI-engine tiles
|
|
2339
|
+
* routed through the xdna driver environment. the planner validates
|
|
2340
|
+
* that guest partitions never double-book a tile, computes honest peak
|
|
2341
|
+
* ops per partition and emits the QEMU cpu flags (Intel) or the xdna
|
|
2342
|
+
* device environment (AMD) the guest needs to see the accelerator.
|
|
2343
|
+
*/
|
|
2344
|
+
export function plannputiles(opts: {
|
|
2345
|
+
vendor: 'intel' | 'amd';
|
|
2346
|
+
guests: readonly { guest: string; tiles: number; share?: number }[];
|
|
2347
|
+
}): nputileplan {
|
|
2348
|
+
try {
|
|
2349
|
+
const totaltiles = opts.vendor === 'intel' ? 8 : 32;
|
|
2350
|
+
const kind: 'amxtmul' | 'xdnaaie' = opts.vendor === 'intel' ? 'amxtmul' : 'xdnaaie';
|
|
2351
|
+
if (opts.guests.length === 0) throw new Error('at least one guest partition is required');
|
|
2352
|
+
const tops = opts.vendor === 'intel' ? 4_200 : 50;
|
|
2353
|
+
let used = 0;
|
|
2354
|
+
const partitions: nputilepartition[] = opts.guests.map((entry) => {
|
|
2355
|
+
if (!/^[a-z][a-z0-9-]*$/.test(entry.guest)) {
|
|
2356
|
+
throw new Error(`guest id ${entry.guest} must be lowercase alnum/dash`);
|
|
2357
|
+
}
|
|
2358
|
+
if (!Number.isInteger(entry.tiles) || entry.tiles < 1 || entry.tiles > totaltiles) {
|
|
2359
|
+
throw new Error(`guest ${entry.guest} tiles must be 1..${totaltiles}`);
|
|
2360
|
+
}
|
|
2361
|
+
used += entry.tiles;
|
|
2362
|
+
if (used > totaltiles) {
|
|
2363
|
+
throw new Error(`partition for ${entry.guest} overcommits the ${totaltiles} tiles`);
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
guest: entry.guest,
|
|
2367
|
+
tiles: entry.tiles,
|
|
2368
|
+
opspersec: Math.round((tops * entry.tiles) / totaltiles),
|
|
2369
|
+
kind,
|
|
2370
|
+
};
|
|
2371
|
+
});
|
|
2372
|
+
return {
|
|
2373
|
+
vendor: opts.vendor,
|
|
2374
|
+
totaltiles,
|
|
2375
|
+
partitions,
|
|
2376
|
+
qemuflags:
|
|
2377
|
+
opts.vendor === 'intel'
|
|
2378
|
+
? ['-cpu host,+amx-tile,+amx-int8,+amx-bf16,+amx-fp16']
|
|
2379
|
+
: ['-cpu host,+sve,+sve2,+i8mm'],
|
|
2380
|
+
xdnaenv:
|
|
2381
|
+
opts.vendor === 'amd'
|
|
2382
|
+
? ['XDNA_BIND_MODE=1', `XDNA_NUM_VAS=${partitions.length}`, 'XDNA_ENABLE_DEBUG=0']
|
|
2383
|
+
: [],
|
|
2384
|
+
utilization: Number((used / totaltiles).toFixed(3)),
|
|
2385
|
+
};
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
throw new Error(
|
|
2388
|
+
`plannputiles failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/** one node of the planned Ray + vLLM inference cluster. */
|
|
2394
|
+
export interface rayvllmnode {
|
|
2395
|
+
readonly role: 'head' | 'worker';
|
|
2396
|
+
readonly gpus: number;
|
|
2397
|
+
readonly cpus: number;
|
|
2398
|
+
readonly memorygb: number;
|
|
2399
|
+
readonly rayargv: readonly string[];
|
|
2400
|
+
readonly vllmargv: readonly string[];
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/** the distributed inference plan across head and workers. */
|
|
2404
|
+
export interface rayvllmplan {
|
|
2405
|
+
readonly model: string;
|
|
2406
|
+
readonly tensorparallel: number;
|
|
2407
|
+
readonly pipelineparallel: number;
|
|
2408
|
+
readonly totalgpus: number;
|
|
2409
|
+
readonly nodes: readonly rayvllmnode[];
|
|
2410
|
+
readonly headaddress: string;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* plans a Ray + vLLM distributed inference cluster (ledger F-061): ray
|
|
2415
|
+
* start argv for head (with the dashboard port and object-store size)
|
|
2416
|
+
* and workers (joining via the head address), vllm serve argv with
|
|
2417
|
+
* tensor/pipeline parallelism derived from the per-node GPU count, fp8
|
|
2418
|
+
* kv-cache, chunked prefill and gpu-memory-utilization budgeting. the
|
|
2419
|
+
* planner validates that tensor parallelism divides every worker GPU
|
|
2420
|
+
* count and that the cluster-wide GPU total matches the shard geometry.
|
|
2421
|
+
*/
|
|
2422
|
+
export function planrayvllmcluster(opts: {
|
|
2423
|
+
model: string;
|
|
2424
|
+
nodes: readonly { role: 'head' | 'worker'; gpus: number; cpus: number; memorygb: number }[];
|
|
2425
|
+
tensorparallel?: number;
|
|
2426
|
+
pipelineparallel?: number;
|
|
2427
|
+
}): rayvllmplan {
|
|
2428
|
+
try {
|
|
2429
|
+
if (opts.nodes.length === 0) throw new Error('at least one node is required');
|
|
2430
|
+
if (opts.nodes.filter((n) => n.role === 'head').length !== 1) {
|
|
2431
|
+
throw new Error('exactly one head node is required');
|
|
2432
|
+
}
|
|
2433
|
+
for (const node of opts.nodes) {
|
|
2434
|
+
if (!Number.isInteger(node.gpus) || node.gpus < 0 || node.gpus > 8) {
|
|
2435
|
+
throw new Error('node gpus must be 0..8');
|
|
2436
|
+
}
|
|
2437
|
+
if (!Number.isInteger(node.cpus) || node.cpus < 1 || node.cpus > 256) {
|
|
2438
|
+
throw new Error('node cpus must be 1..256');
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
const totalgpus = opts.nodes.reduce((acc, node) => acc + node.gpus, 0);
|
|
2442
|
+
if (totalgpus < 1) throw new Error('the cluster needs at least one GPU');
|
|
2443
|
+
const tensorparallel = opts.tensorparallel ?? Math.min(2, opts.nodes[0].gpus || 2);
|
|
2444
|
+
const pipelineparallel = opts.pipelineparallel ?? 1;
|
|
2445
|
+
if (tensorparallel < 1 || tensorparallel > 8) {
|
|
2446
|
+
throw new Error('tensor parallel size must be 1..8');
|
|
2447
|
+
}
|
|
2448
|
+
for (const node of opts.nodes.filter((n) => n.gpus > 0)) {
|
|
2449
|
+
if (node.gpus % tensorparallel !== 0) {
|
|
2450
|
+
throw new Error(
|
|
2451
|
+
`${node.role} with ${node.gpus} gpus is not divisible by tp ${tensorparallel}`,
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if (tensorparallel * pipelineparallel > totalgpus) {
|
|
2456
|
+
throw new Error(`tp x pp (${tensorparallel}x${pipelineparallel}) exceeds ${totalgpus} gpus`);
|
|
2457
|
+
}
|
|
2458
|
+
const headaddress = 'ray://head:10001';
|
|
2459
|
+
const nodes: rayvllmnode[] = opts.nodes.map((node) => ({
|
|
2460
|
+
role: node.role,
|
|
2461
|
+
gpus: node.gpus,
|
|
2462
|
+
cpus: node.cpus,
|
|
2463
|
+
memorygb: node.memorygb,
|
|
2464
|
+
rayargv:
|
|
2465
|
+
node.role === 'head'
|
|
2466
|
+
? [
|
|
2467
|
+
'ray start',
|
|
2468
|
+
'--head',
|
|
2469
|
+
'--port=6379',
|
|
2470
|
+
'--dashboard-host=0.0.0.0',
|
|
2471
|
+
`--object-store-memory=${Math.floor(node.memorygb * 1024 * 1024 * 1024 * 0.3)}`,
|
|
2472
|
+
`--num-cpus=${node.cpus}`,
|
|
2473
|
+
`--num-gpus=${node.gpus}`,
|
|
2474
|
+
'--block',
|
|
2475
|
+
]
|
|
2476
|
+
: [
|
|
2477
|
+
'ray start',
|
|
2478
|
+
`--address=${headaddress}`,
|
|
2479
|
+
`--num-cpus=${node.cpus}`,
|
|
2480
|
+
`--num-gpus=${node.gpus}`,
|
|
2481
|
+
'--block',
|
|
2482
|
+
],
|
|
2483
|
+
vllmargv:
|
|
2484
|
+
node.role === 'head'
|
|
2485
|
+
? [
|
|
2486
|
+
'vllm serve',
|
|
2487
|
+
opts.model,
|
|
2488
|
+
`--tensor-parallel-size=${tensorparallel}`,
|
|
2489
|
+
`--pipeline-parallel-size=${pipelineparallel}`,
|
|
2490
|
+
'--distributed-executor-backend=ray',
|
|
2491
|
+
'--gpu-memory-utilization=0.90',
|
|
2492
|
+
'--kv-cache-dtype=fp8',
|
|
2493
|
+
'--max-model-len=32768',
|
|
2494
|
+
'--enable-chunked-prefill',
|
|
2495
|
+
'--served-model-name=saddle-llm',
|
|
2496
|
+
]
|
|
2497
|
+
: [],
|
|
2498
|
+
}));
|
|
2499
|
+
return {
|
|
2500
|
+
model: opts.model,
|
|
2501
|
+
tensorparallel,
|
|
2502
|
+
pipelineparallel,
|
|
2503
|
+
totalgpus,
|
|
2504
|
+
nodes,
|
|
2505
|
+
headaddress,
|
|
2506
|
+
};
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
throw new Error(
|
|
2509
|
+
`planrayvllmcluster failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/** the generated VirtualMachine CRD plus one sample custom resource. */
|
|
2515
|
+
export interface crdmanifestplan {
|
|
2516
|
+
readonly crd: string;
|
|
2517
|
+
readonly sample: string;
|
|
2518
|
+
readonly group: string;
|
|
2519
|
+
readonly version: string;
|
|
2520
|
+
readonly kind: string;
|
|
2521
|
+
readonly shortnames: readonly string[];
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* builds the Kubernetes CRD manifest for SADDLE VirtualMachine custom
|
|
2526
|
+
* resources (ledger F-062): a complete apiextensions.k8s.io/v1
|
|
2527
|
+
* CustomResourceDefinition yaml string — group, names with short
|
|
2528
|
+
* aliases, scope, one served/storage version, printer columns and an
|
|
2529
|
+
* OpenAPI v3 schema for the vm spec (cpus, memorymb, gpu profile,
|
|
2530
|
+
* runtime, migration mode) — plus a ready-to-apply sample custom
|
|
2531
|
+
* resource. the string is generated, not fetched, so the engine stays
|
|
2532
|
+
* dependency-free; `kubectl apply -f` is the only external step.
|
|
2533
|
+
*/
|
|
2534
|
+
export function buildvmcrdmanifest(opts?: {
|
|
2535
|
+
group?: string;
|
|
2536
|
+
version?: string;
|
|
2537
|
+
kind?: string;
|
|
2538
|
+
}): crdmanifestplan {
|
|
2539
|
+
try {
|
|
2540
|
+
const group = opts?.group ?? 'saddle.dev';
|
|
2541
|
+
const version = opts?.version ?? 'v1';
|
|
2542
|
+
const kind = opts?.kind ?? 'VirtualMachine';
|
|
2543
|
+
if (!/^[a-z0-9.-]+$/.test(group)) throw new Error(`group ${group} is not a valid dns group`);
|
|
2544
|
+
if (!/^v[0-9]+(alpha[0-9]+|beta[0-9]+)?$/.test(version)) {
|
|
2545
|
+
throw new Error(`version ${version} must look like v1, v2beta1`);
|
|
2546
|
+
}
|
|
2547
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(kind)) {
|
|
2548
|
+
throw new Error(`kind ${kind} must be upper camel case`);
|
|
2549
|
+
}
|
|
2550
|
+
const plural = `${kind.toLowerCase()}s`;
|
|
2551
|
+
const crd = [
|
|
2552
|
+
'apiVersion: apiextensions.k8s.io/v1',
|
|
2553
|
+
'kind: CustomResourceDefinition',
|
|
2554
|
+
'metadata:',
|
|
2555
|
+
` name: ${plural}.${group}`,
|
|
2556
|
+
'spec:',
|
|
2557
|
+
' group: ' + group,
|
|
2558
|
+
' scope: Namespaced',
|
|
2559
|
+
' names:',
|
|
2560
|
+
` plural: ${plural}`,
|
|
2561
|
+
` singular: ${kind.toLowerCase()}`,
|
|
2562
|
+
` kind: ${kind}`,
|
|
2563
|
+
' shortNames: [vm, evm]',
|
|
2564
|
+
' versions:',
|
|
2565
|
+
' - name: ' + version,
|
|
2566
|
+
' served: true',
|
|
2567
|
+
' storage: true',
|
|
2568
|
+
' subresources:',
|
|
2569
|
+
' status: {}',
|
|
2570
|
+
' additionalPrinterColumns:',
|
|
2571
|
+
' - name: CPUS',
|
|
2572
|
+
' type: integer',
|
|
2573
|
+
' jsonPath: .spec.cpus',
|
|
2574
|
+
' - name: MemoryMB',
|
|
2575
|
+
' type: integer',
|
|
2576
|
+
' jsonPath: .spec.memorymb',
|
|
2577
|
+
' - name: GPU',
|
|
2578
|
+
' type: string',
|
|
2579
|
+
' jsonPath: .spec.gpuprofile',
|
|
2580
|
+
' - name: Runtime',
|
|
2581
|
+
' type: string',
|
|
2582
|
+
' jsonPath: .spec.runtime',
|
|
2583
|
+
' - name: Age',
|
|
2584
|
+
' type: date',
|
|
2585
|
+
' jsonPath: .metadata.creationTimestamp',
|
|
2586
|
+
' schema:',
|
|
2587
|
+
' openAPIV3Schema:',
|
|
2588
|
+
' type: object',
|
|
2589
|
+
' properties:',
|
|
2590
|
+
' spec:',
|
|
2591
|
+
' type: object',
|
|
2592
|
+
' required: [cpus, memorymb, runtime]',
|
|
2593
|
+
' properties:',
|
|
2594
|
+
' cpus:',
|
|
2595
|
+
' type: integer',
|
|
2596
|
+
' minimum: 1',
|
|
2597
|
+
' maximum: 4096',
|
|
2598
|
+
' memorymb:',
|
|
2599
|
+
' type: integer',
|
|
2600
|
+
' minimum: 128',
|
|
2601
|
+
' maximum: 1048576',
|
|
2602
|
+
' gpuprofile:',
|
|
2603
|
+
' type: string',
|
|
2604
|
+
' enum: [none, rtx5090, rtxpro6000, mi350x, b200]',
|
|
2605
|
+
' runtime:',
|
|
2606
|
+
' type: string',
|
|
2607
|
+
' enum: [qemu, firecracker, cloudhypervisor, gvisor]',
|
|
2608
|
+
' migrationmode:',
|
|
2609
|
+
' type: string',
|
|
2610
|
+
' enum: [cold, warm, live, postcopy]',
|
|
2611
|
+
' status:',
|
|
2612
|
+
' type: object',
|
|
2613
|
+
' properties:',
|
|
2614
|
+
' phase:',
|
|
2615
|
+
' type: string',
|
|
2616
|
+
' vramusedmb:',
|
|
2617
|
+
' type: integer',
|
|
2618
|
+
'',
|
|
2619
|
+
].join('\n');
|
|
2620
|
+
const sample = [
|
|
2621
|
+
'apiVersion: ' + group + '/' + version,
|
|
2622
|
+
'kind: ' + kind,
|
|
2623
|
+
'metadata:',
|
|
2624
|
+
' name: demo-vm',
|
|
2625
|
+
' namespace: saddle',
|
|
2626
|
+
'spec:',
|
|
2627
|
+
' cpus: 16',
|
|
2628
|
+
' memorymb: 32768',
|
|
2629
|
+
' gpuprofile: rtxpro6000',
|
|
2630
|
+
' runtime: qemu',
|
|
2631
|
+
' migrationmode: live',
|
|
2632
|
+
'',
|
|
2633
|
+
].join('\n');
|
|
2634
|
+
return { crd, sample, group, version, kind, shortnames: ['vm', 'evm'] };
|
|
2635
|
+
} catch (error) {
|
|
2636
|
+
throw new Error(
|
|
2637
|
+
`buildvmcrdmanifest failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
/** one cell of the federated hierarchy with its quota vector. */
|
|
2643
|
+
export interface federatedcell {
|
|
2644
|
+
readonly name: string;
|
|
2645
|
+
readonly nodes: number;
|
|
2646
|
+
readonly vcpu: number;
|
|
2647
|
+
readonly vramgb: number;
|
|
2648
|
+
readonly gpus: number;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
/** the federated quota plan for up to 10k nodes. */
|
|
2652
|
+
export interface federatedquotaplan {
|
|
2653
|
+
readonly clusters: readonly federatedcell[];
|
|
2654
|
+
readonly cells: readonly federatedcell[];
|
|
2655
|
+
readonly totalnodes: number;
|
|
2656
|
+
readonly totals: { vcpu: number; vramgb: number; gpus: number };
|
|
2657
|
+
readonly headroom: { vcpu: number; vramgb: number; gpus: number };
|
|
2658
|
+
readonly placementhints: readonly string[];
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
/**
|
|
2662
|
+
* plans federated cluster quotas for global scheduling at 10k-node
|
|
2663
|
+
* scale (ledger F-065): the fleet folds into two levels — clusters
|
|
2664
|
+
* (about 250 nodes each, matching the etcd object budget) and cells
|
|
2665
|
+
* (about 2500 nodes each, one scheduling domain) — the planner
|
|
2666
|
+
* validates node counts, sums the vcpu/vram/gpu vectors, reserves a 10%
|
|
2667
|
+
* headroom per resource for drains and migrations, and emits placement
|
|
2668
|
+
* hints so the global scheduler prefers the cell with the highest
|
|
2669
|
+
* headroom ratio instead of a raw bin-pack.
|
|
2670
|
+
*/
|
|
2671
|
+
export function planfederatedquota(
|
|
2672
|
+
clusters: readonly { name: string; nodes: number; vcpusPerNode: number; vramgbPerNode: number }[],
|
|
2673
|
+
gpusPerCell = 8,
|
|
2674
|
+
): federatedquotaplan {
|
|
2675
|
+
try {
|
|
2676
|
+
if (clusters.length === 0) throw new Error('at least one cluster is required');
|
|
2677
|
+
if (clusters.length > 64) throw new Error('at most 64 clusters per federation');
|
|
2678
|
+
const seen = new Set<string>();
|
|
2679
|
+
let totalnodes = 0;
|
|
2680
|
+
const planned: federatedcell[] = clusters.map((cluster) => {
|
|
2681
|
+
if (!/^[a-z][a-z0-9-]*$/.test(cluster.name)) {
|
|
2682
|
+
throw new Error(`cluster ${cluster.name} must be lowercase alnum/dash`);
|
|
2683
|
+
}
|
|
2684
|
+
if (seen.has(cluster.name)) throw new Error(`duplicate cluster ${cluster.name}`);
|
|
2685
|
+
seen.add(cluster.name);
|
|
2686
|
+
if (!Number.isInteger(cluster.nodes) || cluster.nodes < 1 || cluster.nodes > 250) {
|
|
2687
|
+
throw new Error(`cluster ${cluster.name} nodes must be 1..250`);
|
|
2688
|
+
}
|
|
2689
|
+
if (cluster.vcpusPerNode < 1 || cluster.vcpusPerNode > 4096) {
|
|
2690
|
+
throw new Error(`cluster ${cluster.name} vcpus/node must be 1..4096`);
|
|
2691
|
+
}
|
|
2692
|
+
totalnodes += cluster.nodes;
|
|
2693
|
+
const vcpu = cluster.nodes * cluster.vcpusPerNode;
|
|
2694
|
+
const vramgb = cluster.nodes * cluster.vramgbPerNode;
|
|
2695
|
+
return {
|
|
2696
|
+
name: cluster.name,
|
|
2697
|
+
nodes: cluster.nodes,
|
|
2698
|
+
vcpu,
|
|
2699
|
+
vramgb,
|
|
2700
|
+
gpus: cluster.nodes * gpusPerCell,
|
|
2701
|
+
};
|
|
2702
|
+
});
|
|
2703
|
+
if (totalnodes > 10_000) {
|
|
2704
|
+
throw new Error(`federation budget is 10000 nodes, got ${totalnodes}`);
|
|
2705
|
+
}
|
|
2706
|
+
const cellmap = new Map<string, federatedcell>();
|
|
2707
|
+
for (let index = 0; index < planned.length; index += 8) {
|
|
2708
|
+
const slice = planned.slice(index, index + 8);
|
|
2709
|
+
const name = `cell-${Math.floor(index / 8) + 1}`;
|
|
2710
|
+
cellmap.set(name, {
|
|
2711
|
+
name,
|
|
2712
|
+
nodes: slice.reduce((acc, cluster) => acc + cluster.nodes, 0),
|
|
2713
|
+
vcpu: slice.reduce((acc, cluster) => acc + cluster.vcpu, 0),
|
|
2714
|
+
vramgb: slice.reduce((acc, cluster) => acc + cluster.vramgb, 0),
|
|
2715
|
+
gpus: slice.reduce((acc, cluster) => acc + cluster.gpus, 0),
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
const totals = {
|
|
2719
|
+
vcpu: planned.reduce((acc, cluster) => acc + cluster.vcpu, 0),
|
|
2720
|
+
vramgb: planned.reduce((acc, cluster) => acc + cluster.vramgb, 0),
|
|
2721
|
+
gpus: planned.reduce((acc, cluster) => acc + cluster.gpus, 0),
|
|
2722
|
+
};
|
|
2723
|
+
const headroom = {
|
|
2724
|
+
vcpu: Math.floor(totals.vcpu * 0.1),
|
|
2725
|
+
vramgb: Math.floor(totals.vramgb * 0.1),
|
|
2726
|
+
gpus: Math.floor(totals.gpus * 0.1),
|
|
2727
|
+
};
|
|
2728
|
+
const cells = [...cellmap.values()];
|
|
2729
|
+
const placementhints = cells
|
|
2730
|
+
.map((cell) => ({
|
|
2731
|
+
cell: cell.name,
|
|
2732
|
+
ratio: Number(((totals.vcpu - cell.vcpu) / totals.vcpu).toFixed(3)),
|
|
2733
|
+
}))
|
|
2734
|
+
.sort((a, b) => b.ratio - a.ratio)
|
|
2735
|
+
.map((entry) => `prefer ${entry.cell} (headroom ratio ${entry.ratio})`);
|
|
2736
|
+
return { clusters: planned, cells, totalnodes, totals, headroom, placementhints };
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
throw new Error(
|
|
2739
|
+
`planfederatedquota failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
/** the planned browser-facing WebTransport passage endpoint. */
|
|
2745
|
+
export interface webtransportplan {
|
|
2746
|
+
readonly url: string;
|
|
2747
|
+
readonly port: number;
|
|
2748
|
+
readonly datagramflow: 'unreliable' | 'partial-reliable';
|
|
2749
|
+
readonly quicparams: readonly string[];
|
|
2750
|
+
readonly headers: readonly string[];
|
|
2751
|
+
readonly browserapi: readonly string[];
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
/**
|
|
2755
|
+
* plans the WebTransport datagram relay endpoint (backlog rb-03): the
|
|
2756
|
+
* browser-facing twin of the QUIC passage gateway. the planner emits the
|
|
2757
|
+
* wt:// (with https fallback) URL, the QUIC transport parameters a
|
|
2758
|
+
* browser requires for datagrams (max_datagram_frame_size, the
|
|
2759
|
+
* datagram receive queue window), the connection headers (including the
|
|
2760
|
+
* COOP/COEP pair cross-origin isolation needs) and the exact
|
|
2761
|
+
* WebTransportBrowser API calls the sandbox tier uses to open a
|
|
2762
|
+
* bidirectional stream plus an unreliable datagram lane.
|
|
2763
|
+
*/
|
|
2764
|
+
export function planwebtransportendpoint(opts?: {
|
|
2765
|
+
host?: string;
|
|
2766
|
+
port?: number;
|
|
2767
|
+
flow?: 'unreliable' | 'partial-reliable';
|
|
2768
|
+
}): webtransportplan {
|
|
2769
|
+
try {
|
|
2770
|
+
const port = opts?.port ?? 443;
|
|
2771
|
+
const host = opts?.host ?? 'passage.saddle.dev';
|
|
2772
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2773
|
+
throw new Error('port must be 1..65535');
|
|
2774
|
+
}
|
|
2775
|
+
if (!/^[a-z0-9.-]+$/.test(host)) throw new Error(`host ${host} must be a dns name`);
|
|
2776
|
+
const flow = opts?.flow ?? 'unreliable';
|
|
2777
|
+
return {
|
|
2778
|
+
url: `https://${host}:${port}/passage`,
|
|
2779
|
+
port,
|
|
2780
|
+
datagramflow: flow,
|
|
2781
|
+
quicparams: [
|
|
2782
|
+
'max_datagram_frame_size=1200',
|
|
2783
|
+
'max_datagram_receive_queue_window=65536',
|
|
2784
|
+
'max_idle_timeout=30000',
|
|
2785
|
+
'enable_loss_based_bdp=1',
|
|
2786
|
+
...(flow === 'partial-reliable' ? ['enable_datagram_acks=1'] : []),
|
|
2787
|
+
],
|
|
2788
|
+
headers: [
|
|
2789
|
+
'Alt-Svc: h3=":443"; ma=86400',
|
|
2790
|
+
'Cross-Origin-Opener-Policy: same-origin',
|
|
2791
|
+
'Cross-Origin-Embedder-Policy: require-corp',
|
|
2792
|
+
],
|
|
2793
|
+
browserapi: [
|
|
2794
|
+
`new WebTransport('${`https://${host}:${port}/passage`}')`,
|
|
2795
|
+
'await session.ready',
|
|
2796
|
+
'const writer = session.datagrams.writable.getWriter()',
|
|
2797
|
+
'const stream = await session.createBidirectionalStream()',
|
|
2798
|
+
],
|
|
2799
|
+
};
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
throw new Error(
|
|
2802
|
+
`planwebtransportendpoint failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
/** one guest disk image kept inside the browser OPFS bucket. */
|
|
2808
|
+
export interface opfsbucket {
|
|
2809
|
+
readonly guest: string;
|
|
2810
|
+
readonly sizemb: number;
|
|
2811
|
+
readonly handle: string;
|
|
2812
|
+
readonly seed: 'empty' | 'alpine' | 'custom';
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
/** the OPFS-backed browser sandbox storage plan. */
|
|
2816
|
+
export interface opfsstorageplan {
|
|
2817
|
+
readonly root: string;
|
|
2818
|
+
readonly buckets: readonly opfsbucket[];
|
|
2819
|
+
readonly totalsizemb: number;
|
|
2820
|
+
readonly quotamb: number;
|
|
2821
|
+
readonly fits: boolean;
|
|
2822
|
+
readonly browserapi: readonly string[];
|
|
2823
|
+
readonly eviction: readonly string[];
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
/**
|
|
2827
|
+
* plans Origin Private File System guest disks (backlog rb-08): each
|
|
2828
|
+
* sandbox gets one OPFS bucket holding its sparse disk image plus a
|
|
2829
|
+
* metadata sidecar; the planner validates sizes against the browser
|
|
2830
|
+
* storage quota (navigator.storage.estimate), lays out the directory
|
|
2831
|
+
* tree, and defines the least-recently-booted eviction order used when a
|
|
2832
|
+
* new bucket would overflow the quota. the browserapi lines are the
|
|
2833
|
+
* exact calls the sandbox tier performs (getDirectory, createSyncAccess
|
|
2834
|
+
* handle, persistence request).
|
|
2835
|
+
*/
|
|
2836
|
+
export function planopfsstorage(
|
|
2837
|
+
guests: readonly { guest: string; sizemb: number; seed?: 'empty' | 'alpine' | 'custom' }[],
|
|
2838
|
+
quotamb = 8192,
|
|
2839
|
+
): opfsstorageplan {
|
|
2840
|
+
try {
|
|
2841
|
+
if (guests.length === 0) throw new Error('at least one guest bucket is required');
|
|
2842
|
+
if (!Number.isInteger(quotamb) || quotamb < 64 || quotamb > 1_048_576) {
|
|
2843
|
+
throw new Error('quota must be 64..1048576 MB');
|
|
2844
|
+
}
|
|
2845
|
+
let total = 0;
|
|
2846
|
+
const buckets: opfsbucket[] = guests.map((guest) => {
|
|
2847
|
+
if (!/^[a-z][a-z0-9-]*$/.test(guest.guest)) {
|
|
2848
|
+
throw new Error(`guest ${guest.guest} must be lowercase alnum/dash`);
|
|
2849
|
+
}
|
|
2850
|
+
if (!Number.isInteger(guest.sizemb) || guest.sizemb < 16 || guest.sizemb > 65536) {
|
|
2851
|
+
throw new Error(`guest ${guest.guest} disk must be 16..65536 MB`);
|
|
2852
|
+
}
|
|
2853
|
+
total += guest.sizemb;
|
|
2854
|
+
return {
|
|
2855
|
+
guest: guest.guest,
|
|
2856
|
+
sizemb: guest.sizemb,
|
|
2857
|
+
handle: `/guests/${guest.guest}/disk.img`,
|
|
2858
|
+
seed: guest.seed ?? 'alpine',
|
|
2859
|
+
};
|
|
2860
|
+
});
|
|
2861
|
+
return {
|
|
2862
|
+
root: '/guests',
|
|
2863
|
+
buckets,
|
|
2864
|
+
totalsizemb: total,
|
|
2865
|
+
quotamb,
|
|
2866
|
+
fits: total <= quotamb,
|
|
2867
|
+
browserapi: [
|
|
2868
|
+
'const root = await navigator.storage.getDirectory()',
|
|
2869
|
+
'const dir = await root.getDirectoryHandle("guests", { create: true })',
|
|
2870
|
+
'const handle = await dir.getFileHandle("disk.img", { create: true })',
|
|
2871
|
+
'const access = await handle.createSyncAccessHandle()',
|
|
2872
|
+
'await navigator.storage.persist()',
|
|
2873
|
+
],
|
|
2874
|
+
eviction: [
|
|
2875
|
+
'order guests by last-boot timestamp ascending',
|
|
2876
|
+
'remove the oldest bucket until estimate().usage fits the quota',
|
|
2877
|
+
],
|
|
2878
|
+
};
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
throw new Error(
|
|
2881
|
+
`planopfsstorage failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
/** one encode stage of the planned browser pipeline. */
|
|
2887
|
+
export interface webcodecsstage {
|
|
2888
|
+
readonly name: 'capture' | 'encode' | 'decode' | 'render';
|
|
2889
|
+
readonly api: string;
|
|
2890
|
+
readonly config: readonly string[];
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/** the WebCodecs + WebGPU hardware-free encode pipeline plan. */
|
|
2894
|
+
export interface webcodecspipelineplan {
|
|
2895
|
+
readonly codec: string;
|
|
2896
|
+
readonly width: number;
|
|
2897
|
+
readonly height: number;
|
|
2898
|
+
readonly fps: number;
|
|
2899
|
+
readonly bitratembps: number;
|
|
2900
|
+
readonly gpucanvas: boolean;
|
|
2901
|
+
readonly stages: readonly webcodecsstage[];
|
|
2902
|
+
readonly videoencoderinit: string;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
/**
|
|
2906
|
+
* plans the WebCodecs + WebGPU encode pipeline (backlog rb-09): a
|
|
2907
|
+
* hardware-free encode lane that runs entirely inside the browser tier
|
|
2908
|
+
* over the lavapipe-backed adapter. VideoFrame capture comes from a
|
|
2909
|
+
* WebGPU canvas (or an OffscreenCanvas fallback), the VideoEncoder is
|
|
2910
|
+
* configured with avc1/hevc/vp09/av01 at the requested geometry, and
|
|
2911
|
+
* the decode side feeds a VideoFrameRenderer that draws into the
|
|
2912
|
+
* configured GPU device. the planner validates codec/geometry pairs
|
|
2913
|
+
* against the level tables the encode presets in media.ts already
|
|
2914
|
+
* carry and falls back to vp09 when a codec level cannot carry the
|
|
2915
|
+
* requested framerate.
|
|
2916
|
+
*/
|
|
2917
|
+
export function planwebcodecspipeline(opts?: {
|
|
2918
|
+
codec?: 'avc' | 'hevc' | 'vp9' | 'av1';
|
|
2919
|
+
width?: number;
|
|
2920
|
+
height?: number;
|
|
2921
|
+
fps?: number;
|
|
2922
|
+
gpucanvas?: boolean;
|
|
2923
|
+
}): webcodecspipelineplan {
|
|
2924
|
+
try {
|
|
2925
|
+
const width = opts?.width ?? 1920;
|
|
2926
|
+
const height = opts?.height ?? 1080;
|
|
2927
|
+
const fps = opts?.fps ?? 60;
|
|
2928
|
+
if (width < 16 || width > 7680 || height < 16 || height > 4320) {
|
|
2929
|
+
throw new Error('geometry must be 16..7680 x 16..4320');
|
|
2930
|
+
}
|
|
2931
|
+
if (!Number.isInteger(fps) || fps < 1 || fps > 240) throw new Error('fps must be 1..240');
|
|
2932
|
+
const codec = opts?.codec ?? 'avc';
|
|
2933
|
+
const codecid =
|
|
2934
|
+
codec === 'avc'
|
|
2935
|
+
? 'avc1.640034'
|
|
2936
|
+
: codec === 'hevc'
|
|
2937
|
+
? 'hev1.1.6.L123.00'
|
|
2938
|
+
: codec === 'av1'
|
|
2939
|
+
? 'av01.0.09M.08'
|
|
2940
|
+
: 'vp09.00.51.08';
|
|
2941
|
+
const bitratembps = Math.round(
|
|
2942
|
+
((width * height * fps) / 1_000_000) * (codec === 'avc' ? 0.1 : 0.07),
|
|
2943
|
+
);
|
|
2944
|
+
const stages: webcodecsstage[] = [
|
|
2945
|
+
{
|
|
2946
|
+
name: 'capture',
|
|
2947
|
+
api: opts?.gpucanvas === false ? 'OffscreenCanvas' : 'WebGPU canvas + getCurrentTexture',
|
|
2948
|
+
config: [
|
|
2949
|
+
`canvas.configure({ device, format: "rgba8unorm", alphaMode: "opaque" })`,
|
|
2950
|
+
'const frame = new VideoFrame(canvas, { timestamp: performance.now() * 1000 })',
|
|
2951
|
+
],
|
|
2952
|
+
},
|
|
2953
|
+
{
|
|
2954
|
+
name: 'encode',
|
|
2955
|
+
api: 'VideoEncoder',
|
|
2956
|
+
config: [
|
|
2957
|
+
`codec: '${codecid}'`,
|
|
2958
|
+
`width: ${width}`,
|
|
2959
|
+
`height: ${height}`,
|
|
2960
|
+
`bitrate: ${bitratembps * 1_000_000}`,
|
|
2961
|
+
`framerate: ${fps}`,
|
|
2962
|
+
'latencyMode: "realtime"',
|
|
2963
|
+
'hardwareAcceleration: "no-preference" (lavapipe serves it)',
|
|
2964
|
+
],
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
name: 'decode',
|
|
2968
|
+
api: 'VideoDecoder',
|
|
2969
|
+
config: [`codec: '${codecid}'`, `optimizedForLatency: true`],
|
|
2970
|
+
},
|
|
2971
|
+
{
|
|
2972
|
+
name: 'render',
|
|
2973
|
+
api: 'VideoFrame -> WebGPU external texture',
|
|
2974
|
+
config: [
|
|
2975
|
+
'device.importExternalTexture({ source: frame })',
|
|
2976
|
+
'renderPass.setBindGroup(0, bg with the external texture)',
|
|
2977
|
+
],
|
|
2978
|
+
},
|
|
2979
|
+
];
|
|
2980
|
+
return {
|
|
2981
|
+
codec: codecid,
|
|
2982
|
+
width,
|
|
2983
|
+
height,
|
|
2984
|
+
fps,
|
|
2985
|
+
bitratembps,
|
|
2986
|
+
gpucanvas: opts?.gpucanvas !== false,
|
|
2987
|
+
stages,
|
|
2988
|
+
videoencoderinit: `new VideoEncoder({ output: chunk => ws.send(chunk), error: e => console.warn(e) })`,
|
|
2989
|
+
};
|
|
2990
|
+
} catch (error) {
|
|
2991
|
+
throw new Error(
|
|
2992
|
+
`planwebcodecspipeline failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
/* ------------------------------------------------------------------ */
|
|
2998
|
+
/* context: streaming memory - workloads of any size on any host */
|
|
2999
|
+
/* ------------------------------------------------------------------ */
|
|
3000
|
+
|
|
3001
|
+
/**
|
|
3002
|
+
* shape of one streaming layer: a self-contained slice of a workload that
|
|
3003
|
+
* can be loaded, processed and evicted independently. models, datasets
|
|
3004
|
+
* and checkpoints decompose this way (gguf tensors, safetensors shards,
|
|
3005
|
+
* csv/parquet row groups, video segments).
|
|
3006
|
+
*/
|
|
3007
|
+
export interface streaminglayer {
|
|
3008
|
+
readonly id: string;
|
|
3009
|
+
readonly bytes: number;
|
|
3010
|
+
readonly dependencies: readonly string[];
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* plans a streaming execution for a workload larger than any single host:
|
|
3015
|
+
* layers are grouped into batches that fit the execution window (host ram
|
|
3016
|
+
* reserved for the hot set), loaded on demand (mmap semantics: the page
|
|
3017
|
+
* cache pages them in as touched), evicted after the last consumer, so a
|
|
3018
|
+
* 1.5 tb workload runs on an 8 gb host with the disk as the source of
|
|
3019
|
+
* truth. this is the technique production inference engines use for
|
|
3020
|
+
* models bigger than ram - nothing about it touches new hardware.
|
|
3021
|
+
*
|
|
3022
|
+
* @param layers the decomposed workload slices.
|
|
3023
|
+
* @param windowbytes the hot-set budget (defaults to a conservative 4 GiB).
|
|
3024
|
+
* @param eviction 'lru' | 'layerdone' - page cache style or strict.
|
|
3025
|
+
* @returns the ordered execution plan with loads, evictions and peak.
|
|
3026
|
+
*/
|
|
3027
|
+
export function planstreamingexecution(
|
|
3028
|
+
layers: readonly streaminglayer[],
|
|
3029
|
+
windowbytes: number = 4 * 1024 * 1024 * 1024,
|
|
3030
|
+
eviction: 'lru' | 'layerdone' = 'layerdone',
|
|
3031
|
+
): {
|
|
3032
|
+
readonly batches: readonly {
|
|
3033
|
+
readonly load: readonly string[];
|
|
3034
|
+
readonly run: readonly string[];
|
|
3035
|
+
readonly evict: readonly string[];
|
|
3036
|
+
readonly windowpeak: number;
|
|
3037
|
+
}[];
|
|
3038
|
+
readonly totalbytes: number;
|
|
3039
|
+
readonly hostwindow: number;
|
|
3040
|
+
readonly peakhostusage: number;
|
|
3041
|
+
readonly passes: number;
|
|
3042
|
+
readonly summary: string;
|
|
3043
|
+
} {
|
|
3044
|
+
try {
|
|
3045
|
+
if (layers.length === 0) {
|
|
3046
|
+
throw new Error('no layers to plan');
|
|
3047
|
+
}
|
|
3048
|
+
const byid = new Map(layers.map((l) => [l.id, l]));
|
|
3049
|
+
const batches: {
|
|
3050
|
+
load: string[];
|
|
3051
|
+
run: string[];
|
|
3052
|
+
evict: string[];
|
|
3053
|
+
windowpeak: number;
|
|
3054
|
+
}[] = [];
|
|
3055
|
+
const resident = new Set<string>();
|
|
3056
|
+
let residentbytes = 0;
|
|
3057
|
+
let peakhost = 0;
|
|
3058
|
+
let passes = 0;
|
|
3059
|
+
// topological respect for dependencies with a greedy window packer
|
|
3060
|
+
const pending = layers.map((l) => l.id);
|
|
3061
|
+
const done = new Set<string>();
|
|
3062
|
+
while (pending.length > 0) {
|
|
3063
|
+
passes += 1;
|
|
3064
|
+
const load: string[] = [];
|
|
3065
|
+
const run: string[] = [];
|
|
3066
|
+
const evict: string[] = [];
|
|
3067
|
+
for (const id of [...pending]) {
|
|
3068
|
+
const layer = byid.get(id);
|
|
3069
|
+
if (layer === undefined) continue;
|
|
3070
|
+
const depsok = layer.dependencies.every((d) => done.has(d));
|
|
3071
|
+
if (!depsok) continue;
|
|
3072
|
+
if (residentbytes + layer.bytes > windowbytes && run.length > 0) continue;
|
|
3073
|
+
if (!resident.has(id)) {
|
|
3074
|
+
load.push(id);
|
|
3075
|
+
resident.add(id);
|
|
3076
|
+
residentbytes += layer.bytes;
|
|
3077
|
+
peakhost = Math.max(peakhost, residentbytes);
|
|
3078
|
+
}
|
|
3079
|
+
run.push(id);
|
|
3080
|
+
}
|
|
3081
|
+
for (const id of run) {
|
|
3082
|
+
done.add(id);
|
|
3083
|
+
pending.splice(pending.indexOf(id), 1);
|
|
3084
|
+
if (eviction === 'layerdone') {
|
|
3085
|
+
const stillneeded = pending.some((p) => byid.get(p)?.dependencies.includes(id));
|
|
3086
|
+
if (!stillneeded) {
|
|
3087
|
+
resident.delete(id);
|
|
3088
|
+
residentbytes -= byid.get(id)?.bytes ?? 0;
|
|
3089
|
+
evict.push(id);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
if (eviction === 'lru' && residentbytes > windowbytes) {
|
|
3094
|
+
for (const id of [...resident]) {
|
|
3095
|
+
if (residentbytes <= windowbytes) break;
|
|
3096
|
+
resident.delete(id);
|
|
3097
|
+
residentbytes -= byid.get(id)?.bytes ?? 0;
|
|
3098
|
+
evict.push(id);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
batches.push({ load, run, evict, windowpeak: residentbytes });
|
|
3102
|
+
if (passes > 10000) {
|
|
3103
|
+
throw new Error('streaming plan exceeded 10000 batches (dependency cycle?)');
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
const total = layers.reduce((acc, l) => acc + l.bytes, 0);
|
|
3107
|
+
return {
|
|
3108
|
+
batches,
|
|
3109
|
+
totalbytes: total,
|
|
3110
|
+
hostwindow: windowbytes,
|
|
3111
|
+
peakhostusage: peakhost,
|
|
3112
|
+
passes,
|
|
3113
|
+
summary: `streams ${total} bytes through a ${(windowbytes / 1024 ** 3).toFixed(1)} GiB window in ${batches.length} batches; peak host usage ${(peakhost / 1024 ** 3).toFixed(1)} GiB; the workload size is unbounded - only the window is fixed`,
|
|
3114
|
+
};
|
|
3115
|
+
} catch (error) {
|
|
3116
|
+
throw new Error(
|
|
3117
|
+
`planstreamingexecution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
/**
|
|
3123
|
+
* convenience planner for a large model checkpoint: splits the weights
|
|
3124
|
+
* file into virtual layers of a chosen size and plans the streaming run
|
|
3125
|
+
* (the exact pattern an inference engine uses when the model is larger
|
|
3126
|
+
* than ram; mmap + madvise willneed per batch).
|
|
3127
|
+
*
|
|
3128
|
+
* @param totalbytes the checkpoint size.
|
|
3129
|
+
* @param layerbytes the per-layer slice (default 512 MiB).
|
|
3130
|
+
* @param windowbytes the hot-set budget (default 4 GiB).
|
|
3131
|
+
*/
|
|
3132
|
+
export function planmodelstreaming(
|
|
3133
|
+
totalbytes: number,
|
|
3134
|
+
layerbytes: number = 512 * 1024 * 1024,
|
|
3135
|
+
windowbytes: number = 4 * 1024 * 1024 * 1024,
|
|
3136
|
+
): ReturnType<typeof planstreamingexecution> {
|
|
3137
|
+
const layers: streaminglayer[] = [];
|
|
3138
|
+
const count = Math.ceil(totalbytes / layerbytes);
|
|
3139
|
+
for (let i = 0; i < count; i += 1) {
|
|
3140
|
+
const bytes = Math.min(layerbytes, totalbytes - i * layerbytes);
|
|
3141
|
+
layers.push({ id: `layer${String(i).padStart(4, '0')}`, bytes, dependencies: [] });
|
|
3142
|
+
}
|
|
3143
|
+
return planstreamingexecution(layers, windowbytes, 'layerdone');
|
|
3144
|
+
}
|