Skip to content

fix(device-plugin): resolve the GPU container by its real pod index - #23

Merged
dkeven merged 1 commit into
feat/nvsharefrom
fix/device-plugin-container-index
Aug 24, 2026
Merged

dkeven merged 1 commit into
feat/nvsharefrom
fix/device-plugin-container-index

Conversation

@dkeven

@dkeven dkeven commented Aug 24, 2026

Copy link
Copy Markdown
Member

Problem

Per-container GPU metrics are attributed to the wrong container whenever the GPU container is not the first container in the pod. Dashboards show 0% utilization and 0 GiB memory for a task while the card itself reads 100% / 97% — the device-level numbers come from DCGM and are fine, but the task-level ones never join.

Root cause is a positional contract that gets broken in decode:

  1. The scheduler encodes one annotation segment per container, in pod.Spec.Containers order. A pod of [linkerd-proxy, llamacpp, olares-mesh-in-agent] with the GPU on llamacpp produces ;GPU-…,NVIDIA,0,0,2:;; — the device sits at index 1.
  2. DecodePodDevices skipped segments that decode to no devices, compacting the slice. The GPU entry moved from index 1 to index 0.
  3. GetNextDeviceRequest returned p.Spec.Containers[ctridx] using that compacted index, so it resolved linkerd-proxy.
  4. Allocate names the libvgpu shared-cache directory <podUID>_<ctrName> from that container, producing /usr/local/vgpu/containers/<podUID>_linkerd-proxy.
  5. vGPUmonitor derives its ctrname label from the directory name, so vGPU_device_memory_usage_in_bytes and Device_utilization_desc_of_container are published under ctrname="linkerd-proxy". Consumers query the real container name and match nothing.

The values themselves were never wrong — only the label. The vGPUmonitor MIG collector (collectPodAndContainerMigInfo) compares ctrIdx != ctrDevIdx and so relies on the same alignment; it was silently broken too.

Any injected sidecar that lands ahead of the workload triggers this, so in a service-mesh cluster it affects every GPU pod. Multi-GPU-container pods were worse: every Allocate resolved index 0, so all of them collapsed onto the same wrong container.

Fix

Keep empty entries in DecodePodDevices so index i stays aligned with pod.Spec.Containers[i]. The encoder terminates every segment with the separator, so decoding yields one extra empty entry past the container count; GetNextDeviceRequest bounds-checks instead of indexing blindly, and logs the container it picked.

This mirrors upstream Project-HAMi/HAMi#1650, minus its init-container offset. Upstream encodes init containers into the annotation and therefore subtracts len(InitContainers); this fork builds the annotation from pod.Spec.Containers alone (k8sutil.Resourcereqs), so applying that offset here would introduce a shift the encoder never produces.

Callers audited: the scheduler (onAddPod → node usage accounting) and the vGPUmonitor MIG collector only range over the decoded slices, so extra empty entries are inert. EraseNextDeviceTypeFromAnnotation re-encodes what it decodes and keeps index alignment across successive Allocate calls.

Test plan

Unit tests — TestGetNextDeviceRequestContainerIndex covers GPU container first / behind a sidecar / last / none / more device entries than containers. Reverting only the decode change makes it fail with exactly the production symptom:

--- FAIL: TestGetNextDeviceRequestContainerIndex/gpu_container_behind_an_injected_sidecar
    container = "linkerd-proxy", want "llamacpp"

TestDecodePodDevicesPreservesContainerIndex guards the index mapping directly.

Real hardware — single-node k3s, RTX 5090 Laptop 24 GB, llama.cpp serving Qwen3.8-27B, containers [linkerd-proxy, llamacpp, olares-mesh-in-agent]:

before after
cache directory <podUID>_linkerd-proxy <podUID>_llamacpp
decoded annotation [[{GPU}]] [[] [{GPU}] [] []]
container resolved linkerd-proxy llamacpp at index 1
vGPU_device_memory_usage_in_bytes 22.29 GiB as ctrname="linkerd-proxy" 22.29 GiB as ctrname="llamacpp"
Device_utilization_desc_of_container under load — 99, against 98-100% from nvidia-smi
hami_container_core_util / hami_container_memory_used no series 100 / 22824 MB

Note on the test files

pkg/util did not compile before this change — positional ContainerDevice literals predate the ShareMode field, so that package's tests had not been running and its expectations had drifted. They are converted to keyed fields and refreshed, which also brings back Test_DecodePodDevices and Test_EncodeNodeDevices. The package is green now; no other package changes status, and the set of failing tests elsewhere is byte-identical before and after.

Made with Cursor

DecodePodDevices dropped container entries carrying no devices, which
compacted the slice and broke its positional contract with
pod.Spec.Containers. GetNextDeviceRequest then resolved the wrong
container whenever the GPU container was not first in the pod, and named
the libvgpu shared-cache directory after it
(/usr/local/vgpu/containers/<podUID>_<ctrName>). vGPUmonitor derives its
ctrname label from that directory name, so every per-container GPU
metric was published under the wrong container and never joined with the
real workload: dashboards read 0% utilization and 0 GiB memory while the
card was fully loaded. The vGPUmonitor MIG collector makes the same
positional assumption and was equally affected.

Keep empty entries so index i stays aligned with pod.Spec.Containers[i].
The encoder's trailing separator yields one extra empty entry past the
container count, so GetNextDeviceRequest bounds-checks the index rather
than indexing blindly, and logs which container it picked.

This mirrors upstream Project-HAMi#1650 minus its init-container
offset: this fork builds the annotation from pod.Spec.Containers alone
(k8sutil.Resourcereqs), so no offset applies here.

Verified on a single-node cluster with an RTX 5090 24 GB running a
llama.cpp pod whose containers are [linkerd-proxy, llamacpp,
olares-mesh-in-agent]. Before, the cache directory was
<podUID>_linkerd-proxy and vGPU_device_memory_usage_in_bytes was
labelled ctrname="linkerd-proxy". After, the directory is
<podUID>_llamacpp, the metric carries ctrname="llamacpp" at 22.29 GiB,
and Device_utilization_desc_of_container reports 99 under inference load
against 98-100% from nvidia-smi.

The pkg/util test binary did not compile before this change: positional
ContainerDevice literals predate the ShareMode field, so the package's
tests had not run in a while and its expectations had gone stale. They
are converted to keyed fields and refreshed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@dkeven
dkeven merged commit 4ab1960 into feat/nvshare Aug 24, 2026
1 check passed
@dkeven
dkeven deleted the fix/device-plugin-container-index branch August 24, 2026 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant