fix(deployer): resolve device injection from the target node, and pin ordering by device count - #18
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors device ordering pinning and resource injection policy resolution. It pins device ordering whenever a container sees more than one device or is privileged, rather than special-casing "all" devices. Additionally, under the "auto" policy, it now probes the target node's allocatable resources to detect the presence of a device plugin, resolving this policy once per Pod. Feedback suggests caching the resolved default node name in self._node_name during the node allocatable probe to avoid redundant API calls and ensure consistency across other deployer methods.
There was a problem hiding this comment.
Pull request overview
This PR fixes Kubernetes device injection behavior to avoid bypassing node device plugins (and thus operator accounting) by resolving the default resource-injection policy from the target node rather than the deploying process’s local kubelet socket. It also corrects when CUDA_DEVICE_ORDER is pinned by basing the decision on the resolved device count (or privilege) instead of special-casing "all".
Changes:
- Resolve
Autoresource injection policy by probing target node allocatable resources for operator-style device-plugin families, with conservative fallback to KDP when probing can’t answer. - Pin
CUDA_DEVICE_ORDER=PCI_BUS_IDwhen a container will see >1 device (or is privileged), shared viaDeployer.count_requested_devices. - Add/extend tests for resource injection policy resolution, privilege dropping under KDP, and visible-device ordering across deployers.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/gpustack_runtime/deployer/test_visible_devices_ordering.py | Adds ordering-pin test cases for “several specific devices” and “all on single-device host”; adjusts monkeypatch for new policy resolver signature. |
| tests/gpustack_runtime/deployer/test_resource_injection_policy.py | New tests for node allocatable family detection and Auto/explicit policy behavior (including “explicit policy does not probe”). |
| tests/gpustack_runtime/deployer/test_privileged.py | Updates privileged-resolution test to pass the resolved KDP decision explicitly instead of monkeypatching. |
| gpustack_runtime/deployer/podman.py | Updates ordering pin condition to use privilege or resolved device count > 1. |
| gpustack_runtime/deployer/kuberentes.py | Probes target node allocatable for Auto policy, resolves KDP once per Pod, passes KDP decision into privilege resolution, and updates ordering pin condition. |
| gpustack_runtime/deployer/k8s/devicemanager/init.py | Adds target-node allocatable probing + family detection, and updates get_resource_injection_policy() API to accept a probe callback. |
| gpustack_runtime/deployer/docker.py | Updates ordering pin condition to use privilege or resolved device count > 1. |
| gpustack_runtime/deployer/types.py | Adds count_requested_devices() and updates ordering documentation to reflect “>1 device” semantics. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… ordering by device count
- decide the Kubernetes KDP policy from the target node's allocatable
resources instead of the deploying process's own kubelet socket: the
Kubernetes deployer orchestrates remotely, so a socket it cannot see says
nothing about whether that node runs a device plugin. Deployed from a plain
Pod, every accelerated workload fell back to env injection -- privileged,
seeing every device of the host, with empty limits and its allocation
absent from the plugin's ledger, hence from the operator's accounting
- treat only the suffixed families (".shared" / ".sliced" / ".partitioned")
as evidence of such a plugin: a bare CDI kind on its own is what a stock
vendor plugin advertises, and requesting a device from it accounts for
nothing
- probe through `list_node` with a field selector, so it needs no permission
beyond the node list the deployer already requires, and resolve it once per
Pod rather than once per container. An explicit policy never probes
- fall back to KDP when the probe cannot answer -- absent, failing or
unauthorized -- since env injection is the more damaging of the two guesses
- pin `CUDA_DEVICE_ORDER` from the number of devices a request resolves to
rather than from the literal "all": a container holding several specific
devices numbers them itself exactly as one holding all of them does, and
it went unpinned; conversely "all" on a single-device host has no ordering
to pin. The Docker and Podman deployers carried the same condition, so the
count moves to `Deployer.count_requested_devices` and all three share it
Signed-off-by: thxCode <thxcode0824@gmail.com>
5b4b974 to
89054be
Compare
Picks up the deployer fixes released in gpustack/runtime#18: - the Kubernetes KDP policy is resolved from the target node's allocatable resources instead of the deploying process's own kubelet socket, so a workload deployed from a worker Pod stops falling back to env injection -- privileged, holding every device of the node, with empty limits and its allocation absent from the device plugin's ledger, hence from the operator's accounting - CUDA_DEVICE_ORDER is pinned from the number of devices a request resolves to rather than from the literal "all", so a multi-device request pins it and "all" on a single-device host no longer does Signed-off-by: thxCode <thxcode0824@gmail.com>
Picks up the deployer fixes released in gpustack/runtime#18: - the Kubernetes KDP policy is resolved from the target node's allocatable resources instead of the deploying process's own kubelet socket, so a workload deployed from a worker Pod stops falling back to env injection -- privileged, holding every device of the node, with empty limits and its allocation absent from the device plugin's ledger, hence from the operator's accounting - CUDA_DEVICE_ORDER is pinned from the number of devices a request resolves to rather than from the literal "all", so a multi-device request pins it and "all" on a single-device host no longer does Signed-off-by: thxCode <thxcode0824@gmail.com>
Two fixes on the deployer's device-injection path, both found while deploying an exclusive (whole-card)
model onto a Kubernetes cluster running the GPUStack Operator: the workload bypassed the operator
entirely, so the card it occupied never appeared in the operator's accounting.
The KDP policy was decided from the wrong machine
get_resource_injection_policy()resolvedautoby probing the local/var/lib/kubelet/device-plugins/kubelet.sock. That reads correctly for the Docker and Podmandeployers, which deploy onto the host they run on. The Kubernetes deployer orchestrates remotely —
whether the deploying process can reach a kubelet socket says nothing about whether the target node
runs a device plugin.
Deployed from a plain Pod (a GPUStack worker), that socket is absent, so every accelerated workload fell
back to env injection. Observed on a live cluster, the Pod came out as:
Privileged, seeing every device on the node, with the allocation absent from the device plugin's ledger —
so the operator's
InstanceTypestatus reported the card as free while a workload was using it.autonow probes the target node's allocatable resources instead:.shared/.sliced/.partitioned), mirroring the operator's ownfamilies in
pkg/nodefeature. A bare CDI kind (nvidia.com/gpu) on its own is what a stock vendorplugin advertises, and requesting a device from that accounts for nothing;
list_nodewith a field selector, so it needs no permission beyond the nodelist the deployer already requires to resolve a default node name;
Env/KDPpolicy never probes atall;
hands the container every device of the host and leaves the allocation off the ledger, so it is the
more damaging of the two guesses.
After the fix, the same workload comes out as
nvidia.com/gpu.shared: "1"with the operator'sdevice.gpustack.ai/accelerator.allocatedand…/accelerator.preferred-indexannotations present, andprivilegeddropped —_resolve_privilegedalready drops privilege for plugin-allocated devices, itsimply never saw the KDP policy before.
CUDA_DEVICE_ORDERwas pinned by the wrong conditionThe ordering pin fired on
r_v == "all" or privileged. Requesting several specific devices produces acontainer that numbers them itself exactly as one holding all of them does, and it went unpinned — the
case gpustack/gpustack#6041 is about. Conversely
"all"on a single-device host has nothing to reorderand got pinned anyway.
The condition is now
privileged or <devices the request resolves to> > 1, with"all"measuredrather than special-cased. The Docker and Podman deployers carried the same condition, so the count moves
to
Deployer.count_requested_devicesand all three share it.Tests
tests/gpustack_runtime/deployer/test_resource_injection_policy.pyis new (14 cases: family detection,explicit-policy precedence, probe failure fallback, and that an explicit policy spends no API call).
test_visible_devices_ordering.pygains the two cases the old condition got wrong, across all threedeployers. Suite: 491 passed, 20 skipped;
make lintclean.Verified on a live cluster
k3s + gpustack-operator v0.8.3, one RTX 4090 node. Exclusive request before/after as quoted above;
sliced requests (
nvidia.com/gpu.sliced+ percentage keys) unchanged, confirming the passthrough pathwas not disturbed.