Skip to main content
Version: Next

GPU Memory Enforcement: How It Works and How to Debug It

HAMi enforces GPU memory limits differently from kernel-level mechanisms such as Linux cgroups or NVIDIA MIG. Understanding the enforcement model is essential for diagnosing situations where a container appears to ignore its nvidia.com/gpumem quota.

How HAMi Enforces Memory Limits

HAMi uses a user-space library called libvgpu.so (part of HAMi-core) to intercept CUDA API calls inside each container. The enforcement chain works as follows:

When hami-device-plugin runs its Allocate handler for a new Pod, it performs four injections:

  1. Device files — mounts /dev/nvidia* into the container.
  2. libvgpu.so — hostPath-mounts /usr/local/vgpu/libvgpu.so into the container at the same path.
  3. ld.so.preload — hostPath-mounts /usr/local/vgpu/ld.so.preload (which contains the single line /usr/local/vgpu/libvgpu.so) into the container as /etc/ld.so.preload. The Linux dynamic linker reads this file when any process starts and loads the listed libraries first, achieving transparent interception without modifying environment variables.
  4. Environment variables — sets CUDA_DEVICE_MEMORY_LIMIT_<index>=<N>m (per-device VRAM quota in MiB) and CUDA_DEVICE_SM_LIMIT=<percentage> (compute quota).

Once loaded, libvgpu.so overrides dlsym and intercepts the specific CUDA and NVML symbols listed in its hook table — not every function whose name starts with cu or nvml. Calls to cu*/nvml* functions that aren't in the hook table resolve normally to the real driver. The key interceptions are:

Intercepted functionWhat HAMi does
cuMemAlloc_v2, cuMemAllocManaged, cuMemAllocHost_v2Checks current usage + request ≤ CUDA_DEVICE_MEMORY_LIMIT; returns CUDA_ERROR_OUT_OF_MEMORY if exceeded
nvmlDeviceGetMemoryInfo, nvmlDeviceGetMemoryInfo_v2Reports the quota value instead of physical VRAM, so nvidia-smi inside the container shows only the allocated share
cuLaunchKernel, cuLaunchKernelExFeeds a token-bucket rate limiter (g_cur_cuda_cores) to throttle compute to CUDA_DEVICE_SM_LIMIT percent

For the full interception architecture, see GPU Virtualization Principles.

Soft Enforcement vs Hard Enforcement

HAMi's memory limit is a soft, user-space enforcement. It is not equivalent to hardware partitioning or kernel-level isolation:

PropertyHAMi vGPU (libvgpu.so)NVIDIA MIGLinux cgroups (CPU/RAM)
Enforcement layerUser-space library preloadGPU hardware engineLinux kernel
Bypassable?Yes — if the interception chain is brokenNoNo (without root/CAP_SYS_ADMIN)
Requires hardware support?No — any NVIDIA GPUAmpere+ only (A100, H100)N/A
Granularity1 MiB memory, 1% computeFixed MIG profilesN/A for GPU
Multi-tenant noise isolationBest-effortStrong (separate SM partitions)N/A for GPU

Key takeaway: any mechanism that prevents libvgpu.so from being loaded, or that calls the GPU driver without going through the intercepted symbol table, will bypass HAMi's memory limit. This is by design — HAMi trades absolute isolation for flexibility, zero hardware requirements, and fine-grained partitioning.

Common Bypass Scenarios and How to Fix Them

1. CUDA_DISABLE_CONTROL=true is set

Symptoms: Container uses the full physical GPU memory. nvidia-smi inside the container shows total physical VRAM.

Root cause: When the environment variable CUDA_DISABLE_CONTROL is set to true, hami-device-plugin skips the ld.so.preload mount entirely. The libvgpu.so library is never loaded, and no interception occurs.

Diagnostic:

# Check if the env var is set in a running Pod
kubectl exec -it <pod-name> -- env | grep CUDA_DISABLE_CONTROL

If the output shows CUDA_DISABLE_CONTROL=true, enforcement is disabled.

Resolution: Remove CUDA_DISABLE_CONTROL from the Pod spec (or set it to false). If a third-party Helm chart or operator is injecting it, trace the source with:

kubectl get pod <pod-name> -o jsonpath='{.spec.containers[*].env[*]}' | tr ',' '\n' | grep -i disable

2. libvgpu.so or ld.so.preload not mounted

Symptoms: Container uses full physical VRAM. No [HAMi-core] log lines appear in container stdout/stderr.

Root cause: The nvidia-container-runtime is not configured as the default containerd runtime, or the hostPath files are missing on the node. Without the nvidia runtime, containerd does not invoke the NVIDIA container hook that sets up GPU device access, and HAMi's hostPath mounts may not resolve correctly.

Diagnostic:

# Step 1: Verify the containerd default runtime on the GPU node
kubectl debug node/<node-name> -it --image=busybox -- \
chroot /host containerd config dump | grep default_runtime_name
# Expected output: default_runtime_name = "nvidia"

# Step 2: Verify libvgpu.so exists on the host node
kubectl debug node/<node-name> -it --image=busybox -- \
ls -la /host/usr/local/vgpu/libvgpu.so
# Expected: file exists with non-zero size

# Step 3: Verify ld.so.preload content on the host node
kubectl debug node/<node-name> -it --image=busybox -- \
cat /host/usr/local/vgpu/ld.so.preload
# Expected output: /usr/local/vgpu/libvgpu.so

# Step 4: Verify the mounts are present inside the Pod
kubectl exec -it <pod-name> -- cat /etc/ld.so.preload
# Expected output: /usr/local/vgpu/libvgpu.so

kubectl exec -it <pod-name> -- ls -la /usr/local/vgpu/libvgpu.so
# Expected: file exists

Resolution:

  • If the containerd default runtime is not nvidia, follow the Prerequisites guide to configure the NVIDIA Container Toolkit.

  • If libvgpu.so is missing on the host, verify that hami-device-plugin is running and healthy on that node:

    kubectl get pods -n kube-system -l app.kubernetes.io/component=device-plugin -o wide

3. Docker-in-Docker (DinD)

Symptoms: Inner containers launched by a DinD daemon inside a HAMi Pod use full GPU memory. The outer container respects the HAMi limit; inner containers do not.

Root cause: The /etc/ld.so.preload file is mounted into the outer container via hostPath. When the inner Docker daemon creates its own containers, those containers get a fresh filesystem and do not inherit the outer container's hostPath mounts. The inner containers never load libvgpu.so.

Diagnostic:

# From inside the outer (HAMi) container, run a command inside the inner container
docker exec <inner-container-id> cat /etc/ld.so.preload
# Expected: empty or "No such file or directory"

Resolution: HAMi enforcement does not extend to DinD inner containers. This is a fundamental limitation of the hostPath-based injection model. Options:

  • Avoid DinD for GPU workloads; use Kubernetes-native pod scheduling instead.
  • If DinD is required, manually copy libvgpu.so into the inner container image and configure its /etc/ld.so.preload. This is fragile and not officially supported.

4. Statically linked CUDA or direct Driver API usage

Symptoms: A specific application exceeds its memory limit while other applications on the same node respect it. No [HAMi-core Warn] log lines appear for the offending process, but they do appear for other processes.

Root cause: libvgpu.so intercepts calls by overriding dynamic symbol resolution (dlsym). Applications that statically link libcuda.so or libcudart.so, or that load the CUDA driver via dlopen with RTLD_DEEPBIND, bypass the ld.so.preload interception entirely. Similarly, applications that call the GPU kernel driver directly via ioctl on /dev/nvidia* bypass all user-space interception.

Diagnostic:

# Check if the application dynamically links to CUDA
kubectl exec -it <pod-name> -- ldd /path/to/application | grep -E "libcuda|libcudart"
# Expected: shows "libcuda.so => /usr/lib/..." (dynamic linking)
# If output shows "not a dynamic executable" or no CUDA entries, it may be statically linked

# Find the PID of the actual workload process inside the container
# (PID 1 may be a shell, init wrapper, or supervisor, not the CUDA application itself)
kubectl exec -it <pod-name> -- ps aux

# Check if libvgpu.so is loaded by that process (replace <workload-pid> with the PID found above)
kubectl exec -it <pod-name> -- cat /proc/<workload-pid>/maps | grep libvgpu
# Expected: at least one line showing libvgpu.so mapped into the process

Note: ldd only lists shared library dependencies declared at link time — it will not reveal CUDA libraries that an application loads later via dlopen, which is common in Python-based frameworks that resolve libcuda.so lazily at runtime. A binary can appear dynamically linked and still bypass interception if it (or a library it loads) calls dlopen with RTLD_DEEPBIND, which lets the newly loaded library resolve its own symbols first instead of deferring to the already-preloaded libvgpu.so interceptor. The /proc/<pid>/maps check only confirms libvgpu.so is preloaded into the process — it is not proof that interception is active for the symbols that process actually calls. To confirm enforcement, run a controlled test: attempt an allocation past the configured quota and confirm it fails with CUDA_ERROR_OUT_OF_MEMORY, or check that nvidia-smi inside the container reports the quota rather than physical VRAM.

Resolution: There is no general workaround for statically linked binaries or for code paths that use RTLD_DEEPBIND. Rebuild the application with standard dynamic CUDA linking if possible. Most common AI frameworks (PyTorch, TensorFlow, vLLM, SGLang) dynamically link CUDA and are typically unaffected, but this isn't a guarantee for every build or every custom extension they load — verify with the /proc/<pid>/maps check and a controlled enforcement test above rather than assuming based on framework alone.

5. readOnlyRootFilesystem or restrictive SecurityContext

Symptoms: Pod fails to start, or libvgpu.so is not loaded despite the hostPath mounts being present. Container logs may show permission errors related to /etc/ld.so.preload.

Root cause: If the Pod's securityContext sets readOnlyRootFilesystem: true, the hostPath mount of /etc/ld.so.preload may fail or be ignored depending on the container runtime version. Some hardened container images also strip or ignore LD_PRELOAD-style mechanisms.

Diagnostic:

# Check the Pod's security context
kubectl get pod <pod-name> -o jsonpath='{.spec.containers[0].securityContext}'

# Check if ld.so.preload is readable inside the container
kubectl exec -it <pod-name> -- cat /etc/ld.so.preload

Resolution: Ensure that the /etc/ld.so.preload hostPath mount is present and readable inside the container — the dynamic linker only needs to read it at process startup, not write to it. In practice, hostPath mounts to specific files (like /etc/ld.so.preload) typically work even with readOnlyRootFilesystem: true because the mount overlays the path. If the mount is failing, check for Pod Security Standards or admission controllers that may be blocking hostPath mounts.

Quick Diagnostic Checklist

Use this checklist when a container ignores its nvidia.com/gpumem limit:

StepCommandExpected result
1. Check CUDA_DISABLE_CONTROLkubectl exec <pod> -- env | grep CUDA_DISABLEUnset or false
2. Check HAMi env varskubectl exec <pod> -- env | grep CUDA_DEVICE_MEMORYCUDA_DEVICE_MEMORY_LIMIT_<index>=<N>m
3. Check ld.so.preloadkubectl exec <pod> -- cat /etc/ld.so.preload/usr/local/vgpu/libvgpu.so
4. Check libvgpu.so existskubectl exec <pod> -- ls -la /usr/local/vgpu/libvgpu.soFile exists, non-zero size
5. Check library is loadedFind the workload PID (kubectl exec <pod> -- ps aux), then kubectl exec <pod> -- cat /proc/<workload-pid>/maps | grep libvgpuAt least one mapped region
6. Check containerd runtimecontainerd config dump | grep default_runtime_name (on node)nvidia
7. Check device-plugin healthkubectl get pods -n kube-system -l app.kubernetes.io/component=device-pluginAll pods Running

If all seven checks pass and the limit is still not enforced, the workload may be using a static CUDA binary or direct driver API. Don't rely on the absence of [HAMi-core] log lines as proof of a bypass — the environment variable LIBCUDA_LOG_LEVEL can suppress HAMi-core's logging entirely, so a quiet log stream doesn't mean interception isn't happening. Instead, follow the checks from scenario 4: confirming libvgpu.so is mapped in /proc/<pid>/maps for the actual offending process only verifies the library is preloaded, not that interception is active — run a controlled enforcement test (attempt an over-quota allocation, or compare nvidia-smi output inside the container against physical VRAM) to confirm calls are actually being intercepted.

CNCFHAMi is a CNCF Incubating project