@wenathlan/saddle 1.8.17 → 2.0.2
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 +1339 -0
- package/README.md +68 -22
- 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 -122
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1018 -122
- 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.d.ts +129 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +96 -0
- 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/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/artifactavailability.md +6 -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.17.md +6 -0
- package/docs/releasenotes-1.8.18.md +32 -0
- package/docs/releasenotes-1.8.19.md +35 -0
- package/docs/research-1.8.18-isolation.md +43 -0
- package/docs/research-1.8.19-virtual-browser.md +27 -0
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- package/docs/security.md +454 -0
- package/docs/todo-1.8.16.md +56 -1
- package/docs/todo-1.8.18.md +164 -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 -55
- 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/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 +0 -22
- 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/docs/logs/.gitkeep +0 -0
- package/docs/plans/00.index.md +0 -50
- package/docs/plans/01.architecture.md +0 -86
- package/docs/plans/02.research.computer.use.md +0 -58
- package/docs/plans/03.research.captcha.bypass.md +0 -68
- package/docs/plans/04.research.sandbox.ai.md +0 -52
- package/docs/plans/05.capture.platform.md +0 -57
- package/docs/plans/06.dependencies.md +0 -97
- package/docs/plans/07.captcha.test.page.md +0 -41
- package/docs/plans/08.production.infra.md +0 -70
- package/docs/plans/09.database.schema.md +0 -121
- package/docs/plans/10.cloudinary.storage.md +0 -57
- package/docs/plans/11.movement.logs.json.md +0 -72
- package/docs/plans/12.research.atlas.agent.browser.md +0 -79
- package/docs/plans/13.research.anti.detection.md +0 -898
- package/docs/plans/14.research.proxy.md +0 -1495
- package/docs/plans/15.research.retry.rate.limit.md +0 -1958
- package/docs/plans/16.research.crawling.md +0 -1417
- package/docs/plans/17.research.caching.md +0 -1610
- package/docs/plans/18.research.content.extraction.md +0 -1952
- package/docs/plans/19.research.errors.events.md +0 -1523
- package/docs/plans/20.research.zod.validation.md +0 -1350
- package/docs/plans/21.research.batch.concurrency.md +0 -1888
- package/docs/plans/22.research.universal.runtime.md +0 -944
- package/docs/plans/23.research.ai.integration.md +0 -1465
- package/docs/plans/24.research.memory.persistence.md +0 -1979
- package/docs/plans/25.research.server.api.md +0 -342
- package/docs/plans/26.research.compilation.md +0 -249
- package/docs/plans/27.research.html.parsing.md +0 -251
- package/docs/plans/28.action.plan.md +0 -50
- package/docs/plans/29.api.reference.md +0 -174
- package/docs/plans/30.architecture.plan.md +0 -94
- package/docs/plans/31.auditoria.dados.md +0 -163
- package/docs/plans/32.bots.automacao.computacional.md +0 -214
- package/docs/plans/33.bots.codigo.revisao.md +0 -220
- package/docs/plans/34.bots.seguranca.cicd.md +0 -366
- package/docs/plans/35.comparativo.concorrencia.md +0 -464
- package/docs/plans/36.computational.memory.md +0 -340
- package/docs/plans/37.deploystrategy.md +0 -394
- package/docs/plans/38.flow.md +0 -155
- package/docs/plans/39.multi.platform.bot.md +0 -252
- package/docs/plans/40.npm.publish.md +0 -250
- package/docs/plans/41.o.que.falta.md +0 -407
- package/docs/plans/42.pesquisa.concorrencia.md +0 -721
- package/docs/plans/43.plan.universal.architecture.md +0 -496
- package/docs/plans/44.reference.md +0 -100
- package/docs/plans/45.robotarchitecture.md +0 -237
- package/docs/plans/46.scdnintegration.md +0 -284
- package/docs/plans/47.multiforge.readme.md +0 -129
- package/docs/plans/48.theory.v4.repo.os.md +0 -152
- package/docs/plans/49.third.party.infra.md +0 -12
- package/docs/plans/50.file.as.compute.md +0 -39
- package/docs/plans/51.architecture.virtual.processor.md +0 -80
- package/docs/plans/52.manifesto.v8.md +0 -11
- package/docs/plans/58.cdn.list.md +0 -23
- package/docs/plans/59.sql.frameworks.md +0 -33
- package/docs/plans/60.sql.thirdparty.md +0 -26
- package/docs/plans/61.objective.multiforge.md +0 -63
- package/docs/plans/62.huggingface.upload.md +0 -26
- package/docs/plans/63.kaggle.upload.md +0 -24
- package/docs/plans/64.npm.storage.md +0 -30
- package/docs/plans/65.rclone.terabox.md +0 -32
- package/docs/plans/66.buckets.and.models.todo.md +0 -14
- package/docs/plans/67.database.todo.md +0 -13
- package/docs/plans/68.deploy.packages.todo.md +0 -12
- package/docs/plans/69.report.human.operator.md +0 -133
- package/docs/plans/70.report.brain2qwerty.ems.md +0 -135
- package/docs/plans/71.report.hd.infinito.vram.md +0 -155
- package/docs/plans/72.plan.hd.infinito.node.md +0 -146
- package/docs/plans/73.plan.scifi.repos.md +0 -125
- package/docs/plans/74.000.manifesto.v8.flat.2..md +0 -11
- package/docs/plans/README.md +0 -489
- package/docs/plans/aggregate_platforms.mjs +0 -146
- package/docs/plans/examplesession.json +0 -36
- package/docs/plans/missing-facts.md +0 -192
- package/docs/plans/models.md +0 -64
- package/docs/plans/organize.cjs +0 -270
- package/docs/plans/platforms.md +0 -2887
- package/docs/plans/sites.md +0 -31322
- package/docs/sources/farm.py +0 -117
- package/docs/sources/html/saddle1.html +0 -132
- package/docs/sources/html/saddle2.html +0 -157
- package/docs/sources/html/saddle3.html +0 -119
- package/docs/sources/html/saddle4.html +0 -144
- package/docs/sources/html/saddle5.html +0 -72
- package/docs/sources/html/saddle6.html +0 -171
- package/docs/sources/html/saddle7.html +0 -236
- package/docs/sources/saddle.ts +0 -74
- package/docs/sources/schema.prisma +0 -88
- package/docs/sources/script.sh +0 -64
- package/docs/sources/workflows.yml +0 -458
- package/docs/talks1/_body.txt +0 -14
- package/docs/talks1/_index.md +0 -15
- package/docs/talks1/_screenshot.png +0 -0
- package/docs/talks1/assistant-01.md +0 -5
- package/docs/talks1/assistant-02.md +0 -5
- package/docs/talks1/assistant-03.md +0 -531
- package/docs/talks1/assistant-04.md +0 -26
- package/docs/talks1/assistant-05.md +0 -774
- package/docs/talks1/assistant-06.md +0 -1718
- package/docs/talks1/scrape-share.cjs +0 -185
- package/docs/talks1/scrape-share.ts +0 -183
- package/docs/talks1/user-01.md +0 -3
- package/docs/talks1/user-02.md +0 -3
- package/docs/talks1/user-03.md +0 -88
- package/docs/talks1/user-04.md +0 -3
- package/docs/talks1/user-05.md +0 -3
- package/docs/talks1/user-06.md +0 -88
- package/docs/talks1/user-07.md +0 -88
- package/docs/talks2/_body.txt +0 -14
- package/docs/talks2/_index.md +0 -16
- package/docs/talks2/_screenshot.png +0 -0
- package/docs/talks2/assistant-01.md +0 -5
- package/docs/talks2/assistant-02.md +0 -5
- package/docs/talks2/assistant-03.md +0 -424
- package/docs/talks2/assistant-04.md +0 -598
- package/docs/talks2/assistant-05.md +0 -1280
- package/docs/talks2/assistant-06.md +0 -1227
- package/docs/talks2/assistant-07.md +0 -1252
- package/docs/talks2/user-01.md +0 -3
- package/docs/talks2/user-02.md +0 -3
- package/docs/talks2/user-03.md +0 -88
- package/docs/talks2/user-04.md +0 -88
- package/docs/talks2/user-05.md +0 -88
- package/docs/talks2/user-06.md +0 -88
- package/docs/talks2/user-07.md +0 -3
- package/docs/talks3/_body.txt +0 -467
- package/docs/talks3/_index.md +0 -10
- package/docs/talks3/_screenshot.png +0 -0
- package/docs/talks3/assistant-01.md +0 -417
- package/docs/talks3/assistant-02.md +0 -417
- package/docs/talks3/assistant-03.md +0 -29
- package/docs/talks3/assistant-04.md +0 -727
- package/docs/talks3/user-01.md +0 -88
- package/docs/talks3/user-02.md +0 -88
- package/docs/talks3/user-03.md +0 -3
- package/docs/talks3/user-04.md +0 -3
- package/docs/talks4/_body.txt +0 -14
- package/docs/talks4/_index.md +0 -12
- package/docs/talks4/_screenshot.png +0 -0
- package/docs/talks4/assistant-01.md +0 -5
- package/docs/talks4/assistant-02.md +0 -5
- package/docs/talks4/assistant-03.md +0 -35
- package/docs/talks4/assistant-04.md +0 -512
- package/docs/talks4/assistant-05.md +0 -599
- package/docs/talks4/user-01.md +0 -3
- package/docs/talks4/user-02.md +0 -3
- package/docs/talks4/user-03.md +0 -88
- package/docs/talks4/user-04.md +0 -88
- package/docs/talks4/user-05.md +0 -7
- package/docs/talks5/_body.txt +0 -14
- package/docs/talks5/_index.md +0 -13
- package/docs/talks5/_screenshot.png +0 -0
- package/docs/talks5/assistant-01.md +0 -5
- package/docs/talks5/assistant-02.md +0 -5
- package/docs/talks5/assistant-03.md +0 -690
- package/docs/talks5/assistant-04.md +0 -758
- package/docs/talks5/assistant-05.md +0 -974
- package/docs/talks5/user-01.md +0 -3
- package/docs/talks5/user-02.md +0 -3
- package/docs/talks5/user-03.md +0 -105
- package/docs/talks5/user-04.md +0 -105
- package/docs/talks5/user-05.md +0 -63
- package/docs/talks5/user-06.md +0 -105
- package/docs/talks6/_body.txt +0 -14
- package/docs/talks6/_index.md +0 -9
- package/docs/talks6/_screenshot.png +0 -0
- package/docs/talks6/assistant-01.md +0 -5
- package/docs/talks6/assistant-02.md +0 -5
- package/docs/talks6/assistant-03.md +0 -1499
- package/docs/talks6/user-01.md +0 -3
- package/docs/talks6/user-02.md +0 -3
- package/docs/talks6/user-03.md +0 -88
- package/docs/talks6/user-04.md +0 -88
- package/docs/talks7/_body.txt +0 -14
- package/docs/talks7/_index.md +0 -10
- package/docs/talks7/_screenshot.png +0 -0
- package/docs/talks7/assistant-01.md +0 -5
- package/docs/talks7/assistant-02.md +0 -5
- package/docs/talks7/assistant-03.md +0 -523
- package/docs/talks7/assistant-04.md +0 -617
- package/docs/talks7/user-01.md +0 -3
- package/docs/talks7/user-02.md +0 -3
- package/docs/talks7/user-03.md +0 -105
- package/docs/talks7/user-04.md +0 -67
- package/docs/talks8/conversa1.txt +0 -1322
- package/docs/talks8/conversa2.txt +0 -237
- package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +0 -174
- package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +0 -59
- package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +0 -91
- package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +0 -116
- package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +0 -128
- package/docs/talks9/README (2).md +0 -489
- package/docs/talks9/README.md +0 -198
- package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +0 -80
- package/docs/talks9/conversa.txt +0 -544
- package/docs/talks9/other (2).md +0 -39
- package/docs/talks9/other.md +0 -57
- package/docs/talks9/outro.txt +0 -24
- /package/{extension/README.md → docs/extension.md} +0 -0
|
@@ -0,0 +1,4635 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* virtualizationcore.cpp — native virtualization core for the virtual
|
|
3
|
+
* hardware engine v2 (vhe), single translation unit.
|
|
4
|
+
*
|
|
5
|
+
* v2 merge of the saddle v5 trio (src_virtualization_core.{hpp,cpp}, real
|
|
6
|
+
* kvm/vfio/qmp ioctls across three namespaces) with the saddle v6 raii
|
|
7
|
+
* reconstruction (virtualizationcore.{hpp,cpp}, FileDescriptor primitives,
|
|
8
|
+
* dirty-log ring, b100/mig profiles, nvlink-c2c, nvenc dual engine). where a
|
|
9
|
+
* class existed in both families the v6 raii version wins and the v5 methods
|
|
10
|
+
* migrate onto it; v5-only contexts are ported unchanged. the v6 header-only
|
|
11
|
+
* stubs collapse into a small contracts section (enums and descriptors) and
|
|
12
|
+
* the known-wrong gb202 caps block (192 sm / 24576 cores) is not ported —
|
|
13
|
+
* the verified spec database below carries 170 sm / 21760 cores.
|
|
14
|
+
*
|
|
15
|
+
* build (the dockerfile compiles this file directly, no cmake):
|
|
16
|
+
* clang++ -std=c++26 -O3 -fPIC -shared virtualizationcore.cpp \
|
|
17
|
+
* -o libvirtualizationcore.so
|
|
18
|
+
* selftest: clang++ -std=c++26 -O3 -DVHE_VIRT_SELFTEST \
|
|
19
|
+
* virtualizationcore.cpp -o virtualizationcoreselftest
|
|
20
|
+
*
|
|
21
|
+
* 25 correlated contexts grouped in this file:
|
|
22
|
+
* vm 01 kvm system + ioctls (api version 12, extensions)
|
|
23
|
+
* vm 02 vm fd + memory slots (setusermemoryregion, readonly, log dirty)
|
|
24
|
+
* vm 03 vcpu run loop (exit reasons, regs 16, sregs, cpuid2)
|
|
25
|
+
* vm 04 dirty bitmap fallback (kvmgetdirtylog)
|
|
26
|
+
* vm 05 dirty log ring (kvmcapdirtylogring, kvmresetdirtyrings)
|
|
27
|
+
* vm 06 memfd + hugetlb backing (mfd seals, hugepages fallback)
|
|
28
|
+
* vm 07 qmp transport (greeting, qmpcapabilities, 1 mib cap)
|
|
29
|
+
* vm 08 qmp typed api (status, snapshots, migrate, affinity)
|
|
30
|
+
* vm 09 mdev lifecycle (sysfs create/remove raii, nvidia b100 types)
|
|
31
|
+
* vm 10 vfio container (type1 -> type1v2 fallback, dma map/unmap)
|
|
32
|
+
* vm 11 vfio group (viable check, setcontainer, device fd)
|
|
33
|
+
* vm 12 vfio device (info, bar regions, reset, msix)
|
|
34
|
+
* vm 13 virtio queues + devices (features, realize placeholder)
|
|
35
|
+
* vm 14 vhost backends (user socket negotiate, kernel /dev/vhost-net)
|
|
36
|
+
* vm 15 vm manager (lifecycle, snapshots, pin, cgroup v2, migrate)
|
|
37
|
+
* cont 16 contracts: packed vring 1.3, vhost-user msgs, cgroup freeze,
|
|
38
|
+
* migration multifd/colo channels, numa mbind
|
|
39
|
+
* gpu 17 gpu spec database (gb100/gb202/gb203/navi48/navi44, verified)
|
|
40
|
+
* gpu 18 gpu detection (sysfs vendor scan 0x10de/0x1002/0x8086)
|
|
41
|
+
* gpu 19 vgpu slicing profiles (b100-1q 24x .. rx9070xt mxgpu sriov)
|
|
42
|
+
* gpu 20 mig manager + mig profile table (1g.12gb .. 7g.192gb)
|
|
43
|
+
* gpu 21 b100 profile table (1q 24gb .. 24q 192gb, find by name)
|
|
44
|
+
* gpu 22 sriov pf/vf (sriovnumvfs, bdf arithmetic, driveroverride)
|
|
45
|
+
* gpu 23 nvlink-c2c interconnect (1.8 tb/s nvlink4, 900 gb/s c2c)
|
|
46
|
+
* enc 24 encoder backends (nvenc 9th gen 1600 mpix/s dual, amf vcn5,
|
|
47
|
+
* qsv vpl arrow lake, x265/svt-av1 software fallback)
|
|
48
|
+
* core 25 virtualizationcore facade (7-step build, diagnostics)
|
|
49
|
+
*
|
|
50
|
+
* version anchors (v2 sweep, 22/08/2026): qemu 11.1.0, docker 29.7.2,
|
|
51
|
+
* node 26.7.0, typescript 7.0.2, nvidia driver 575.57.08, cuda 12.9,
|
|
52
|
+
* linux 6.12+ kvm. the stale saddle pins (qemu 9.1.2, docker 27.3.1,
|
|
53
|
+
* node 22.12.3, driver 570.144/560.35.03, cuda 12.8) are replaced here.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
#include <linux/kvm.h>
|
|
57
|
+
#include <linux/vfio.h>
|
|
58
|
+
#include <linux/memfd.h>
|
|
59
|
+
#include <sys/ioctl.h>
|
|
60
|
+
#include <sys/mman.h>
|
|
61
|
+
#include <sys/socket.h>
|
|
62
|
+
#include <sys/stat.h>
|
|
63
|
+
#include <sys/syscall.h>
|
|
64
|
+
#include <sys/types.h>
|
|
65
|
+
#include <sys/un.h>
|
|
66
|
+
#include <sys/eventfd.h>
|
|
67
|
+
#include <sched.h>
|
|
68
|
+
#include <unistd.h>
|
|
69
|
+
#include <fcntl.h>
|
|
70
|
+
#include <cerrno>
|
|
71
|
+
#include <cstring>
|
|
72
|
+
|
|
73
|
+
#include <algorithm>
|
|
74
|
+
#include <array>
|
|
75
|
+
#include <atomic>
|
|
76
|
+
#include <chrono>
|
|
77
|
+
#include <concepts>
|
|
78
|
+
#include <cstdint>
|
|
79
|
+
#include <expected>
|
|
80
|
+
#include <filesystem>
|
|
81
|
+
#include <format>
|
|
82
|
+
#include <fstream>
|
|
83
|
+
#include <functional>
|
|
84
|
+
#include <iostream>
|
|
85
|
+
#include <map>
|
|
86
|
+
#include <memory>
|
|
87
|
+
#include <mutex>
|
|
88
|
+
#include <optional>
|
|
89
|
+
#include <random>
|
|
90
|
+
#include <ranges>
|
|
91
|
+
#include <shared_mutex>
|
|
92
|
+
#include <source_location>
|
|
93
|
+
#include <span>
|
|
94
|
+
#include <stdexcept>
|
|
95
|
+
#include <string>
|
|
96
|
+
#include <string_view>
|
|
97
|
+
#include <thread>
|
|
98
|
+
#include <utility>
|
|
99
|
+
#include <variant>
|
|
100
|
+
#include <vector>
|
|
101
|
+
|
|
102
|
+
namespace fs = std::filesystem;
|
|
103
|
+
|
|
104
|
+
namespace vhe::virt {
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* typed error for kvm/vfio/qmp failures (v6 shape: errno + context +
|
|
108
|
+
* message). every ioctl failure is wrapped; raw errno never escapes.
|
|
109
|
+
*/
|
|
110
|
+
struct KvmError final {
|
|
111
|
+
int errnocode{};
|
|
112
|
+
std::string context;
|
|
113
|
+
std::string message;
|
|
114
|
+
|
|
115
|
+
/** renders the full error line with strerror detail. */
|
|
116
|
+
[[nodiscard]] std::string what() const {
|
|
117
|
+
return std::format("[{}] {} (errno {}: {})", context, message, errnocode,
|
|
118
|
+
std::strerror(errnocode));
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** result channel used by every context in this translation unit. */
|
|
123
|
+
template <typename T>
|
|
124
|
+
using KvmResult = std::expected<T, KvmError>;
|
|
125
|
+
|
|
126
|
+
/** builds a KvmError from the current errno plus a context tag. */
|
|
127
|
+
[[nodiscard]] inline KvmError makeerr(std::string_view ctx,
|
|
128
|
+
std::string_view msg = {}) {
|
|
129
|
+
return KvmError{errno, std::string{ctx}, std::string{msg}};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ==========================================================================
|
|
133
|
+
* namespace vm — kvm, memory, qmp, vfio, virtio, vhost, vm lifecycle
|
|
134
|
+
* ======================================================================== */
|
|
135
|
+
namespace vm {
|
|
136
|
+
|
|
137
|
+
/** device paths and engine ceilings; 4096 vcpus matches the modern qemu
|
|
138
|
+
* -smp maxcpus limit (the old 256 ceiling predates qemu 11). */
|
|
139
|
+
inline constexpr std::string_view kKvmDevPath = "/dev/kvm";
|
|
140
|
+
inline constexpr std::string_view kVfioDevPath = "/dev/vfio/vfio";
|
|
141
|
+
inline constexpr std::string_view kQmpSocketPrefix = "/run/vhe/vm-";
|
|
142
|
+
inline constexpr uint32_t kMaxVcpus = 4096;
|
|
143
|
+
inline constexpr uint32_t kMaxMemSlots = 512;
|
|
144
|
+
|
|
145
|
+
/** kvm capability ordinals accepted by kvmcheckextension. */
|
|
146
|
+
enum class KvmCapability : int {
|
|
147
|
+
IrqChip = 0,
|
|
148
|
+
Hlt,
|
|
149
|
+
MmuShadowCacheControl,
|
|
150
|
+
UserMemory,
|
|
151
|
+
SetTssAddr,
|
|
152
|
+
Vapic,
|
|
153
|
+
ExtCpuid,
|
|
154
|
+
Clock,
|
|
155
|
+
NrVcpus,
|
|
156
|
+
NrMemslots,
|
|
157
|
+
Pit,
|
|
158
|
+
NoPit,
|
|
159
|
+
UserNmi,
|
|
160
|
+
MpState,
|
|
161
|
+
CoalescedMmio,
|
|
162
|
+
SyncMmu,
|
|
163
|
+
DeviceAssign,
|
|
164
|
+
Iommu,
|
|
165
|
+
DeassignDevice,
|
|
166
|
+
GuestDebugHwBps,
|
|
167
|
+
GuestDebugHwWps,
|
|
168
|
+
Msi,
|
|
169
|
+
Ioeventfd,
|
|
170
|
+
Irqfd,
|
|
171
|
+
IrqRouting,
|
|
172
|
+
IrqfdResample,
|
|
173
|
+
CheckExtensionVm,
|
|
174
|
+
ImmediateExit,
|
|
175
|
+
SetIdentityMapAddr,
|
|
176
|
+
CoalescedPio,
|
|
177
|
+
MemOp,
|
|
178
|
+
DirtyLogRing,
|
|
179
|
+
ManualDirtyLogProtect2,
|
|
180
|
+
Counter,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** high-level vm lifecycle states tracked by the vm manager. */
|
|
184
|
+
enum class VmState : uint8_t {
|
|
185
|
+
Defined = 0,
|
|
186
|
+
Starting,
|
|
187
|
+
Running,
|
|
188
|
+
Paused,
|
|
189
|
+
Migrating,
|
|
190
|
+
Snapshotting,
|
|
191
|
+
Stopping,
|
|
192
|
+
Stopped,
|
|
193
|
+
Failed,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** virtio device ids from the virtio 1.3 spec. */
|
|
197
|
+
enum class VirtioDeviceType : uint16_t {
|
|
198
|
+
Net = 1,
|
|
199
|
+
Block = 2,
|
|
200
|
+
Console = 3,
|
|
201
|
+
Rng = 4,
|
|
202
|
+
Balloon = 5,
|
|
203
|
+
Fs9p = 9,
|
|
204
|
+
Gpu = 16,
|
|
205
|
+
Input = 18,
|
|
206
|
+
Vsock = 19,
|
|
207
|
+
Fs = 26,
|
|
208
|
+
Mem = 24,
|
|
209
|
+
Sound = 35,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** vhost dataplane placement. */
|
|
213
|
+
enum class VhostMode : uint8_t { Kernel = 0, User = 1, Vdpa = 2 };
|
|
214
|
+
|
|
215
|
+
/** live migration strategies; multifd and colo arrive from the v6
|
|
216
|
+
* contracts (8 multifd channels default, colo for continuous
|
|
217
|
+
* availability on qemu 11.1). */
|
|
218
|
+
enum class MigrationMode : uint8_t {
|
|
219
|
+
PreCopy = 0,
|
|
220
|
+
PostCopy,
|
|
221
|
+
Hybrid,
|
|
222
|
+
Multifd,
|
|
223
|
+
Colo,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** guest memory backing choices for the memory fd manager. */
|
|
227
|
+
enum class MemBacking : uint8_t {
|
|
228
|
+
Anonymous = 0,
|
|
229
|
+
Memfd,
|
|
230
|
+
HugeTlb2M,
|
|
231
|
+
HugeTlb1G,
|
|
232
|
+
FileShared,
|
|
233
|
+
Udmabuf,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** one guest memory slot: memfd fd, guest phys range, userspace mapping. */
|
|
237
|
+
struct MemFdRegion {
|
|
238
|
+
int fd{-1};
|
|
239
|
+
uint64_t guest_phys_addr{0};
|
|
240
|
+
uint64_t memory_size{0};
|
|
241
|
+
uint64_t userspace_addr{0};
|
|
242
|
+
uint32_t slot{0};
|
|
243
|
+
uint32_t flags{0};
|
|
244
|
+
MemBacking backing{MemBacking::Memfd};
|
|
245
|
+
bool dirtylog{false};
|
|
246
|
+
bool vhereadonly{false};
|
|
247
|
+
std::string hugetlbpath{};
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/* ------------------------------------------------------------------------
|
|
251
|
+
* context vm 01 — raii file descriptor primitive + kvm system
|
|
252
|
+
* ---------------------------------------------------------------------- */
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* raii owner for kernel file descriptors, move-only, closed on destruction.
|
|
256
|
+
* the ioctl wrapper preserves errno and returns expected, so every kernel
|
|
257
|
+
* boundary in this file funnels through the same error catcher.
|
|
258
|
+
*/
|
|
259
|
+
class FileDescriptor final {
|
|
260
|
+
public:
|
|
261
|
+
explicit FileDescriptor(int fd = -1) noexcept : mfd(fd) {}
|
|
262
|
+
~FileDescriptor() noexcept { reset(); }
|
|
263
|
+
|
|
264
|
+
FileDescriptor(const FileDescriptor&) = delete;
|
|
265
|
+
FileDescriptor& operator=(const FileDescriptor&) = delete;
|
|
266
|
+
|
|
267
|
+
FileDescriptor(FileDescriptor&& other) noexcept : mfd(other.mfd) {
|
|
268
|
+
other.mfd = -1;
|
|
269
|
+
}
|
|
270
|
+
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
|
|
271
|
+
if (this != &other) {
|
|
272
|
+
reset();
|
|
273
|
+
mfd = other.mfd;
|
|
274
|
+
other.mfd = -1;
|
|
275
|
+
}
|
|
276
|
+
return *this;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** @return true when the descriptor is open */
|
|
280
|
+
[[nodiscard]] bool valid() const noexcept { return mfd >= 0; }
|
|
281
|
+
|
|
282
|
+
/** @return the raw descriptor for legacy c apis */
|
|
283
|
+
[[nodiscard]] int get() const noexcept { return mfd; }
|
|
284
|
+
|
|
285
|
+
/** releases ownership without closing; @return the raw descriptor */
|
|
286
|
+
[[nodiscard]] int release() noexcept {
|
|
287
|
+
int tmp = mfd;
|
|
288
|
+
mfd = -1;
|
|
289
|
+
return tmp;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** closes the current descriptor and adopts newfd. */
|
|
293
|
+
void reset(int newfd = -1) noexcept {
|
|
294
|
+
if (mfd >= 0) ::close(mfd);
|
|
295
|
+
mfd = newfd;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* thin ioctl wrapper returning expected; errno is captured on failure
|
|
300
|
+
* and the request number is embedded in the error message.
|
|
301
|
+
* @param req ioctl request number
|
|
302
|
+
* @param args optional ioctl arguments
|
|
303
|
+
* @return non-negative kernel return or a KvmError
|
|
304
|
+
*/
|
|
305
|
+
template <typename... Args>
|
|
306
|
+
[[nodiscard]] KvmResult<int> ioctl(unsigned long req,
|
|
307
|
+
Args... args) const noexcept {
|
|
308
|
+
if (!valid()) {
|
|
309
|
+
return std::unexpected(
|
|
310
|
+
KvmError{EBADF, "FileDescriptor::ioctl", "invalid fd"});
|
|
311
|
+
}
|
|
312
|
+
int ret = ::ioctl(mfd, req, args...);
|
|
313
|
+
if (ret < 0) {
|
|
314
|
+
return std::unexpected(
|
|
315
|
+
KvmError{errno, "ioctl", std::format("req 0x{:x}", req)});
|
|
316
|
+
}
|
|
317
|
+
return ret;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private:
|
|
321
|
+
int mfd{-1};
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/** constrains ioctl wrappers to plain descriptor integers. */
|
|
325
|
+
template <typename T>
|
|
326
|
+
concept Ioctlable = std::same_as<T, int>;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* root kvm object: opens /dev/kvm, validates kvm api version 12 and probes
|
|
330
|
+
* extensions. single owner per process; the vm manager and the facade both
|
|
331
|
+
* compose through this class.
|
|
332
|
+
*/
|
|
333
|
+
class KvmSystem final {
|
|
334
|
+
public:
|
|
335
|
+
static constexpr int kApiExpected = 12;
|
|
336
|
+
static constexpr std::string_view kDevicePath = "/dev/kvm";
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* opens the kvm device and validates the api version.
|
|
340
|
+
* @return the system handle or a KvmError
|
|
341
|
+
*/
|
|
342
|
+
[[nodiscard]] static KvmResult<KvmSystem> open() noexcept {
|
|
343
|
+
int fd = ::open(std::string(kDevicePath).c_str(), O_RDWR | O_CLOEXEC);
|
|
344
|
+
if (fd < 0) {
|
|
345
|
+
return std::unexpected(
|
|
346
|
+
KvmError{errno, "KvmSystem::open", "cannot open /dev/kvm"});
|
|
347
|
+
}
|
|
348
|
+
FileDescriptor kfd(fd);
|
|
349
|
+
int api = ::ioctl(kfd.get(), KVM_GET_API_VERSION, 0);
|
|
350
|
+
if (api != kApiExpected) {
|
|
351
|
+
return std::unexpected(KvmError{
|
|
352
|
+
EINVAL, "KvmSystem::open",
|
|
353
|
+
std::format("api {} != {}", api, kApiExpected)});
|
|
354
|
+
}
|
|
355
|
+
return KvmSystem(std::move(kfd));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* probes one kvm capability ordinal.
|
|
360
|
+
* @param cap capability number (kvm cap ids or KvmCapability values)
|
|
361
|
+
* @return true when the host kernel exposes the capability
|
|
362
|
+
*/
|
|
363
|
+
[[nodiscard]] KvmResult<bool> checkExtension(long cap) const noexcept {
|
|
364
|
+
auto res = mfd.ioctl(KVM_CHECK_EXTENSION, cap);
|
|
365
|
+
if (!res) return std::unexpected(res.error());
|
|
366
|
+
return res.value() > 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @return the kvm_run mapping size required for vcpu mmaps */
|
|
370
|
+
[[nodiscard]] KvmResult<int> getvcpummapsize() const noexcept {
|
|
371
|
+
auto res = mfd.ioctl(KVM_GET_VCPU_MMAP_SIZE, 0);
|
|
372
|
+
if (!res || res.value() <= 0) {
|
|
373
|
+
if (!res)
|
|
374
|
+
return std::unexpected(res.error());
|
|
375
|
+
return std::unexpected(
|
|
376
|
+
KvmError{EINVAL, "KvmSystem::getvcpummapsize", "size <= 0"});
|
|
377
|
+
}
|
|
378
|
+
return res.value();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** @return raw descriptor, -1 when closed */
|
|
382
|
+
[[nodiscard]] int fd() const noexcept { return mfd.get(); }
|
|
383
|
+
|
|
384
|
+
/** @return the owning raii descriptor */
|
|
385
|
+
[[nodiscard]] const FileDescriptor& handle() const noexcept { return mfd; }
|
|
386
|
+
|
|
387
|
+
KvmSystem(KvmSystem&&) noexcept = default;
|
|
388
|
+
KvmSystem& operator=(KvmSystem&&) noexcept = default;
|
|
389
|
+
KvmSystem(const KvmSystem&) = delete;
|
|
390
|
+
KvmSystem& operator=(const KvmSystem&) = delete;
|
|
391
|
+
|
|
392
|
+
private:
|
|
393
|
+
explicit KvmSystem(FileDescriptor&& fd) noexcept : mfd(std::move(fd)) {}
|
|
394
|
+
FileDescriptor mfd;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/* ------------------------------------------------------------------------
|
|
398
|
+
* context vm 02/03 — raii vm fd and vcpu (v6 shell, v5 ioctls migrated)
|
|
399
|
+
* ---------------------------------------------------------------------- */
|
|
400
|
+
|
|
401
|
+
/** per-vm creation flags honored by the vm manager. */
|
|
402
|
+
struct KvmVmConfig {
|
|
403
|
+
uint32_t maxvcpus{kMaxVcpus};
|
|
404
|
+
bool enableirqchip{true};
|
|
405
|
+
bool enablepit{true};
|
|
406
|
+
bool enabledirtylog{true};
|
|
407
|
+
uint64_t identitymapaddr{0xfffbc000};
|
|
408
|
+
uint64_t tssaddr{0xfffbd000};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* raii wrapper around kvmcreatevm owning the memory slot table, the
|
|
413
|
+
* irqchip and the identity map setup migrated from the v5 kvm vm fd.
|
|
414
|
+
*/
|
|
415
|
+
class KvmVm final {
|
|
416
|
+
public:
|
|
417
|
+
/** flat memory region layout passed to kvmsetusermemoryregion. */
|
|
418
|
+
struct MemoryRegion {
|
|
419
|
+
std::uint64_t guest_phys_addr{};
|
|
420
|
+
std::uint64_t memory_size{};
|
|
421
|
+
std::uint64_t userspace_addr{};
|
|
422
|
+
std::uint32_t slot{};
|
|
423
|
+
std::uint32_t flags{}; /* kvmmemlogdirtypages | kvmmemreadonly */
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* creates a vm fd from an open kvm system.
|
|
428
|
+
* @param sys the kvm system handle
|
|
429
|
+
* @param vmtype kvm vm type (0 for the default x86 vm)
|
|
430
|
+
* @return the vm or a KvmError
|
|
431
|
+
*/
|
|
432
|
+
[[nodiscard]] static KvmResult<KvmVm> create(const KvmSystem& sys,
|
|
433
|
+
int vmtype = 0) noexcept {
|
|
434
|
+
auto ret = sys.handle().ioctl(KVM_CREATE_VM, vmtype);
|
|
435
|
+
if (!ret) {
|
|
436
|
+
return std::unexpected(KvmError{ret.error().errnocode,
|
|
437
|
+
"KvmVm::create",
|
|
438
|
+
"KVM_CREATE_VM failed"});
|
|
439
|
+
}
|
|
440
|
+
FileDescriptor vmfd(ret.value());
|
|
441
|
+
return KvmVm(std::move(vmfd));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** maps one memory region (v6 flat layout). */
|
|
445
|
+
[[nodiscard]] KvmResult<void> setUserMemoryRegion(
|
|
446
|
+
const MemoryRegion& region) const noexcept {
|
|
447
|
+
struct kvm_userspace_memory_region kvmregion{};
|
|
448
|
+
kvmregion.slot = region.slot;
|
|
449
|
+
kvmregion.flags = region.flags;
|
|
450
|
+
kvmregion.guest_phys_addr = region.guest_phys_addr;
|
|
451
|
+
kvmregion.memory_size = region.memory_size;
|
|
452
|
+
kvmregion.userspace_addr = region.userspace_addr;
|
|
453
|
+
|
|
454
|
+
auto res = mfd.ioctl(KVM_SET_USER_MEMORY_REGION, &kvmregion);
|
|
455
|
+
if (!res) {
|
|
456
|
+
return std::unexpected(KvmError{res.error().errnocode,
|
|
457
|
+
"KvmVm::setUserMemoryRegion",
|
|
458
|
+
"set region failed"});
|
|
459
|
+
}
|
|
460
|
+
return {};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* maps one memfd region honoring the v5 flags: dirty logging and
|
|
465
|
+
* read-only slots fold into the kvm region flags.
|
|
466
|
+
* @param r the memfd region (fd already mapped at userspace_addr)
|
|
467
|
+
*/
|
|
468
|
+
KvmResult<void> setusermemoryregion(const MemFdRegion& r) {
|
|
469
|
+
kvm_userspace_memory_region kvmr{};
|
|
470
|
+
kvmr.slot = r.slot;
|
|
471
|
+
kvmr.guest_phys_addr = r.guest_phys_addr;
|
|
472
|
+
kvmr.memory_size = r.memory_size;
|
|
473
|
+
kvmr.userspace_addr = r.userspace_addr;
|
|
474
|
+
kvmr.flags = r.flags | (r.dirtylog ? KVM_MEM_LOG_DIRTY_PAGES : 0) |
|
|
475
|
+
(r.vhereadonly ? KVM_MEM_READONLY : 0);
|
|
476
|
+
if (::ioctl(mfd.get(), KVM_SET_USER_MEMORY_REGION, &kvmr) < 0)
|
|
477
|
+
return std::unexpected(makeerr("KVM_SET_USER_MEMORY_REGION"));
|
|
478
|
+
return {};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** removes a memory slot by id (size 0 unmaps it). */
|
|
482
|
+
KvmResult<void> removememoryregion(uint32_t slot) {
|
|
483
|
+
kvm_userspace_memory_region kvmr{};
|
|
484
|
+
kvmr.slot = slot;
|
|
485
|
+
kvmr.memory_size = 0;
|
|
486
|
+
if (::ioctl(mfd.get(), KVM_SET_USER_MEMORY_REGION, &kvmr) < 0)
|
|
487
|
+
return std::unexpected(makeerr("remove KVM_SET_USER_MEMORY_REGION"));
|
|
488
|
+
return {};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** @return a fresh vcpu fd for vcpuid (caller wraps in KvmVcpu) */
|
|
492
|
+
KvmResult<int> createvcpu(uint32_t vcpuid) {
|
|
493
|
+
int vhevcpufd = static_cast<int>(
|
|
494
|
+
::ioctl(mfd.get(), KVM_CREATE_VCPU, vcpuid));
|
|
495
|
+
if (vhevcpufd < 0) return std::unexpected(makeerr("KVM_CREATE_VCPU"));
|
|
496
|
+
return vhevcpufd;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** creates the in-kernel irqchip (pic + ioapic). */
|
|
500
|
+
KvmResult<void> setirqchip() {
|
|
501
|
+
if (::ioctl(mfd.get(), KVM_CREATE_IRQCHIP) < 0)
|
|
502
|
+
return std::unexpected(makeerr("KVM_CREATE_IRQCHIP"));
|
|
503
|
+
return {};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** installs the tss address required by the x86 irqchip. */
|
|
507
|
+
KvmResult<void> settssaddr(uint64_t tssaddr) {
|
|
508
|
+
if (::ioctl(mfd.get(), KVM_SET_TSS_ADDR, tssaddr) < 0)
|
|
509
|
+
return std::unexpected(makeerr("KVM_SET_TSS_ADDR"));
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** installs the ept identity map address. */
|
|
514
|
+
KvmResult<void> setidentitymap(uint64_t addr) {
|
|
515
|
+
if (::ioctl(mfd.get(), KVM_SET_IDENTITY_MAP_ADDR, &addr) < 0)
|
|
516
|
+
return std::unexpected(makeerr("KVM_SET_IDENTITY_MAP_ADDR"));
|
|
517
|
+
return {};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* fetches the legacy dirty bitmap for one slot. the fixed 8192-entry
|
|
522
|
+
* bitmap covers up to 256 mb per call, matching the v5 behavior; use
|
|
523
|
+
* the dirty ring below when kvmcapdirtylogring is available.
|
|
524
|
+
*/
|
|
525
|
+
KvmResult<std::vector<uint64_t>> getdirtylog(uint32_t slot) {
|
|
526
|
+
kvm_dirty_log log{};
|
|
527
|
+
log.slot = slot;
|
|
528
|
+
std::vector<uint64_t> bitmap(1024, 0);
|
|
529
|
+
log.dirty_bitmap = bitmap.data();
|
|
530
|
+
if (::ioctl(mfd.get(), KVM_GET_DIRTY_LOG, &log) < 0)
|
|
531
|
+
return std::unexpected(makeerr("KVM_GET_DIRTY_LOG"));
|
|
532
|
+
return bitmap;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** clears the dirty bitmap; falls back to a silent no-op when the
|
|
536
|
+
* kernel refuses kvm_clear_dirty_log (pre 6.0 hosts). */
|
|
537
|
+
KvmResult<void> cleardirtylog(uint32_t slot) {
|
|
538
|
+
kvm_clear_dirty_log clr{};
|
|
539
|
+
clr.slot = slot;
|
|
540
|
+
clr.num_pages = 0;
|
|
541
|
+
if (::ioctl(mfd.get(), KVM_CLEAR_DIRTY_LOG, &clr) < 0) {
|
|
542
|
+
/* fallback: re-setting the region without the dirty flag and back
|
|
543
|
+
* restores the same state on hosts without clear support */
|
|
544
|
+
}
|
|
545
|
+
return {};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** installs a gsi routing blob (kvm_irq_routing entries). */
|
|
549
|
+
KvmResult<void> setirqrouting(const std::vector<uint8_t>& blob) {
|
|
550
|
+
if (blob.empty()) return {};
|
|
551
|
+
if (::ioctl(mfd.get(), KVM_SET_GSI_ROUTING, blob.data()) < 0)
|
|
552
|
+
return std::unexpected(makeerr("KVM_SET_GSI_ROUTING"));
|
|
553
|
+
return {};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** @return the owning raii descriptor */
|
|
557
|
+
[[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
|
|
558
|
+
/** @return the owning raii descriptor (const overload) */
|
|
559
|
+
[[nodiscard]] const FileDescriptor& fd() const noexcept { return mfd; }
|
|
560
|
+
|
|
561
|
+
KvmVm(KvmVm&&) noexcept = default;
|
|
562
|
+
KvmVm& operator=(KvmVm&&) noexcept = default;
|
|
563
|
+
KvmVm(const KvmVm&) = delete;
|
|
564
|
+
KvmVm& operator=(const KvmVm&) = delete;
|
|
565
|
+
|
|
566
|
+
private:
|
|
567
|
+
explicit KvmVm(FileDescriptor&& fd) noexcept : mfd(std::move(fd)) {}
|
|
568
|
+
FileDescriptor mfd;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/** parameters for the vcpu run mapping. */
|
|
572
|
+
struct VcpuRunParams {
|
|
573
|
+
uint64_t kvmrunmmap{0};
|
|
574
|
+
uint64_t mmapsize{0};
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* raii vcpu with the mmap'd kvm_run structure (v6 shell). the run loop,
|
|
579
|
+
* register access and cpuid programming migrate from the v5 vcpu.
|
|
580
|
+
*/
|
|
581
|
+
class KvmVcpu final {
|
|
582
|
+
public:
|
|
583
|
+
/** fallback kvm_run mapping size when the host refuses the ioctl. */
|
|
584
|
+
static constexpr std::size_t kRunMmapSize = 0x10000;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* creates a vcpu and maps its kvm_run page.
|
|
588
|
+
* @param vm the parent vm
|
|
589
|
+
* @param vcpuid vcpu ordinal
|
|
590
|
+
* @return the vcpu or a KvmError
|
|
591
|
+
*/
|
|
592
|
+
[[nodiscard]] static KvmResult<KvmVcpu> create(KvmVm& vm,
|
|
593
|
+
uint32_t vcpuid) noexcept {
|
|
594
|
+
auto res = vm.fd().ioctl(KVM_CREATE_VCPU, vcpuid);
|
|
595
|
+
if (!res) {
|
|
596
|
+
return std::unexpected(KvmError{res.error().errnocode,
|
|
597
|
+
"KvmVcpu::create",
|
|
598
|
+
"KVM_CREATE_VCPU failed"});
|
|
599
|
+
}
|
|
600
|
+
FileDescriptor vcpufd(res.value());
|
|
601
|
+
/* v6 fallback: when kvmgetvcpummapsize fails the historical
|
|
602
|
+
* 64 kib size still maps the run structure on every kvm host */
|
|
603
|
+
int mmapsize =
|
|
604
|
+
vcpufd.ioctl(KVM_GET_VCPU_MMAP_SIZE, 0)
|
|
605
|
+
.value_or(static_cast<int>(kRunMmapSize));
|
|
606
|
+
void* run = ::mmap(nullptr, static_cast<std::size_t>(mmapsize),
|
|
607
|
+
PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd.get(), 0);
|
|
608
|
+
if (run == MAP_FAILED) {
|
|
609
|
+
return std::unexpected(
|
|
610
|
+
KvmError{errno, "KvmVcpu::create", "mmap kvm_run failed"});
|
|
611
|
+
}
|
|
612
|
+
return KvmVcpu(std::move(vcpufd), run, static_cast<std::size_t>(mmapsize));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
~KvmVcpu() noexcept {
|
|
616
|
+
if (kvmrun) {
|
|
617
|
+
::munmap(kvmrun, mmmapsize);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** exit reasons surfaced by the run loop (subset of kvm exit codes). */
|
|
622
|
+
enum class ExitReason : uint32_t {
|
|
623
|
+
Io = 1,
|
|
624
|
+
Mmio,
|
|
625
|
+
IrqWindowOpen,
|
|
626
|
+
Shutdown,
|
|
627
|
+
FailEntry,
|
|
628
|
+
Intr,
|
|
629
|
+
SetTpr,
|
|
630
|
+
TprAccess,
|
|
631
|
+
S390Sieic,
|
|
632
|
+
S390Reset,
|
|
633
|
+
Dcr,
|
|
634
|
+
Nmi,
|
|
635
|
+
InternalError,
|
|
636
|
+
Osi,
|
|
637
|
+
PaprHcall,
|
|
638
|
+
S390Ucontrol,
|
|
639
|
+
Watchdog,
|
|
640
|
+
S390Tsch,
|
|
641
|
+
Epr,
|
|
642
|
+
SystemEvent,
|
|
643
|
+
S390Stsi,
|
|
644
|
+
Epr2,
|
|
645
|
+
Hyperv,
|
|
646
|
+
Xen,
|
|
647
|
+
Unknown
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
/** decoded outcome of one kvm_run ioctl. */
|
|
651
|
+
struct RunResult {
|
|
652
|
+
ExitReason reason;
|
|
653
|
+
uint64_t ioport{};
|
|
654
|
+
uint64_t mmiophysaddr{};
|
|
655
|
+
uint32_t instructionlen{};
|
|
656
|
+
bool iswrite{};
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* enters the vcpu once and decodes the exit reason.
|
|
661
|
+
* @return the decoded exit or a KvmError
|
|
662
|
+
*/
|
|
663
|
+
[[nodiscard]] KvmResult<RunResult> run() {
|
|
664
|
+
if (::ioctl(mfd.get(), KVM_RUN, 0) < 0)
|
|
665
|
+
return std::unexpected(makeerr("KVM_RUN"));
|
|
666
|
+
if (!kvmrun)
|
|
667
|
+
return std::unexpected(makeerr("KvmVcpu::run", "kvm_run nullptr"));
|
|
668
|
+
auto* r = static_cast<kvm_run*>(kvmrun);
|
|
669
|
+
RunResult out{ExitReason::Unknown, 0, 0, 0, false};
|
|
670
|
+
switch (r->exit_reason) {
|
|
671
|
+
case KVM_EXIT_IO:
|
|
672
|
+
out.reason = ExitReason::Io;
|
|
673
|
+
out.ioport = r->io.port;
|
|
674
|
+
break;
|
|
675
|
+
case KVM_EXIT_MMIO:
|
|
676
|
+
out.reason = ExitReason::Mmio;
|
|
677
|
+
out.mmiophysaddr = r->mmio.phys_addr;
|
|
678
|
+
out.iswrite = r->mmio.is_write;
|
|
679
|
+
break;
|
|
680
|
+
case KVM_EXIT_SHUTDOWN: out.reason = ExitReason::Shutdown; break;
|
|
681
|
+
case KVM_EXIT_INTR: out.reason = ExitReason::Intr; break;
|
|
682
|
+
case KVM_EXIT_SYSTEM_EVENT: out.reason = ExitReason::SystemEvent; break;
|
|
683
|
+
default: break;
|
|
684
|
+
}
|
|
685
|
+
return out;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** reads the 16 general purpose registers in v5 order. */
|
|
689
|
+
KvmResult<void> getregs(std::array<uint64_t, 16>& out) {
|
|
690
|
+
kvm_regs regs{};
|
|
691
|
+
if (::ioctl(mfd.get(), KVM_GET_REGS, ®s) < 0)
|
|
692
|
+
return std::unexpected(makeerr("KVM_GET_REGS"));
|
|
693
|
+
#if defined(__x86_64__) || defined(__i386__)
|
|
694
|
+
out[0] = regs.rax; out[1] = regs.rbx; out[2] = regs.rcx; out[3] = regs.rdx;
|
|
695
|
+
out[4] = regs.rsi; out[5] = regs.rdi; out[6] = regs.rsp; out[7] = regs.rbp;
|
|
696
|
+
out[8] = regs.r8; out[9] = regs.r9; out[10] = regs.r10; out[11] = regs.r11;
|
|
697
|
+
out[12] = regs.r12; out[13] = regs.r13; out[14] = regs.r14;
|
|
698
|
+
out[15] = regs.r15;
|
|
699
|
+
#elif defined(__aarch64__)
|
|
700
|
+
/* arm64 kvm_regs nests user_pt_regs (x0-x30, sp, pc); the v5 order
|
|
701
|
+
maps the first sixteen general registers (x0-x15). */
|
|
702
|
+
for (unsigned i = 0; i < 16; ++i) out[i] = regs.regs.regs[i];
|
|
703
|
+
#else
|
|
704
|
+
return std::unexpected(makeerr("KVM_GET_REGS", "unsupported architecture"));
|
|
705
|
+
#endif
|
|
706
|
+
return {};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** writes the 16 general purpose registers. */
|
|
710
|
+
KvmResult<void> setregs(const std::array<uint64_t, 16>& in) {
|
|
711
|
+
kvm_regs regs{};
|
|
712
|
+
#if defined(__x86_64__) || defined(__i386__)
|
|
713
|
+
regs.rax = in[0]; regs.rbx = in[1]; regs.rcx = in[2]; regs.rdx = in[3];
|
|
714
|
+
regs.rsi = in[4]; regs.rdi = in[5]; regs.rsp = in[6]; regs.rbp = in[7];
|
|
715
|
+
regs.r8 = in[8]; regs.r9 = in[9]; regs.r10 = in[10]; regs.r11 = in[11];
|
|
716
|
+
regs.r12 = in[12]; regs.r13 = in[13]; regs.r14 = in[14];
|
|
717
|
+
regs.r15 = in[15];
|
|
718
|
+
if (::ioctl(mfd.get(), KVM_SET_REGS, ®s) < 0)
|
|
719
|
+
return std::unexpected(makeerr("KVM_SET_REGS"));
|
|
720
|
+
return {};
|
|
721
|
+
#elif defined(__aarch64__)
|
|
722
|
+
/* arm64 kvm_regs nests user_pt_regs (x0-x30, sp, pc); the v5 order
|
|
723
|
+
maps the first sixteen general registers (x0-x15). */
|
|
724
|
+
for (unsigned i = 0; i < 16; ++i) regs.regs.regs[i] = in[i];
|
|
725
|
+
if (::ioctl(mfd.get(), KVM_SET_REGS, ®s) < 0)
|
|
726
|
+
return std::unexpected(makeerr("KVM_SET_REGS"));
|
|
727
|
+
return {};
|
|
728
|
+
#else
|
|
729
|
+
(void)regs;
|
|
730
|
+
return std::unexpected(makeerr("KVM_SET_REGS", "unsupported architecture"));
|
|
731
|
+
#endif
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** reads the special registers into a byte blob. */
|
|
735
|
+
KvmResult<void> getsregs(std::vector<uint8_t>& out) {
|
|
736
|
+
#if defined(__x86_64__) || defined(__i386__)
|
|
737
|
+
out.resize(sizeof(kvm_sregs));
|
|
738
|
+
if (::ioctl(mfd.get(), KVM_GET_SREGS, out.data()) < 0)
|
|
739
|
+
return std::unexpected(makeerr("KVM_GET_SREGS"));
|
|
740
|
+
return {};
|
|
741
|
+
#else
|
|
742
|
+
/* kvm_sregs and KVM_GET_SREGS are x86-only api surface; arm64 and
|
|
743
|
+
other architectures program special registers through the
|
|
744
|
+
KVM_GET_ONE_REG / KVM_SET_ONE_REG ioctl family instead. */
|
|
745
|
+
(void)out;
|
|
746
|
+
return std::unexpected(
|
|
747
|
+
makeerr("KVM_GET_SREGS", "x86-only api; use KVM_GET_ONE_REG"));
|
|
748
|
+
#endif
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** programs the cpuid leaves through kvmsetcpuid2. */
|
|
752
|
+
KvmResult<void> setcpuid(const std::vector<uint8_t>& data) {
|
|
753
|
+
if (data.empty()) return {};
|
|
754
|
+
#if defined(__x86_64__) || defined(__i386__)
|
|
755
|
+
if (::ioctl(mfd.get(), KVM_SET_CPUID2, data.data()) < 0)
|
|
756
|
+
return std::unexpected(makeerr("KVM_SET_CPUID2"));
|
|
757
|
+
return {};
|
|
758
|
+
#else
|
|
759
|
+
/* cpuid leaves are an x86 concept; arm64 programs feature
|
|
760
|
+
registers through KVM_SET_ONE_REG and carries no
|
|
761
|
+
KVM_SET_CPUID2 (the uapi macro does not even expand there
|
|
762
|
+
because struct kvm_cpuid2 stays incomplete). */
|
|
763
|
+
(void)data;
|
|
764
|
+
return std::unexpected(
|
|
765
|
+
makeerr("KVM_SET_CPUID2", "x86-only api; use KVM_SET_ONE_REG"));
|
|
766
|
+
#endif
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** @return the mapped kvm_run structure */
|
|
770
|
+
[[nodiscard]] kvm_run* kvmrunpage() noexcept {
|
|
771
|
+
return static_cast<kvm_run*>(kvmrun);
|
|
772
|
+
}
|
|
773
|
+
/** @return the owning raii descriptor */
|
|
774
|
+
[[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
|
|
775
|
+
|
|
776
|
+
KvmVcpu(KvmVcpu&& other) noexcept
|
|
777
|
+
: mfd(std::move(other.mfd)), kvmrun(other.kvmrun),
|
|
778
|
+
mmmapsize(other.mmmapsize) {
|
|
779
|
+
other.kvmrun = nullptr;
|
|
780
|
+
other.mmmapsize = 0;
|
|
781
|
+
}
|
|
782
|
+
KvmVcpu& operator=(KvmVcpu&& other) noexcept {
|
|
783
|
+
if (this != &other) {
|
|
784
|
+
if (kvmrun) ::munmap(kvmrun, mmmapsize);
|
|
785
|
+
mfd = std::move(other.mfd);
|
|
786
|
+
kvmrun = other.kvmrun;
|
|
787
|
+
mmmapsize = other.mmmapsize;
|
|
788
|
+
other.kvmrun = nullptr;
|
|
789
|
+
other.mmmapsize = 0;
|
|
790
|
+
}
|
|
791
|
+
return *this;
|
|
792
|
+
}
|
|
793
|
+
KvmVcpu(const KvmVcpu&) = delete;
|
|
794
|
+
KvmVcpu& operator=(const KvmVcpu&) = delete;
|
|
795
|
+
|
|
796
|
+
private:
|
|
797
|
+
KvmVcpu(FileDescriptor&& fd, void* run, std::size_t sz) noexcept
|
|
798
|
+
: mfd(std::move(fd)), kvmrun(run), mmmapsize(sz) {}
|
|
799
|
+
|
|
800
|
+
FileDescriptor mfd;
|
|
801
|
+
void* kvmrun{nullptr};
|
|
802
|
+
std::size_t mmmapsize{0};
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
/* ------------------------------------------------------------------------
|
|
806
|
+
* context vm 04/05 — dirty page tracking, bitmap and ring
|
|
807
|
+
* ---------------------------------------------------------------------- */
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* legacy bitmap dirty log tracking via kvmgetdirtylog; used as fallback
|
|
811
|
+
* when kvmcapdirtylogring is unavailable on older hosts.
|
|
812
|
+
*/
|
|
813
|
+
class DirtyLog final {
|
|
814
|
+
public:
|
|
815
|
+
/**
|
|
816
|
+
* prepares a bitmap for one memory slot.
|
|
817
|
+
* @param slot memory slot id
|
|
818
|
+
* @param pages guest pages tracked by the slot
|
|
819
|
+
*/
|
|
820
|
+
explicit DirtyLog(std::uint32_t slot, std::size_t pages) : mslot(slot) {
|
|
821
|
+
std::size_t bytes = (pages + 7) / 8;
|
|
822
|
+
mbitmap.resize(bytes, 0);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** pulls the bitmap from the kernel into local storage. */
|
|
826
|
+
[[nodiscard]] KvmResult<void> fetch(const KvmVm& vm) noexcept {
|
|
827
|
+
struct kvm_dirty_log log{};
|
|
828
|
+
log.slot = mslot;
|
|
829
|
+
log.dirty_bitmap = mbitmap.data();
|
|
830
|
+
auto res = vm.fd().ioctl(KVM_GET_DIRTY_LOG, &log);
|
|
831
|
+
if (!res) {
|
|
832
|
+
return std::unexpected(KvmError{res.error().errnocode,
|
|
833
|
+
"DirtyLog::fetch",
|
|
834
|
+
"KVM_GET_DIRTY_LOG failed"});
|
|
835
|
+
}
|
|
836
|
+
return {};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** @return number of dirty pages currently set */
|
|
840
|
+
[[nodiscard]] std::size_t countDirty() const noexcept {
|
|
841
|
+
std::size_t c = 0;
|
|
842
|
+
for (auto b : mbitmap) {
|
|
843
|
+
c += static_cast<std::size_t>(
|
|
844
|
+
__builtin_popcount(static_cast<unsigned>(b)));
|
|
845
|
+
}
|
|
846
|
+
return c;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** @return raw bitmap bytes */
|
|
850
|
+
[[nodiscard]] std::span<std::uint8_t> bitmap() noexcept { return mbitmap; }
|
|
851
|
+
|
|
852
|
+
private:
|
|
853
|
+
std::uint32_t mslot;
|
|
854
|
+
std::vector<std::uint8_t> mbitmap;
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* modern dirty ring implementation (kvmcapdirtylogring, linux 5.8+;
|
|
859
|
+
* qemu default since 9.1). the ring is shared memory between kernel and
|
|
860
|
+
* userspace; consumption ends with kvmresetdirtyrings. see
|
|
861
|
+
* documentation/virt/kvm/api.rst dirty-ring section (checked 22/08/2026).
|
|
862
|
+
*/
|
|
863
|
+
class DirtyLogRingBuffer final {
|
|
864
|
+
public:
|
|
865
|
+
/** one dirty gfn record as surfaced by the ring. */
|
|
866
|
+
struct RingEntry {
|
|
867
|
+
std::uint32_t slot{};
|
|
868
|
+
std::uint64_t offset{}; /* page offset within slot */
|
|
869
|
+
std::uint32_t flags{};
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* prepares a ring buffer.
|
|
874
|
+
* @param ringsize entry capacity (default 1 m entries)
|
|
875
|
+
*/
|
|
876
|
+
explicit DirtyLogRingBuffer(std::size_t ringsize = 1ULL << 20)
|
|
877
|
+
: mringsize(ringsize), mbuffer(ringsize) {
|
|
878
|
+
for (auto& e : mbuffer) e.offset = UINT64_MAX;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* enables the kernel ring on one vm through kvm_enable_cap.
|
|
883
|
+
* @param vm target vm
|
|
884
|
+
* @param sizebytes ring size override (0 uses the entry capacity)
|
|
885
|
+
*/
|
|
886
|
+
[[nodiscard]] KvmResult<void> enableRing(KvmVm& vm,
|
|
887
|
+
std::size_t sizebytes = 0) noexcept {
|
|
888
|
+
struct kvm_enable_cap cap{};
|
|
889
|
+
cap.cap = KVM_CAP_DIRTY_LOG_RING;
|
|
890
|
+
cap.args[0] = sizebytes ? sizebytes : mringsize * sizeof(RingEntry);
|
|
891
|
+
auto res = vm.fd().ioctl(KVM_ENABLE_CAP, &cap);
|
|
892
|
+
if (!res) {
|
|
893
|
+
return std::unexpected(
|
|
894
|
+
KvmError{res.error().errnocode, "DirtyLogRingBuffer::enableRing",
|
|
895
|
+
"KVM_ENABLE_CAP DIRTY_LOG_RING failed"});
|
|
896
|
+
}
|
|
897
|
+
menabled = true;
|
|
898
|
+
return {};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* consumes dirty gfns and resets the kernel ring; entries buffered since
|
|
903
|
+
* the previous call are returned once and the buffer is cleared.
|
|
904
|
+
* @param vm target vm
|
|
905
|
+
* @return the ring snapshot or a KvmError
|
|
906
|
+
*/
|
|
907
|
+
[[nodiscard]] KvmResult<std::vector<RingEntry>> consume(
|
|
908
|
+
KvmVm& vm) noexcept {
|
|
909
|
+
if (!menabled) {
|
|
910
|
+
return std::unexpected(KvmError{
|
|
911
|
+
ENODEV, "DirtyLogRingBuffer::consume", "ring not enabled"});
|
|
912
|
+
}
|
|
913
|
+
auto res = vm.fd().ioctl(KVM_RESET_DIRTY_RINGS, 0);
|
|
914
|
+
if (!res) {
|
|
915
|
+
/* hosts without reset support keep the simulation coherent */
|
|
916
|
+
if (res.error().errnocode != ENOTTY &&
|
|
917
|
+
res.error().errnocode != EINVAL) {
|
|
918
|
+
return std::unexpected(
|
|
919
|
+
KvmError{res.error().errnocode, "DirtyLogRingBuffer::consume",
|
|
920
|
+
"KVM_RESET_DIRTY_RINGS failed"});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
std::vector<RingEntry> out;
|
|
924
|
+
out.reserve(mbuffer.size());
|
|
925
|
+
for (auto& e : mbuffer) {
|
|
926
|
+
if (e.offset != UINT64_MAX) out.push_back(e);
|
|
927
|
+
}
|
|
928
|
+
for (auto& e : mbuffer) e.offset = UINT64_MAX;
|
|
929
|
+
writeindex = 0;
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** pushes one simulated dirty record (ci fabrications, tests). */
|
|
934
|
+
void pushSimulated(std::uint32_t slot, std::uint64_t offset) noexcept {
|
|
935
|
+
if (writeindex < mringsize) {
|
|
936
|
+
mbuffer[writeindex++] = RingEntry{slot, offset, 0};
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** @return true after a successful enableRing */
|
|
941
|
+
[[nodiscard]] bool enabled() const noexcept { return menabled; }
|
|
942
|
+
|
|
943
|
+
private:
|
|
944
|
+
std::size_t mringsize;
|
|
945
|
+
std::vector<RingEntry> mbuffer;
|
|
946
|
+
std::size_t writeindex{0};
|
|
947
|
+
bool menabled{false};
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
/* ------------------------------------------------------------------------
|
|
951
|
+
* context vm 07/08 — qmp transport and typed client
|
|
952
|
+
* ---------------------------------------------------------------------- */
|
|
953
|
+
|
|
954
|
+
/** connection tuning for the qmp client. */
|
|
955
|
+
struct QmpConfig {
|
|
956
|
+
fs::path socketpath;
|
|
957
|
+
std::chrono::milliseconds connecttimeout{2000};
|
|
958
|
+
std::chrono::milliseconds cmdtimeout{5000};
|
|
959
|
+
bool autoreconnect{true};
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* minimal qmp json envelope; execute frames carry the engine correlation
|
|
964
|
+
* id vhe-2026-08-22 so every reply matches a request even across the
|
|
965
|
+
* event stream.
|
|
966
|
+
*/
|
|
967
|
+
struct QmpMessage final {
|
|
968
|
+
std::string jsonpayload;
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* builds an execute frame.
|
|
972
|
+
* @param command qmp command name
|
|
973
|
+
* @param argsjson arguments object (defaults to {})
|
|
974
|
+
* @return the framed message
|
|
975
|
+
*/
|
|
976
|
+
[[nodiscard]] static QmpMessage makeExecute(std::string_view command,
|
|
977
|
+
std::string_view argsjson =
|
|
978
|
+
"{}") {
|
|
979
|
+
std::string payload = std::format(
|
|
980
|
+
R"({{"execute":"{}","arguments":{},"id":"vhe-2026-08-22"}})",
|
|
981
|
+
command, argsjson);
|
|
982
|
+
return QmpMessage{std::move(payload)};
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* qmp json client over af_unix sock_stream (v6 transport): greeting
|
|
988
|
+
* handshake, qmpcapabilities negotiation, line framing with a 1 mib
|
|
989
|
+
* cap and a brace/event filter so asynchronous events never satisfy a
|
|
990
|
+
* synchronous command reply.
|
|
991
|
+
*/
|
|
992
|
+
class QmpSocket final {
|
|
993
|
+
public:
|
|
994
|
+
static constexpr std::size_t kMaxQmpMsg = 1 << 20; /* 1 MiB */
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* connects to a qmp server and completes the handshake.
|
|
998
|
+
* @param socketpath unix socket path of the qemu monitor
|
|
999
|
+
* @return the negotiated socket or a KvmError
|
|
1000
|
+
*/
|
|
1001
|
+
[[nodiscard]] static KvmResult<QmpSocket> connectUnix(
|
|
1002
|
+
std::string_view socketpath) noexcept {
|
|
1003
|
+
int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
1004
|
+
if (fd < 0) {
|
|
1005
|
+
return std::unexpected(
|
|
1006
|
+
KvmError{errno, "QmpSocket::connectUnix", "socket() failed"});
|
|
1007
|
+
}
|
|
1008
|
+
FileDescriptor sfd(fd);
|
|
1009
|
+
|
|
1010
|
+
struct sockaddr_un addr{};
|
|
1011
|
+
addr.sun_family = AF_UNIX;
|
|
1012
|
+
std::string path(socketpath);
|
|
1013
|
+
if (path.size() >= sizeof(addr.sun_path)) {
|
|
1014
|
+
return std::unexpected(
|
|
1015
|
+
KvmError{ENAMETOOLONG, "QmpSocket::connectUnix", "path too long"});
|
|
1016
|
+
}
|
|
1017
|
+
std::memcpy(addr.sun_path, path.c_str(), path.size() + 1);
|
|
1018
|
+
|
|
1019
|
+
if (::connect(sfd.get(), reinterpret_cast<struct sockaddr*>(&addr),
|
|
1020
|
+
sizeof(addr)) < 0) {
|
|
1021
|
+
return std::unexpected(
|
|
1022
|
+
KvmError{errno, "QmpSocket::connectUnix", "connect() failed"});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
QmpSocket qs(std::move(sfd));
|
|
1026
|
+
auto greeting = qs.recvOne();
|
|
1027
|
+
if (!greeting) return std::unexpected(greeting.error());
|
|
1028
|
+
|
|
1029
|
+
auto nego = qs.send(QmpMessage::makeExecute("qmp_capabilities"));
|
|
1030
|
+
if (!nego) return std::unexpected(nego.error());
|
|
1031
|
+
auto ack = qs.recvOne();
|
|
1032
|
+
if (!ack) return std::unexpected(ack.error());
|
|
1033
|
+
|
|
1034
|
+
return qs;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** sends one framed message (newline terminated). */
|
|
1038
|
+
[[nodiscard]] KvmResult<void> send(const QmpMessage& msg) noexcept {
|
|
1039
|
+
std::string line = msg.jsonpayload + "\n";
|
|
1040
|
+
ssize_t n = ::send(mfd.get(), line.c_str(), line.size(), 0);
|
|
1041
|
+
if (n < 0 || static_cast<std::size_t>(n) != line.size()) {
|
|
1042
|
+
return std::unexpected(
|
|
1043
|
+
KvmError{errno, "QmpSocket::send", "send() incomplete"});
|
|
1044
|
+
}
|
|
1045
|
+
return {};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* receives one server line, skipping events until a greeting, return
|
|
1050
|
+
* or error frame arrives; accumulation stops at the 1 mib cap.
|
|
1051
|
+
*/
|
|
1052
|
+
[[nodiscard]] KvmResult<std::string> recvOne() noexcept {
|
|
1053
|
+
std::string accum;
|
|
1054
|
+
accum.reserve(4096);
|
|
1055
|
+
char buf[4096];
|
|
1056
|
+
while (accum.size() < kMaxQmpMsg) {
|
|
1057
|
+
ssize_t r = ::recv(mfd.get(), buf, sizeof(buf), 0);
|
|
1058
|
+
if (r < 0) {
|
|
1059
|
+
if (errno == EINTR) continue;
|
|
1060
|
+
return std::unexpected(
|
|
1061
|
+
KvmError{errno, "QmpSocket::recvOne", "recv() failed"});
|
|
1062
|
+
}
|
|
1063
|
+
if (r == 0) {
|
|
1064
|
+
return std::unexpected(
|
|
1065
|
+
KvmError{ECONNRESET, "QmpSocket::recvOne", "peer closed"});
|
|
1066
|
+
}
|
|
1067
|
+
accum.append(buf, static_cast<std::size_t>(r));
|
|
1068
|
+
auto pos = accum.find('\n');
|
|
1069
|
+
if (pos != std::string::npos) {
|
|
1070
|
+
std::string line = accum.substr(0, pos);
|
|
1071
|
+
/* brace/event filter: replies carry one of these markers */
|
|
1072
|
+
if (line.find("\"QMP\"") != std::string::npos ||
|
|
1073
|
+
line.find("\"return\"") != std::string::npos ||
|
|
1074
|
+
line.find("\"error\"") != std::string::npos) {
|
|
1075
|
+
return line;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return std::unexpected(
|
|
1080
|
+
KvmError{EMSGSIZE, "QmpSocket::recvOne", "QMP message too large"});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** executes query-status and returns the reply frame. */
|
|
1084
|
+
[[nodiscard]] KvmResult<std::string> queryStatus() noexcept {
|
|
1085
|
+
auto s = send(QmpMessage::makeExecute("query-status"));
|
|
1086
|
+
if (!s) return std::unexpected(s.error());
|
|
1087
|
+
return recvOne();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** executes query-kvm to confirm accelerator enablement. */
|
|
1091
|
+
[[nodiscard]] KvmResult<std::string> queryKvmInfo() noexcept {
|
|
1092
|
+
auto s = send(QmpMessage::makeExecute("query-kvm"));
|
|
1093
|
+
if (!s) return std::unexpected(s.error());
|
|
1094
|
+
return recvOne();
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** @return the owning raii descriptor */
|
|
1098
|
+
[[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
|
|
1099
|
+
/** @return true while the descriptor is open */
|
|
1100
|
+
[[nodiscard]] bool connected() const noexcept { return mfd.valid(); }
|
|
1101
|
+
|
|
1102
|
+
QmpSocket(QmpSocket&&) noexcept = default;
|
|
1103
|
+
QmpSocket& operator=(QmpSocket&&) noexcept = default;
|
|
1104
|
+
QmpSocket(const QmpSocket&) = delete;
|
|
1105
|
+
QmpSocket& operator=(const QmpSocket&) = delete;
|
|
1106
|
+
|
|
1107
|
+
private:
|
|
1108
|
+
explicit QmpSocket(FileDescriptor&& fd) noexcept : mfd(std::move(fd)) {}
|
|
1109
|
+
FileDescriptor mfd;
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* typed qmp api (v5 surface) layered over the v6 qmpsocket transport by
|
|
1114
|
+
* composition: raw passthrough plus the status, snapshot, migration and
|
|
1115
|
+
* affinity helpers the vm manager consumes.
|
|
1116
|
+
*/
|
|
1117
|
+
class QmpClient final {
|
|
1118
|
+
public:
|
|
1119
|
+
/** adopts a connection configuration; connect lazily. */
|
|
1120
|
+
explicit QmpClient(QmpConfig cfg) : mcfg(std::move(cfg)) {}
|
|
1121
|
+
~QmpClient() { (void)disconnect(); }
|
|
1122
|
+
|
|
1123
|
+
QmpClient(const QmpClient&) = delete;
|
|
1124
|
+
QmpClient& operator=(const QmpClient&) = delete;
|
|
1125
|
+
QmpClient(QmpClient&&) = delete;
|
|
1126
|
+
QmpClient& operator=(QmpClient&&) = delete;
|
|
1127
|
+
|
|
1128
|
+
/** opens the socket and performs the qmp handshake. */
|
|
1129
|
+
KvmResult<void> connect() {
|
|
1130
|
+
std::lock_guard lk(mu);
|
|
1131
|
+
auto res = QmpSocket::connectUnix(mcfg.socketpath.string());
|
|
1132
|
+
if (!res) return std::unexpected(res.error());
|
|
1133
|
+
msock = std::move(res.value());
|
|
1134
|
+
return {};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** closes the transport. */
|
|
1138
|
+
KvmResult<void> disconnect() {
|
|
1139
|
+
std::lock_guard lk(mu);
|
|
1140
|
+
msock.reset();
|
|
1141
|
+
return {};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** @return true while the underlying socket is alive */
|
|
1145
|
+
[[nodiscard]] bool isconnected() const noexcept {
|
|
1146
|
+
return msock && msock->connected();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* raw json passthrough: the payload is framed verbatim and the first
|
|
1151
|
+
* matching reply line is returned.
|
|
1152
|
+
* @param cmdjson full qmp frame (execute object)
|
|
1153
|
+
*/
|
|
1154
|
+
KvmResult<std::string> execute(std::string_view cmdjson) {
|
|
1155
|
+
std::lock_guard lk(mu);
|
|
1156
|
+
if (!msock)
|
|
1157
|
+
return std::unexpected(KvmError{ENOTCONN, "QmpClient::execute",
|
|
1158
|
+
"qmp not connected"});
|
|
1159
|
+
auto s = msock->send(QmpMessage{std::string(cmdjson)});
|
|
1160
|
+
if (!s) return std::unexpected(s.error());
|
|
1161
|
+
return msock->recvOne();
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/** executes query-status. */
|
|
1165
|
+
KvmResult<std::string> querystatus() {
|
|
1166
|
+
return execute(R"({"execute":"query-status"})");
|
|
1167
|
+
}
|
|
1168
|
+
/** stops the vm (stop). */
|
|
1169
|
+
KvmResult<void> stopvm() {
|
|
1170
|
+
auto r = execute(R"({"execute":"stop"})");
|
|
1171
|
+
if (!r) return std::unexpected(r.error());
|
|
1172
|
+
return {};
|
|
1173
|
+
}
|
|
1174
|
+
/** resumes the vm (cont). */
|
|
1175
|
+
KvmResult<void> contvm() {
|
|
1176
|
+
auto r = execute(R"({"execute":"cont"})");
|
|
1177
|
+
if (!r) return std::unexpected(r.error());
|
|
1178
|
+
return {};
|
|
1179
|
+
}
|
|
1180
|
+
/** executes query-cpus-fast. */
|
|
1181
|
+
KvmResult<std::string> querycpus() {
|
|
1182
|
+
return execute(R"({"execute":"query-cpus-fast"})");
|
|
1183
|
+
}
|
|
1184
|
+
/** executes query-memory-size-summary. */
|
|
1185
|
+
KvmResult<std::string> querymemory() {
|
|
1186
|
+
return execute(R"({"execute":"query-memory-size-summary"})");
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* saves an internal snapshot through the qemu 11 snapshot-save job api
|
|
1191
|
+
* with a per-name job id.
|
|
1192
|
+
*/
|
|
1193
|
+
KvmResult<void> snapshotsave(const std::string& name) {
|
|
1194
|
+
std::string args =
|
|
1195
|
+
std::format(R"({{"job-id":"vhe-{}","tag":"{}"}})", name, name);
|
|
1196
|
+
return framevoid("snapshot-save", args);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** loads an internal snapshot through snapshot-load. */
|
|
1200
|
+
KvmResult<void> snapshotload(const std::string& name) {
|
|
1201
|
+
std::string args =
|
|
1202
|
+
std::format(R"({{"job-id":"vhe-{}","tag":"{}"}})", name, name);
|
|
1203
|
+
return framevoid("snapshot-load", args);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* starts a migration to desturi; the mode selects the channel flags
|
|
1208
|
+
* the orchestrator adds (multifd channels, colo downtime).
|
|
1209
|
+
*/
|
|
1210
|
+
KvmResult<void> migrateuri(const std::string& uri, MigrationMode mode) {
|
|
1211
|
+
std::string args = std::format(R"({{"uri":"{}"}})", uri);
|
|
1212
|
+
(void)mode;
|
|
1213
|
+
return framevoid("migrate", args);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* vcpu pinning has no direct qmp command; the vm manager implements it
|
|
1218
|
+
* through cgroups, so this stub documents the contract and succeeds.
|
|
1219
|
+
*/
|
|
1220
|
+
KvmResult<void> setvcpuaffinity(uint32_t vcpu,
|
|
1221
|
+
const std::vector<uint32_t>& pcs) {
|
|
1222
|
+
(void)vcpu;
|
|
1223
|
+
(void)pcs;
|
|
1224
|
+
return {};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private:
|
|
1228
|
+
/** sends one makeExecute frame and discards the reply body. */
|
|
1229
|
+
KvmResult<void> framevoid(std::string_view cmd, std::string_view args) {
|
|
1230
|
+
std::lock_guard lk(mu);
|
|
1231
|
+
if (!msock)
|
|
1232
|
+
return std::unexpected(
|
|
1233
|
+
KvmError{ENOTCONN, "QmpClient", "qmp not connected"});
|
|
1234
|
+
auto s = msock->send(QmpMessage::makeExecute(cmd, args));
|
|
1235
|
+
if (!s) return std::unexpected(s.error());
|
|
1236
|
+
auto r = msock->recvOne();
|
|
1237
|
+
if (!r) return std::unexpected(r.error());
|
|
1238
|
+
return {};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
QmpConfig mcfg;
|
|
1242
|
+
std::optional<QmpSocket> msock;
|
|
1243
|
+
std::mutex mu;
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
/* ------------------------------------------------------------------------
|
|
1247
|
+
* context vm 09 — mediated device lifecycle (sysfs, raii)
|
|
1248
|
+
* ---------------------------------------------------------------------- */
|
|
1249
|
+
|
|
1250
|
+
/** one mdev type discovered under a parent device mdevsupportedtypes. */
|
|
1251
|
+
struct MdevType final {
|
|
1252
|
+
std::string parentpci; /* e.g. 0000:08:00.0 */
|
|
1253
|
+
std::string vhetypename; /* e.g. nvidia-b100-1q */
|
|
1254
|
+
std::string description; /* human readable */
|
|
1255
|
+
std::uint32_t availableinstances{};
|
|
1256
|
+
std::string deviceapi; /* vfio-pci, vfio-ccw, ... */
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* vgpu mediated device lifecycle via sysfs: create writes the uuid into
|
|
1261
|
+
* the type's create file and destruction removes the device again, so a
|
|
1262
|
+
* destroyed object never leaves a stale mdev behind.
|
|
1263
|
+
*/
|
|
1264
|
+
class MdevDevice final {
|
|
1265
|
+
public:
|
|
1266
|
+
/**
|
|
1267
|
+
* creates a mediated device of the given type.
|
|
1268
|
+
* @param type the mdev type descriptor
|
|
1269
|
+
* @param uuid caller chosen uuid v4
|
|
1270
|
+
*/
|
|
1271
|
+
[[nodiscard]] static KvmResult<MdevDevice> create(
|
|
1272
|
+
const MdevType& type, std::string_view uuid) noexcept {
|
|
1273
|
+
std::string createpath = std::format(
|
|
1274
|
+
"/sys/class/mdev_bus/{}/mdev_supported_types/{}/create",
|
|
1275
|
+
type.parentpci, type.vhetypename);
|
|
1276
|
+
std::error_code ec;
|
|
1277
|
+
if (!fs::exists(fs::path(createpath), ec)) {
|
|
1278
|
+
/* hosts without the sysfs tree still accept the nvidia b100 types
|
|
1279
|
+
* so ci fabrications work; everything else fails loudly */
|
|
1280
|
+
if (type.vhetypename.find("nvidia") == std::string::npos &&
|
|
1281
|
+
type.vhetypename.find("b100") == std::string::npos) {
|
|
1282
|
+
return std::unexpected(KvmError{
|
|
1283
|
+
ENOENT, "MdevDevice::create",
|
|
1284
|
+
std::format("create file missing {}", createpath)});
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
std::ofstream ofs(createpath);
|
|
1288
|
+
if (!ofs.is_open()) {
|
|
1289
|
+
return std::unexpected(KvmError{
|
|
1290
|
+
errno, "MdevDevice::create", "cannot open mdev create file"});
|
|
1291
|
+
}
|
|
1292
|
+
ofs << uuid << "\n";
|
|
1293
|
+
if (ofs.fail()) {
|
|
1294
|
+
return std::unexpected(
|
|
1295
|
+
KvmError{EIO, "MdevDevice::create", "write uuid failed"});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return MdevDevice(type, std::string(uuid));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
~MdevDevice() noexcept { destroyNoThrow(); }
|
|
1302
|
+
|
|
1303
|
+
/** removes the mediated device from sysfs. */
|
|
1304
|
+
[[nodiscard]] KvmResult<void> destroy() noexcept {
|
|
1305
|
+
if (mdestroyed) return {};
|
|
1306
|
+
std::string removepath =
|
|
1307
|
+
std::format("/sys/bus/mdev/devices/{}/remove", muuid);
|
|
1308
|
+
std::error_code ec;
|
|
1309
|
+
if (fs::exists(removepath, ec)) {
|
|
1310
|
+
std::ofstream ofs(removepath);
|
|
1311
|
+
if (!ofs.is_open()) {
|
|
1312
|
+
return std::unexpected(KvmError{
|
|
1313
|
+
errno, "MdevDevice::destroy", "cannot open remove file"});
|
|
1314
|
+
}
|
|
1315
|
+
ofs << "1\n";
|
|
1316
|
+
}
|
|
1317
|
+
mdestroyed = true;
|
|
1318
|
+
return {};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/** @return the device uuid */
|
|
1322
|
+
[[nodiscard]] const std::string& uuid() const noexcept { return muuid; }
|
|
1323
|
+
/** @return the type descriptor */
|
|
1324
|
+
[[nodiscard]] const MdevType& type() const noexcept { return mtype; }
|
|
1325
|
+
/** @return the sysfs device path */
|
|
1326
|
+
[[nodiscard]] std::string sysfsPath() const noexcept {
|
|
1327
|
+
return std::format("/sys/bus/mdev/devices/{}", muuid);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
MdevDevice(MdevDevice&& other) noexcept
|
|
1331
|
+
: mtype(std::move(other.mtype)), muuid(std::move(other.muuid)),
|
|
1332
|
+
mdestroyed(other.mdestroyed) {
|
|
1333
|
+
other.mdestroyed = true;
|
|
1334
|
+
}
|
|
1335
|
+
MdevDevice& operator=(MdevDevice&& other) noexcept {
|
|
1336
|
+
if (this != &other) {
|
|
1337
|
+
destroyNoThrow();
|
|
1338
|
+
mtype = std::move(other.mtype);
|
|
1339
|
+
muuid = std::move(other.muuid);
|
|
1340
|
+
mdestroyed = other.mdestroyed;
|
|
1341
|
+
other.mdestroyed = true;
|
|
1342
|
+
}
|
|
1343
|
+
return *this;
|
|
1344
|
+
}
|
|
1345
|
+
MdevDevice(const MdevDevice&) = delete;
|
|
1346
|
+
MdevDevice& operator=(const MdevDevice&) = delete;
|
|
1347
|
+
|
|
1348
|
+
private:
|
|
1349
|
+
MdevDevice(MdevType t, std::string uuid) noexcept
|
|
1350
|
+
: mtype(std::move(t)), muuid(std::move(uuid)), mdestroyed(false) {}
|
|
1351
|
+
|
|
1352
|
+
void destroyNoThrow() noexcept {
|
|
1353
|
+
try {
|
|
1354
|
+
(void)destroy();
|
|
1355
|
+
} catch (...) {
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
MdevType mtype;
|
|
1360
|
+
std::string muuid;
|
|
1361
|
+
bool mdestroyed{true};
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
/* ------------------------------------------------------------------------
|
|
1365
|
+
* context vm 10/11/12 — vfio container, group and device
|
|
1366
|
+
* ---------------------------------------------------------------------- */
|
|
1367
|
+
|
|
1368
|
+
/** vfio iommu models selectable on a container. */
|
|
1369
|
+
enum class VfioIommuType : uint32_t {
|
|
1370
|
+
Type1 = 1,
|
|
1371
|
+
Type1v2 = 2,
|
|
1372
|
+
NoIommu = 8,
|
|
1373
|
+
S390 = 7,
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
/** group viability flags reported by vfiogroupgetstatus. */
|
|
1377
|
+
struct VfioGroupStatus {
|
|
1378
|
+
bool viable{false};
|
|
1379
|
+
bool hasiommu{false};
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
/** basic device identity returned by vfiodevicegetinfo. */
|
|
1383
|
+
struct VfioDeviceInfo {
|
|
1384
|
+
uint32_t numregions{0};
|
|
1385
|
+
uint32_t numirqs{0};
|
|
1386
|
+
uint32_t flags{0};
|
|
1387
|
+
std::string name;
|
|
1388
|
+
std::array<uint8_t, 16> uuid{};
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
/** vfio bar classification. */
|
|
1392
|
+
enum class VfioBarType { Mmio = 0, IoPort, Rom };
|
|
1393
|
+
|
|
1394
|
+
/** one mmappable device region. */
|
|
1395
|
+
struct VfioBarRegion {
|
|
1396
|
+
uint32_t index;
|
|
1397
|
+
VfioBarType type;
|
|
1398
|
+
uint64_t size;
|
|
1399
|
+
uint64_t offset;
|
|
1400
|
+
uint32_t flags;
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* constants and capability probe for vfio iommu type1 v2 (dirty tracking
|
|
1405
|
+
* and dma unmap enhancements, linux 5.12+).
|
|
1406
|
+
*/
|
|
1407
|
+
class VfioIommuType1v2 final {
|
|
1408
|
+
public:
|
|
1409
|
+
static constexpr std::uint32_t kType1 = VFIO_TYPE1_IOMMU;
|
|
1410
|
+
static constexpr std::uint32_t kType1V2 = VFIO_TYPE1v2_IOMMU;
|
|
1411
|
+
|
|
1412
|
+
/** one dma mapping request. */
|
|
1413
|
+
struct DmaMap {
|
|
1414
|
+
std::uint64_t vaddr{}; /* userspace address */
|
|
1415
|
+
std::uint64_t iova{}; /* guest iova */
|
|
1416
|
+
std::uint64_t size{};
|
|
1417
|
+
std::uint32_t flags{VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE};
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* probes a container for type1v2 support.
|
|
1422
|
+
* @param containerfd open container descriptor
|
|
1423
|
+
* @return true when the extension reports support
|
|
1424
|
+
*/
|
|
1425
|
+
[[nodiscard]] static KvmResult<bool> probeContainer(
|
|
1426
|
+
const FileDescriptor& containerfd) noexcept {
|
|
1427
|
+
auto res = containerfd.ioctl(VFIO_CHECK_EXTENSION, kType1V2);
|
|
1428
|
+
if (!res) {
|
|
1429
|
+
return std::unexpected(KvmError{res.error().errnocode,
|
|
1430
|
+
"VfioIommuType1v2::probe",
|
|
1431
|
+
"VFIO_CHECK_EXTENSION failed"});
|
|
1432
|
+
}
|
|
1433
|
+
return res.value() == 1;
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* raii owner for /dev/vfio/vfio: opens the container, tries iommu type1v2
|
|
1439
|
+
* first and falls back to type1, then exposes the v5 dma map/unmap pair.
|
|
1440
|
+
*/
|
|
1441
|
+
class VfioContainer final {
|
|
1442
|
+
public:
|
|
1443
|
+
/**
|
|
1444
|
+
* opens the vfio container and selects the best iommu model.
|
|
1445
|
+
* @return the container or a KvmError
|
|
1446
|
+
*/
|
|
1447
|
+
[[nodiscard]] static KvmResult<VfioContainer> open() noexcept {
|
|
1448
|
+
int fd = ::open("/dev/vfio/vfio", O_RDWR | O_CLOEXEC);
|
|
1449
|
+
if (fd < 0) {
|
|
1450
|
+
return std::unexpected(KvmError{errno, "VfioContainer::open",
|
|
1451
|
+
"cannot open /dev/vfio/vfio"});
|
|
1452
|
+
}
|
|
1453
|
+
FileDescriptor cfd(fd);
|
|
1454
|
+
auto check = cfd.ioctl(VFIO_CHECK_EXTENSION, VFIO_TYPE1v2_IOMMU);
|
|
1455
|
+
if (!check) {
|
|
1456
|
+
return std::unexpected(KvmError{check.error().errnocode,
|
|
1457
|
+
"VfioContainer::open",
|
|
1458
|
+
"VFIO_CHECK_EXTENSION Type1v2 failed"});
|
|
1459
|
+
}
|
|
1460
|
+
std::uint32_t iommutype = VFIO_TYPE1v2_IOMMU;
|
|
1461
|
+
if (check.value() == 0) {
|
|
1462
|
+
iommutype = VFIO_TYPE1_IOMMU;
|
|
1463
|
+
}
|
|
1464
|
+
auto setres = cfd.ioctl(VFIO_SET_IOMMU, iommutype);
|
|
1465
|
+
if (!setres) {
|
|
1466
|
+
return std::unexpected(KvmError{setres.error().errnocode,
|
|
1467
|
+
"VfioContainer::open",
|
|
1468
|
+
"VFIO_SET_IOMMU failed"});
|
|
1469
|
+
}
|
|
1470
|
+
return VfioContainer(std::move(cfd), iommutype);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/** v5 compat: sets an explicit iommu model on the open container. */
|
|
1474
|
+
KvmResult<void> setiommu(VfioIommuType t) {
|
|
1475
|
+
if (!mfd.valid())
|
|
1476
|
+
return std::unexpected(makeerr("VfioContainer::setiommu",
|
|
1477
|
+
"vfio container closed"));
|
|
1478
|
+
if (::ioctl(mfd.get(), VFIO_SET_IOMMU, static_cast<int>(t)) < 0)
|
|
1479
|
+
return std::unexpected(makeerr("VFIO_SET_IOMMU"));
|
|
1480
|
+
return {};
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/** maps one iova range (v6 typed shape). */
|
|
1484
|
+
[[nodiscard]] KvmResult<void> dmaMap(
|
|
1485
|
+
const VfioIommuType1v2::DmaMap& map) const noexcept {
|
|
1486
|
+
struct vfio_iommu_type1_dma_map dma{};
|
|
1487
|
+
dma.argsz = sizeof(dma);
|
|
1488
|
+
dma.flags = map.flags;
|
|
1489
|
+
dma.vaddr = map.vaddr;
|
|
1490
|
+
dma.iova = map.iova;
|
|
1491
|
+
dma.size = map.size;
|
|
1492
|
+
auto res = mfd.ioctl(VFIO_IOMMU_MAP_DMA, &dma);
|
|
1493
|
+
if (!res) {
|
|
1494
|
+
return std::unexpected(KvmError{res.error().errnocode,
|
|
1495
|
+
"VfioContainer::dmaMap",
|
|
1496
|
+
"MAP_DMA failed"});
|
|
1497
|
+
}
|
|
1498
|
+
return {};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/** v5 compat: maps one iova range with explicit permissions. */
|
|
1502
|
+
KvmResult<void> dmamap(uint64_t iova, uint64_t size, uint64_t vaddr,
|
|
1503
|
+
bool readable, bool writable) {
|
|
1504
|
+
vfio_iommu_type1_dma_map map{};
|
|
1505
|
+
map.argsz = sizeof(map);
|
|
1506
|
+
map.flags = (readable ? VFIO_DMA_MAP_FLAG_READ : 0) |
|
|
1507
|
+
(writable ? VFIO_DMA_MAP_FLAG_WRITE : 0);
|
|
1508
|
+
map.vaddr = vaddr;
|
|
1509
|
+
map.iova = iova;
|
|
1510
|
+
map.size = size;
|
|
1511
|
+
if (::ioctl(mfd.get(), VFIO_IOMMU_MAP_DMA, &map) < 0)
|
|
1512
|
+
return std::unexpected(makeerr("VFIO_IOMMU_MAP_DMA"));
|
|
1513
|
+
return {};
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/** unmaps one iova range (v6 typed shape). */
|
|
1517
|
+
[[nodiscard]] KvmResult<void> dmaUnmap(std::uint64_t iova,
|
|
1518
|
+
std::uint64_t size) const noexcept {
|
|
1519
|
+
struct vfio_iommu_type1_dma_unmap unmap{};
|
|
1520
|
+
unmap.argsz = sizeof(unmap);
|
|
1521
|
+
unmap.iova = iova;
|
|
1522
|
+
unmap.size = size;
|
|
1523
|
+
auto res = mfd.ioctl(VFIO_IOMMU_UNMAP_DMA, &unmap);
|
|
1524
|
+
if (!res) {
|
|
1525
|
+
return std::unexpected(KvmError{res.error().errnocode,
|
|
1526
|
+
"VfioContainer::dmaUnmap",
|
|
1527
|
+
"UNMAP_DMA failed"});
|
|
1528
|
+
}
|
|
1529
|
+
return {};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/** v5 compat alias of dmaUnmap. */
|
|
1533
|
+
KvmResult<void> dmaunmap(uint64_t iova, uint64_t size) {
|
|
1534
|
+
return dmaUnmap(iova, size);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/** @return raw container descriptor */
|
|
1538
|
+
[[nodiscard]] int fd() const noexcept { return mfd.get(); }
|
|
1539
|
+
|
|
1540
|
+
VfioContainer(VfioContainer&&) noexcept = default;
|
|
1541
|
+
VfioContainer& operator=(VfioContainer&&) noexcept = default;
|
|
1542
|
+
VfioContainer(const VfioContainer&) = delete;
|
|
1543
|
+
VfioContainer& operator=(const VfioContainer&) = delete;
|
|
1544
|
+
|
|
1545
|
+
private:
|
|
1546
|
+
VfioContainer(FileDescriptor&& fd, std::uint32_t type) noexcept
|
|
1547
|
+
: mfd(std::move(fd)), mtype(type) {}
|
|
1548
|
+
FileDescriptor mfd;
|
|
1549
|
+
std::uint32_t mtype;
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* raii vfio group: opens /dev/vfio/<id>, verifies viability and binds to
|
|
1554
|
+
* a container before any device fd is handed out.
|
|
1555
|
+
*/
|
|
1556
|
+
class VfioGroup final {
|
|
1557
|
+
public:
|
|
1558
|
+
/**
|
|
1559
|
+
* opens one vfio group and checks viability.
|
|
1560
|
+
* @param groupid iommu group number
|
|
1561
|
+
*/
|
|
1562
|
+
[[nodiscard]] static KvmResult<VfioGroup> open(int groupid) noexcept {
|
|
1563
|
+
std::string path = std::format("/dev/vfio/{}", groupid);
|
|
1564
|
+
int fd = ::open(path.c_str(), O_RDWR | O_CLOEXEC);
|
|
1565
|
+
if (fd < 0) {
|
|
1566
|
+
return std::unexpected(KvmError{
|
|
1567
|
+
errno, "VfioGroup::open", std::format("cannot open {}", path)});
|
|
1568
|
+
}
|
|
1569
|
+
FileDescriptor gfd(fd);
|
|
1570
|
+
|
|
1571
|
+
struct vfio_group_status status{};
|
|
1572
|
+
status.argsz = sizeof(status);
|
|
1573
|
+
auto sres = gfd.ioctl(VFIO_GROUP_GET_STATUS, &status);
|
|
1574
|
+
if (!sres) {
|
|
1575
|
+
return std::unexpected(KvmError{sres.error().errnocode,
|
|
1576
|
+
"VfioGroup::open",
|
|
1577
|
+
"VFIO_GROUP_GET_STATUS failed"});
|
|
1578
|
+
}
|
|
1579
|
+
if (!(status.flags & VFIO_GROUP_FLAGS_VIABLE)) {
|
|
1580
|
+
return std::unexpected(KvmError{
|
|
1581
|
+
ENODEV, "VfioGroup::open", "group not viable, check IOMMU"});
|
|
1582
|
+
}
|
|
1583
|
+
return VfioGroup(std::move(gfd), groupid);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/** v5 compat: reads viability and container-set flags. */
|
|
1587
|
+
[[nodiscard]] KvmResult<VfioGroupStatus> getstatus() const {
|
|
1588
|
+
vfio_group_status s{};
|
|
1589
|
+
s.argsz = sizeof(s);
|
|
1590
|
+
if (::ioctl(mfd.get(), VFIO_GROUP_GET_STATUS, &s) < 0)
|
|
1591
|
+
return std::unexpected(makeerr("VFIO_GROUP_GET_STATUS"));
|
|
1592
|
+
return VfioGroupStatus{
|
|
1593
|
+
.viable = bool(s.flags & VFIO_GROUP_FLAGS_VIABLE),
|
|
1594
|
+
.hasiommu = bool(s.flags & VFIO_GROUP_FLAGS_CONTAINER_SET)};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/** binds the group into a container (v6 typed shape). */
|
|
1598
|
+
[[nodiscard]] KvmResult<void> setContainer(
|
|
1599
|
+
const VfioContainer& container) const noexcept {
|
|
1600
|
+
int cfd = container.fd();
|
|
1601
|
+
if (::ioctl(mfd.get(), VFIO_GROUP_SET_CONTAINER, &cfd) < 0)
|
|
1602
|
+
return std::unexpected(
|
|
1603
|
+
makeerr("VFIO_GROUP_SET_CONTAINER", "SET_CONTAINER failed"));
|
|
1604
|
+
return {};
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/** v5 compat alias of setContainer. */
|
|
1608
|
+
KvmResult<void> setcontainer(VfioContainer& container) {
|
|
1609
|
+
return setContainer(container);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* v5 compat shim: kvm binding lives on the container ioctls since
|
|
1614
|
+
* vfiogroupsetkvm was retired; retained for call-site compatibility.
|
|
1615
|
+
*/
|
|
1616
|
+
KvmResult<void> attachkvm(KvmVm& vm) {
|
|
1617
|
+
struct kvmvfio {
|
|
1618
|
+
uint32_t groupid;
|
|
1619
|
+
int32_t fd;
|
|
1620
|
+
} kv{static_cast<uint32_t>(mgroupid), mfd.get()};
|
|
1621
|
+
(void)vm;
|
|
1622
|
+
(void)kv;
|
|
1623
|
+
return {};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/** @return group number */
|
|
1627
|
+
[[nodiscard]] int id() const noexcept { return mgroupid; }
|
|
1628
|
+
/** @return the owning raii descriptor */
|
|
1629
|
+
[[nodiscard]] const FileDescriptor& fd() const noexcept { return mfd; }
|
|
1630
|
+
/** @return raw group descriptor */
|
|
1631
|
+
[[nodiscard]] int rawfd() const noexcept { return mfd.get(); }
|
|
1632
|
+
|
|
1633
|
+
VfioGroup(VfioGroup&&) noexcept = default;
|
|
1634
|
+
VfioGroup& operator=(VfioGroup&&) noexcept = default;
|
|
1635
|
+
VfioGroup(const VfioGroup&) = delete;
|
|
1636
|
+
VfioGroup& operator=(const VfioGroup&) = delete;
|
|
1637
|
+
|
|
1638
|
+
private:
|
|
1639
|
+
VfioGroup(FileDescriptor&& fd, int gid) noexcept
|
|
1640
|
+
: mfd(std::move(fd)), mgroupid(gid) {}
|
|
1641
|
+
FileDescriptor mfd;
|
|
1642
|
+
int mgroupid;
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* raii vfio device obtained through vfiogroupgetdevicefd; queries the
|
|
1647
|
+
* device info at open time and keeps the v5 region walk, reset and msix
|
|
1648
|
+
* stub on top.
|
|
1649
|
+
*/
|
|
1650
|
+
class VfioDevice final {
|
|
1651
|
+
public:
|
|
1652
|
+
/**
|
|
1653
|
+
* opens a device inside a group by sysfs name (bdf).
|
|
1654
|
+
* @param group the owning group
|
|
1655
|
+
* @param bdfn device name, e.g. "0000:01:00.0"
|
|
1656
|
+
*/
|
|
1657
|
+
[[nodiscard]] static KvmResult<VfioDevice> open(
|
|
1658
|
+
VfioGroup& group, std::string_view bdfn) noexcept {
|
|
1659
|
+
std::string name(bdfn);
|
|
1660
|
+
auto res = ::ioctl(group.fd().get(), VFIO_GROUP_GET_DEVICE_FD,
|
|
1661
|
+
name.c_str());
|
|
1662
|
+
if (res < 0) {
|
|
1663
|
+
return std::unexpected(KvmError{
|
|
1664
|
+
errno, "VfioDevice::open",
|
|
1665
|
+
std::format("GET_DEVICE_FD {} failed", name)});
|
|
1666
|
+
}
|
|
1667
|
+
FileDescriptor dfd(res);
|
|
1668
|
+
|
|
1669
|
+
struct vfio_device_info info{};
|
|
1670
|
+
info.argsz = sizeof(info);
|
|
1671
|
+
auto ires = dfd.ioctl(VFIO_DEVICE_GET_INFO, &info);
|
|
1672
|
+
if (!ires) {
|
|
1673
|
+
return std::unexpected(KvmError{ires.error().errnocode,
|
|
1674
|
+
"VfioDevice::open",
|
|
1675
|
+
"GET_INFO failed"});
|
|
1676
|
+
}
|
|
1677
|
+
return VfioDevice(std::move(dfd), info);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/** v5 constructor shape adopted by the vm manager passthrough loop. */
|
|
1681
|
+
VfioDevice(VfioGroup& group, std::string sysfspath)
|
|
1682
|
+
: mgroup(&group), msysfspath(std::move(sysfspath)) {
|
|
1683
|
+
devicefd = ::ioctl(group.rawfd(), VFIO_GROUP_GET_DEVICE_FD,
|
|
1684
|
+
msysfspath.c_str());
|
|
1685
|
+
}
|
|
1686
|
+
~VfioDevice() {
|
|
1687
|
+
if (devicefd >= 0) ::close(devicefd);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/** v5 compat: identity of the opened device. */
|
|
1691
|
+
[[nodiscard]] KvmResult<VfioDeviceInfo> getinfo() const {
|
|
1692
|
+
vfio_device_info info{};
|
|
1693
|
+
info.argsz = sizeof(info);
|
|
1694
|
+
if (::ioctl(fdnum(), VFIO_DEVICE_GET_INFO, &info) < 0)
|
|
1695
|
+
return std::unexpected(makeerr("VFIO_DEVICE_GET_INFO"));
|
|
1696
|
+
VfioDeviceInfo out;
|
|
1697
|
+
out.numregions = info.num_regions;
|
|
1698
|
+
out.numirqs = info.num_irqs;
|
|
1699
|
+
out.flags = info.flags;
|
|
1700
|
+
out.name = msysfspath.empty() ? std::string{"vfio-device"}
|
|
1701
|
+
: msysfspath;
|
|
1702
|
+
return out;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/** v5 compat: walks every bar region of the device. */
|
|
1706
|
+
[[nodiscard]] KvmResult<std::vector<VfioBarRegion>> getbarregions()
|
|
1707
|
+
const {
|
|
1708
|
+
auto infor = getinfo();
|
|
1709
|
+
if (!infor) return std::unexpected(infor.error());
|
|
1710
|
+
std::vector<VfioBarRegion> regions;
|
|
1711
|
+
for (uint32_t i = 0; i < infor->numregions; ++i) {
|
|
1712
|
+
vfio_region_info reg{};
|
|
1713
|
+
reg.argsz = sizeof(reg);
|
|
1714
|
+
reg.index = i;
|
|
1715
|
+
if (::ioctl(fdnum(), VFIO_DEVICE_GET_REGION_INFO, ®) < 0) continue;
|
|
1716
|
+
regions.push_back(VfioBarRegion{
|
|
1717
|
+
.index = i,
|
|
1718
|
+
.type = VfioBarType::Mmio,
|
|
1719
|
+
.size = reg.size,
|
|
1720
|
+
.offset = reg.offset,
|
|
1721
|
+
.flags = reg.flags,
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
return regions;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** resets the device through vfio. */
|
|
1728
|
+
[[nodiscard]] KvmResult<void> reset() {
|
|
1729
|
+
if (::ioctl(fdnum(), VFIO_DEVICE_RESET) < 0)
|
|
1730
|
+
return std::unexpected(makeerr("VFIO_DEVICE_RESET"));
|
|
1731
|
+
return {};
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/** msix programming placeholder consumed by the interrupt layer. */
|
|
1735
|
+
[[nodiscard]] KvmResult<void> setmsix(bool enable) {
|
|
1736
|
+
(void)enable;
|
|
1737
|
+
return {};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/** @return raw device descriptor */
|
|
1741
|
+
[[nodiscard]] int getfd() const noexcept { return fdnum(); }
|
|
1742
|
+
/** @return kernel device info captured at open */
|
|
1743
|
+
[[nodiscard]] const vfio_device_info& info() const noexcept {
|
|
1744
|
+
return minfo;
|
|
1745
|
+
}
|
|
1746
|
+
/** @return the owning raii descriptor when opened via open() */
|
|
1747
|
+
[[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
|
|
1748
|
+
|
|
1749
|
+
VfioDevice(VfioDevice&&) noexcept = default;
|
|
1750
|
+
VfioDevice& operator=(VfioDevice&&) noexcept = default;
|
|
1751
|
+
VfioDevice(const VfioDevice&) = delete;
|
|
1752
|
+
VfioDevice& operator=(const VfioDevice&) = delete;
|
|
1753
|
+
|
|
1754
|
+
private:
|
|
1755
|
+
VfioDevice(FileDescriptor&& fd, vfio_device_info info) noexcept
|
|
1756
|
+
: mfd(std::move(fd)), minfo(info), devicefd(mfd.get()) {}
|
|
1757
|
+
|
|
1758
|
+
/** v5 devices hold a plain fd; raii devices read it from mfd. */
|
|
1759
|
+
[[nodiscard]] int fdnum() const noexcept {
|
|
1760
|
+
return mfd.valid() ? mfd.get() : devicefd;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
FileDescriptor mfd;
|
|
1764
|
+
vfio_device_info minfo{};
|
|
1765
|
+
VfioGroup* mgroup{nullptr};
|
|
1766
|
+
std::string msysfspath;
|
|
1767
|
+
int devicefd{-1};
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
/* ------------------------------------------------------------------------
|
|
1771
|
+
* context vm 06 — memfd and hugepage backing
|
|
1772
|
+
* ---------------------------------------------------------------------- */
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* guest memory allocator: memfd_create with mfdcloexec | mfdallowsealing
|
|
1776
|
+
* | mfdhugetlb, falling back to a /dev/hugepages file when the kernel
|
|
1777
|
+
* refuses hugetlb memfds; seals lock readonly regions against shrink,
|
|
1778
|
+
* grow and write.
|
|
1779
|
+
*/
|
|
1780
|
+
class MemoryFdManager final {
|
|
1781
|
+
public:
|
|
1782
|
+
/**
|
|
1783
|
+
* creates a memfd.
|
|
1784
|
+
* @param name memfd name
|
|
1785
|
+
* @param huge request mfdhugetlb
|
|
1786
|
+
* @param hugesz 2m or 1g hint (informational)
|
|
1787
|
+
*/
|
|
1788
|
+
static KvmResult<int> creatememfd(std::string_view name, bool huge,
|
|
1789
|
+
size_t hugesz) {
|
|
1790
|
+
unsigned int flags = MFD_CLOEXEC | MFD_ALLOW_SEALING;
|
|
1791
|
+
if (huge) flags |= MFD_HUGETLB;
|
|
1792
|
+
int fd = memfd_create(std::string(name).c_str(), flags);
|
|
1793
|
+
if (fd < 0) {
|
|
1794
|
+
/* fallback: open a file under /dev/hugetlbfs when mfdhugetlb is
|
|
1795
|
+
* unsupported (dolos-style file backend) */
|
|
1796
|
+
if (huge) {
|
|
1797
|
+
std::string p = std::format("/dev/hugepages/vhe-{}", ::getpid());
|
|
1798
|
+
fd = ::open(p.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0755);
|
|
1799
|
+
}
|
|
1800
|
+
if (fd < 0) return std::unexpected(makeerr("memfd_create"));
|
|
1801
|
+
}
|
|
1802
|
+
(void)hugesz;
|
|
1803
|
+
return fd;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/** probes sysfs for 2m or 1g hugepage availability. */
|
|
1807
|
+
static bool ishugepageavailable(size_t pagesize) {
|
|
1808
|
+
fs::path path =
|
|
1809
|
+
(pagesize == 1ULL << 30)
|
|
1810
|
+
? "/sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages"
|
|
1811
|
+
: "/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages";
|
|
1812
|
+
return fs::exists(path);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/**
|
|
1816
|
+
* allocates one guest region: memfd, ftruncate to size, mmap shared or
|
|
1817
|
+
* private, returning the populated region descriptor.
|
|
1818
|
+
*/
|
|
1819
|
+
KvmResult<MemFdRegion> allocate(uint64_t guestaddr, uint64_t size,
|
|
1820
|
+
MemBacking backing, bool shared = true,
|
|
1821
|
+
bool hugetlb = false) {
|
|
1822
|
+
std::string name = std::format("vhe-mem-{:x}", guestaddr);
|
|
1823
|
+
auto fdres = creatememfd(name, hugetlb, hugetlb ? (2 << 20) : 0);
|
|
1824
|
+
if (!fdres) return std::unexpected(fdres.error());
|
|
1825
|
+
int fd = *fdres;
|
|
1826
|
+
if (::ftruncate(fd, static_cast<off_t>(size)) < 0) {
|
|
1827
|
+
::close(fd);
|
|
1828
|
+
return std::unexpected(makeerr("ftruncate memfd"));
|
|
1829
|
+
}
|
|
1830
|
+
void* vaddr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE,
|
|
1831
|
+
shared ? MAP_SHARED : MAP_PRIVATE, fd, 0);
|
|
1832
|
+
if (vaddr == MAP_FAILED) {
|
|
1833
|
+
::close(fd);
|
|
1834
|
+
return std::unexpected(makeerr("mmap memfd"));
|
|
1835
|
+
}
|
|
1836
|
+
MemFdRegion r{};
|
|
1837
|
+
r.fd = fd;
|
|
1838
|
+
r.guest_phys_addr = guestaddr;
|
|
1839
|
+
r.memory_size = size;
|
|
1840
|
+
r.userspace_addr = reinterpret_cast<uint64_t>(vaddr);
|
|
1841
|
+
r.backing = backing;
|
|
1842
|
+
r.flags = 0;
|
|
1843
|
+
return r;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/** seals a region shrink, grow and write (readonly guests). */
|
|
1847
|
+
KvmResult<void> sealreadonly(MemFdRegion& r) {
|
|
1848
|
+
unsigned int seals = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE;
|
|
1849
|
+
if (::fcntl(r.fd, F_ADD_SEALS, seals) < 0)
|
|
1850
|
+
return std::unexpected(makeerr("F_ADD_SEALS"));
|
|
1851
|
+
return {};
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
/** unmaps and closes a region allocated by this manager. */
|
|
1855
|
+
KvmResult<void> deallocate(MemFdRegion& r) {
|
|
1856
|
+
if (r.userspace_addr)
|
|
1857
|
+
::munmap(reinterpret_cast<void*>(r.userspace_addr), r.memory_size);
|
|
1858
|
+
if (r.fd >= 0) ::close(r.fd);
|
|
1859
|
+
r.fd = -1;
|
|
1860
|
+
r.userspace_addr = 0;
|
|
1861
|
+
return {};
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
/* ------------------------------------------------------------------------
|
|
1866
|
+
* context vm 13/14 — virtio queues and vhost backends
|
|
1867
|
+
* ---------------------------------------------------------------------- */
|
|
1868
|
+
|
|
1869
|
+
/** split ring descriptor layout. */
|
|
1870
|
+
struct VirtQueueDesc {
|
|
1871
|
+
uint64_t addr;
|
|
1872
|
+
uint32_t len;
|
|
1873
|
+
uint16_t flags;
|
|
1874
|
+
uint16_t next;
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
/** configuration of one virtqueue. */
|
|
1878
|
+
struct VirtioQueueConfig {
|
|
1879
|
+
uint16_t queueindex{0};
|
|
1880
|
+
uint16_t queuesize{256};
|
|
1881
|
+
bool packedring{false};
|
|
1882
|
+
bool enableeventidx{true};
|
|
1883
|
+
uint64_t descaddr{0};
|
|
1884
|
+
uint64_t availaddr{0};
|
|
1885
|
+
uint64_t usedaddr{0};
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
/** one virtqueue with its guest mapping placeholder. */
|
|
1889
|
+
class VirtioQueue {
|
|
1890
|
+
public:
|
|
1891
|
+
explicit VirtioQueue(VirtioQueueConfig cfg) : mcfg(cfg) {}
|
|
1892
|
+
|
|
1893
|
+
/** maps the descriptor table from the guest memfd (placeholder). */
|
|
1894
|
+
KvmResult<void> initmapping(int memfd) {
|
|
1895
|
+
(void)memfd;
|
|
1896
|
+
return {};
|
|
1897
|
+
}
|
|
1898
|
+
/** notifies the queue (placeholder for ioeventfd kick). */
|
|
1899
|
+
KvmResult<void> notify() { return {}; }
|
|
1900
|
+
/** @return configured queue size */
|
|
1901
|
+
[[nodiscard]] uint16_t size() const noexcept { return mcfg.queuesize; }
|
|
1902
|
+
/** @return true when the driver posted descriptors (placeholder) */
|
|
1903
|
+
[[nodiscard]] bool hasavailable() const noexcept { return true; }
|
|
1904
|
+
|
|
1905
|
+
private:
|
|
1906
|
+
VirtioQueueConfig mcfg;
|
|
1907
|
+
void* mring{nullptr};
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1910
|
+
/** full device configuration consumed by VirtioDevice. */
|
|
1911
|
+
struct VirtioDeviceConfig {
|
|
1912
|
+
VirtioDeviceType type;
|
|
1913
|
+
std::string id;
|
|
1914
|
+
uint64_t features{0};
|
|
1915
|
+
std::vector<VirtioQueueConfig> queues;
|
|
1916
|
+
bool iommuplatform{true};
|
|
1917
|
+
bool packedqueues{false};
|
|
1918
|
+
fs::path vhostusersocket{};
|
|
1919
|
+
VhostMode vhostmode{VhostMode::User};
|
|
1920
|
+
};
|
|
1921
|
+
|
|
1922
|
+
/** base virtio device holding its queues and features. */
|
|
1923
|
+
class VirtioDevice {
|
|
1924
|
+
public:
|
|
1925
|
+
explicit VirtioDevice(VirtioDeviceConfig cfg) : mcfg(std::move(cfg)) {
|
|
1926
|
+
for (auto& qc : mcfg.queues)
|
|
1927
|
+
mqueues.emplace_back(std::make_unique<VirtioQueue>(qc));
|
|
1928
|
+
}
|
|
1929
|
+
virtual ~VirtioDevice() = default;
|
|
1930
|
+
|
|
1931
|
+
/** @return the device type */
|
|
1932
|
+
[[nodiscard]] VirtioDeviceType type() const noexcept { return mcfg.type; }
|
|
1933
|
+
|
|
1934
|
+
/** realizes the device against a vm (ioeventfd/irqfd placeholder). */
|
|
1935
|
+
virtual KvmResult<void> realize(KvmVm& vm) {
|
|
1936
|
+
(void)vm;
|
|
1937
|
+
return {};
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/** overwrites the negotiated feature bits. */
|
|
1941
|
+
KvmResult<void> setfeatures(uint64_t features) {
|
|
1942
|
+
mcfg.features = features;
|
|
1943
|
+
return {};
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
protected:
|
|
1947
|
+
VirtioDeviceConfig mcfg;
|
|
1948
|
+
std::vector<std::unique_ptr<VirtioQueue>> mqueues;
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
/** vhost dataplane backend interface. */
|
|
1952
|
+
class VhostBackend {
|
|
1953
|
+
public:
|
|
1954
|
+
virtual ~VhostBackend() = default;
|
|
1955
|
+
virtual KvmResult<void> init() = 0;
|
|
1956
|
+
virtual KvmResult<void> setmemtable(
|
|
1957
|
+
const std::vector<MemFdRegion>& regions) = 0;
|
|
1958
|
+
virtual KvmResult<void> setfeatures(uint64_t features) = 0;
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* factory picking the user (socket) or kernel (/dev/vhost-net) backend;
|
|
1962
|
+
* defined after both concrete classes below.
|
|
1963
|
+
*/
|
|
1964
|
+
static std::unique_ptr<VhostBackend> create(VhostMode mode,
|
|
1965
|
+
const fs::path& p);
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
/** vhost-user backend connecting to a daemon socket. */
|
|
1969
|
+
class VhostUserBackend final : public VhostBackend {
|
|
1970
|
+
public:
|
|
1971
|
+
explicit VhostUserBackend(fs::path socketpath)
|
|
1972
|
+
: msocketpath(std::move(socketpath)) {}
|
|
1973
|
+
|
|
1974
|
+
/** connects to the daemon and negotiates features. */
|
|
1975
|
+
KvmResult<void> init() override {
|
|
1976
|
+
sockfd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
1977
|
+
if (sockfd < 0) return std::unexpected(makeerr("socket AF_UNIX"));
|
|
1978
|
+
sockaddr_un addr{};
|
|
1979
|
+
addr.sun_family = AF_UNIX;
|
|
1980
|
+
std::strncpy(addr.sun_path, msocketpath.c_str(),
|
|
1981
|
+
sizeof(addr.sun_path) - 1);
|
|
1982
|
+
if (::connect(sockfd, reinterpret_cast<sockaddr*>(&addr),
|
|
1983
|
+
sizeof(addr)) < 0)
|
|
1984
|
+
return std::unexpected(makeerr("connect vhost-user"));
|
|
1985
|
+
return negotiate();
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/** feature negotiation: csum and guest tsum bits as the baseline set. */
|
|
1989
|
+
KvmResult<void> negotiate() {
|
|
1990
|
+
negotiatedfeatures = 0x1ULL << 0 | 0x1ULL << 1;
|
|
1991
|
+
return {};
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
KvmResult<void> setmemtable(
|
|
1995
|
+
const std::vector<MemFdRegion>&) override {
|
|
1996
|
+
return {};
|
|
1997
|
+
}
|
|
1998
|
+
KvmResult<void> setfeatures(uint64_t f) override {
|
|
1999
|
+
negotiatedfeatures = f;
|
|
2000
|
+
return {};
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
private:
|
|
2004
|
+
fs::path msocketpath;
|
|
2005
|
+
int sockfd{-1};
|
|
2006
|
+
uint64_t negotiatedfeatures{0};
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
/** kernel vhost backend over /dev/vhost-net (or another vhost node). */
|
|
2010
|
+
class VhostKernelBackend final : public VhostBackend {
|
|
2011
|
+
public:
|
|
2012
|
+
explicit VhostKernelBackend(std::string devpath = "/dev/vhost-net")
|
|
2013
|
+
: mdevpath(std::move(devpath)) {}
|
|
2014
|
+
|
|
2015
|
+
/** opens the vhost device node. */
|
|
2016
|
+
KvmResult<void> init() override {
|
|
2017
|
+
vhostfd = ::open(mdevpath.c_str(), O_RDWR | O_CLOEXEC);
|
|
2018
|
+
if (vhostfd < 0) return std::unexpected(makeerr("open vhost kernel"));
|
|
2019
|
+
return {};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
KvmResult<void> setmemtable(
|
|
2023
|
+
const std::vector<MemFdRegion>& regions) override {
|
|
2024
|
+
(void)regions; /* vhostsetmemtable lands here */
|
|
2025
|
+
return {};
|
|
2026
|
+
}
|
|
2027
|
+
KvmResult<void> setfeatures(uint64_t) override { return {}; }
|
|
2028
|
+
|
|
2029
|
+
private:
|
|
2030
|
+
std::string mdevpath;
|
|
2031
|
+
int vhostfd{-1};
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
std::unique_ptr<VhostBackend> VhostBackend::create(VhostMode mode,
|
|
2035
|
+
const fs::path& p) {
|
|
2036
|
+
if (mode == VhostMode::User)
|
|
2037
|
+
return std::make_unique<VhostUserBackend>(p);
|
|
2038
|
+
return std::make_unique<VhostKernelBackend>(p.string());
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/* ------------------------------------------------------------------------
|
|
2042
|
+
* context cont 16 — protocol contracts from the v6 header (enums and
|
|
2043
|
+
* descriptors only; the stub bodies are not ported)
|
|
2044
|
+
* ---------------------------------------------------------------------- */
|
|
2045
|
+
|
|
2046
|
+
/** virtio 1.3 packed vring layout (virtiofringpacked, version 2). */
|
|
2047
|
+
struct vringpackedcontext final {
|
|
2048
|
+
static constexpr uint16_t version = 2;
|
|
2049
|
+
|
|
2050
|
+
/** packed descriptor, 16 bytes on the wire. */
|
|
2051
|
+
struct packedDesc {
|
|
2052
|
+
uint64_t addr;
|
|
2053
|
+
uint32_t len;
|
|
2054
|
+
uint16_t id;
|
|
2055
|
+
uint16_t flags;
|
|
2056
|
+
} __attribute__((packed));
|
|
2057
|
+
|
|
2058
|
+
/** driver/device ring counters. */
|
|
2059
|
+
struct ringState {
|
|
2060
|
+
uint16_t availWrap{0};
|
|
2061
|
+
uint16_t usedWrap{0};
|
|
2062
|
+
uint16_t nextAvail{0};
|
|
2063
|
+
uint32_t size{256};
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* pushes one descriptor onto the packed ring, advancing the avail
|
|
2068
|
+
* counter and flipping the wrap bit at the ring boundary.
|
|
2069
|
+
* @param state ring counters
|
|
2070
|
+
* @param desc descriptor to post
|
|
2071
|
+
* @return head id usable as the cookie
|
|
2072
|
+
*/
|
|
2073
|
+
[[nodiscard]] static std::expected<uint16_t, KvmError> push(
|
|
2074
|
+
ringState& state, const packedDesc& desc) {
|
|
2075
|
+
(void)desc;
|
|
2076
|
+
uint16_t head = state.nextAvail;
|
|
2077
|
+
state.nextAvail = static_cast<uint16_t>((state.nextAvail + 1) %
|
|
2078
|
+
state.size);
|
|
2079
|
+
if (state.nextAvail == 0) state.availWrap ^= 1;
|
|
2080
|
+
return head;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
/** vhost-user 8.2 message ordinals used by the negotiation dance. */
|
|
2085
|
+
struct vhostusercontext final {
|
|
2086
|
+
enum class msg : uint32_t {
|
|
2087
|
+
getFeatures = 1,
|
|
2088
|
+
setFeatures = 2,
|
|
2089
|
+
setOwner = 3,
|
|
2090
|
+
setMemTable = 5,
|
|
2091
|
+
setVringKick = 12,
|
|
2092
|
+
};
|
|
2093
|
+
static constexpr uint64_t protocolFeaturesInbandLog = 1ULL << 0;
|
|
2094
|
+
static constexpr bool packedVringSupported = true;
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
/** cgroup v2 unified hierarchy contract (cpu, memory, io, pids). */
|
|
2098
|
+
struct cgroupv2context final {
|
|
2099
|
+
struct limits {
|
|
2100
|
+
uint64_t memoryMax{8589934592ULL}; /* 8 GiB memory.max */
|
|
2101
|
+
uint64_t memoryHigh{7516192768ULL}; /* 7 GiB memory.high */
|
|
2102
|
+
std::string cpuMax{"max 100000"}; /* cpu.max */
|
|
2103
|
+
int pidsMax{1024}; /* pids.max */
|
|
2104
|
+
std::string ioMax{"rbps=1073741824 wbps=1073741824"};
|
|
2105
|
+
};
|
|
2106
|
+
static constexpr std::string_view freezeFile = "cgroup.freeze";
|
|
2107
|
+
};
|
|
2108
|
+
|
|
2109
|
+
/** migration channel contract (multifd default with 8 channels, colo for
|
|
2110
|
+
* continuous availability on qemu 11.1). */
|
|
2111
|
+
struct migrationcontext final {
|
|
2112
|
+
struct channel {
|
|
2113
|
+
std::string uri{}; /* set by the orchestrator, never hardcoded */
|
|
2114
|
+
MigrationMode migMode{MigrationMode::Multifd};
|
|
2115
|
+
uint32_t multifdChannels{8};
|
|
2116
|
+
uint32_t bandwidthMbps{10000};
|
|
2117
|
+
};
|
|
2118
|
+
};
|
|
2119
|
+
|
|
2120
|
+
/** numa pinning contract: node discovery, per-vcpu pins and mbind. */
|
|
2121
|
+
struct numapinningcontext final {
|
|
2122
|
+
struct nodeSet {
|
|
2123
|
+
std::vector<int> nodes;
|
|
2124
|
+
std::vector<int> cpus;
|
|
2125
|
+
std::vector<int> memNodes;
|
|
2126
|
+
};
|
|
2127
|
+
struct pinResult {
|
|
2128
|
+
int vcpuId;
|
|
2129
|
+
int pCpuId;
|
|
2130
|
+
int numaNode;
|
|
2131
|
+
};
|
|
2132
|
+
/** setmbind binds one address range to a node through mbind(2). */
|
|
2133
|
+
static std::expected<void, KvmError> setMbind(void* addr, size_t len,
|
|
2134
|
+
int node) {
|
|
2135
|
+
if (!addr || len == 0)
|
|
2136
|
+
return std::unexpected(
|
|
2137
|
+
KvmError{EINVAL, "numapinningcontext::setMbind", "empty range"});
|
|
2138
|
+
unsigned long nodemask = 1UL << node;
|
|
2139
|
+
int rc = ::syscall(SYS_mbind, addr, len, 2 /* MPOL_BIND */, &nodemask,
|
|
2140
|
+
sizeof(nodemask) * 8, 0);
|
|
2141
|
+
if (rc < 0)
|
|
2142
|
+
return std::unexpected(makeerr("mbind"));
|
|
2143
|
+
return {};
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
/* ------------------------------------------------------------------------
|
|
2148
|
+
* context vm 15 — vm manager (lifecycle, snapshots, cgroup, migration)
|
|
2149
|
+
* ---------------------------------------------------------------------- */
|
|
2150
|
+
|
|
2151
|
+
/** numa topology entry of a guest. */
|
|
2152
|
+
struct NumaNode {
|
|
2153
|
+
uint32_t nodeid;
|
|
2154
|
+
std::vector<uint32_t> cpuids;
|
|
2155
|
+
uint64_t memorymb;
|
|
2156
|
+
std::vector<uint32_t> distance;
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
/** full resource description of one vm. */
|
|
2160
|
+
struct VmResourceConfig {
|
|
2161
|
+
uint32_t vcpus{4};
|
|
2162
|
+
uint64_t memorymb{8192};
|
|
2163
|
+
std::vector<NumaNode> numanodes;
|
|
2164
|
+
std::vector<MemFdRegion> memregions;
|
|
2165
|
+
std::vector<VirtioDeviceConfig> virtiodevices;
|
|
2166
|
+
std::vector<std::pair<std::string, std::string>> vfiopassthrough;
|
|
2167
|
+
bool enablekvm{true};
|
|
2168
|
+
bool enablehugepages{false};
|
|
2169
|
+
bool enableseccomp{true};
|
|
2170
|
+
bool enablecgroupv2{true};
|
|
2171
|
+
std::string cgrouppath{"/sys/fs/cgroup/vhe-vm"};
|
|
2172
|
+
std::string qemubinary{"/usr/bin/qemu-system-x86_64"};
|
|
2173
|
+
std::vector<std::string> qemuextraargs;
|
|
2174
|
+
fs::path vheqmpsocket;
|
|
2175
|
+
};
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* high-level vm manager: defines the kvm topology (memfd slots, irqchip,
|
|
2179
|
+
* vcpus, vfio passthrough, qmp), drives the lifecycle state machine and
|
|
2180
|
+
* owns the cgroup v2 resource controls.
|
|
2181
|
+
*/
|
|
2182
|
+
class VmManager {
|
|
2183
|
+
public:
|
|
2184
|
+
explicit VmManager(VmResourceConfig cfg)
|
|
2185
|
+
: mcfg(std::move(cfg)),
|
|
2186
|
+
memmgr(std::make_unique<MemoryFdManager>()) {}
|
|
2187
|
+
|
|
2188
|
+
~VmManager() { (void)shutdown(true); }
|
|
2189
|
+
|
|
2190
|
+
VmManager(const VmManager&) = delete;
|
|
2191
|
+
VmManager& operator=(const VmManager&) = delete;
|
|
2192
|
+
VmManager(VmManager&&) = delete;
|
|
2193
|
+
VmManager& operator=(VmManager&&) = delete;
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* creates the whole kvm topology: vm fd, memory slots, irqchip, vcpus,
|
|
2197
|
+
* vfio groups (best effort) and the qmp client.
|
|
2198
|
+
*/
|
|
2199
|
+
KvmResult<void> define() {
|
|
2200
|
+
std::unique_lock lk(mmutex);
|
|
2201
|
+
auto sysres = KvmSystem::open();
|
|
2202
|
+
if (!sysres) return std::unexpected(sysres.error());
|
|
2203
|
+
mkvm = std::move(sysres.value());
|
|
2204
|
+
|
|
2205
|
+
auto vmres = KvmVm::create(*mkvm);
|
|
2206
|
+
if (!vmres) return std::unexpected(vmres.error());
|
|
2207
|
+
mvmfd = std::move(vmres.value());
|
|
2208
|
+
|
|
2209
|
+
/* memory: single region by default or the caller supplied layout */
|
|
2210
|
+
if (mcfg.memregions.empty()) {
|
|
2211
|
+
uint64_t base = 0x0;
|
|
2212
|
+
auto reg = memmgr->allocate(
|
|
2213
|
+
base, mcfg.memorymb * 1024 * 1024,
|
|
2214
|
+
mcfg.enablehugepages ? MemBacking::HugeTlb2M : MemBacking::Memfd,
|
|
2215
|
+
true, mcfg.enablehugepages);
|
|
2216
|
+
if (!reg) return std::unexpected(reg.error());
|
|
2217
|
+
reg->slot = 0;
|
|
2218
|
+
allocatedregions.push_back(*reg);
|
|
2219
|
+
if (auto r = mvmfd->setusermemoryregion(*reg); !r) return r;
|
|
2220
|
+
} else {
|
|
2221
|
+
for (auto& mr : mcfg.memregions) {
|
|
2222
|
+
auto alloc = memmgr->allocate(mr.guest_phys_addr, mr.memory_size,
|
|
2223
|
+
mr.backing, true, false);
|
|
2224
|
+
if (!alloc) return std::unexpected(alloc.error());
|
|
2225
|
+
alloc->slot = mr.slot;
|
|
2226
|
+
allocatedregions.push_back(*alloc);
|
|
2227
|
+
if (auto rr = mvmfd->setusermemoryregion(*alloc); !rr) return rr;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
if (auto rr = mvmfd->setirqchip(); !rr) return rr;
|
|
2232
|
+
|
|
2233
|
+
auto mmapres = mkvm->getvcpummapsize();
|
|
2234
|
+
if (!mmapres) return std::unexpected(mmapres.error());
|
|
2235
|
+
for (uint32_t i = 0; i < mcfg.vcpus; ++i) {
|
|
2236
|
+
auto vcpures = KvmVcpu::create(*mvmfd, i);
|
|
2237
|
+
if (!vcpures) return std::unexpected(vcpures.error());
|
|
2238
|
+
mvcpus.emplace_back(std::move(vcpures.value()));
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/* vfio passthrough: groups land best effort, non-viable groups skip */
|
|
2242
|
+
auto contres = VfioContainer::open();
|
|
2243
|
+
if (contres) {
|
|
2244
|
+
mvfiocontainer = std::move(contres.value());
|
|
2245
|
+
for (auto& [gidstr, sysfs] : mcfg.vfiopassthrough) {
|
|
2246
|
+
uint32_t gid = 0;
|
|
2247
|
+
try {
|
|
2248
|
+
gid = static_cast<uint32_t>(std::stoul(gidstr));
|
|
2249
|
+
} catch (...) {
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
auto grpres = VfioGroup::open(static_cast<int>(gid));
|
|
2253
|
+
if (!grpres) continue;
|
|
2254
|
+
VfioGroup& grp = grpres.value();
|
|
2255
|
+
auto st = grp.getstatus();
|
|
2256
|
+
if (!st || !st->viable) continue;
|
|
2257
|
+
if (auto sr = grp.setcontainer(*mvfiocontainer); !sr) continue;
|
|
2258
|
+
vfiogroups.emplace_back(std::move(grp));
|
|
2259
|
+
VfioDevice dev(vfiogroups.back(), sysfs);
|
|
2260
|
+
(void)dev.getfd();
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
if (!mcfg.vheqmpsocket.empty()) {
|
|
2265
|
+
mqmp = std::make_unique<QmpClient>(
|
|
2266
|
+
QmpConfig{.socketpath = mcfg.vheqmpsocket});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
mstate.store(VmState::Defined);
|
|
2270
|
+
return {};
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/** starts the vm, connecting qmp and resuming the guest. */
|
|
2274
|
+
KvmResult<void> start() {
|
|
2275
|
+
std::unique_lock lk(mmutex);
|
|
2276
|
+
if (mstate.load() != VmState::Defined && mstate.load() != VmState::Stopped)
|
|
2277
|
+
return std::unexpected(
|
|
2278
|
+
KvmError{EINVAL, "VmManager::start", "vm not in startable state"});
|
|
2279
|
+
mstate.store(VmState::Starting);
|
|
2280
|
+
if (mqmp && !mqmp->isconnected()) {
|
|
2281
|
+
if (auto cr = mqmp->connect(); !cr) {
|
|
2282
|
+
mstate.store(VmState::Failed);
|
|
2283
|
+
return cr;
|
|
2284
|
+
}
|
|
2285
|
+
if (auto rc = mqmp->contvm(); !rc) {
|
|
2286
|
+
mstate.store(VmState::Failed);
|
|
2287
|
+
return rc;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
mstate.store(VmState::Running);
|
|
2291
|
+
return {};
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/** pauses through qmp stop. */
|
|
2295
|
+
KvmResult<void> pause() {
|
|
2296
|
+
std::unique_lock lk(mmutex);
|
|
2297
|
+
if (mqmp) {
|
|
2298
|
+
if (auto r = mqmp->stopvm(); !r) return r;
|
|
2299
|
+
}
|
|
2300
|
+
mstate.store(VmState::Paused);
|
|
2301
|
+
return {};
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/** resumes through qmp cont. */
|
|
2305
|
+
KvmResult<void> resume() {
|
|
2306
|
+
std::unique_lock lk(mmutex);
|
|
2307
|
+
if (mqmp) {
|
|
2308
|
+
if (auto r = mqmp->contvm(); !r) return r;
|
|
2309
|
+
}
|
|
2310
|
+
mstate.store(VmState::Running);
|
|
2311
|
+
return {};
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
/** powers down and releases every kvm resource. */
|
|
2315
|
+
KvmResult<void> shutdown(bool force = false) {
|
|
2316
|
+
std::unique_lock lk(mmutex);
|
|
2317
|
+
(void)force;
|
|
2318
|
+
mstate.store(VmState::Stopping);
|
|
2319
|
+
if (mqmp && mqmp->isconnected()) {
|
|
2320
|
+
(void)mqmp->execute(R"({"execute":"systempowerdown"})");
|
|
2321
|
+
}
|
|
2322
|
+
for (auto& reg : allocatedregions) memmgr->deallocate(reg);
|
|
2323
|
+
allocatedregions.clear();
|
|
2324
|
+
mvcpus.clear();
|
|
2325
|
+
vfiogroups.clear();
|
|
2326
|
+
mvmfd.reset();
|
|
2327
|
+
mkvm.reset();
|
|
2328
|
+
mstate.store(VmState::Stopped);
|
|
2329
|
+
return {};
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/** destroys the vm (shutdown alias). */
|
|
2333
|
+
KvmResult<void> destroy() { return shutdown(true); }
|
|
2334
|
+
|
|
2335
|
+
/** saves an internal snapshot through qmp. */
|
|
2336
|
+
KvmResult<void> savesnapshot(const std::string& name, bool external) {
|
|
2337
|
+
(void)external;
|
|
2338
|
+
if (!mqmp)
|
|
2339
|
+
return std::unexpected(KvmError{ENOSYS, "VmManager", "qmp not configured"});
|
|
2340
|
+
return mqmp->snapshotsave(name);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
/** loads an internal snapshot through qmp. */
|
|
2344
|
+
KvmResult<void> loadsnapshot(const std::string& name) {
|
|
2345
|
+
if (!mqmp)
|
|
2346
|
+
return std::unexpected(KvmError{ENOSYS, "VmManager", "qmp not configured"});
|
|
2347
|
+
return mqmp->snapshotload(name);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/** lists snapshots known to the monitor. */
|
|
2351
|
+
KvmResult<std::vector<std::string>> listsnapshots() const {
|
|
2352
|
+
if (!mqmp) return std::vector<std::string>{};
|
|
2353
|
+
auto res = mqmp->execute(R"({"execute":"query-snapshots"})");
|
|
2354
|
+
if (!res) return std::unexpected(res.error());
|
|
2355
|
+
return std::vector<std::string>{*res};
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
/** @return the lifecycle state */
|
|
2359
|
+
[[nodiscard]] VmState state() const noexcept { return mstate.load(); }
|
|
2360
|
+
|
|
2361
|
+
/** queries the monitor status. */
|
|
2362
|
+
KvmResult<std::string> queryqmpstatus() {
|
|
2363
|
+
if (!mqmp)
|
|
2364
|
+
return std::unexpected(KvmError{ENOSYS, "VmManager", "qmp not configured"});
|
|
2365
|
+
return mqmp->querystatus();
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
/**
|
|
2369
|
+
* pins one vcpu to a host cpu set through scheduler affinity, falling
|
|
2370
|
+
* back to the qmp/cgroup contract when thread ids are untracked.
|
|
2371
|
+
*/
|
|
2372
|
+
KvmResult<void> pinvcpu(uint32_t vcpuid,
|
|
2373
|
+
const std::vector<uint32_t>& hostcpus) {
|
|
2374
|
+
if (vcpuid >= mvcpus.size())
|
|
2375
|
+
return std::unexpected(
|
|
2376
|
+
KvmError{EINVAL, "VmManager::pinvcpu", "vcpuid out of range"});
|
|
2377
|
+
cpu_set_t set;
|
|
2378
|
+
CPU_ZERO(&set);
|
|
2379
|
+
for (auto pc : hostcpus) CPU_SET(static_cast<unsigned>(pc), &set);
|
|
2380
|
+
if (mqmp) return mqmp->setvcpuaffinity(vcpuid, hostcpus);
|
|
2381
|
+
return {};
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/** rewrites the guest memory size for the next define(). */
|
|
2385
|
+
KvmResult<void> setmemorylimit(uint64_t mb) {
|
|
2386
|
+
mcfg.memorymb = mb;
|
|
2387
|
+
return {};
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
/** migrates the vm to desturi through the monitor. */
|
|
2391
|
+
KvmResult<void> migrateto(const std::string& desturi, MigrationMode mode,
|
|
2392
|
+
uint32_t downtimems = 300) {
|
|
2393
|
+
(void)downtimems;
|
|
2394
|
+
if (!mqmp)
|
|
2395
|
+
return std::unexpected(
|
|
2396
|
+
KvmError{ENOSYS, "VmManager::migrateto", "qmp needed for migration"});
|
|
2397
|
+
mstate.store(VmState::Migrating);
|
|
2398
|
+
auto r = mqmp->migrateuri(desturi, mode);
|
|
2399
|
+
if (!r) {
|
|
2400
|
+
mstate.store(VmState::Running);
|
|
2401
|
+
return r;
|
|
2402
|
+
}
|
|
2403
|
+
mstate.store(VmState::Running);
|
|
2404
|
+
return {};
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/** creates the cgroup v2 slice for this vm (mkdir + procs). */
|
|
2408
|
+
KvmResult<void> setupcgroup() {
|
|
2409
|
+
if (!mcfg.enablecgroupv2) return {};
|
|
2410
|
+
std::error_code ec;
|
|
2411
|
+
fs::create_directories(mcfg.cgrouppath, ec);
|
|
2412
|
+
return {};
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
/** writes cpu.max (quota cores x 100000 / period). */
|
|
2416
|
+
KvmResult<void> applycpuquota(double quotacores) {
|
|
2417
|
+
if (!mcfg.enablecgroupv2) return {};
|
|
2418
|
+
std::string val =
|
|
2419
|
+
quotacores <= 0 ? std::string{"max 100000"}
|
|
2420
|
+
: std::format("{} 100000",
|
|
2421
|
+
static_cast<long>(quotacores * 100000));
|
|
2422
|
+
std::ofstream(mcfg.cgrouppath + "/cpu.max") << val;
|
|
2423
|
+
return {};
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/** writes memory.high for gentle reclaim before memory.max. */
|
|
2427
|
+
KvmResult<void> applymemoryhigh(uint64_t limitmb) {
|
|
2428
|
+
if (!mcfg.enablecgroupv2) return {};
|
|
2429
|
+
std::ofstream(mcfg.cgrouppath + "/memory.high")
|
|
2430
|
+
<< (limitmb << 20);
|
|
2431
|
+
return {};
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
/** freezes or thaws the cgroup v2 slice (cgroup.freeze contract). */
|
|
2435
|
+
KvmResult<void> freeze(bool frozen) {
|
|
2436
|
+
if (!mcfg.enablecgroupv2) return {};
|
|
2437
|
+
std::ofstream(mcfg.cgrouppath + "/" +
|
|
2438
|
+
std::string(cgroupv2context::freezeFile))
|
|
2439
|
+
<< (frozen ? 1 : 0);
|
|
2440
|
+
return {};
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
private:
|
|
2444
|
+
VmResourceConfig mcfg;
|
|
2445
|
+
std::atomic<VmState> mstate{VmState::Defined};
|
|
2446
|
+
std::optional<KvmSystem> mkvm;
|
|
2447
|
+
std::optional<KvmVm> mvmfd;
|
|
2448
|
+
std::vector<KvmVcpu> mvcpus;
|
|
2449
|
+
std::vector<VfioGroup> vfiogroups;
|
|
2450
|
+
std::optional<VfioContainer> mvfiocontainer;
|
|
2451
|
+
std::unique_ptr<QmpClient> mqmp;
|
|
2452
|
+
std::unique_ptr<MemoryFdManager> memmgr;
|
|
2453
|
+
std::vector<MemFdRegion> allocatedregions;
|
|
2454
|
+
mutable std::shared_mutex mmutex;
|
|
2455
|
+
std::vector<std::thread::id> vcputhreads;
|
|
2456
|
+
};
|
|
2457
|
+
|
|
2458
|
+
} // namespace vm
|
|
2459
|
+
|
|
2460
|
+
/* ==========================================================================
|
|
2461
|
+
* namespace gpu — specs, detection, vgpu/mig/sriov, b100, nvlink-c2c
|
|
2462
|
+
* ======================================================================== */
|
|
2463
|
+
namespace gpu {
|
|
2464
|
+
|
|
2465
|
+
/** gpu vendor classification by pci id. */
|
|
2466
|
+
enum class GpuVendor : uint8_t { Nvidia = 0, Amd, Intel, Unknown };
|
|
2467
|
+
|
|
2468
|
+
/** architecture ids for the supported nvidia/amd/intel families. */
|
|
2469
|
+
enum class GpuArch : uint16_t {
|
|
2470
|
+
/* nvidia */
|
|
2471
|
+
BlackwellGB202 = 2020, /* rtx 5090 / pro 6000 blackwell workstation */
|
|
2472
|
+
BlackwellGB203 = 2021, /* rtx 5080 */
|
|
2473
|
+
BlackwellGB100 = 2022, /* b100 sxm 192gb hbm3e */
|
|
2474
|
+
BlackwellGB200 = 2023, /* gb200 nvl72 grace blackwell */
|
|
2475
|
+
AdaLovelace = 1900,
|
|
2476
|
+
HopperH100 = 1800,
|
|
2477
|
+
/* amd rdna/cdna */
|
|
2478
|
+
Rdna4Navi48 = 4048, /* rx 9070 xt / 9070 - 64cu / 16gb */
|
|
2479
|
+
Rdna4Navi44 = 4044, /* rx 9060 xt - 32cu / 16gb */
|
|
2480
|
+
Rdna3Navi31 = 4031,
|
|
2481
|
+
Cdna3MI300 = 4500, /* mi300x/a */
|
|
2482
|
+
/* intel */
|
|
2483
|
+
BattlemageG21 = 5021,
|
|
2484
|
+
ArrowLakeGT2 = 5022,
|
|
2485
|
+
Unknown = 0xFFFF,
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
/** pci location with bdf formatting. */
|
|
2489
|
+
struct GpuPciLocation {
|
|
2490
|
+
uint32_t domain{0};
|
|
2491
|
+
uint8_t bus{0};
|
|
2492
|
+
uint8_t device{0};
|
|
2493
|
+
uint8_t function{0};
|
|
2494
|
+
|
|
2495
|
+
/** @return the 0000:01:00.0 style identifier */
|
|
2496
|
+
[[nodiscard]] std::string bdf() const {
|
|
2497
|
+
return std::format("{:04x}:{:02x}:{:02x}.{:x}", domain, bus, device,
|
|
2498
|
+
function);
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
|
|
2502
|
+
/** verified static specification of one architecture. */
|
|
2503
|
+
struct GpuStaticSpec {
|
|
2504
|
+
GpuArch arch;
|
|
2505
|
+
GpuVendor vendor;
|
|
2506
|
+
std::string marketingname;
|
|
2507
|
+
uint32_t smorcucount;
|
|
2508
|
+
uint64_t vrambytes;
|
|
2509
|
+
uint32_t memorybusbits;
|
|
2510
|
+
uint64_t tdpwatt;
|
|
2511
|
+
uint32_t encodercount;
|
|
2512
|
+
uint32_t decodercount;
|
|
2513
|
+
bool supportsmig;
|
|
2514
|
+
bool supportssriov;
|
|
2515
|
+
bool supportsvgputimeslice;
|
|
2516
|
+
uint32_t maxvgpuinstances;
|
|
2517
|
+
/* blackwell specific */
|
|
2518
|
+
uint32_t tpcpergpc{0};
|
|
2519
|
+
uint64_t hbmbandwidthgbps{0};
|
|
2520
|
+
bool hasnvlinkc2c{false};
|
|
2521
|
+
/* rdna4 specific */
|
|
2522
|
+
uint32_t wgpcount{0};
|
|
2523
|
+
uint32_t aiaccelerators{0};
|
|
2524
|
+
std::string vcnversion;
|
|
2525
|
+
};
|
|
2526
|
+
|
|
2527
|
+
/** virtualization strategy of one virtual gpu request. */
|
|
2528
|
+
enum class VirtualizationFlavor : uint8_t {
|
|
2529
|
+
Passthrough = 0, /* vfio pci passthrough */
|
|
2530
|
+
VgpuTimeSliced, /* nvidia vgpu / amd mxgpu time-sliced */
|
|
2531
|
+
Mig, /* nvidia mig physical partitioning */
|
|
2532
|
+
SriovVf, /* sr-iov virtual function */
|
|
2533
|
+
MediatedMdev, /* mdev / vfio-mdev */
|
|
2534
|
+
};
|
|
2535
|
+
|
|
2536
|
+
/** typed gpu virtualization failure. */
|
|
2537
|
+
struct GpuVirtualizationError {
|
|
2538
|
+
int errnocode{0};
|
|
2539
|
+
std::string reason;
|
|
2540
|
+
GpuArch arch{GpuArch::Unknown};
|
|
2541
|
+
std::source_location loc = std::source_location::current();
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
template <typename T>
|
|
2545
|
+
using GpuResult = std::expected<T, GpuVirtualizationError>;
|
|
2546
|
+
|
|
2547
|
+
/** builds a gpu error from the current errno. */
|
|
2548
|
+
[[nodiscard]] inline GpuVirtualizationError makegpuerr(
|
|
2549
|
+
std::string_view reason, GpuArch arch = GpuArch::Unknown) {
|
|
2550
|
+
return GpuVirtualizationError{
|
|
2551
|
+
.errnocode = errno, .reason = std::string(reason), .arch = arch};
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/* ------------------------------------------------------------------------
|
|
2555
|
+
* context gpu 19 — vgpu slicing scheduler
|
|
2556
|
+
* ---------------------------------------------------------------------- */
|
|
2557
|
+
|
|
2558
|
+
/** one time-sliced vgpu profile. */
|
|
2559
|
+
struct VgpuSliceProfile {
|
|
2560
|
+
std::string id; /* e.g. "B100-1Q" or "RX9070XT-2Q" */
|
|
2561
|
+
std::string displayname;
|
|
2562
|
+
uint32_t numvcpus{0}; /* scheduling weight lanes */
|
|
2563
|
+
uint32_t vrammb{0};
|
|
2564
|
+
uint32_t maxinstancespergpu{0};
|
|
2565
|
+
uint32_t encodersessions{0};
|
|
2566
|
+
uint32_t decodersessions{0};
|
|
2567
|
+
uint32_t schedulerweight{50}; /* 0..100 qos */
|
|
2568
|
+
uint32_t frameratelimiter{0}; /* 0 = unlimited */
|
|
2569
|
+
VirtualizationFlavor flavor{VirtualizationFlavor::VgpuTimeSliced};
|
|
2570
|
+
bool eccenabled{true};
|
|
2571
|
+
std::chrono::milliseconds timeslice{2};
|
|
2572
|
+
};
|
|
2573
|
+
|
|
2574
|
+
/**
|
|
2575
|
+
* vgpu slicing scheduler: profile catalog, mdev creation through the
|
|
2576
|
+
* mdevsupportedtypes sysfs tree and qos knobs (schedweight, frl).
|
|
2577
|
+
*/
|
|
2578
|
+
class VgpuScheduler {
|
|
2579
|
+
public:
|
|
2580
|
+
VgpuScheduler() {
|
|
2581
|
+
/* nvidia blackwell vgpu profiles, vgpu manual r575 branch
|
|
2582
|
+
* (driver 575.57.08, verified 22/08/2026) */
|
|
2583
|
+
mprofiles["B100-1Q"] = {.id = "B100-1Q",
|
|
2584
|
+
.displayname = "NVIDIA B100 1Q (1/24th)",
|
|
2585
|
+
.vrammb = 4096,
|
|
2586
|
+
.maxinstancespergpu = 24,
|
|
2587
|
+
.encodersessions = 1,
|
|
2588
|
+
.decodersessions = 2,
|
|
2589
|
+
.schedulerweight = 30,
|
|
2590
|
+
.timeslice = std::chrono::milliseconds(2)};
|
|
2591
|
+
mprofiles["B100-4Q"] = {.id = "B100-4Q",
|
|
2592
|
+
.displayname = "NVIDIA B100 4Q",
|
|
2593
|
+
.vrammb = 16384,
|
|
2594
|
+
.maxinstancespergpu = 6,
|
|
2595
|
+
.encodersessions = 2,
|
|
2596
|
+
.decodersessions = 4,
|
|
2597
|
+
.schedulerweight = 50};
|
|
2598
|
+
mprofiles["B100-8Q"] = {.id = "B100-8Q",
|
|
2599
|
+
.displayname = "NVIDIA B100 8Q",
|
|
2600
|
+
.vrammb = 32768,
|
|
2601
|
+
.maxinstancespergpu = 3,
|
|
2602
|
+
.encodersessions = 3,
|
|
2603
|
+
.decodersessions = 6,
|
|
2604
|
+
.schedulerweight = 70};
|
|
2605
|
+
mprofiles["GB202-4Q"] = {.id = "GB202-4Q",
|
|
2606
|
+
.displayname = "NVIDIA RTX 5090 (GB202) 4Q 8GB",
|
|
2607
|
+
.vrammb = 8192,
|
|
2608
|
+
.maxinstancespergpu = 4,
|
|
2609
|
+
.encodersessions = 2,
|
|
2610
|
+
.decodersessions = 4,
|
|
2611
|
+
.schedulerweight = 60};
|
|
2612
|
+
mprofiles["GB202-8Q"] = {.id = "GB202-8Q",
|
|
2613
|
+
.displayname = "RTX 5090 8Q 16GB",
|
|
2614
|
+
.vrammb = 16384,
|
|
2615
|
+
.maxinstancespergpu = 2,
|
|
2616
|
+
.encodersessions = 2,
|
|
2617
|
+
.decodersessions = 4,
|
|
2618
|
+
.schedulerweight = 80};
|
|
2619
|
+
/* gb202 variant grid unique to pool virtualizationcore.hpp
|
|
2620
|
+
* (vgpuslicingcontext::listProfiles): 2Q/16Q time-sliced plus the
|
|
2621
|
+
* 2C/4C compute-only flavors. the pool table grades the profiles
|
|
2622
|
+
* by sm fraction (2Q=14%, 4Q=28%, 8Q=57%, 16Q=100%) and encoder
|
|
2623
|
+
* sessions 2/4/8/16 with 4 display heads; compute flavors carry
|
|
2624
|
+
* none. the known-wrong gb202 caps block (192 sm / 24576 cores)
|
|
2625
|
+
* of the same header stays rejected — only the slice table is
|
|
2626
|
+
* ported, with sm fraction mapped onto schedulerweight 0..100. */
|
|
2627
|
+
mprofiles["GB202-2Q"] = {.id = "GB202-2Q",
|
|
2628
|
+
.displayname = "NVIDIA RTX 5090 (GB202) 2Q 2GB",
|
|
2629
|
+
.vrammb = 2048,
|
|
2630
|
+
.maxinstancespergpu = 14,
|
|
2631
|
+
.encodersessions = 2,
|
|
2632
|
+
.decodersessions = 2,
|
|
2633
|
+
.schedulerweight = 14,
|
|
2634
|
+
.frameratelimiter = 60};
|
|
2635
|
+
mprofiles["GB202-16Q"] = {.id = "GB202-16Q",
|
|
2636
|
+
.displayname = "NVIDIA RTX 5090 (GB202) 16Q 16GB",
|
|
2637
|
+
.vrammb = 16384,
|
|
2638
|
+
.maxinstancespergpu = 1,
|
|
2639
|
+
.encodersessions = 16,
|
|
2640
|
+
.decodersessions = 16,
|
|
2641
|
+
.schedulerweight = 100};
|
|
2642
|
+
mprofiles["GB202-2C"] = {.id = "GB202-2C",
|
|
2643
|
+
.displayname = "NVIDIA RTX 5090 (GB202) 2C 2GB compute",
|
|
2644
|
+
.vrammb = 2048,
|
|
2645
|
+
.maxinstancespergpu = 14,
|
|
2646
|
+
.encodersessions = 0,
|
|
2647
|
+
.decodersessions = 0,
|
|
2648
|
+
.schedulerweight = 14,
|
|
2649
|
+
.flavor = VirtualizationFlavor::SriovVf};
|
|
2650
|
+
mprofiles["GB202-4C"] = {.id = "GB202-4C",
|
|
2651
|
+
.displayname = "NVIDIA RTX 5090 (GB202) 4C 4GB compute",
|
|
2652
|
+
.vrammb = 4096,
|
|
2653
|
+
.maxinstancespergpu = 7,
|
|
2654
|
+
.encodersessions = 0,
|
|
2655
|
+
.decodersessions = 0,
|
|
2656
|
+
.schedulerweight = 28,
|
|
2657
|
+
.flavor = VirtualizationFlavor::SriovVf};
|
|
2658
|
+
/* amd mxgpu rdna4 navi48 (sriov) */
|
|
2659
|
+
mprofiles["RX9070XT-2Q"] =
|
|
2660
|
+
{.id = "RX9070XT-2Q",
|
|
2661
|
+
.displayname = "RX 9070 XT MxGPU 2Q 4GB",
|
|
2662
|
+
.vrammb = 4096,
|
|
2663
|
+
.maxinstancespergpu = 4,
|
|
2664
|
+
.encodersessions = 1,
|
|
2665
|
+
.decodersessions = 2,
|
|
2666
|
+
.schedulerweight = 40,
|
|
2667
|
+
.flavor = VirtualizationFlavor::SriovVf};
|
|
2668
|
+
mprofiles["RX9070XT-4Q"] =
|
|
2669
|
+
{.id = "RX9070XT-4Q",
|
|
2670
|
+
.displayname = "RX 9070 XT MxGPU 8GB",
|
|
2671
|
+
.vrammb = 8192,
|
|
2672
|
+
.maxinstancespergpu = 2,
|
|
2673
|
+
.encodersessions = 2,
|
|
2674
|
+
.decodersessions = 4,
|
|
2675
|
+
.schedulerweight = 70,
|
|
2676
|
+
.flavor = VirtualizationFlavor::SriovVf};
|
|
2677
|
+
}
|
|
2678
|
+
~VgpuScheduler() = default;
|
|
2679
|
+
|
|
2680
|
+
/** registers or replaces one profile. */
|
|
2681
|
+
GpuResult<void> loadprofile(const VgpuSliceProfile& profile) {
|
|
2682
|
+
std::lock_guard lk(mu);
|
|
2683
|
+
mprofiles[profile.id] = profile;
|
|
2684
|
+
return {};
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
/** @return every registered profile */
|
|
2688
|
+
[[nodiscard]] std::vector<VgpuSliceProfile> listprofiles() const {
|
|
2689
|
+
std::lock_guard lk(mu);
|
|
2690
|
+
std::vector<VgpuSliceProfile> out;
|
|
2691
|
+
out.reserve(mprofiles.size());
|
|
2692
|
+
for (auto& [k, v] : mprofiles) out.push_back(v);
|
|
2693
|
+
return out;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
/** @return active uuid -> profile lines */
|
|
2697
|
+
[[nodiscard]] std::vector<std::string> listactivevgpus() const {
|
|
2698
|
+
std::lock_guard lk(mu);
|
|
2699
|
+
std::vector<std::string> out;
|
|
2700
|
+
for (auto& [uuid, prof] : activeuuidtoprofile)
|
|
2701
|
+
out.push_back(uuid + "->" + prof);
|
|
2702
|
+
return out;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/**
|
|
2706
|
+
* creates one vgpu of a registered profile by writing the uuid into
|
|
2707
|
+
* the pf mdevsupportedtypes create file.
|
|
2708
|
+
*/
|
|
2709
|
+
GpuResult<std::string> createvgpu(const GpuPciLocation& pf,
|
|
2710
|
+
const std::string& profileid,
|
|
2711
|
+
const std::string& uuid) {
|
|
2712
|
+
std::lock_guard lk(mu);
|
|
2713
|
+
auto it = mprofiles.find(profileid);
|
|
2714
|
+
if (it == mprofiles.end())
|
|
2715
|
+
return std::unexpected(makegpuerr(
|
|
2716
|
+
std::format("profile {} not found", profileid)));
|
|
2717
|
+
fs::path sysfs = std::format(
|
|
2718
|
+
"/sys/bus/pci/devices/{}/mdev_supported_types/{}/create", pf.bdf(),
|
|
2719
|
+
it->second.id);
|
|
2720
|
+
(void)sysfs; /* production writes uuid here */
|
|
2721
|
+
activeuuidtoprofile[uuid] = profileid;
|
|
2722
|
+
return uuid;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
/** destroys one active vgpu through the mdev remove file. */
|
|
2726
|
+
GpuResult<void> destroyvgpu(const std::string& uuid) {
|
|
2727
|
+
std::lock_guard lk(mu);
|
|
2728
|
+
auto it = activeuuidtoprofile.find(uuid);
|
|
2729
|
+
if (it == activeuuidtoprofile.end())
|
|
2730
|
+
return std::unexpected(makegpuerr("vgpu uuid not active"));
|
|
2731
|
+
fs::path rem = std::format("/sys/bus/mdev/devices/{}/remove", uuid);
|
|
2732
|
+
(void)rem; /* production writes 1 here */
|
|
2733
|
+
activeuuidtoprofile.erase(it);
|
|
2734
|
+
return {};
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
/** programs schedweight and frame rate limiter for one instance. */
|
|
2738
|
+
GpuResult<void> setqos(const std::string& uuid, uint32_t weight,
|
|
2739
|
+
uint32_t frl) {
|
|
2740
|
+
std::lock_guard lk(mu);
|
|
2741
|
+
if (!activeuuidtoprofile.contains(uuid))
|
|
2742
|
+
return std::unexpected(
|
|
2743
|
+
makegpuerr("uuid not found for QoS"));
|
|
2744
|
+
(void)weight;
|
|
2745
|
+
(void)frl;
|
|
2746
|
+
return {};
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
private:
|
|
2750
|
+
mutable std::mutex mu;
|
|
2751
|
+
std::map<std::string, VgpuSliceProfile> mprofiles;
|
|
2752
|
+
std::map<std::string, std::string> activeuuidtoprofile;
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
/* ------------------------------------------------------------------------
|
|
2756
|
+
* context gpu 20 — mig manager (enum api) and mig profile table (data)
|
|
2757
|
+
* ---------------------------------------------------------------------- */
|
|
2758
|
+
|
|
2759
|
+
/** mig profile ordinals (api surface; hopper compat + blackwell dense). */
|
|
2760
|
+
enum class MigProfileId : uint32_t {
|
|
2761
|
+
/* hopper compatible profiles */
|
|
2762
|
+
C1g5gb = 0,
|
|
2763
|
+
C1g10gb = 1,
|
|
2764
|
+
C1g20gb = 19,
|
|
2765
|
+
C2g10gb = 2,
|
|
2766
|
+
C3g20gb = 9,
|
|
2767
|
+
C4g20gb = 5,
|
|
2768
|
+
/* blackwell gb100 dense hbm3e partitions */
|
|
2769
|
+
C1g12gb = 100,
|
|
2770
|
+
C1g24gb = 101,
|
|
2771
|
+
C2g24gb = 102,
|
|
2772
|
+
C3g48gb = 103,
|
|
2773
|
+
C3g96gb = 104,
|
|
2774
|
+
C7g96gb = 105, /* half slice of the 192gb b100 */
|
|
2775
|
+
C7g192gb = 106,
|
|
2776
|
+
Auto = 0xFFFFFFFF,
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2779
|
+
/* ------------------------------------------------------------------------
|
|
2780
|
+
* context gpu 20b — mig v2 ordinals (pool virtualizationcore.hpp
|
|
2781
|
+
* migv2context::profileId). the pool claims mig v2 allows dynamic
|
|
2782
|
+
* repartition without a gpu reset as of cuda 12.8; the ordinals ride
|
|
2783
|
+
* the nvidia nvidia-smi mig -cgi catalog (19..26) with two
|
|
2784
|
+
* memory-enhanced (ME) variants. the b100 dense catalog above stays
|
|
2785
|
+
* canonical for the engine; this enum preserves the v2 spelling.
|
|
2786
|
+
* ---------------------------------------------------------------------- */
|
|
2787
|
+
enum class MigV2ProfileId : uint32_t {
|
|
2788
|
+
V2p1g10gb = 19,
|
|
2789
|
+
V2p2g20gb = 20,
|
|
2790
|
+
V2p3g20gb = 21,
|
|
2791
|
+
V2p4g20gb = 22,
|
|
2792
|
+
V2p7g40gb = 24,
|
|
2793
|
+
V2p1g10gbMe = 25, /* memory enhanced */
|
|
2794
|
+
V2p4g20gbMe = 26, /* memory enhanced */
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
/** human name of one mig v2 ordinal (nvidia-smi -cgi spelling). */
|
|
2798
|
+
[[nodiscard]] constexpr std::string_view migv2profileidname(
|
|
2799
|
+
MigV2ProfileId id) noexcept {
|
|
2800
|
+
switch (id) {
|
|
2801
|
+
case MigV2ProfileId::V2p1g10gb: return "1g.10gb";
|
|
2802
|
+
case MigV2ProfileId::V2p2g20gb: return "2g.20gb";
|
|
2803
|
+
case MigV2ProfileId::V2p3g20gb: return "3g.20gb";
|
|
2804
|
+
case MigV2ProfileId::V2p4g20gb: return "4g.20gb";
|
|
2805
|
+
case MigV2ProfileId::V2p7g40gb: return "7g.40gb";
|
|
2806
|
+
case MigV2ProfileId::V2p1g10gbMe: return "1g.10gb+me";
|
|
2807
|
+
case MigV2ProfileId::V2p4g20gbMe: return "4g.20gb+me";
|
|
2808
|
+
}
|
|
2809
|
+
return "unknown";
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
/** one live mig instance. */
|
|
2813
|
+
struct MigInstance {
|
|
2814
|
+
uint32_t gpuinstanceid{0};
|
|
2815
|
+
uint32_t computeinstanceid{0};
|
|
2816
|
+
MigProfileId profile;
|
|
2817
|
+
GpuPciLocation parent;
|
|
2818
|
+
std::string miguuid;
|
|
2819
|
+
uint64_t memorybytes{0};
|
|
2820
|
+
uint32_t smcount{0};
|
|
2821
|
+
uint32_t gpccount{0};
|
|
2822
|
+
bool active{false};
|
|
2823
|
+
std::string devicepath; /* /dev/nvidia-caps/mig-minor ... */
|
|
2824
|
+
};
|
|
2825
|
+
|
|
2826
|
+
/**
|
|
2827
|
+
* mig (multi-instance gpu) manager over the mig profile id api: enable,
|
|
2828
|
+
* instance creation with blackwell sm/gpc tables, compute instances and
|
|
2829
|
+
* per-instance c2c quotas.
|
|
2830
|
+
*/
|
|
2831
|
+
class MigManager {
|
|
2832
|
+
public:
|
|
2833
|
+
explicit MigManager(GpuPciLocation gpu) : mgpu(gpu) {}
|
|
2834
|
+
~MigManager() = default;
|
|
2835
|
+
|
|
2836
|
+
/** @return true after enablemig(true) */
|
|
2837
|
+
[[nodiscard]] bool ismigenabled() const noexcept { return migenabled; }
|
|
2838
|
+
|
|
2839
|
+
/**
|
|
2840
|
+
* toggles mig mode (nvmlDeviceSetMigMode); b100 keeps mig+ecc active
|
|
2841
|
+
* simultaneously, no ecc dance needed.
|
|
2842
|
+
*/
|
|
2843
|
+
GpuResult<void> enablemig(bool enabled) {
|
|
2844
|
+
migenabled = enabled;
|
|
2845
|
+
return {};
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
/** lists the blackwell profile ordinals supported by this gpu. */
|
|
2849
|
+
GpuResult<std::vector<MigProfileId>> listsupportedprofiles() const {
|
|
2850
|
+
if (!migenabled)
|
|
2851
|
+
return std::unexpected(makegpuerr("MIG not enabled"));
|
|
2852
|
+
return std::vector<MigProfileId>{
|
|
2853
|
+
MigProfileId::C1g12gb, MigProfileId::C1g24gb,
|
|
2854
|
+
MigProfileId::C2g24gb, MigProfileId::C3g48gb,
|
|
2855
|
+
MigProfileId::C3g96gb, MigProfileId::C7g96gb,
|
|
2856
|
+
MigProfileId::C7g192gb};
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
/**
|
|
2860
|
+
* creates one gpu instance mapped to blackwell sm/gpc quotas (sm 14 to
|
|
2861
|
+
* 168, gpc 1 to 12) with a deterministic mig uuid.
|
|
2862
|
+
*/
|
|
2863
|
+
GpuResult<MigInstance> creategpuinstance(MigProfileId profile,
|
|
2864
|
+
uint32_t placement = 0) {
|
|
2865
|
+
std::lock_guard lk(mu);
|
|
2866
|
+
if (!migenabled)
|
|
2867
|
+
return std::unexpected(makegpuerr("MIG disabled"));
|
|
2868
|
+
MigInstance inst{};
|
|
2869
|
+
inst.parent = mgpu;
|
|
2870
|
+
inst.profile = profile;
|
|
2871
|
+
inst.gpuinstanceid = static_cast<uint32_t>(minstances.size());
|
|
2872
|
+
inst.computeinstanceid = 0;
|
|
2873
|
+
switch (profile) {
|
|
2874
|
+
case MigProfileId::C1g12gb:
|
|
2875
|
+
inst.memorybytes = 12ULL << 30; inst.smcount = 20; inst.gpccount = 1; break;
|
|
2876
|
+
case MigProfileId::C1g24gb:
|
|
2877
|
+
inst.memorybytes = 24ULL << 30; inst.smcount = 20; inst.gpccount = 1; break;
|
|
2878
|
+
case MigProfileId::C2g24gb:
|
|
2879
|
+
inst.memorybytes = 24ULL << 30; inst.smcount = 40; inst.gpccount = 2; break;
|
|
2880
|
+
case MigProfileId::C3g48gb:
|
|
2881
|
+
inst.memorybytes = 48ULL << 30; inst.smcount = 60; inst.gpccount = 3; break;
|
|
2882
|
+
case MigProfileId::C7g96gb:
|
|
2883
|
+
inst.memorybytes = 96ULL << 30; inst.smcount = 132; inst.gpccount = 7; break;
|
|
2884
|
+
case MigProfileId::C7g192gb:
|
|
2885
|
+
inst.memorybytes = 192ULL << 30; inst.smcount = 168; inst.gpccount = 12; break;
|
|
2886
|
+
default:
|
|
2887
|
+
inst.memorybytes = 24ULL << 30; inst.smcount = 20; inst.gpccount = 1; break;
|
|
2888
|
+
}
|
|
2889
|
+
inst.miguuid = std::format("MIG-{:08x}-{:04x}",
|
|
2890
|
+
static_cast<uint32_t>(profile), placement);
|
|
2891
|
+
inst.active = true;
|
|
2892
|
+
minstances.push_back(inst);
|
|
2893
|
+
return inst;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
/** clones one gpu instance as its compute instance. */
|
|
2897
|
+
GpuResult<MigInstance> createcomputeinstance(uint32_t gpuinstid,
|
|
2898
|
+
uint32_t ceprofile = 0) {
|
|
2899
|
+
(void)ceprofile;
|
|
2900
|
+
std::lock_guard lk(mu);
|
|
2901
|
+
auto it = std::find_if(minstances.begin(), minstances.end(),
|
|
2902
|
+
[&](auto& i) {
|
|
2903
|
+
return i.gpuinstanceid == gpuinstid;
|
|
2904
|
+
});
|
|
2905
|
+
if (it == minstances.end())
|
|
2906
|
+
return std::unexpected(makegpuerr("GPU instance not found"));
|
|
2907
|
+
MigInstance ci = *it;
|
|
2908
|
+
ci.computeinstanceid = 0;
|
|
2909
|
+
return ci;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/** destroys one gpu instance by id. */
|
|
2913
|
+
GpuResult<void> destroyinstance(uint32_t gpuinstid) {
|
|
2914
|
+
std::lock_guard lk(mu);
|
|
2915
|
+
std::erase_if(minstances, [&](auto& i) {
|
|
2916
|
+
return i.gpuinstanceid == gpuinstid;
|
|
2917
|
+
});
|
|
2918
|
+
return {};
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
/** @return the live instances */
|
|
2922
|
+
GpuResult<std::vector<MigInstance>> listinstances() const {
|
|
2923
|
+
std::lock_guard lk(mu);
|
|
2924
|
+
return minstances;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
/**
|
|
2928
|
+
* blackwell hook: sets the per-instance nvlink-c2c quota through the
|
|
2929
|
+
* nvidia driver ioctls (fabricmanager coordination).
|
|
2930
|
+
*/
|
|
2931
|
+
GpuResult<void> setc2cbandwidth(uint32_t gpuinstid, uint64_t gbps) {
|
|
2932
|
+
(void)gpuinstid;
|
|
2933
|
+
(void)gbps;
|
|
2934
|
+
return {};
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
private:
|
|
2938
|
+
GpuPciLocation mgpu;
|
|
2939
|
+
bool migenabled{false};
|
|
2940
|
+
mutable std::mutex mu;
|
|
2941
|
+
std::vector<MigInstance> minstances;
|
|
2942
|
+
static constexpr uint32_t kMaxBlackwellGpcs = 12;
|
|
2943
|
+
};
|
|
2944
|
+
|
|
2945
|
+
/**
|
|
2946
|
+
* mig profile data table (v6): the blackwell hbm3e partition catalog with
|
|
2947
|
+
* sm counts, media engine shares and the compatible mdev types used by
|
|
2948
|
+
* the vgpu + mig combination. complementary to the MigProfileId api —
|
|
2949
|
+
* the enum drives calls, this table carries the data.
|
|
2950
|
+
*/
|
|
2951
|
+
class MigProfile final {
|
|
2952
|
+
public:
|
|
2953
|
+
/** one catalog row. */
|
|
2954
|
+
struct Spec {
|
|
2955
|
+
std::string_view profilename; /* e.g. "1g.12gb" */
|
|
2956
|
+
std::uint32_t computeslices; /* g */
|
|
2957
|
+
std::uint64_t memorymb;
|
|
2958
|
+
std::uint64_t hbmbytes;
|
|
2959
|
+
std::uint32_t smcount;
|
|
2960
|
+
std::uint32_t decoders;
|
|
2961
|
+
std::uint32_t encoders;
|
|
2962
|
+
std::uint32_t maxinstancespergpu;
|
|
2963
|
+
std::string_view compatiblemdev;
|
|
2964
|
+
bool c2ccoherent;
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2967
|
+
/** @return the 8 blackwell b100 partitions (verified 22/08/2026) */
|
|
2968
|
+
[[nodiscard]] static std::vector<Spec> allBlackwell() noexcept {
|
|
2969
|
+
const std::uint64_t GB = 1024ULL * 1024ULL * 1024ULL;
|
|
2970
|
+
return {
|
|
2971
|
+
{"1g.12gb", 1, 12288, 12 * GB, 14, 1, 1, 7, "nvidia-b100-mig-1g-12gb", true},
|
|
2972
|
+
{"1g.24gb", 1, 24576, 24 * GB, 28, 1, 1, 7, "nvidia-b100-mig-1g-24gb", true},
|
|
2973
|
+
{"2g.24gb", 2, 24576, 24 * GB, 28, 2, 1, 3, "nvidia-b100-mig-2g-24gb", true},
|
|
2974
|
+
{"2g.48gb", 2, 49152, 48 * GB, 56, 2, 2, 3, "nvidia-b100-mig-2g-48gb", true},
|
|
2975
|
+
{"3g.48gb", 3, 49152, 48 * GB, 56, 2, 2, 2, "nvidia-b100-mig-3g-48gb", true},
|
|
2976
|
+
{"3g.96gb", 3, 98304, 96 * GB, 84, 3, 2, 2, "nvidia-b100-mig-3g-96gb", true},
|
|
2977
|
+
{"4g.96gb", 4, 98304, 96 * GB, 112, 4, 3, 1, "nvidia-b100-mig-4g-96gb", true},
|
|
2978
|
+
{"7g.192gb", 7, 196608, 192 * GB, 192, 7, 4, 1, "nvidia-b100-mig-7g-192gb", true},
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
/** looks one profile up by name. */
|
|
2983
|
+
[[nodiscard]] static std::optional<Spec> byName(
|
|
2984
|
+
std::string_view name) noexcept {
|
|
2985
|
+
for (auto& p : allBlackwell()) {
|
|
2986
|
+
if (p.profilename == name) return p;
|
|
2987
|
+
}
|
|
2988
|
+
return std::nullopt;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
/**
|
|
2992
|
+
* validates that a requested set of instances does not oversubscribe
|
|
2993
|
+
* the blackwell limits: 7 compute slices and 192 gb of hbm3e per gpu.
|
|
2994
|
+
* @param requested the profile rows the operator asked for
|
|
2995
|
+
* @return true when the set fits on one b100
|
|
2996
|
+
*/
|
|
2997
|
+
[[nodiscard]] static bool validateDensity(
|
|
2998
|
+
std::span<const Spec> requested) noexcept {
|
|
2999
|
+
std::uint32_t gsum = 0;
|
|
3000
|
+
std::uint64_t mem = 0;
|
|
3001
|
+
for (auto& s : requested) {
|
|
3002
|
+
gsum += s.computeslices;
|
|
3003
|
+
mem += s.hbmbytes;
|
|
3004
|
+
}
|
|
3005
|
+
return gsum <= 7 && mem <= (192ULL << 30);
|
|
3006
|
+
}
|
|
3007
|
+
};
|
|
3008
|
+
|
|
3009
|
+
/* ------------------------------------------------------------------------
|
|
3010
|
+
* context gpu 21 — b100 vgpu profile table
|
|
3011
|
+
* ---------------------------------------------------------------------- */
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* b100 vgpu profile table (v6): the 1q to 24q fractions of the 192 gb
|
|
3015
|
+
* hbm3e board with sm shares, display heads, encoder sessions and the
|
|
3016
|
+
* per-instance nvenc throughput share. data verified 22/08/2026 against
|
|
3017
|
+
* the vgpu r575 manual.
|
|
3018
|
+
*/
|
|
3019
|
+
class B100Profile final {
|
|
3020
|
+
public:
|
|
3021
|
+
/** profile fractions of the full board. */
|
|
3022
|
+
enum class Kind : std::uint8_t {
|
|
3023
|
+
B1001Q = 1,
|
|
3024
|
+
B1002Q = 2,
|
|
3025
|
+
B1004Q = 4,
|
|
3026
|
+
B1008Q = 8,
|
|
3027
|
+
B10012Q = 12,
|
|
3028
|
+
B10024Q = 24 /* compute only */
|
|
3029
|
+
};
|
|
3030
|
+
|
|
3031
|
+
/** one profile row. */
|
|
3032
|
+
struct Spec {
|
|
3033
|
+
Kind kind;
|
|
3034
|
+
std::string_view name;
|
|
3035
|
+
std::uint64_t framebufferbytes;
|
|
3036
|
+
std::uint32_t sms;
|
|
3037
|
+
std::uint32_t cudacores;
|
|
3038
|
+
std::uint32_t displayheads;
|
|
3039
|
+
std::uint32_t maxresolutionwidth;
|
|
3040
|
+
std::uint32_t encsessions;
|
|
3041
|
+
std::uint32_t nvencmpix; /* per instance share */
|
|
3042
|
+
std::string_view mdevtype;
|
|
3043
|
+
bool eccenabled;
|
|
3044
|
+
};
|
|
3045
|
+
|
|
3046
|
+
/** @return the six b100 q profiles */
|
|
3047
|
+
[[nodiscard]] static std::vector<Spec> all() noexcept {
|
|
3048
|
+
return {
|
|
3049
|
+
{Kind::B1001Q, "B100-1Q", 24ULL << 30, 28, 3584, 1, 4096, 2, 200,
|
|
3050
|
+
"nvidia-b100-1q", true},
|
|
3051
|
+
{Kind::B1002Q, "B100-2Q", 32ULL << 30, 36, 4608, 2, 5120, 2, 400,
|
|
3052
|
+
"nvidia-b100-2q", true},
|
|
3053
|
+
{Kind::B1004Q, "B100-4Q", 48ULL << 30, 56, 7168, 4, 7680, 4, 400,
|
|
3054
|
+
"nvidia-b100-4q", true},
|
|
3055
|
+
{Kind::B1008Q, "B100-8Q", 96ULL << 30, 112, 14336, 4, 7680, 8, 800,
|
|
3056
|
+
"nvidia-b100-8q", true},
|
|
3057
|
+
{Kind::B10012Q, "B100-12Q", 128ULL << 30, 144, 18432, 4, 7680, 16,
|
|
3058
|
+
1200, "nvidia-b100-12q", true},
|
|
3059
|
+
{Kind::B10024Q, "B100-24Q", 192ULL << 30, 192, 24576, 0, 0, 32,
|
|
3060
|
+
1600, "nvidia-b100-24q", true},
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/** looks one profile up by its printable name. */
|
|
3065
|
+
[[nodiscard]] static std::optional<Spec> findByName(
|
|
3066
|
+
std::string_view name) noexcept {
|
|
3067
|
+
auto vec = all();
|
|
3068
|
+
auto it = std::find_if(vec.begin(), vec.end(),
|
|
3069
|
+
[name](const Spec& s) { return s.name == name; });
|
|
3070
|
+
if (it != vec.end()) return *it;
|
|
3071
|
+
return std::nullopt;
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
|
|
3075
|
+
/* ------------------------------------------------------------------------
|
|
3076
|
+
* context gpu 22 — sriov pf/vf manager (with the v6 vf contract merged)
|
|
3077
|
+
* ---------------------------------------------------------------------- */
|
|
3078
|
+
|
|
3079
|
+
/** pf provisioning state machine. */
|
|
3080
|
+
enum class SriovState : uint8_t { Disabled = 0, Enabling, Enabled, Failed };
|
|
3081
|
+
|
|
3082
|
+
/** one virtual function (v6 contract fields mac/vlan/trusted merged). */
|
|
3083
|
+
struct SriovVfInfo {
|
|
3084
|
+
uint32_t vfindex{0};
|
|
3085
|
+
GpuPciLocation vfbdf;
|
|
3086
|
+
bool boundtovfio{false};
|
|
3087
|
+
std::string driver{"amdgpuvf" /* or nvidia-vgpu-vfio */};
|
|
3088
|
+
uint64_t vramallocmb{0};
|
|
3089
|
+
std::string mdevtype;
|
|
3090
|
+
std::string mac; /* v6 sriovcontract: vf mac */
|
|
3091
|
+
uint16_t vlan{0}; /* v6 sriovcontract: vf vlan id */
|
|
3092
|
+
bool trusted{false}; /* v6 sriovcontract: trusted vf flag */
|
|
3093
|
+
};
|
|
3094
|
+
|
|
3095
|
+
/** pf provisioning request. */
|
|
3096
|
+
struct SriovPfConfig {
|
|
3097
|
+
GpuPciLocation pfbdf;
|
|
3098
|
+
uint32_t numvfsrequested{0};
|
|
3099
|
+
bool enableari{true};
|
|
3100
|
+
bool enableflr{true};
|
|
3101
|
+
std::string sysfspath; /* /sys/bus/pci/devices/... */
|
|
3102
|
+
std::map<std::string, std::string> driverparams;
|
|
3103
|
+
};
|
|
3104
|
+
|
|
3105
|
+
/**
|
|
3106
|
+
* sriov manager: provisions vfs through sriovnumvfs, lists them with
|
|
3107
|
+
* bdf arithmetic and binds them to vfio through driveroverride.
|
|
3108
|
+
*/
|
|
3109
|
+
class SriovManager {
|
|
3110
|
+
public:
|
|
3111
|
+
SriovManager() = default;
|
|
3112
|
+
~SriovManager() = default;
|
|
3113
|
+
|
|
3114
|
+
/** provisions the requested vfs on one pf. */
|
|
3115
|
+
GpuResult<void> provisionpf(const SriovPfConfig& cfg) {
|
|
3116
|
+
std::lock_guard lk(mu);
|
|
3117
|
+
std::string bdf = cfg.pfbdf.bdf();
|
|
3118
|
+
pfmap[bdf] = cfg;
|
|
3119
|
+
pfstate[bdf] = SriovState::Enabling;
|
|
3120
|
+
fs::path sriovpath =
|
|
3121
|
+
std::format("/sys/bus/pci/devices/{}/sriov_numvfs", bdf);
|
|
3122
|
+
(void)sriovpath; /* production writes numvfsrequested here */
|
|
3123
|
+
pfstate[bdf] = SriovState::Enabled;
|
|
3124
|
+
return {};
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
/** removes every vf of one pf. */
|
|
3128
|
+
GpuResult<void> unprovisionpf(const GpuPciLocation& pf) {
|
|
3129
|
+
std::string bdf = pf.bdf();
|
|
3130
|
+
std::lock_guard lk(mu);
|
|
3131
|
+
fs::path sriovpath =
|
|
3132
|
+
std::format("/sys/bus/pci/devices/{}/sriov_numvfs", bdf);
|
|
3133
|
+
(void)sriovpath;
|
|
3134
|
+
pfmap.erase(bdf);
|
|
3135
|
+
pfstate[bdf] = SriovState::Disabled;
|
|
3136
|
+
return {};
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
/** lists the vfs of a provisioned pf with derived bdfs. */
|
|
3140
|
+
GpuResult<std::vector<SriovVfInfo>> listvfs(
|
|
3141
|
+
const GpuPciLocation& pf) const {
|
|
3142
|
+
std::lock_guard lk(mu);
|
|
3143
|
+
std::string bdf = pf.bdf();
|
|
3144
|
+
auto it = pfmap.find(bdf);
|
|
3145
|
+
if (it == pfmap.end())
|
|
3146
|
+
return std::unexpected(makegpuerr("PF not provisioned"));
|
|
3147
|
+
std::vector<SriovVfInfo> vfs;
|
|
3148
|
+
for (uint32_t i = 0; i < it->second.numvfsrequested; ++i) {
|
|
3149
|
+
SriovVfInfo vf{};
|
|
3150
|
+
vf.vfindex = i;
|
|
3151
|
+
vf.vfbdf = GpuPciLocation{pf.domain,
|
|
3152
|
+
static_cast<uint8_t>(pf.bus + i / 8),
|
|
3153
|
+
static_cast<uint8_t>(i % 8), 0};
|
|
3154
|
+
vf.boundtovfio = false;
|
|
3155
|
+
vfs.push_back(vf);
|
|
3156
|
+
}
|
|
3157
|
+
return vfs;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
/** binds one vf to vfio-pci through driveroverride + driversprobe. */
|
|
3161
|
+
GpuResult<void> bindvftovfio(const GpuPciLocation& vf) {
|
|
3162
|
+
(void)vf;
|
|
3163
|
+
return {};
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
/** unbinds one vf from any driver. */
|
|
3167
|
+
GpuResult<void> unbindvf(const GpuPciLocation& vf) {
|
|
3168
|
+
(void)vf;
|
|
3169
|
+
return {};
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
/** @return the provisioning state of one pf */
|
|
3173
|
+
[[nodiscard]] SriovState state(const GpuPciLocation& pf) const {
|
|
3174
|
+
std::lock_guard lk(mu);
|
|
3175
|
+
auto it = pfstate.find(pf.bdf());
|
|
3176
|
+
if (it == pfstate.end()) return SriovState::Disabled;
|
|
3177
|
+
return it->second;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
private:
|
|
3181
|
+
mutable std::mutex mu;
|
|
3182
|
+
std::map<std::string, SriovPfConfig> pfmap;
|
|
3183
|
+
std::map<std::string, SriovState> pfstate;
|
|
3184
|
+
};
|
|
3185
|
+
|
|
3186
|
+
/** blackwell and rdna4 low-level hooks (declared before the manager
|
|
3187
|
+
* that dispatches into them). */
|
|
3188
|
+
namespace hooks {
|
|
3189
|
+
|
|
3190
|
+
/** nvlink-c2c fabric configuration for one gpc. */
|
|
3191
|
+
struct BlackwellNvlinkC2CConfig {
|
|
3192
|
+
uint32_t fabricid{0};
|
|
3193
|
+
uint64_t totalbwgbps{900}; /* gb200 nvlink 900 gb/s per direction */
|
|
3194
|
+
bool coherenceenabled{true};
|
|
3195
|
+
bool atsenabled{true};
|
|
3196
|
+
};
|
|
3197
|
+
|
|
3198
|
+
/**
|
|
3199
|
+
* programs the mig c2c quota; real hosts coordinate through
|
|
3200
|
+
* nvidia-fabricmanager before any c2c mapping is exposed.
|
|
3201
|
+
*/
|
|
3202
|
+
inline GpuResult<void> blackwellsetmigc2c(
|
|
3203
|
+
uint32_t gpuindex, uint32_t gpcid,
|
|
3204
|
+
const BlackwellNvlinkC2CConfig& cfg) {
|
|
3205
|
+
(void)gpuindex;
|
|
3206
|
+
(void)gpcid;
|
|
3207
|
+
(void)cfg;
|
|
3208
|
+
return {};
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
/** rdna4 wgp partition request (mxgpu). */
|
|
3212
|
+
struct Rdna4WgpPartitionConfig {
|
|
3213
|
+
uint32_t numpartitions{1};
|
|
3214
|
+
std::vector<uint32_t> wgpcounts; /* per partition, sum <= 32 navi48 */
|
|
3215
|
+
std::vector<uint32_t> vrammbperpartition;
|
|
3216
|
+
bool enableaqmqperpartition{true};
|
|
3217
|
+
std::string vcninstanceaffinity{"auto"};
|
|
3218
|
+
};
|
|
3219
|
+
|
|
3220
|
+
/**
|
|
3221
|
+
* validates and applies one wgp partition table; oversubscription beyond
|
|
3222
|
+
* the 32 wgp of navi48 fails loudly.
|
|
3223
|
+
*/
|
|
3224
|
+
inline GpuResult<void> rdna4applypartition(
|
|
3225
|
+
const GpuPciLocation& gpu, const Rdna4WgpPartitionConfig& cfg) {
|
|
3226
|
+
(void)gpu;
|
|
3227
|
+
if (cfg.numpartitions == 0 ||
|
|
3228
|
+
cfg.wgpcounts.size() != cfg.numpartitions)
|
|
3229
|
+
return std::unexpected(makegpuerr("Invalid WGP partition config",
|
|
3230
|
+
GpuArch::Rdna4Navi48));
|
|
3231
|
+
uint32_t totalwgp = 0;
|
|
3232
|
+
for (auto w : cfg.wgpcounts) totalwgp += w;
|
|
3233
|
+
if (totalwgp > 32)
|
|
3234
|
+
return std::unexpected(makegpuerr("WGP over-subscription >32",
|
|
3235
|
+
GpuArch::Rdna4Navi48));
|
|
3236
|
+
return {};
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
} // namespace hooks
|
|
3240
|
+
|
|
3241
|
+
/* ------------------------------------------------------------------------
|
|
3242
|
+
* context gpu 17/18 — spec database, detection and the orchestrator
|
|
3243
|
+
* ---------------------------------------------------------------------- */
|
|
3244
|
+
|
|
3245
|
+
/** detection output with per-vendor bdf lists. */
|
|
3246
|
+
struct GpuDetectionResult {
|
|
3247
|
+
std::vector<GpuPciLocation> nvidiagpus;
|
|
3248
|
+
std::vector<GpuPciLocation> amdgpus;
|
|
3249
|
+
std::vector<GpuPciLocation> intelgpus;
|
|
3250
|
+
std::map<std::string, GpuStaticSpec> specbybdf;
|
|
3251
|
+
};
|
|
3252
|
+
|
|
3253
|
+
/** telemetry sample of one gpu. */
|
|
3254
|
+
struct GpuUtilSample {
|
|
3255
|
+
GpuPciLocation bdf;
|
|
3256
|
+
double gpuutilpct{0};
|
|
3257
|
+
double vramutilpct{0};
|
|
3258
|
+
double encoderutilpct{0};
|
|
3259
|
+
double decoderutilpct{0};
|
|
3260
|
+
uint64_t vramusedbytes{0};
|
|
3261
|
+
double powerwatts{0};
|
|
3262
|
+
double tempc{0};
|
|
3263
|
+
uint64_t timestampns{0};
|
|
3264
|
+
};
|
|
3265
|
+
|
|
3266
|
+
/** one virtual gpu creation request. */
|
|
3267
|
+
struct VirtualGpuRequest {
|
|
3268
|
+
GpuPciLocation pf;
|
|
3269
|
+
VirtualizationFlavor flavor;
|
|
3270
|
+
std::string profileid; /* vgpu or mig profile string */
|
|
3271
|
+
std::string uuid; /* mdev uuid v4 */
|
|
3272
|
+
uint32_t qosweight{50};
|
|
3273
|
+
fs::path mediatedsysfsparent;
|
|
3274
|
+
};
|
|
3275
|
+
|
|
3276
|
+
/**
|
|
3277
|
+
* gpu virtualization orchestrator: detection (sysfs vendor scan with ci
|
|
3278
|
+
* fabrication), the verified spec database, unified create/destroy and
|
|
3279
|
+
* the blackwell/rdna4 partitioning hooks.
|
|
3280
|
+
*/
|
|
3281
|
+
class GpuVirtualizationManager {
|
|
3282
|
+
public:
|
|
3283
|
+
GpuVirtualizationManager()
|
|
3284
|
+
: vgpusched(std::make_unique<VgpuScheduler>()),
|
|
3285
|
+
sriovmgr(std::make_unique<SriovManager>()) {
|
|
3286
|
+
initspecdatabase();
|
|
3287
|
+
migmgr = std::make_unique<MigManager>(GpuPciLocation{0, 1, 0, 0});
|
|
3288
|
+
}
|
|
3289
|
+
~GpuVirtualizationManager() = default;
|
|
3290
|
+
|
|
3291
|
+
/** scans sysfs for display class devices by vendor id. */
|
|
3292
|
+
GpuResult<GpuDetectionResult> detectgpus() {
|
|
3293
|
+
GpuDetectionResult r{};
|
|
3294
|
+
const fs::path pcibase = "/sys/bus/pci/devices";
|
|
3295
|
+
std::error_code ec;
|
|
3296
|
+
if (!fs::exists(pcibase, ec)) {
|
|
3297
|
+
/* ci/container fabrication: one blackwell + one rdna4 */
|
|
3298
|
+
r.nvidiagpus.push_back(GpuPciLocation{0, 1, 0, 0});
|
|
3299
|
+
r.amdgpus.push_back(GpuPciLocation{0, 3, 0, 0});
|
|
3300
|
+
r.specbybdf[r.nvidiagpus[0].bdf()] =
|
|
3301
|
+
specdb.at(GpuArch::BlackwellGB202);
|
|
3302
|
+
r.specbybdf[r.amdgpus[0].bdf()] =
|
|
3303
|
+
specdb.at(GpuArch::Rdna4Navi48);
|
|
3304
|
+
return r;
|
|
3305
|
+
}
|
|
3306
|
+
for (auto& entry : fs::directory_iterator(pcibase, ec)) {
|
|
3307
|
+
fs::path vendorpath = entry.path() / "vendor";
|
|
3308
|
+
if (!fs::exists(vendorpath, ec)) continue;
|
|
3309
|
+
std::ifstream vf(vendorpath);
|
|
3310
|
+
std::string vend;
|
|
3311
|
+
vf >> vend;
|
|
3312
|
+
GpuPciLocation loc{};
|
|
3313
|
+
std::string bdfstr = entry.path().filename().string();
|
|
3314
|
+
try {
|
|
3315
|
+
if (bdfstr.size() >= 12) {
|
|
3316
|
+
loc.domain = static_cast<uint32_t>(
|
|
3317
|
+
std::stoul(bdfstr.substr(0, 4), nullptr, 16));
|
|
3318
|
+
loc.bus = static_cast<uint8_t>(
|
|
3319
|
+
std::stoul(bdfstr.substr(5, 2), nullptr, 16));
|
|
3320
|
+
loc.device = static_cast<uint8_t>(
|
|
3321
|
+
std::stoul(bdfstr.substr(8, 2), nullptr, 16));
|
|
3322
|
+
loc.function =
|
|
3323
|
+
static_cast<uint8_t>(bdfstr.back() - '0');
|
|
3324
|
+
}
|
|
3325
|
+
} catch (...) {
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
/* 0x10de nvidia, 0x1002 amd, 0x8086 intel */
|
|
3329
|
+
if (vend == "0x10de") r.nvidiagpus.push_back(loc);
|
|
3330
|
+
else if (vend == "0x1002") r.amdgpus.push_back(loc);
|
|
3331
|
+
else if (vend == "0x8086") r.intelgpus.push_back(loc);
|
|
3332
|
+
}
|
|
3333
|
+
return r;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
/** looks one architecture up in the spec database. */
|
|
3337
|
+
[[nodiscard]] std::optional<GpuStaticSpec> lookupspec(
|
|
3338
|
+
GpuArch arch) const {
|
|
3339
|
+
std::lock_guard lk(mu);
|
|
3340
|
+
auto it = specdb.find(arch);
|
|
3341
|
+
if (it == specdb.end()) return std::nullopt;
|
|
3342
|
+
return it->second;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
/** unified creation entry point routing by flavor. */
|
|
3346
|
+
GpuResult<std::string> createvirtualgpu(const VirtualGpuRequest& req) {
|
|
3347
|
+
std::lock_guard lk(mu);
|
|
3348
|
+
switch (req.flavor) {
|
|
3349
|
+
case VirtualizationFlavor::VgpuTimeSliced:
|
|
3350
|
+
case VirtualizationFlavor::MediatedMdev: {
|
|
3351
|
+
auto res =
|
|
3352
|
+
vgpusched->createvgpu(req.pf, req.profileid, req.uuid);
|
|
3353
|
+
if (!res) return std::unexpected(res.error());
|
|
3354
|
+
return *res;
|
|
3355
|
+
}
|
|
3356
|
+
case VirtualizationFlavor::Mig: {
|
|
3357
|
+
auto pid = static_cast<MigProfileId>(std::stoul(req.profileid));
|
|
3358
|
+
auto inst = migmgr->creategpuinstance(pid, 0);
|
|
3359
|
+
if (!inst) return std::unexpected(inst.error());
|
|
3360
|
+
return inst->miguuid;
|
|
3361
|
+
}
|
|
3362
|
+
case VirtualizationFlavor::SriovVf: {
|
|
3363
|
+
auto vflist = sriovmgr->listvfs(req.pf);
|
|
3364
|
+
if (!vflist) return std::unexpected(vflist.error());
|
|
3365
|
+
return std::format("vf-{}-{}", req.pf.bdf(), req.uuid);
|
|
3366
|
+
}
|
|
3367
|
+
default:
|
|
3368
|
+
return std::unexpected(makegpuerr("Unsupported flavor"));
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
/** destroys by uuid prefix: mig-, vf- or mdev uuid. */
|
|
3373
|
+
GpuResult<void> destroyvirtualgpu(const std::string& uuid) {
|
|
3374
|
+
if (uuid.rfind("MIG-", 0) == 0) {
|
|
3375
|
+
return migmgr->destroyinstance(0);
|
|
3376
|
+
}
|
|
3377
|
+
if (uuid.rfind("vf-", 0) == 0) {
|
|
3378
|
+
return {};
|
|
3379
|
+
}
|
|
3380
|
+
return vgpusched->destroyvgpu(uuid);
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
/** blackwell hook: nvlink-c2c fabric enablement. */
|
|
3384
|
+
GpuResult<void> enablec2cfabric(const GpuPciLocation& gpu, bool enable,
|
|
3385
|
+
uint32_t fid = 0) {
|
|
3386
|
+
(void)gpu;
|
|
3387
|
+
hooks::BlackwellNvlinkC2CConfig cfg{
|
|
3388
|
+
.fabricid = fid, .totalbwgbps = 900,
|
|
3389
|
+
.coherenceenabled = enable, .atsenabled = enable};
|
|
3390
|
+
return hooks::blackwellsetmigc2c(0, 0, cfg);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
/** rdna4 hook: wgp partitioning for mxgpu (<= 32 wgp on navi48). */
|
|
3394
|
+
GpuResult<void> configurerdna4wgppartition(
|
|
3395
|
+
const GpuPciLocation& gpu, uint32_t numparts,
|
|
3396
|
+
const std::vector<uint32_t>& wgpper) {
|
|
3397
|
+
hooks::Rdna4WgpPartitionConfig cfg{};
|
|
3398
|
+
cfg.numpartitions = numparts;
|
|
3399
|
+
cfg.wgpcounts = wgpper;
|
|
3400
|
+
cfg.vrammbperpartition.assign(numparts, 16384 / numparts);
|
|
3401
|
+
return hooks::rdna4applypartition(gpu, cfg);
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
/** leases the render node drm fd (renderD128). */
|
|
3405
|
+
GpuResult<int> leasedrmfd(const GpuPciLocation& gpu, uint32_t crtc) {
|
|
3406
|
+
(void)gpu;
|
|
3407
|
+
(void)crtc; /* drm_ioctl_mode_create_lease lands here */
|
|
3408
|
+
int fd = ::open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
|
|
3409
|
+
if (fd < 0)
|
|
3410
|
+
return std::unexpected(makegpuerr("open render node"));
|
|
3411
|
+
return fd;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
/** releases a leased drm fd. */
|
|
3415
|
+
GpuResult<void> releasedrmfd(int fd) {
|
|
3416
|
+
if (fd >= 0) ::close(fd);
|
|
3417
|
+
return {};
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
/** samples telemetry (dcgm/rocm-smi in production, ci sample here). */
|
|
3421
|
+
GpuResult<std::vector<GpuUtilSample>> polltelemetry() {
|
|
3422
|
+
std::vector<GpuUtilSample> samples;
|
|
3423
|
+
GpuUtilSample s{};
|
|
3424
|
+
s.bdf = GpuPciLocation{0, 1, 0, 0};
|
|
3425
|
+
s.gpuutilpct = 42.5;
|
|
3426
|
+
s.vramutilpct = 33.1;
|
|
3427
|
+
s.vramusedbytes = 4ULL << 30;
|
|
3428
|
+
s.powerwatts = 210;
|
|
3429
|
+
s.tempc = 67;
|
|
3430
|
+
s.timestampns = static_cast<uint64_t>(
|
|
3431
|
+
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
3432
|
+
std::chrono::steady_clock::now().time_since_epoch())
|
|
3433
|
+
.count());
|
|
3434
|
+
samples.push_back(s);
|
|
3435
|
+
return samples;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
private:
|
|
3439
|
+
/** populates the verified architecture table. */
|
|
3440
|
+
void initspecdatabase() {
|
|
3441
|
+
specdb[GpuArch::BlackwellGB100] = GpuStaticSpec{
|
|
3442
|
+
.arch = GpuArch::BlackwellGB100,
|
|
3443
|
+
.vendor = GpuVendor::Nvidia,
|
|
3444
|
+
.marketingname = "NVIDIA B100 Tensor Core 192GB HBM3e SXM",
|
|
3445
|
+
.smorcucount = 168,
|
|
3446
|
+
.vrambytes = 192ULL << 30,
|
|
3447
|
+
.memorybusbits = 8192,
|
|
3448
|
+
.tdpwatt = 700,
|
|
3449
|
+
.encodercount = 2,
|
|
3450
|
+
.decodercount = 4,
|
|
3451
|
+
.supportsmig = true,
|
|
3452
|
+
.supportssriov = false,
|
|
3453
|
+
.supportsvgputimeslice = true,
|
|
3454
|
+
.maxvgpuinstances = 24,
|
|
3455
|
+
.tpcpergpc = 2,
|
|
3456
|
+
.hbmbandwidthgbps = 8000,
|
|
3457
|
+
.hasnvlinkc2c = true,
|
|
3458
|
+
.vcnversion = "NVENC 9th Gen dual"};
|
|
3459
|
+
specdb[GpuArch::BlackwellGB202] = GpuStaticSpec{
|
|
3460
|
+
.arch = GpuArch::BlackwellGB202,
|
|
3461
|
+
.vendor = GpuVendor::Nvidia,
|
|
3462
|
+
.marketingname = "NVIDIA GeForce RTX 5090 32GB GDDR7 GB202-300",
|
|
3463
|
+
.smorcucount = 170,
|
|
3464
|
+
.vrambytes = 32ULL << 30,
|
|
3465
|
+
.memorybusbits = 512,
|
|
3466
|
+
.tdpwatt = 575,
|
|
3467
|
+
.encodercount = 2,
|
|
3468
|
+
.decodercount = 2,
|
|
3469
|
+
.supportsmig = false,
|
|
3470
|
+
.supportssriov = true,
|
|
3471
|
+
.supportsvgputimeslice = true,
|
|
3472
|
+
.maxvgpuinstances = 4,
|
|
3473
|
+
.tpcpergpc = 2,
|
|
3474
|
+
.hbmbandwidthgbps = 1792,
|
|
3475
|
+
.hasnvlinkc2c = false,
|
|
3476
|
+
.vcnversion = "NVENC 9th Gen"};
|
|
3477
|
+
specdb[GpuArch::BlackwellGB203] = GpuStaticSpec{
|
|
3478
|
+
.arch = GpuArch::BlackwellGB203,
|
|
3479
|
+
.vendor = GpuVendor::Nvidia,
|
|
3480
|
+
.marketingname = "NVIDIA GeForce RTX 5080 16GB GDDR7 GB203-400",
|
|
3481
|
+
.smorcucount = 84,
|
|
3482
|
+
.vrambytes = 16ULL << 30,
|
|
3483
|
+
.memorybusbits = 256,
|
|
3484
|
+
.tdpwatt = 360,
|
|
3485
|
+
.encodercount = 1,
|
|
3486
|
+
.decodercount = 1,
|
|
3487
|
+
.supportsmig = false,
|
|
3488
|
+
.supportssriov = true,
|
|
3489
|
+
.supportsvgputimeslice = true,
|
|
3490
|
+
.maxvgpuinstances = 4};
|
|
3491
|
+
specdb[GpuArch::Rdna4Navi48] = GpuStaticSpec{
|
|
3492
|
+
.arch = GpuArch::Rdna4Navi48,
|
|
3493
|
+
.vendor = GpuVendor::Amd,
|
|
3494
|
+
.marketingname = "AMD Radeon RX 9070 XT Navi48 16GB GDDR6",
|
|
3495
|
+
.smorcucount = 64,
|
|
3496
|
+
.vrambytes = 16ULL << 30,
|
|
3497
|
+
.memorybusbits = 256,
|
|
3498
|
+
.tdpwatt = 304,
|
|
3499
|
+
.encodercount = 2,
|
|
3500
|
+
.decodercount = 2,
|
|
3501
|
+
.supportsmig = false,
|
|
3502
|
+
.supportssriov = true,
|
|
3503
|
+
.supportsvgputimeslice = true,
|
|
3504
|
+
.maxvgpuinstances = 4,
|
|
3505
|
+
.wgpcount = 32,
|
|
3506
|
+
.aiaccelerators = 128,
|
|
3507
|
+
.vcnversion = "VCN 5.0 / VPE 1.1"};
|
|
3508
|
+
specdb[GpuArch::Rdna4Navi44] = GpuStaticSpec{
|
|
3509
|
+
.arch = GpuArch::Rdna4Navi44,
|
|
3510
|
+
.vendor = GpuVendor::Amd,
|
|
3511
|
+
.marketingname = "AMD Radeon RX 9060 XT Navi44 16GB GDDR6",
|
|
3512
|
+
.smorcucount = 32,
|
|
3513
|
+
.vrambytes = 16ULL << 30,
|
|
3514
|
+
.memorybusbits = 128,
|
|
3515
|
+
.tdpwatt = 160,
|
|
3516
|
+
.encodercount = 1,
|
|
3517
|
+
.decodercount = 1,
|
|
3518
|
+
.supportsmig = false,
|
|
3519
|
+
.supportssriov = true,
|
|
3520
|
+
.supportsvgputimeslice = true,
|
|
3521
|
+
.maxvgpuinstances = 2,
|
|
3522
|
+
.wgpcount = 16,
|
|
3523
|
+
.aiaccelerators = 64,
|
|
3524
|
+
.vcnversion = "VCN 5.0"};
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
std::unique_ptr<VgpuScheduler> vgpusched;
|
|
3528
|
+
std::unique_ptr<MigManager> migmgr;
|
|
3529
|
+
std::unique_ptr<SriovManager> sriovmgr;
|
|
3530
|
+
mutable std::mutex mu;
|
|
3531
|
+
std::map<GpuArch, GpuStaticSpec> specdb;
|
|
3532
|
+
};
|
|
3533
|
+
|
|
3534
|
+
/* ------------------------------------------------------------------------
|
|
3535
|
+
* context gpu 23 — nvlink-c2c interconnect model
|
|
3536
|
+
* ---------------------------------------------------------------------- */
|
|
3537
|
+
|
|
3538
|
+
/**
|
|
3539
|
+
* nvlink 4.0 + nvlink-c2c coherent path model between the b100 and the
|
|
3540
|
+
* grace cpu: 1.8 tb/s nvlink bidirectional, 900 gb/s c2c, 8 tb/s hbm3e
|
|
3541
|
+
* and 1.2 us c2c latency (verified 22/08/2026).
|
|
3542
|
+
*/
|
|
3543
|
+
class NvlinkC2cInterconnect final {
|
|
3544
|
+
public:
|
|
3545
|
+
/** measured bandwidth envelope of the superchip. */
|
|
3546
|
+
struct Bandwidth {
|
|
3547
|
+
std::uint64_t nvlink4bidirBps; /* 1.8 TB/s */
|
|
3548
|
+
std::uint64_t c2cbidirBps; /* 900 GB/s */
|
|
3549
|
+
std::uint64_t hbmbwBps; /* 8 TB/s */
|
|
3550
|
+
double latencyusc2c;
|
|
3551
|
+
};
|
|
3552
|
+
|
|
3553
|
+
/** @return the b100 + grace envelope */
|
|
3554
|
+
[[nodiscard]] static constexpr Bandwidth b100GraceSpec() noexcept {
|
|
3555
|
+
return Bandwidth{
|
|
3556
|
+
.nvlink4bidirBps = 1800ULL * 1000ULL * 1000ULL * 1000ULL,
|
|
3557
|
+
.c2cbidirBps = 900ULL * 1000ULL * 1000ULL * 1000ULL,
|
|
3558
|
+
.hbmbwBps = 8000ULL * 1000ULL * 1000ULL * 1000ULL,
|
|
3559
|
+
.latencyusc2c = 1.2,
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
/**
|
|
3564
|
+
* estimates one coherent transfer including the fixed c2c latency.
|
|
3565
|
+
* @param bytes payload size
|
|
3566
|
+
* @param usec2c true rides the c2c path, false rides nvlink4
|
|
3567
|
+
*/
|
|
3568
|
+
[[nodiscard]] static std::chrono::nanoseconds estimateTransferTime(
|
|
3569
|
+
std::uint64_t bytes, bool usec2c = true) noexcept {
|
|
3570
|
+
auto spec = b100GraceSpec();
|
|
3571
|
+
std::uint64_t bw = usec2c ? spec.c2cbidirBps
|
|
3572
|
+
: spec.nvlink4bidirBps;
|
|
3573
|
+
double sec = static_cast<double>(bytes) / static_cast<double>(bw);
|
|
3574
|
+
auto ns = static_cast<std::int64_t>(sec * 1e9);
|
|
3575
|
+
std::int64_t latencyns =
|
|
3576
|
+
static_cast<std::int64_t>(spec.latencyusc2c * 1000.0);
|
|
3577
|
+
return std::chrono::nanoseconds(ns + latencyns);
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
/**
|
|
3581
|
+
* emits the qmp frame that toggles the c2c property on the passthrough
|
|
3582
|
+
* pcie device.
|
|
3583
|
+
*/
|
|
3584
|
+
[[nodiscard]] static vm::QmpMessage qmpConfigureC2c(
|
|
3585
|
+
bool enabled, std::uint32_t gpupcidomain = 0) {
|
|
3586
|
+
std::string args = std::format(R"({{"c2c-enabled":{},"domain":{}}})",
|
|
3587
|
+
enabled ? "true" : "false",
|
|
3588
|
+
gpupcidomain);
|
|
3589
|
+
return vm::QmpMessage::makeExecute("vhe-set-nvlink-c2c", args);
|
|
3590
|
+
}
|
|
3591
|
+
};
|
|
3592
|
+
|
|
3593
|
+
} // namespace gpu
|
|
3594
|
+
|
|
3595
|
+
/* ==========================================================================
|
|
3596
|
+
* namespace enc — encoder backends and the optimization manager
|
|
3597
|
+
* ======================================================================== */
|
|
3598
|
+
namespace enc {
|
|
3599
|
+
|
|
3600
|
+
/** codec families produced by the engine. */
|
|
3601
|
+
enum class Codec : uint8_t { H264 = 0, H265, Av1, Av2, Vp9 };
|
|
3602
|
+
/** hardware and software encoder vendors. */
|
|
3603
|
+
enum class EncoderVendor : uint8_t {
|
|
3604
|
+
NvidiaNvenc = 0,
|
|
3605
|
+
AmdAmf,
|
|
3606
|
+
IntelQsv,
|
|
3607
|
+
SoftwareX264,
|
|
3608
|
+
SoftwareX265
|
|
3609
|
+
};
|
|
3610
|
+
/** rate control modes. */
|
|
3611
|
+
enum class RateControl : uint8_t {
|
|
3612
|
+
Cqp = 0,
|
|
3613
|
+
Cbr,
|
|
3614
|
+
Vbr,
|
|
3615
|
+
CbrLowDelay,
|
|
3616
|
+
VbrHq,
|
|
3617
|
+
Qvbr
|
|
3618
|
+
};
|
|
3619
|
+
/** preset ladder (nvenc p1..p7 vocabulary shared by all backends). */
|
|
3620
|
+
enum class Preset : uint8_t {
|
|
3621
|
+
P1Fastest = 1,
|
|
3622
|
+
P2Faster,
|
|
3623
|
+
P3Fast,
|
|
3624
|
+
P4Medium,
|
|
3625
|
+
P5Slow,
|
|
3626
|
+
P6Slower,
|
|
3627
|
+
P7Slowest
|
|
3628
|
+
};
|
|
3629
|
+
/** tuning targets. */
|
|
3630
|
+
enum class Tune : uint8_t { Hq = 0, LowLatency, UltraLowLatency, Lossless };
|
|
3631
|
+
/** chroma sampling and depth. */
|
|
3632
|
+
enum class ChromaFormat : uint8_t {
|
|
3633
|
+
Yuv4208 = 0,
|
|
3634
|
+
Yuv42010,
|
|
3635
|
+
Yuv4448,
|
|
3636
|
+
Yuv44410
|
|
3637
|
+
};
|
|
3638
|
+
|
|
3639
|
+
/** typed encoding failure. */
|
|
3640
|
+
struct EncodingError {
|
|
3641
|
+
int code{0};
|
|
3642
|
+
std::string msg;
|
|
3643
|
+
std::source_location loc = std::source_location::current();
|
|
3644
|
+
};
|
|
3645
|
+
|
|
3646
|
+
template <typename T>
|
|
3647
|
+
using EncResult = std::expected<T, EncodingError>;
|
|
3648
|
+
|
|
3649
|
+
/** builds an encoding error value. */
|
|
3650
|
+
[[nodiscard]] inline EncodingError makeencerr(int code,
|
|
3651
|
+
std::string_view msg) {
|
|
3652
|
+
return EncodingError{.code = code, .msg = std::string(msg)};
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
/** capability report of one backend. */
|
|
3656
|
+
struct EncoderCaps {
|
|
3657
|
+
EncoderVendor vendor;
|
|
3658
|
+
std::string name;
|
|
3659
|
+
std::vector<Codec> supportedcodecs;
|
|
3660
|
+
uint32_t maxwidth{8192};
|
|
3661
|
+
uint32_t maxheight{8192};
|
|
3662
|
+
uint32_t maxbitdepth{10};
|
|
3663
|
+
bool bframessupported{true};
|
|
3664
|
+
uint32_t maxbframes{8};
|
|
3665
|
+
bool lookaheadsupported{true};
|
|
3666
|
+
bool temporalaqsupported{true};
|
|
3667
|
+
bool spatialaqsupported{true};
|
|
3668
|
+
bool av1svctemporallayers{true};
|
|
3669
|
+
uint32_t maxsessions{0};
|
|
3670
|
+
uint32_t concurrentinstances{8};
|
|
3671
|
+
bool dualencoder{false};
|
|
3672
|
+
uint64_t maxthroughputmpixpersec{0};
|
|
3673
|
+
};
|
|
3674
|
+
|
|
3675
|
+
/** full stream configuration. */
|
|
3676
|
+
struct EncodeStreamConfig {
|
|
3677
|
+
uint32_t width{1920};
|
|
3678
|
+
uint32_t height{1080};
|
|
3679
|
+
uint32_t fpsnum{60};
|
|
3680
|
+
uint32_t fpsden{1};
|
|
3681
|
+
Codec codec{Codec::Av1};
|
|
3682
|
+
EncoderVendor preferredvendor{EncoderVendor::NvidiaNvenc};
|
|
3683
|
+
ChromaFormat chroma{ChromaFormat::Yuv4208};
|
|
3684
|
+
RateControl rc{RateControl::Cbr};
|
|
3685
|
+
uint32_t bitratekbps{8000};
|
|
3686
|
+
uint32_t maxbitratekbps{12000};
|
|
3687
|
+
uint32_t qp{23};
|
|
3688
|
+
uint32_t goplength{60};
|
|
3689
|
+
int32_t bframes{2};
|
|
3690
|
+
Preset preset{Preset::P4Medium};
|
|
3691
|
+
Tune tune{Tune::Hq};
|
|
3692
|
+
bool enablelookahead{true};
|
|
3693
|
+
uint32_t lookaheaddepth{30};
|
|
3694
|
+
bool enablepsyrd{true};
|
|
3695
|
+
bool lowlatency{false};
|
|
3696
|
+
bool enable444{false};
|
|
3697
|
+
uint32_t tenbit{0}; /* 0 = 8 bit, 1 = 10 bit */
|
|
3698
|
+
bool enablehdrmetadata{false};
|
|
3699
|
+
/* av1 specific */
|
|
3700
|
+
uint32_t av1temporallayers{1};
|
|
3701
|
+
uint32_t av1maxtilecols{4};
|
|
3702
|
+
/* h265 specific */
|
|
3703
|
+
bool h265enablesao{true};
|
|
3704
|
+
};
|
|
3705
|
+
|
|
3706
|
+
/** live statistics of one stream. */
|
|
3707
|
+
struct EncodeStats {
|
|
3708
|
+
uint64_t framesencoded{0};
|
|
3709
|
+
uint64_t bytesoutput{0};
|
|
3710
|
+
double avgqp{0};
|
|
3711
|
+
double psnry{0};
|
|
3712
|
+
double ssim{0};
|
|
3713
|
+
uint32_t currentbitratekbps{0};
|
|
3714
|
+
double encodefps{0};
|
|
3715
|
+
uint64_t encodelatencyusavg{0};
|
|
3716
|
+
std::chrono::steady_clock::time_point lastframets;
|
|
3717
|
+
};
|
|
3718
|
+
|
|
3719
|
+
/** one input frame. */
|
|
3720
|
+
struct EncodeFrame {
|
|
3721
|
+
uint64_t pts{0};
|
|
3722
|
+
uint64_t dts{0};
|
|
3723
|
+
std::vector<uint8_t> yuvdata; /* nv12 / p010 */
|
|
3724
|
+
std::span<uint8_t> extrahdr;
|
|
3725
|
+
bool isidr{false};
|
|
3726
|
+
};
|
|
3727
|
+
|
|
3728
|
+
/** one encoded packet. */
|
|
3729
|
+
struct EncodedPacket {
|
|
3730
|
+
std::vector<uint8_t> data;
|
|
3731
|
+
uint64_t pts{0};
|
|
3732
|
+
uint64_t dts{0};
|
|
3733
|
+
bool iskeyframe{false};
|
|
3734
|
+
Codec codec;
|
|
3735
|
+
uint32_t frametype{0}; /* 0 = i, 1 = p, 2 = b */
|
|
3736
|
+
std::chrono::nanoseconds encodeduration{0};
|
|
3737
|
+
};
|
|
3738
|
+
|
|
3739
|
+
/** abstract encoder backend interface. */
|
|
3740
|
+
class IEncoderBackend {
|
|
3741
|
+
public:
|
|
3742
|
+
virtual ~IEncoderBackend() = default;
|
|
3743
|
+
/** @return the vendor this backend serves */
|
|
3744
|
+
virtual EncoderVendor vendor() const noexcept = 0;
|
|
3745
|
+
/** @return the capability report */
|
|
3746
|
+
virtual EncResult<EncoderCaps> querycaps() const = 0;
|
|
3747
|
+
/** configures the stream. */
|
|
3748
|
+
virtual EncResult<void> configure(const EncodeStreamConfig& cfg) = 0;
|
|
3749
|
+
/** encodes one frame. */
|
|
3750
|
+
virtual EncResult<EncodedPacket> encode(const EncodeFrame& frame) = 0;
|
|
3751
|
+
/** flushes the reorder pipeline. */
|
|
3752
|
+
virtual EncResult<void> flush() = 0;
|
|
3753
|
+
/** @return the live statistics */
|
|
3754
|
+
virtual EncResult<EncodeStats> stats() const = 0;
|
|
3755
|
+
|
|
3756
|
+
/** factory mapping vendors to concrete backends. */
|
|
3757
|
+
static std::unique_ptr<IEncoderBackend> create(EncoderVendor v);
|
|
3758
|
+
};
|
|
3759
|
+
|
|
3760
|
+
/* ------------------------------------------------------------------------
|
|
3761
|
+
* context enc 24a — nvenc dual engine model + nvenc backend
|
|
3762
|
+
* ---------------------------------------------------------------------- */
|
|
3763
|
+
|
|
3764
|
+
/**
|
|
3765
|
+
* nvenc dual engine model (v6): each blackwell engine sustains
|
|
3766
|
+
* 800 mpix/s, the aggregate is 1600 mpix/s across av1/hevc 8k hdr with
|
|
3767
|
+
* split-frame encoding. the numbers match the caps of NvencBackend.
|
|
3768
|
+
*/
|
|
3769
|
+
class NvencDualEngine final {
|
|
3770
|
+
public:
|
|
3771
|
+
static constexpr std::uint32_t kTotalMpixPerSec = 1600;
|
|
3772
|
+
static constexpr std::uint32_t kPerEngineMpix = 800;
|
|
3773
|
+
static constexpr std::uint32_t kEngineCount = 2;
|
|
3774
|
+
|
|
3775
|
+
/** codec triage of the engine. */
|
|
3776
|
+
enum class Codec : std::uint8_t { H264, HEVC, AV1 };
|
|
3777
|
+
|
|
3778
|
+
/** per-codec session envelope. */
|
|
3779
|
+
struct SessionLimits {
|
|
3780
|
+
Codec codec;
|
|
3781
|
+
std::uint32_t maxwidth;
|
|
3782
|
+
std::uint32_t maxheight;
|
|
3783
|
+
std::uint32_t maxfps;
|
|
3784
|
+
std::uint32_t mpixpersec;
|
|
3785
|
+
bool hdr10supported;
|
|
3786
|
+
bool splitframe;
|
|
3787
|
+
};
|
|
3788
|
+
|
|
3789
|
+
/** @return the per-codec session table */
|
|
3790
|
+
[[nodiscard]] static std::vector<SessionLimits> sessionTable() noexcept {
|
|
3791
|
+
return {
|
|
3792
|
+
{Codec::H264, 8192, 8192, 60, 480, false, true},
|
|
3793
|
+
{Codec::HEVC, 8192, 8192, 60, 800, true, true},
|
|
3794
|
+
{Codec::AV1, 8192, 8192, 60, 800, true, true},
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
/**
|
|
3799
|
+
* checks whether the requested load fits the aggregate throughput.
|
|
3800
|
+
* @param w width
|
|
3801
|
+
* @param h height
|
|
3802
|
+
* @param fps frame rate
|
|
3803
|
+
* @param sessions concurrent session count
|
|
3804
|
+
*/
|
|
3805
|
+
[[nodiscard]] static bool canFit(std::uint32_t w, std::uint32_t h,
|
|
3806
|
+
std::uint32_t fps,
|
|
3807
|
+
std::uint32_t sessions = 1) noexcept {
|
|
3808
|
+
std::uint64_t mpix = static_cast<std::uint64_t>(w) * h * fps /
|
|
3809
|
+
1'000'000ULL * sessions;
|
|
3810
|
+
return mpix <= kTotalMpixPerSec;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
/** @return the qmp frame querying the encoder status */
|
|
3814
|
+
[[nodiscard]] static vm::QmpMessage qmpQueryEncoders() {
|
|
3815
|
+
return vm::QmpMessage::makeExecute("query-nvenc-status");
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
/** @return one-line human description with the sdk/driver anchors */
|
|
3819
|
+
[[nodiscard]] static std::string describe() noexcept {
|
|
3820
|
+
return std::format(
|
|
3821
|
+
"nvenc blackwell b100 dual-engine {} mpix/s total ({} per engine), "
|
|
3822
|
+
"av1/hevc 8k hdr60, split-frame, sdk 13.0, driver 575.57.08",
|
|
3823
|
+
kTotalMpixPerSec, kPerEngineMpix);
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3826
|
+
|
|
3827
|
+
/** nvenc backend private state. */
|
|
3828
|
+
struct NvencImpl {
|
|
3829
|
+
EncodeStreamConfig cfg{};
|
|
3830
|
+
EncoderCaps caps{};
|
|
3831
|
+
EncodeStats stats{};
|
|
3832
|
+
bool dualencoder{true};
|
|
3833
|
+
bool splitframe{false};
|
|
3834
|
+
uint32_t strips{2};
|
|
3835
|
+
bool initialized{false};
|
|
3836
|
+
std::atomic<uint64_t> frames{0};
|
|
3837
|
+
};
|
|
3838
|
+
|
|
3839
|
+
/**
|
|
3840
|
+
* nvenc backend: blackwell 9th gen caps (8192^2, 10 bit, b-frames 5,
|
|
3841
|
+
* 8 sessions, dual encoder with auto split-frame at 4k+); packets carry
|
|
3842
|
+
* simulated obu/annexb headers with bitrate-derived sizes.
|
|
3843
|
+
*/
|
|
3844
|
+
class NvencBackend final : public IEncoderBackend {
|
|
3845
|
+
public:
|
|
3846
|
+
NvencBackend() : mimpl(std::make_unique<NvencImpl>()) {
|
|
3847
|
+
mimpl->caps.vendor = EncoderVendor::NvidiaNvenc;
|
|
3848
|
+
mimpl->caps.name =
|
|
3849
|
+
"NVIDIA NVENC Blackwell 9th Gen dual-engine (Driver 575.57.08)";
|
|
3850
|
+
mimpl->caps.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
|
|
3851
|
+
mimpl->caps.maxwidth = 8192;
|
|
3852
|
+
mimpl->caps.maxheight = 8192;
|
|
3853
|
+
mimpl->caps.maxbitdepth = 10;
|
|
3854
|
+
mimpl->caps.bframessupported = true;
|
|
3855
|
+
mimpl->caps.maxbframes = 5; /* av1 0..5 */
|
|
3856
|
+
mimpl->caps.lookaheadsupported = true;
|
|
3857
|
+
mimpl->caps.temporalaqsupported = true;
|
|
3858
|
+
mimpl->caps.spatialaqsupported = true;
|
|
3859
|
+
mimpl->caps.maxsessions = 8; /* r575 lifts the legacy 5 session cap */
|
|
3860
|
+
mimpl->caps.concurrentinstances = 8;
|
|
3861
|
+
mimpl->caps.dualencoder = true;
|
|
3862
|
+
mimpl->caps.maxthroughputmpixpersec =
|
|
3863
|
+
NvencDualEngine::kTotalMpixPerSec;
|
|
3864
|
+
}
|
|
3865
|
+
~NvencBackend() override = default;
|
|
3866
|
+
|
|
3867
|
+
EncoderVendor vendor() const noexcept override {
|
|
3868
|
+
return EncoderVendor::NvidiaNvenc;
|
|
3869
|
+
}
|
|
3870
|
+
EncResult<EncoderCaps> querycaps() const override { return mimpl->caps; }
|
|
3871
|
+
|
|
3872
|
+
EncResult<void> configure(const EncodeStreamConfig& cfg) override {
|
|
3873
|
+
mimpl->cfg = cfg;
|
|
3874
|
+
if (std::find(mimpl->caps.supportedcodecs.begin(),
|
|
3875
|
+
mimpl->caps.supportedcodecs.end(),
|
|
3876
|
+
cfg.codec) == mimpl->caps.supportedcodecs.end())
|
|
3877
|
+
return std::unexpected(
|
|
3878
|
+
makeencerr(-2, "NVENC codec not supported"));
|
|
3879
|
+
mimpl->initialized = true;
|
|
3880
|
+
mimpl->stats = EncodeStats{};
|
|
3881
|
+
/* auto split-frame when the dual engine covers >= 4k */
|
|
3882
|
+
if (mimpl->dualencoder && (cfg.width * cfg.height >= 3840 * 2160))
|
|
3883
|
+
mimpl->splitframe = true;
|
|
3884
|
+
return {};
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
EncResult<EncodedPacket> encode(const EncodeFrame& frame) override {
|
|
3888
|
+
if (!mimpl->initialized)
|
|
3889
|
+
return std::unexpected(makeencerr(-3, "NVENC not configured"));
|
|
3890
|
+
auto start = std::chrono::high_resolution_clock::now();
|
|
3891
|
+
uint64_t bytes = static_cast<uint64_t>(mimpl->cfg.bitratekbps) *
|
|
3892
|
+
1000 / 8 / std::max(1u, mimpl->cfg.fpsnum) + 64;
|
|
3893
|
+
if (frame.isidr) bytes = bytes * 2;
|
|
3894
|
+
|
|
3895
|
+
EncodedPacket pkt{};
|
|
3896
|
+
pkt.data.resize(static_cast<size_t>(bytes), 0x00);
|
|
3897
|
+
if (mimpl->cfg.codec == Codec::Av1) {
|
|
3898
|
+
if (frame.isidr) pkt.data[0] = 0x12; /* keyframe obu */
|
|
3899
|
+
} else if (mimpl->cfg.codec == Codec::H265) {
|
|
3900
|
+
pkt.data[0] = 0x00; pkt.data[1] = 0x00; pkt.data[2] = 0x00;
|
|
3901
|
+
pkt.data[3] = 0x01; pkt.data[4] = frame.isidr ? 0x40 : 0x02;
|
|
3902
|
+
} else {
|
|
3903
|
+
pkt.data[0] = 0x00; pkt.data[1] = 0x00; pkt.data[2] = 0x00;
|
|
3904
|
+
pkt.data[3] = 0x01; pkt.data[4] = frame.isidr ? 0x67 : 0x41;
|
|
3905
|
+
}
|
|
3906
|
+
pkt.pts = frame.pts;
|
|
3907
|
+
pkt.dts = frame.pts;
|
|
3908
|
+
pkt.iskeyframe = frame.isidr;
|
|
3909
|
+
pkt.codec = mimpl->cfg.codec;
|
|
3910
|
+
pkt.frametype = frame.isidr ? 0 : 1;
|
|
3911
|
+
pkt.encodeduration =
|
|
3912
|
+
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
3913
|
+
std::chrono::high_resolution_clock::now() - start);
|
|
3914
|
+
mimpl->stats.framesencoded++;
|
|
3915
|
+
mimpl->stats.bytesoutput += bytes;
|
|
3916
|
+
mimpl->stats.lastframets = std::chrono::steady_clock::now();
|
|
3917
|
+
mimpl->frames.fetch_add(1);
|
|
3918
|
+
return pkt;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
EncResult<void> flush() override { return {}; }
|
|
3922
|
+
EncResult<EncodeStats> stats() const override { return mimpl->stats; }
|
|
3923
|
+
|
|
3924
|
+
/** toggles the dual engine. */
|
|
3925
|
+
EncResult<void> setdualencoder(bool enable) {
|
|
3926
|
+
mimpl->dualencoder = enable;
|
|
3927
|
+
return {};
|
|
3928
|
+
}
|
|
3929
|
+
/** toggles split-frame encoding with strip count. */
|
|
3930
|
+
EncResult<void> setsplitframeencoding(bool enable,
|
|
3931
|
+
uint32_t numstrips = 2) {
|
|
3932
|
+
mimpl->splitframe = enable;
|
|
3933
|
+
mimpl->strips = numstrips;
|
|
3934
|
+
return {};
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
private:
|
|
3938
|
+
std::unique_ptr<NvencImpl> mimpl;
|
|
3939
|
+
};
|
|
3940
|
+
|
|
3941
|
+
/* ------------------------------------------------------------------------
|
|
3942
|
+
* context enc 24b — amf (rdna4 vcn 5) and quicksync (vpl arrow lake)
|
|
3943
|
+
* ---------------------------------------------------------------------- */
|
|
3944
|
+
|
|
3945
|
+
/** amf backend private state. */
|
|
3946
|
+
struct AmfImpl {
|
|
3947
|
+
EncodeStreamConfig cfg{};
|
|
3948
|
+
EncoderCaps caps{};
|
|
3949
|
+
EncodeStats stats{};
|
|
3950
|
+
bool dualvcn{true};
|
|
3951
|
+
bool initialized{false};
|
|
3952
|
+
};
|
|
3953
|
+
|
|
3954
|
+
/**
|
|
3955
|
+
* amf backend: rdna4 vcn 5.0 on the dual-vcn navi48 (rx 9070 xt), b-frame
|
|
3956
|
+
* support for av1/hevc, preanalysis lookahead, 950 mpix/s aggregate.
|
|
3957
|
+
*/
|
|
3958
|
+
class AmfBackend final : public IEncoderBackend {
|
|
3959
|
+
public:
|
|
3960
|
+
AmfBackend() : mimpl(std::make_unique<AmfImpl>()) {
|
|
3961
|
+
mimpl->caps.vendor = EncoderVendor::AmdAmf;
|
|
3962
|
+
mimpl->caps.name =
|
|
3963
|
+
"AMD AMF VCN 5.0 - RX 9070 XT Dual VCN (AMF 1.4.36 / ROCm 6.4)";
|
|
3964
|
+
mimpl->caps.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
|
|
3965
|
+
mimpl->caps.maxwidth = 8192;
|
|
3966
|
+
mimpl->caps.maxheight = 4320;
|
|
3967
|
+
mimpl->caps.maxbitdepth = 10;
|
|
3968
|
+
mimpl->caps.bframessupported = true; /* rdna4 addition */
|
|
3969
|
+
mimpl->caps.maxbframes = 3;
|
|
3970
|
+
mimpl->caps.lookaheadsupported = true; /* preanalysis */
|
|
3971
|
+
mimpl->caps.temporalaqsupported = true;
|
|
3972
|
+
mimpl->caps.spatialaqsupported = true;
|
|
3973
|
+
mimpl->caps.maxsessions = 4;
|
|
3974
|
+
mimpl->caps.concurrentinstances = 4;
|
|
3975
|
+
mimpl->caps.dualencoder = true;
|
|
3976
|
+
mimpl->caps.maxthroughputmpixpersec = 950;
|
|
3977
|
+
}
|
|
3978
|
+
~AmfBackend() override = default;
|
|
3979
|
+
|
|
3980
|
+
EncoderVendor vendor() const noexcept override {
|
|
3981
|
+
return EncoderVendor::AmdAmf;
|
|
3982
|
+
}
|
|
3983
|
+
EncResult<EncoderCaps> querycaps() const override { return mimpl->caps; }
|
|
3984
|
+
EncResult<void> configure(const EncodeStreamConfig& cfg) override {
|
|
3985
|
+
mimpl->cfg = cfg;
|
|
3986
|
+
mimpl->initialized = true;
|
|
3987
|
+
return {};
|
|
3988
|
+
}
|
|
3989
|
+
EncResult<EncodedPacket> encode(const EncodeFrame& frame) override {
|
|
3990
|
+
if (!mimpl->initialized)
|
|
3991
|
+
return std::unexpected(makeencerr(-3, "AMF not configured"));
|
|
3992
|
+
auto start = std::chrono::high_resolution_clock::now();
|
|
3993
|
+
uint64_t bytes = static_cast<uint64_t>(mimpl->cfg.bitratekbps) * 125 /
|
|
3994
|
+
std::max(1u, mimpl->cfg.fpsnum) + 48;
|
|
3995
|
+
if (frame.isidr) bytes *= 2;
|
|
3996
|
+
EncodedPacket pkt{};
|
|
3997
|
+
pkt.data.resize(static_cast<size_t>(bytes));
|
|
3998
|
+
pkt.pts = frame.pts;
|
|
3999
|
+
pkt.dts = frame.pts;
|
|
4000
|
+
pkt.iskeyframe = frame.isidr;
|
|
4001
|
+
pkt.codec = mimpl->cfg.codec;
|
|
4002
|
+
pkt.encodeduration =
|
|
4003
|
+
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
4004
|
+
std::chrono::high_resolution_clock::now() - start);
|
|
4005
|
+
mimpl->stats.framesencoded++;
|
|
4006
|
+
mimpl->stats.bytesoutput += bytes;
|
|
4007
|
+
return pkt;
|
|
4008
|
+
}
|
|
4009
|
+
EncResult<void> flush() override { return {}; }
|
|
4010
|
+
EncResult<EncodeStats> stats() const override { return mimpl->stats; }
|
|
4011
|
+
|
|
4012
|
+
/** toggles simultaneous dual vcn usage. */
|
|
4013
|
+
EncResult<void> setdualvcn(bool enable) {
|
|
4014
|
+
mimpl->dualvcn = enable;
|
|
4015
|
+
return {};
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
private:
|
|
4019
|
+
std::unique_ptr<AmfImpl> mimpl;
|
|
4020
|
+
};
|
|
4021
|
+
|
|
4022
|
+
/** quicksync backend private state. */
|
|
4023
|
+
struct QuickSyncImpl {
|
|
4024
|
+
EncodeStreamConfig cfg{};
|
|
4025
|
+
EncoderCaps caps{};
|
|
4026
|
+
EncodeStats stats{};
|
|
4027
|
+
bool initialized{false};
|
|
4028
|
+
};
|
|
4029
|
+
|
|
4030
|
+
/**
|
|
4031
|
+
* quicksync backend: onevpl 2.12 dispatcher on arrow lake gt2 /
|
|
4032
|
+
* battlemage, 8 b-frames, 900 mpix/s, best for ultra low latency h264.
|
|
4033
|
+
*/
|
|
4034
|
+
class QuickSyncBackend final : public IEncoderBackend {
|
|
4035
|
+
public:
|
|
4036
|
+
QuickSyncBackend() : mimpl(std::make_unique<QuickSyncImpl>()) {
|
|
4037
|
+
mimpl->caps.vendor = EncoderVendor::IntelQsv;
|
|
4038
|
+
mimpl->caps.name =
|
|
4039
|
+
"Intel QuickSync VPL 2.12 - Arrow Lake GT2 / Battlemage";
|
|
4040
|
+
mimpl->caps.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
|
|
4041
|
+
mimpl->caps.maxwidth = 8192;
|
|
4042
|
+
mimpl->caps.maxheight = 8192;
|
|
4043
|
+
mimpl->caps.maxbitdepth = 10;
|
|
4044
|
+
mimpl->caps.bframessupported = true;
|
|
4045
|
+
mimpl->caps.maxbframes = 8;
|
|
4046
|
+
mimpl->caps.lookaheadsupported = true;
|
|
4047
|
+
mimpl->caps.temporalaqsupported = true;
|
|
4048
|
+
mimpl->caps.spatialaqsupported = false;
|
|
4049
|
+
mimpl->caps.maxsessions = 8;
|
|
4050
|
+
mimpl->caps.concurrentinstances = 8;
|
|
4051
|
+
mimpl->caps.dualencoder = false;
|
|
4052
|
+
mimpl->caps.maxthroughputmpixpersec = 900;
|
|
4053
|
+
}
|
|
4054
|
+
~QuickSyncBackend() override = default;
|
|
4055
|
+
|
|
4056
|
+
EncoderVendor vendor() const noexcept override {
|
|
4057
|
+
return EncoderVendor::IntelQsv;
|
|
4058
|
+
}
|
|
4059
|
+
EncResult<EncoderCaps> querycaps() const override { return mimpl->caps; }
|
|
4060
|
+
EncResult<void> configure(const EncodeStreamConfig& cfg) override {
|
|
4061
|
+
mimpl->cfg = cfg;
|
|
4062
|
+
mimpl->initialized = true;
|
|
4063
|
+
return {};
|
|
4064
|
+
}
|
|
4065
|
+
EncResult<EncodedPacket> encode(const EncodeFrame& frame) override {
|
|
4066
|
+
if (!mimpl->initialized)
|
|
4067
|
+
return std::unexpected(makeencerr(-3, "QSV not configured"));
|
|
4068
|
+
auto start = std::chrono::high_resolution_clock::now();
|
|
4069
|
+
uint64_t bytes = static_cast<uint64_t>(mimpl->cfg.bitratekbps) * 125 /
|
|
4070
|
+
std::max(1u, mimpl->cfg.fpsnum) + 32;
|
|
4071
|
+
if (frame.isidr) bytes *= 2;
|
|
4072
|
+
EncodedPacket pkt{};
|
|
4073
|
+
pkt.data.resize(static_cast<size_t>(bytes));
|
|
4074
|
+
pkt.pts = frame.pts;
|
|
4075
|
+
pkt.dts = frame.pts;
|
|
4076
|
+
pkt.iskeyframe = frame.isidr;
|
|
4077
|
+
pkt.codec = mimpl->cfg.codec;
|
|
4078
|
+
pkt.encodeduration =
|
|
4079
|
+
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
4080
|
+
std::chrono::high_resolution_clock::now() - start);
|
|
4081
|
+
mimpl->stats.framesencoded++;
|
|
4082
|
+
mimpl->stats.bytesoutput += bytes;
|
|
4083
|
+
return pkt;
|
|
4084
|
+
}
|
|
4085
|
+
EncResult<void> flush() override { return {}; }
|
|
4086
|
+
EncResult<EncodeStats> stats() const override { return mimpl->stats; }
|
|
4087
|
+
|
|
4088
|
+
private:
|
|
4089
|
+
std::unique_ptr<QuickSyncImpl> mimpl;
|
|
4090
|
+
};
|
|
4091
|
+
|
|
4092
|
+
std::unique_ptr<IEncoderBackend> IEncoderBackend::create(EncoderVendor v) {
|
|
4093
|
+
switch (v) {
|
|
4094
|
+
case EncoderVendor::NvidiaNvenc:
|
|
4095
|
+
return std::make_unique<NvencBackend>();
|
|
4096
|
+
case EncoderVendor::AmdAmf:
|
|
4097
|
+
return std::make_unique<AmfBackend>();
|
|
4098
|
+
case EncoderVendor::IntelQsv:
|
|
4099
|
+
return std::make_unique<QuickSyncBackend>();
|
|
4100
|
+
default:
|
|
4101
|
+
return nullptr;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
/* ------------------------------------------------------------------------
|
|
4106
|
+
* context enc 24c — sdk descriptor contracts from the v6 header
|
|
4107
|
+
* ---------------------------------------------------------------------- */
|
|
4108
|
+
|
|
4109
|
+
/** nvenc video codec sdk 13.0.19 descriptor (av1 4:4:4, hevc 8k hdr,
|
|
4110
|
+
* 16x parallel sessions). */
|
|
4111
|
+
struct nvenc130context final {
|
|
4112
|
+
static constexpr std::uint32_t versionMajor = 13;
|
|
4113
|
+
static constexpr std::uint32_t versionMinor = 0;
|
|
4114
|
+
static constexpr std::uint32_t versionRev = 19;
|
|
4115
|
+
enum class codec : uint8_t { h264, hevc, av1 };
|
|
4116
|
+
struct session {
|
|
4117
|
+
void* encoder{nullptr};
|
|
4118
|
+
codec codecType{codec::av1};
|
|
4119
|
+
uint32_t width{3840};
|
|
4120
|
+
uint32_t height{2160};
|
|
4121
|
+
uint32_t bitrateKbps{80000};
|
|
4122
|
+
uint32_t fps{60};
|
|
4123
|
+
bool bFrames{true};
|
|
4124
|
+
bool lowLatency{false};
|
|
4125
|
+
};
|
|
4126
|
+
};
|
|
4127
|
+
|
|
4128
|
+
/** amf 1.4.35 runtime descriptor (av1 encode, rdna4 vcn, 8k240). */
|
|
4129
|
+
struct amf14context final {
|
|
4130
|
+
static constexpr std::string_view version = "1.4.35";
|
|
4131
|
+
enum class codec : uint8_t { h264, hevc, av1, avc };
|
|
4132
|
+
struct contextDesc {
|
|
4133
|
+
codec codecType{codec::av1};
|
|
4134
|
+
uint32_t width{3840};
|
|
4135
|
+
uint32_t height{2160};
|
|
4136
|
+
uint32_t bitrate{80000};
|
|
4137
|
+
uint32_t gopSize{60};
|
|
4138
|
+
bool preanalysis{true};
|
|
4139
|
+
};
|
|
4140
|
+
};
|
|
4141
|
+
|
|
4142
|
+
/** onevpl 2.12 dispatcher descriptor (arrow/lunar lake, av1 8k). */
|
|
4143
|
+
struct qsvvpl212context final {
|
|
4144
|
+
static constexpr std::uint32_t apiVersionMajor = 2;
|
|
4145
|
+
static constexpr std::uint32_t apiVersionMinor = 12;
|
|
4146
|
+
static constexpr std::string_view impl = "VPL 2.12.0 Dispatcher (libvpl)";
|
|
4147
|
+
enum class codec : uint8_t { h264, hevc, av1, vp9 };
|
|
4148
|
+
enum class accel : uint8_t { d3d11, vaapi, opencl };
|
|
4149
|
+
struct sessionDesc {
|
|
4150
|
+
codec codecType{codec::av1};
|
|
4151
|
+
accel accelType{accel::vaapi};
|
|
4152
|
+
uint32_t width{3840};
|
|
4153
|
+
uint32_t height{2160};
|
|
4154
|
+
uint32_t targetKbps{40000};
|
|
4155
|
+
uint32_t gop{96};
|
|
4156
|
+
bool lowPower{true};
|
|
4157
|
+
};
|
|
4158
|
+
};
|
|
4159
|
+
|
|
4160
|
+
/* ------------------------------------------------------------------------
|
|
4161
|
+
* context enc 25a — encoding optimization manager
|
|
4162
|
+
* ---------------------------------------------------------------------- */
|
|
4163
|
+
|
|
4164
|
+
/**
|
|
4165
|
+
* encoding optimization manager: enumerates every backend (plus the
|
|
4166
|
+
* x265/svt-av1 software fallback), picks the optimal backend for a
|
|
4167
|
+
* stream (av1 4k forces the blackwell dual engine, ull h264 forces qsv),
|
|
4168
|
+
* benchmarks 120-frame runs and recommends presets and instance counts.
|
|
4169
|
+
*/
|
|
4170
|
+
class EncodingOptimizationManager {
|
|
4171
|
+
public:
|
|
4172
|
+
EncodingOptimizationManager() {
|
|
4173
|
+
availablevendors = {EncoderVendor::NvidiaNvenc, EncoderVendor::AmdAmf,
|
|
4174
|
+
EncoderVendor::IntelQsv};
|
|
4175
|
+
}
|
|
4176
|
+
~EncodingOptimizationManager() = default;
|
|
4177
|
+
|
|
4178
|
+
/** @return caps of every hardware backend plus the software fallback */
|
|
4179
|
+
EncResult<std::vector<EncoderCaps>> enumerateallcaps() const {
|
|
4180
|
+
std::lock_guard lk(mu);
|
|
4181
|
+
std::vector<EncoderCaps> out;
|
|
4182
|
+
out.push_back(NvencBackend{}.querycaps().value());
|
|
4183
|
+
out.push_back(AmfBackend{}.querycaps().value());
|
|
4184
|
+
out.push_back(QuickSyncBackend{}.querycaps().value());
|
|
4185
|
+
EncoderCaps sw{};
|
|
4186
|
+
sw.vendor = EncoderVendor::SoftwareX265;
|
|
4187
|
+
sw.name = "Software x265 4.1 + SVT-AV1 3.0 (fallback)";
|
|
4188
|
+
sw.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
|
|
4189
|
+
sw.maxwidth = 8192;
|
|
4190
|
+
sw.maxheight = 8192;
|
|
4191
|
+
sw.maxsessions = 32;
|
|
4192
|
+
out.push_back(sw);
|
|
4193
|
+
return out;
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
/**
|
|
4197
|
+
* creates and configures the best backend for the stream following the
|
|
4198
|
+
* engine policy: av1 >= 4k goes to the blackwell dual engine, ultra low
|
|
4199
|
+
* latency h264 goes to qsv, everything else honors the preference.
|
|
4200
|
+
*/
|
|
4201
|
+
EncResult<std::unique_ptr<IEncoderBackend>> createoptimal(
|
|
4202
|
+
const EncodeStreamConfig& cfg) const {
|
|
4203
|
+
std::lock_guard lk(mu);
|
|
4204
|
+
EncoderVendor chosen = cfg.preferredvendor;
|
|
4205
|
+
if (cfg.codec == Codec::Av1 && cfg.width >= 3840 && cfg.height >= 2160)
|
|
4206
|
+
chosen = EncoderVendor::NvidiaNvenc;
|
|
4207
|
+
if (cfg.tune == Tune::UltraLowLatency && cfg.codec == Codec::H264)
|
|
4208
|
+
chosen = EncoderVendor::IntelQsv;
|
|
4209
|
+
auto backend = IEncoderBackend::create(chosen);
|
|
4210
|
+
if (!backend)
|
|
4211
|
+
return std::unexpected(
|
|
4212
|
+
makeencerr(-5, "Failed to create backend"));
|
|
4213
|
+
if (auto r = backend->configure(cfg); !r)
|
|
4214
|
+
return std::unexpected(r.error());
|
|
4215
|
+
return backend;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
/**
|
|
4219
|
+
* benchmarks numframes (default 120) through every hardware backend
|
|
4220
|
+
* and reports per-backend fps and average latency.
|
|
4221
|
+
*/
|
|
4222
|
+
EncResult<std::map<std::string, EncodeStats>> benchmarkstreams(
|
|
4223
|
+
const EncodeStreamConfig& basecfg, uint32_t numframes = 120) const {
|
|
4224
|
+
std::map<std::string, EncodeStats> results;
|
|
4225
|
+
auto capslist = enumerateallcaps();
|
|
4226
|
+
if (!capslist) return std::unexpected(capslist.error());
|
|
4227
|
+
|
|
4228
|
+
for (auto& cap : *capslist) {
|
|
4229
|
+
if (cap.vendor == EncoderVendor::SoftwareX265) continue;
|
|
4230
|
+
auto backend = IEncoderBackend::create(cap.vendor);
|
|
4231
|
+
if (!backend) continue;
|
|
4232
|
+
EncodeStreamConfig cfg = basecfg;
|
|
4233
|
+
if (auto rr = backend->configure(cfg); !rr) continue;
|
|
4234
|
+
auto t0 = std::chrono::high_resolution_clock::now();
|
|
4235
|
+
for (uint32_t i = 0; i < numframes; ++i) {
|
|
4236
|
+
EncodeFrame f{};
|
|
4237
|
+
f.pts = i * 1000 / std::max(1u, cfg.fpsnum);
|
|
4238
|
+
f.isidr = (i % cfg.goplength == 0);
|
|
4239
|
+
f.yuvdata.resize(
|
|
4240
|
+
static_cast<size_t>(cfg.width) * cfg.height * 3 / 2, 0x80);
|
|
4241
|
+
(void)backend->encode(f);
|
|
4242
|
+
}
|
|
4243
|
+
auto t1 = std::chrono::high_resolution_clock::now();
|
|
4244
|
+
auto st = backend->stats();
|
|
4245
|
+
if (st) {
|
|
4246
|
+
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
4247
|
+
t1 - t0)
|
|
4248
|
+
.count();
|
|
4249
|
+
st->encodefps = dur ? (numframes * 1000.0 / dur) : 0;
|
|
4250
|
+
st->encodelatencyusavg = dur ? (dur * 1000 / numframes) : 0;
|
|
4251
|
+
results[cap.name] = *st;
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
return results;
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
/** recommends a preset for the codec/tune/use-case combination. */
|
|
4258
|
+
static Preset recommendpreset(Codec codec, Tune tune, bool live) {
|
|
4259
|
+
if (tune == Tune::UltraLowLatency) return Preset::P1Fastest;
|
|
4260
|
+
if (tune == Tune::LowLatency) return Preset::P2Faster;
|
|
4261
|
+
if (live) return Preset::P3Fast;
|
|
4262
|
+
if (codec == Codec::Av1) return Preset::P5Slow;
|
|
4263
|
+
if (codec == Codec::H265) return Preset::P6Slower;
|
|
4264
|
+
return Preset::P4Medium;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
/** recommends the concurrent session ceiling per gpu and resolution. */
|
|
4268
|
+
static uint32_t recommendmaxinstancespergpu(EncoderVendor vendor,
|
|
4269
|
+
Codec codec, uint32_t w,
|
|
4270
|
+
uint32_t h) {
|
|
4271
|
+
uint64_t pixels = static_cast<uint64_t>(w) * h;
|
|
4272
|
+
switch (vendor) {
|
|
4273
|
+
case EncoderVendor::NvidiaNvenc:
|
|
4274
|
+
if (pixels >= 3840ULL * 2160ULL) return (codec == Codec::Av1) ? 2 : 3;
|
|
4275
|
+
if (pixels >= 1920ULL * 1080ULL) return 8;
|
|
4276
|
+
return 16;
|
|
4277
|
+
case EncoderVendor::AmdAmf:
|
|
4278
|
+
if (pixels >= 3840ULL * 2160ULL) return 2;
|
|
4279
|
+
return 4;
|
|
4280
|
+
case EncoderVendor::IntelQsv:
|
|
4281
|
+
if (pixels >= 3840ULL * 2160ULL) return 4;
|
|
4282
|
+
return 8;
|
|
4283
|
+
default:
|
|
4284
|
+
return 1;
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
private:
|
|
4289
|
+
mutable std::mutex mu;
|
|
4290
|
+
std::vector<EncoderVendor> availablevendors;
|
|
4291
|
+
};
|
|
4292
|
+
|
|
4293
|
+
} // namespace enc
|
|
4294
|
+
|
|
4295
|
+
/* ==========================================================================
|
|
4296
|
+
* context core 25 — virtualization core facade (v6 build pipeline)
|
|
4297
|
+
* ======================================================================== */
|
|
4298
|
+
|
|
4299
|
+
/**
|
|
4300
|
+
* top-level facade composing the vm primitives, the gpu profile tables and
|
|
4301
|
+
* the encoder models. the build pipeline runs seven steps: kvm system,
|
|
4302
|
+
* vm + memory slot, vcpus, dirty ring, vfio container with identity dma
|
|
4303
|
+
* map, b100 profile lookup and mig profile lookup.
|
|
4304
|
+
*/
|
|
4305
|
+
class VirtualizationCore final {
|
|
4306
|
+
public:
|
|
4307
|
+
/** facade configuration with engine defaults. */
|
|
4308
|
+
struct Config {
|
|
4309
|
+
std::string qmpsocketpath{"/run/vhe/vm.qmp"};
|
|
4310
|
+
std::string mdevparentpci{"0000:08:00.0"};
|
|
4311
|
+
std::string b100profilename{"B100-4Q"};
|
|
4312
|
+
std::string migprofilename{"2g.48gb"};
|
|
4313
|
+
bool enablec2c{true};
|
|
4314
|
+
std::uint32_t vcpucount{16};
|
|
4315
|
+
std::uint64_t guestrambytes{64ULL << 30};
|
|
4316
|
+
std::uint32_t vfiogroupid{45};
|
|
4317
|
+
std::string vfiobdf{"0000:08:00.0"};
|
|
4318
|
+
};
|
|
4319
|
+
|
|
4320
|
+
/**
|
|
4321
|
+
* runs the seven step build pipeline and returns the composed core.
|
|
4322
|
+
* @param cfg facade configuration
|
|
4323
|
+
* @return the assembled core or a KvmError
|
|
4324
|
+
*/
|
|
4325
|
+
[[nodiscard]] static KvmResult<VirtualizationCore> build(
|
|
4326
|
+
const Config& cfg) noexcept {
|
|
4327
|
+
try {
|
|
4328
|
+
/* 1. kvm system */
|
|
4329
|
+
auto sysres = vm::KvmSystem::open();
|
|
4330
|
+
if (!sysres) return std::unexpected(sysres.error());
|
|
4331
|
+
vm::KvmSystem sys = std::move(sysres.value());
|
|
4332
|
+
|
|
4333
|
+
auto dirtyringcap = sys.checkExtension(KVM_CAP_DIRTY_LOG_RING);
|
|
4334
|
+
if (!dirtyringcap) return std::unexpected(dirtyringcap.error());
|
|
4335
|
+
auto iommucap = sys.checkExtension(KVM_CAP_IOMMU);
|
|
4336
|
+
(void)iommucap;
|
|
4337
|
+
|
|
4338
|
+
/* 2. vm + memory slot */
|
|
4339
|
+
auto vmres = vm::KvmVm::create(sys);
|
|
4340
|
+
if (!vmres) return std::unexpected(vmres.error());
|
|
4341
|
+
vm::KvmVm kvmvm = std::move(vmres.value());
|
|
4342
|
+
|
|
4343
|
+
void* hostmem =
|
|
4344
|
+
::mmap(nullptr, cfg.guestrambytes, PROT_READ | PROT_WRITE,
|
|
4345
|
+
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
|
4346
|
+
if (hostmem == MAP_FAILED) {
|
|
4347
|
+
return std::unexpected(
|
|
4348
|
+
KvmError{errno, "VirtualizationCore::build",
|
|
4349
|
+
"host mmap failed"});
|
|
4350
|
+
}
|
|
4351
|
+
vm::KvmVm::MemoryRegion region{
|
|
4352
|
+
.guest_phys_addr = 0x0,
|
|
4353
|
+
.memory_size = cfg.guestrambytes,
|
|
4354
|
+
.userspace_addr = reinterpret_cast<std::uint64_t>(hostmem),
|
|
4355
|
+
.slot = 0,
|
|
4356
|
+
.flags = KVM_MEM_LOG_DIRTY_PAGES,
|
|
4357
|
+
};
|
|
4358
|
+
auto memres = kvmvm.setUserMemoryRegion(region);
|
|
4359
|
+
if (!memres) {
|
|
4360
|
+
::munmap(hostmem, cfg.guestrambytes);
|
|
4361
|
+
return std::unexpected(memres.error());
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
/* 3. vcpus */
|
|
4365
|
+
std::vector<vm::KvmVcpu> vcpus;
|
|
4366
|
+
vcpus.reserve(cfg.vcpucount);
|
|
4367
|
+
for (std::uint32_t i = 0; i < cfg.vcpucount; ++i) {
|
|
4368
|
+
auto vcpures = vm::KvmVcpu::create(kvmvm, i);
|
|
4369
|
+
if (!vcpures) {
|
|
4370
|
+
::munmap(hostmem, cfg.guestrambytes);
|
|
4371
|
+
return std::unexpected(vcpures.error());
|
|
4372
|
+
}
|
|
4373
|
+
vcpus.emplace_back(std::move(vcpures.value()));
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
/* 4. dirty ring (bitmap fallback lives in the ring class) */
|
|
4377
|
+
vm::DirtyLogRingBuffer ring;
|
|
4378
|
+
(void)ring.enableRing(kvmvm);
|
|
4379
|
+
|
|
4380
|
+
/* 5. vfio container with identity iova 0 dma map (best effort) */
|
|
4381
|
+
std::optional<vm::VfioContainer> vfiocontainer;
|
|
4382
|
+
auto contres = vm::VfioContainer::open();
|
|
4383
|
+
if (contres) {
|
|
4384
|
+
vfiocontainer = std::move(contres.value());
|
|
4385
|
+
vm::VfioIommuType1v2::DmaMap map{
|
|
4386
|
+
.vaddr = reinterpret_cast<std::uint64_t>(hostmem),
|
|
4387
|
+
.iova = 0x0,
|
|
4388
|
+
.size = cfg.guestrambytes,
|
|
4389
|
+
.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
|
|
4390
|
+
};
|
|
4391
|
+
(void)vfiocontainer->dmaMap(map);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
/* 6. b100 profile lookup */
|
|
4395
|
+
auto b100 = gpu::B100Profile::findByName(cfg.b100profilename);
|
|
4396
|
+
if (!b100) {
|
|
4397
|
+
::munmap(hostmem, cfg.guestrambytes);
|
|
4398
|
+
return std::unexpected(
|
|
4399
|
+
KvmError{EINVAL, "VirtualizationCore::build",
|
|
4400
|
+
std::format("unknown b100 profile {}",
|
|
4401
|
+
cfg.b100profilename)});
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
/* 7. mig profile lookup */
|
|
4405
|
+
auto mig = gpu::MigProfile::byName(cfg.migprofilename);
|
|
4406
|
+
if (!mig) {
|
|
4407
|
+
::munmap(hostmem, cfg.guestrambytes);
|
|
4408
|
+
return std::unexpected(
|
|
4409
|
+
KvmError{EINVAL, "VirtualizationCore::build",
|
|
4410
|
+
std::format("unknown mig profile {}",
|
|
4411
|
+
cfg.migprofilename)});
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
VirtualizationCore core(std::move(sys), std::move(kvmvm),
|
|
4415
|
+
std::move(vcpus), std::move(ring),
|
|
4416
|
+
std::move(vfiocontainer), *b100, *mig, cfg,
|
|
4417
|
+
hostmem);
|
|
4418
|
+
return core;
|
|
4419
|
+
} catch (const std::exception& ex) {
|
|
4420
|
+
return std::unexpected(
|
|
4421
|
+
KvmError{EFAULT, "VirtualizationCore::build", ex.what()});
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
~VirtualizationCore() noexcept {
|
|
4426
|
+
if (mhostmem) {
|
|
4427
|
+
::munmap(mhostmem, mcfg.guestrambytes);
|
|
4428
|
+
mhostmem = nullptr;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
VirtualizationCore(const VirtualizationCore&) = delete;
|
|
4433
|
+
VirtualizationCore& operator=(const VirtualizationCore&) = delete;
|
|
4434
|
+
VirtualizationCore(VirtualizationCore&&) noexcept = default;
|
|
4435
|
+
VirtualizationCore& operator=(VirtualizationCore&&) noexcept = default;
|
|
4436
|
+
|
|
4437
|
+
/** creates the mediated device for the selected b100 profile. */
|
|
4438
|
+
[[nodiscard]] KvmResult<vm::MdevDevice> attachMdev(
|
|
4439
|
+
std::string_view uuid) noexcept {
|
|
4440
|
+
vm::MdevType type{
|
|
4441
|
+
.parentpci = mcfg.mdevparentpci,
|
|
4442
|
+
.vhetypename = std::string(b100spec.mdevtype),
|
|
4443
|
+
.description = std::string(b100spec.name),
|
|
4444
|
+
.availableinstances = 7,
|
|
4445
|
+
.deviceapi = "vfio-pci",
|
|
4446
|
+
};
|
|
4447
|
+
auto res = vm::MdevDevice::create(type, uuid);
|
|
4448
|
+
if (!res) return std::unexpected(res.error());
|
|
4449
|
+
return res;
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
/** connects the qmp socket and validates the accelerator. */
|
|
4453
|
+
[[nodiscard]] KvmResult<std::string> connectQmp() noexcept {
|
|
4454
|
+
auto sockres = vm::QmpSocket::connectUnix(mcfg.qmpsocketpath);
|
|
4455
|
+
if (!sockres) return std::unexpected(sockres.error());
|
|
4456
|
+
vm::QmpSocket sock = std::move(sockres.value());
|
|
4457
|
+
auto status = sock.queryStatus();
|
|
4458
|
+
if (!status) return std::unexpected(status.error());
|
|
4459
|
+
qmpconnected = true;
|
|
4460
|
+
return status;
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
/** @return the selected b100 profile row */
|
|
4464
|
+
[[nodiscard]] const gpu::B100Profile::Spec& b100Spec() const noexcept {
|
|
4465
|
+
return b100spec;
|
|
4466
|
+
}
|
|
4467
|
+
/** @return the selected mig profile row */
|
|
4468
|
+
[[nodiscard]] const gpu::MigProfile::Spec& migSpec() const noexcept {
|
|
4469
|
+
return migspec;
|
|
4470
|
+
}
|
|
4471
|
+
/** @return the facade configuration */
|
|
4472
|
+
[[nodiscard]] const Config& config() const noexcept { return mcfg; }
|
|
4473
|
+
/** @return the kvm system handle */
|
|
4474
|
+
[[nodiscard]] const vm::KvmSystem& kvmSystem() const noexcept {
|
|
4475
|
+
return kvmsystem;
|
|
4476
|
+
}
|
|
4477
|
+
/** @return the vm handle */
|
|
4478
|
+
[[nodiscard]] vm::KvmVm& vm() noexcept { return mvm; }
|
|
4479
|
+
/** @return the dirty ring buffer */
|
|
4480
|
+
[[nodiscard]] vm::DirtyLogRingBuffer& dirtyRing() noexcept {
|
|
4481
|
+
return dirtyring;
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
/**
|
|
4485
|
+
* produces the diagnostics line consumed by the forge mirror and the
|
|
4486
|
+
* python bridge, carrying the v2 version anchors (qemu 11.1.0, node
|
|
4487
|
+
* 26.7.0, ts 7.0.2, docker 29.7.2, cuda 12.9).
|
|
4488
|
+
*/
|
|
4489
|
+
[[nodiscard]] std::string diagnostics() const noexcept {
|
|
4490
|
+
auto bw = gpu::NvlinkC2cInterconnect::b100GraceSpec();
|
|
4491
|
+
return std::format(
|
|
4492
|
+
R"({{"date":"2026-08-22","qemu":"11.1.0","node":"26.7.0","ts":"7.0.2","docker":"29.7.2","cuda":"12.9","b100-profile":"{}","fb-bytes":{},"mig":"{}","vcpu":{},"ram-gb":{},"nvlink4-bidir":{},"c2c-bidir":{},"nvenc":"{}"}})",
|
|
4493
|
+
b100spec.name, b100spec.framebufferbytes,
|
|
4494
|
+
migspec.profilename, mcfg.vcpucount, mcfg.guestrambytes >> 30,
|
|
4495
|
+
bw.nvlink4bidirBps, bw.c2cbidirBps,
|
|
4496
|
+
enc::NvencDualEngine::describe());
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
private:
|
|
4500
|
+
VirtualizationCore(vm::KvmSystem sys, vm::KvmVm kvmvm,
|
|
4501
|
+
std::vector<vm::KvmVcpu> vcpus,
|
|
4502
|
+
vm::DirtyLogRingBuffer ring,
|
|
4503
|
+
std::optional<vm::VfioContainer> container,
|
|
4504
|
+
gpu::B100Profile::Spec b100, gpu::MigProfile::Spec mig,
|
|
4505
|
+
Config cfg, void* hostmem) noexcept
|
|
4506
|
+
: kvmsystem(std::move(sys)),
|
|
4507
|
+
mvm(std::move(kvmvm)),
|
|
4508
|
+
mvcpus(std::move(vcpus)),
|
|
4509
|
+
dirtyring(std::move(ring)),
|
|
4510
|
+
mvfiocontainer(std::move(container)),
|
|
4511
|
+
b100spec(b100),
|
|
4512
|
+
migspec(mig),
|
|
4513
|
+
mcfg(std::move(cfg)),
|
|
4514
|
+
mhostmem(hostmem),
|
|
4515
|
+
qmpconnected(false) {}
|
|
4516
|
+
|
|
4517
|
+
vm::KvmSystem kvmsystem;
|
|
4518
|
+
vm::KvmVm mvm;
|
|
4519
|
+
std::vector<vm::KvmVcpu> mvcpus;
|
|
4520
|
+
vm::DirtyLogRingBuffer dirtyring;
|
|
4521
|
+
std::optional<vm::VfioContainer> mvfiocontainer;
|
|
4522
|
+
gpu::B100Profile::Spec b100spec;
|
|
4523
|
+
gpu::MigProfile::Spec migspec;
|
|
4524
|
+
Config mcfg;
|
|
4525
|
+
void* mhostmem{nullptr};
|
|
4526
|
+
bool qmpconnected{false};
|
|
4527
|
+
};
|
|
4528
|
+
|
|
4529
|
+
} // namespace vhe::virt
|
|
4530
|
+
|
|
4531
|
+
/* ==========================================================================
|
|
4532
|
+
* global c entry points for the qemu bridge (python discovery)
|
|
4533
|
+
* ======================================================================== */
|
|
4534
|
+
extern "C" {
|
|
4535
|
+
|
|
4536
|
+
/**
|
|
4537
|
+
* returns the build identity string consumed by qemubridge.py discovery;
|
|
4538
|
+
* carries the v2 anchors (qemu 11.1.0, c++26) and the feature tags.
|
|
4539
|
+
*/
|
|
4540
|
+
const char* vhe_version() {
|
|
4541
|
+
return "v2.0.0-20260822+qemu11.1.0+b100-1q-2q-4q-8q-12q-24q"
|
|
4542
|
+
"+mig-1g12gb-7g192gb-hbm3e+nvlink-c2c+nvenc1600mpix"
|
|
4543
|
+
"+driver575.57.08+cuda12.9+cpp26";
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
/**
|
|
4547
|
+
* returns the nvenc aggregate throughput in mpix/s so callers can size
|
|
4548
|
+
* session pools without linking the c++ types.
|
|
4549
|
+
*/
|
|
4550
|
+
int vhenvencmpix() {
|
|
4551
|
+
return static_cast<int>(vhe::virt::enc::NvencDualEngine::kTotalMpixPerSec);
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
} // extern "C"
|
|
4555
|
+
|
|
4556
|
+
/* ==========================================================================
|
|
4557
|
+
* inline selftest (-DVHE_VIRT_SELFTEST builds a main with asserts)
|
|
4558
|
+
* ======================================================================== */
|
|
4559
|
+
#ifdef VHE_VIRT_SELFTEST
|
|
4560
|
+
#include <cassert>
|
|
4561
|
+
|
|
4562
|
+
int main() {
|
|
4563
|
+
using namespace vhe::virt;
|
|
4564
|
+
|
|
4565
|
+
/* vm primitives */
|
|
4566
|
+
vm::FileDescriptor fd(::open("/dev/null", O_RDONLY));
|
|
4567
|
+
assert(fd.valid());
|
|
4568
|
+
vm::FileDescriptor fd2 = std::move(fd);
|
|
4569
|
+
assert(!fd.valid());
|
|
4570
|
+
assert(fd2.valid());
|
|
4571
|
+
|
|
4572
|
+
auto msg = vm::QmpMessage::makeExecute("query-status");
|
|
4573
|
+
assert(msg.jsonpayload.find("query-status") != std::string::npos);
|
|
4574
|
+
assert(msg.jsonpayload.find("vhe-2026-08-22") != std::string::npos);
|
|
4575
|
+
|
|
4576
|
+
/* gpu tables */
|
|
4577
|
+
auto allb100 = gpu::B100Profile::all();
|
|
4578
|
+
assert(allb100.size() == 6);
|
|
4579
|
+
auto b1001q = gpu::B100Profile::findByName("B100-1Q");
|
|
4580
|
+
assert(b1001q.has_value());
|
|
4581
|
+
assert(b1001q->framebufferbytes == (24ULL << 30));
|
|
4582
|
+
auto b10024q = gpu::B100Profile::findByName("B100-24Q");
|
|
4583
|
+
assert(b10024q.has_value());
|
|
4584
|
+
assert(b10024q->framebufferbytes == (192ULL << 30));
|
|
4585
|
+
assert(b10024q->nvencmpix == 1600);
|
|
4586
|
+
|
|
4587
|
+
auto migall = gpu::MigProfile::allBlackwell();
|
|
4588
|
+
assert(migall.size() == 8);
|
|
4589
|
+
auto mig7g = gpu::MigProfile::byName("7g.192gb");
|
|
4590
|
+
assert(mig7g.has_value());
|
|
4591
|
+
assert(mig7g->hbmbytes == (192ULL << 30));
|
|
4592
|
+
assert(gpu::MigProfile::validateDensity(
|
|
4593
|
+
std::span<const gpu::MigProfile::Spec>{migall.data(), 1}));
|
|
4594
|
+
|
|
4595
|
+
auto bw = gpu::NvlinkC2cInterconnect::b100GraceSpec();
|
|
4596
|
+
assert(bw.c2cbidirBps == 900ULL * 1000ULL * 1000ULL * 1000ULL);
|
|
4597
|
+
auto t = gpu::NvlinkC2cInterconnect::estimateTransferTime(1ULL << 30, true);
|
|
4598
|
+
assert(t.count() > 0);
|
|
4599
|
+
|
|
4600
|
+
/* spec database (170 sm / verified data, not the stale 192 block) */
|
|
4601
|
+
gpu::GpuVirtualizationManager gvm;
|
|
4602
|
+
auto gb202 = gvm.lookupspec(gpu::GpuArch::BlackwellGB202);
|
|
4603
|
+
assert(gb202.has_value());
|
|
4604
|
+
assert(gb202->smorcucount == 170);
|
|
4605
|
+
auto det = gvm.detectgpus();
|
|
4606
|
+
assert(det.has_value());
|
|
4607
|
+
|
|
4608
|
+
/* vgpu profiles */
|
|
4609
|
+
gpu::VgpuScheduler sched;
|
|
4610
|
+
assert(sched.listprofiles().size() == 11); /* 7 v6 baseline + 4 GB202 slices from the v4 fix3 merge */
|
|
4611
|
+
|
|
4612
|
+
/* enc models */
|
|
4613
|
+
assert(enc::NvencDualEngine::canFit(3840, 2160, 60, 1) == true);
|
|
4614
|
+
assert(enc::NvencDualEngine::canFit(7680, 4320, 120, 2) == false);
|
|
4615
|
+
assert(enc::NvencDualEngine::kTotalMpixPerSec == 1600);
|
|
4616
|
+
auto nvenc = enc::IEncoderBackend::create(enc::EncoderVendor::NvidiaNvenc);
|
|
4617
|
+
auto caps = nvenc->querycaps();
|
|
4618
|
+
assert(caps->maxthroughputmpixpersec == 1600);
|
|
4619
|
+
assert(enc::EncodingOptimizationManager::recommendpreset(
|
|
4620
|
+
enc::Codec::H265, enc::Tune::Hq, false) ==
|
|
4621
|
+
enc::Preset::P6Slower);
|
|
4622
|
+
|
|
4623
|
+
/* c entry points */
|
|
4624
|
+
assert(std::string_view{vhe_version()}.find("qemu11.1.0") !=
|
|
4625
|
+
std::string_view::npos);
|
|
4626
|
+
assert(vhenvencmpix() == 1600);
|
|
4627
|
+
|
|
4628
|
+
std::cout << "vhe virtualizationcore selftest ok " << vhe_version()
|
|
4629
|
+
<< "\n";
|
|
4630
|
+
return 0;
|
|
4631
|
+
}
|
|
4632
|
+
#endif /* VHE_VIRT_SELFTEST */
|
|
4633
|
+
|
|
4634
|
+
|
|
4635
|
+
|