In Part I, we completed the foundation of the intelligent computing center. We delved into the core of AI chips, laid down RDMA high-speed networks, and built parallel storage systems. Now, we have powerful but raw bare-metal compute power. This is like owning a well-equipped factory with no standardized production lines. Deploying AI applications directly on bare-metal servers would quickly plunge us into "dependency hell" -- Python version conflicts between projects, incompatible system libraries, and environments that are difficult to migrate and reproduce. This is unacceptable in the fast-iterating AI era.
The answer is containerization. Container technology (represented by Docker) packages an application and all its dependencies into a lightweight, portable "shipping container," achieving good isolation and environment consistency. It is the core technology we need to build standardized production lines for the AI factory.
However, getting AI applications to "live in" containers is not trivial. There is a natural gap between standard containers and the underlying GPU/NPU hardware. This chapter will guide you step by step in bridging this gap. We will first solve the most fundamental problem: how to build a container environment that can "see" and use GPUs and NPUs. Next, we will integrate this capability into the grand narrative of Kubernetes, allowing K8s to intelligently schedule and manage AI compute just as it schedules CPUs. Finally, we will impart the unique skills of "image engineering," teaching you how to build lightweight, efficient, and cross-platform standardized images for large models. Mastering this chapter will enable you to build the first and most critical pillar of a cloud-native AI platform.
4.1 Building the Foundation: NVIDIA Container Toolkit and Ascend Docker Runtime
4.1.1 The Root of the Problem: The Container's "Blindness"
Let us start with a simple experiment. On a host with NVIDIA drivers correctly installed, open a terminal, type nvidia-smi, and you will see detailed GPU information. Now, let us start a standard Ubuntu container and try to execute the same command inside it:
# Execute on the host, successfully displays GPU information
$ nvidia-smi
# Start a standard Ubuntu container
$ docker run --rm -it ubuntu:20.04 /bin/bash
# Execute inside the container, command not found or error
root@container:/# nvidia-smi
bash: nvidia-smi: command not found
Why does this happen? The reason lies in the core isolation mechanism of containers: Linux namespaces. Containers have their own independent filesystem, process space, and network stack. They are isolated from the host. By default, they cannot "see" the host's device files (e.g., /dev/nvidia0) or driver libraries (e.g., libcuda.so).
To break this isolation and allow containers to access GPUs and NPUs, we need a "middleman" or "interpreter." This role is played by NVIDIA's Container Toolkit and Huawei's Ascend Docker Runtime.
4.1.2 NVIDIA Container Toolkit: Injecting CUDA Power into Containers
The NVIDIA Container Toolkit is a suite of components that extends the standard container runtime (e.g., Docker's runc) to be aware of and utilize NVIDIA GPUs.
Core Component Analysis
libnvidia-container: A core library that provides the low-level API for interacting with the NVIDIA driver, responsible for querying GPU information, configuring the container environment, etc.nvidia-container-cli: A command-line tool called by the container runtime. It useslibnvidia-containerto prepare the GPU environment.nvidia-container-runtime: The critical "interpreter." It is a custom OCI (Open Container Initiative) runtime that intercepts standard container creation requests. When it detects that a request requires a GPU, it callsnvidia-container-clito dynamically mount the necessary NVIDIA driver files, device nodes, and library files into the container's namespace, and then calls the standard runtime (runc) to start the container.
Workflow
- The user executes
docker run --gpus all .... - The Docker Daemon receives the request, sees the
--gpusparameter, and knows that a GPU is needed. - Instead of directly calling the default
runc, the Docker Daemon callsnvidia-container-runtime. nvidia-container-runtimetakes over the request. It callsnvidia-container-clito query the host for available GPU devices and the location of driver libraries.nvidia-container-cliautomatically adds these device files (e.g.,/dev/nvidia0,/dev/nvidia-uvm) and library files to the container's configuration.- Finally,
nvidia-container-runtimecallsruncwith this "enhanced" configuration to create and start the container. - As a result, when the container starts, it already has everything it needs to access the GPU.
Installation and Configuration
- Ensure the NVIDIA driver is installed. This is a prerequisite for everything.
- Add the NVIDIA software repository
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
- Install the Toolkit
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
- Configure and restart Docker Daemon
The installation script usually configures Docker automatically. You can verify the settings in /etc/docker/daemon.json and ensure nvidia-container-runtime is set as the default runtime. Then restart the Docker service:
sudo systemctl restart docker
- Verify the installation
docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi
If the GPU information is printed successfully, your basic environment is ready!
The Power of Environment Variables:
The Toolkit also injects several key environment variables into the container for fine-grained GPU control:
NVIDIA_VISIBLE_DEVICES: This is the most important one. Its value determines which GPUs are "visible" inside the container. It can beall, or a list of GPU indices (e.g.,0,1) or UUIDs.docker run --gpus '"device=0,1"'ultimately results inNVIDIA_VISIBLE_DEVICES=0,1inside the container.NVIDIA_DRIVER_CAPABILITIES: Controls which driver libraries are mounted into the container. The default iscompute,utility; graphics applications may needgraphics.
4.1.3 Ascend Docker Runtime: The Containerization Cornerstone for the Ascend Platform
Following a similar approach to NVIDIA, Huawei also provides a mechanism for containers to use Ascend NPUs. This mechanism is typically provided as part of the CANN (Compute Architecture for Neural Networks) software package.
Core Components and Principles
- Ascend Docker Runtime: Also a custom Docker runtime. Its role is to mount the host's CANN drivers, firmware, necessary libraries, and NPU device files (e.g.,
/dev/davinci0,/dev/devmm_svm) into the container when it starts. - Difference from NVIDIA: Compared to NVIDIA Toolkit's independent installation, the installation and configuration of Ascend Docker Runtime are more tightly coupled with the CANN Toolkit installation process.
Installation and Configuration
- Ensure the CANN software package is correctly installed. This includes the driver, firmware, and toolkit.
- Install Ascend Docker Runtime: The CANN package usually includes an
ascend-docker-runtimedeb or rpm package.
# Assuming you are in the CANN installation directory
sudo dpkg -i ascend-docker-runtime_*.deb
- Configure Docker Daemon
Edit /etc/docker/daemon.json to add the Ascend runtime, and possibly set it as the default.
{
"runtimes": {
"ascend": {
"path": "/usr/local/bin/ascend-docker-runtime",
"runtimeArgs": []
}
},
"default-runtime": "ascend"
}
- Restart Docker service
sudo systemctl restart docker
- Verify the installation
When starting a container, you need to use the --device parameter to manually specify the NPU devices to map.
docker run -it --device=/dev/davinci0 --device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc ascendhub.huawei.com/public-ascendhub/mindspore-ascend:2.2.11-cann7.0.1-py39-euleros2.10-aarch64 npu-smi info
If NPU 0 information is displayed, the configuration is successful.
Environment Variables: Ascend containers also rely on environment variables to specify which devices to use. The most critical is ASCEND_VISIBLE_DEVICES, which functions identically to NVIDIA_VISIBLE_DEVICES.
Summary: Whether for NVIDIA or Huawei Ascend, the core idea of their containerization solutions is the same: provide a privileged, hardware-aware custom runtime that "interferes" with the standard container startup process to "smuggle" the host's drivers and devices into the isolated container environment. Completing this step paves the way for managing AI compute in K8s.
4.2 The K8s Device Plugin: How to Make K8s "See" and Allocate GPUs and NPUs
Running containers with --gpus or --device on a single machine is just the first step. In a real intelligent computing center, we face a massive cluster of hundreds or thousands of servers. We cannot manually ssh into each machine to execute docker run. We need a "central brain" -- Kubernetes -- to uniformly schedule and manage these valuable AI compute resources.
The problem: K8s natively only knows about CPU and Memory resources. How does it know that node-01 has 8 H800 GPUs while node-02 has 8 910B NPUs? The answer lies in the K8s Device Plugin framework.
4.2.1 Device Plugin: K8s's "Hardware Interpreter"
Device Plugin is a standard, open extension mechanism provided by K8s. It allows third-party hardware vendors to register their proprietary hardware (e.g., GPUs, NPUs, FPGAs, high-performance NICs) as first-class resources in the K8s cluster, making them schedulable and requestable.
Work Mode:
- A Device Plugin is typically a Pod running as a DaemonSet on every compute node (or nodes with specific labels).
- It communicates with the Kubelet on that node via a defined gRPC interface. The communication uses a Unix socket file typically located at
/var/lib/kubelet/device-plugins/kubelet.sock.
4.2.2 The Device Plugin Lifecycle (Core Principles)
Understanding the Device Plugin workflow is key to understanding how K8s manages AI compute. The entire process can be broken down into three steps:
Step 1: Discovery and Registration
- When a Device Plugin Pod starts on a node, it first scans the node, discovering the AI accelerator cards and their IDs by calling native tools like
nvidia-smiornpu-smi. - It then connects to the Kubelet's gRPC service and calls the
Registermethod. - In this call, it tells the Kubelet: "Hello, I am a plugin from NVIDIA. I provide a new resource called
nvidia.com/gpu. Please note this." (Huawei's plugin would register a resource likehuawei.com/npu). - Upon receiving the registration, the Kubelet is aware of this new resource.
Step 2: Reporting and Monitoring (ListAndWatch)
- After successful registration, the Kubelet calls back the Device Plugin's
ListAndWatchmethod. - The Device Plugin immediately returns a list of all currently available device IDs on that node, e.g.,
[GPU-UUID-1, GPU-UUID-2, ...]. - Upon receiving this list, the Kubelet updates the Node object's
status.capacityandstatus.allocatablefields, writing information likenvidia.com/gpu: 8. - It is at this moment that the K8s "central brain" (API Server and Scheduler) truly "sees" those 8 GPUs!
ListAndWatchis a streaming RPC. The Device Plugin continuously monitors the hardware state. If a GPU goes offline due to a fault, it immediately informs the Kubelet through this stream. The Kubelet updates the node's allocatable resources accordingly, preventing the scheduler from scheduling tasks onto a malfunctioning card.
Step 3: Request and Allocation (Allocate)
Now, a user can submit a Pod YAML file, requesting GPU resources in resources.limits:
resources:
limits:
nvidia.com/gpu: 2
- During scheduling, Kube-scheduler iterates through all nodes, searching for nodes whose
status.allocatablefornvidia.com/gpuis greater than or equal to 2. It then schedules the Pod to one of those nodes. - After the Pod arrives at the target node, before creating the container, the Kubelet calls the Device Plugin's
Allocatemethod again, telling it: "This Pod needs 2 GPUs. Please tell me the allocation result." - The Device Plugin selects 2 cards from its maintained list of free devices (e.g., IDs
GPU-UUID-3andGPU-UUID-5) and returns aContainerAllocateResponse. This response contains the necessary information to start the container:- Devices: The device file paths that need to be mounted into the container, e.g.,
/dev/nvidia3,/dev/nvidia5. - Envs: The environment variables to be injected into the container. The most important one is
NVIDIA_VISIBLE_DEVICES=3,5.
- Devices: The device file paths that need to be mounted into the container, e.g.,
- The Kubelet passes this information to the underlying container runtime (Docker + NVIDIA Container Toolkit). The Toolkit, based on
NVIDIA_VISIBLE_DEVICES=3,5, precisely exposes only the 3rd and 5th GPUs to the container. - At this point, a Pod requesting specific GPUs is successfully running.
4.2.3 Practical Deployment
Deploying the NVIDIA Device Plugin:
NVIDIA provides Helm Charts and YAML files, making deployment very simple.
# Install using Helm
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update
helm install \
--generate-name \
--set-string nodeSelector.accelerator=nvidia-h800 \
nvdp/nvidia-device-plugin
Note the nodeSelector here; it ensures the NVIDIA plugin only runs on nodes with NVIDIA GPUs.
Deploying the Ascend Device Plugin:
Huawei also provides YAML deployment files for its plugin, typically included in the CANN software package or its open-source community.
# Example DaemonSet snippet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ascend-device-plugin-daemonset
spec:
template:
spec:
nodeSelector:
accelerator: ascend-910b # Ensure it only runs on Ascend nodes
containers:
- name: ascend-device-plugin
image: ascend-device-plugin:latest
# ... mount necessary sockets and device directories
Summary: Device Plugin is a model example of the "open and extensible" design philosophy of Kubernetes. Through an elegant gRPC protocol, it abstracts a wide variety of hardware into unified, declarable resources. It is the cornerstone of unified heterogeneous compute scheduling. As an AI Infra engineer, you must not only know how to deploy it but also deeply understand the core flow of Register -> ListAndWatch -> Allocate. This will give you a clear line of reasoning when troubleshooting issues like "Why won't the Pod schedule?" or "Why was the GPU allocated incorrectly?"
4.3 Image Engineering: Slimming and Multi-Stage Building for Docker Images in Heterogeneous AI Environments
We can now schedule Pods that require GPUs or NPUs on K8s. Now we face another equally tricky problem: where do the container images for these Pods come from?
For LLM applications, the dependency environment is extremely complex: specific versions of CUDA or CANN, specific versions of PyTorch, massive Python dependency packages, plus the model weights themselves. An unoptimized "naive" image can easily exceed 20 GB. In an LLMOps workflow requiring frequent updates and rapid deployment, such a "behemoth" image is catastrophic:
- Slow Pull Times: When a new node starts up in the cluster, pulling a 20 GB image could take tens of minutes, severely impacting auto-scaling efficiency.
- Expensive Storage: Storing hundreds of versions, each 20 GB, in the image registry is a significant cost.
- Poor Security: The image contains many unnecessary build tools and libraries, increasing the attack surface.
Image Engineering is the "art" of transforming bloated AI application images into lightweight, efficient, secure, and maintainable ones.
4.3.1 Core Principle 1: Choose the Right "Foundation" -- Base Image
Choosing a good base image is the first and most important step in image slimming.
Avoid using
-develimages for production:- NVIDIA's
nvidia/cudaimage tags typically come in three varieties:-base,-runtime, and-devel. -develcontains the full CUDA compiler (nvcc), header files, and debugging tools. It has the largest size and should only be used for the compilation stage.-runtimecontains only the runtime libraries necessary for running CUDA programs. It is much smaller.-baseis even more stripped down, potentially not even including common libraries like cuDNN.- Principle: The final production image must be built from a
-runtimeor-baseimage.
- NVIDIA's
The Ascend ecosystem equivalent: Huawei's base images (e.g.,
mindspore-ascend) also have different versions. Choose a version that contains only the CANN runtime environment, not the complete development toolkit, as your production base.
4.3.2 Core Principle 2: "Two-Phase Construction" -- Multi-Stage Builds
This is the "nuclear weapon" of image slimming. The idea is to use multiple FROM instructions in a single Dockerfile, dividing the build process into multiple stages. Only the final stage's output becomes the final image.
Scenario: We have a custom PyTorch C++ extension that needs compilation.
Bad Dockerfile (Single Stage):
# Image is huge (e.g., 15 GB)
FROM nvidia/cuda:12.1.0-devel-ubuntu22.04
# Install build tools and Python
RUN apt-get update && apt-get install -y g++ python3-pip
# Install Python dependencies
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
# Compile and install custom extension
COPY . .
RUN python3 setup.py install
CMD ["python3", "main.py"]
This image contains the g++ compiler, the complete CUDA SDK, pip cache, and all other "construction waste."
Good Dockerfile (Multi-Stage):
# --- Stage 1: Builder ---
# Use a development image for compilation
FROM nvidia/cuda:12.1.0-devel-ubuntu22.04 AS builder
RUN apt-get update && apt-get install -y g++ python3-pip python3.10-venv
WORKDIR /app
# Install in a virtual environment for easy packaging
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN python3 setup.py install
# --- Stage 2: Final ---
# Use a lightweight runtime image
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
# Copy only the necessary runtime files and compiled artifacts
COPY --from=builder /app/venv /app/venv
COPY --from=builder /app/main.py /app/main.py
ENV PATH="/app/venv/bin:$PATH"
WORKDIR /app
CMD ["python3", "main.py"]
The final generated image might be only 5 GB in size. It contains no compile-time dependencies like g++ or the CUDA SDK -- only the clean Python virtual environment and the final runtime script.
4.3.3 Core Principle 3: "Be Frugal" -- Optimize Dockerfile Instructions
Combine RUN Instructions: Each RUN, COPY, or ADD instruction in a Dockerfile creates a new image layer. Too many layers increase image size and build time.
# Bad: creates multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Good: combine into one layer and clean up promptly
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
Leverage Build Cache: Docker caches the result of each layer. Place infrequently changing instructions (like installing system packages) early, and frequently changing ones (like COPY source code) later, to maximize cache usage and speed up subsequent builds.
Use .dockerignore: Create a .dockerignore file in the project root to exclude files that do not need to be copied into the image (e.g., .git directory, test data, local configuration files). This reduces the build context size and prevents sensitive information leaks.
4.3.4 Challenge: Image Strategy in Heterogeneous Environments
When your cluster contains both NVIDIA and Ascend hardware, how do you manage application images?
Strategy 1 (Recommended): Separate Images
Build two separate images for the same application, with clear tags:
myapp:1.2.0-cuda12.1myapp:1.2.0-cann7.0
In K8s deployment files (e.g., Deployment or Job), use nodeSelector to decide which image to use:
# Pod template for NVIDIA nodes
spec:
nodeSelector:
accelerator: nvidia-h800
containers:
- name: main
image: my-registry/myapp:1.2.0-cuda12.1
# Pod template for Ascend nodes
spec:
nodeSelector:
accelerator: ascend-910b
containers:
- name: main
image: my-registry/myapp:1.2.0-cann7.0
- Advantages: Images are clean, minimal, and have no redundancy. This is the cleanest approach and most aligned with cloud-native principles.
- Disadvantages: Requires maintaining two CI/CD pipelines.
Strategy 2 (Advanced, Use with Caution): Fat Image with Runtime Detection
Build a single "fat image" containing both CUDA and CANN dependencies.
Modify the container's entry point script (ENTRYPOINT) to detect the current environment at startup and choose the correct execution path.
#!/bin/bash
if [ -d "/usr/local/cuda" ]; then
echo "CUDA environment detected. Starting application for NVIDIA."
# Execute NVIDIA version start command
exec python3 main_cuda.py "$@"
elif [ -d "/usr/local/ascend" ]; then
echo "Ascend/CANN environment detected. Starting application for Huawei."
# Execute Ascend version start command
exec python3 main_ascend.py "$@"
else
echo "Error: No supported AI accelerator environment found."
exit 1
fi
- Advantages: Only one image tag to manage.
- Disadvantages: The image is extremely bloated, violates the single responsibility principle, and is more complex to manage and debug. Generally not recommended unless there are very special unified delivery requirements.
Summary: In this chapter, we started from scratch and successfully packaged AI applications into standardized containers that can be uniformly scheduled in K8s. We mastered the "runtime" technology for connecting containers to hardware, understood the "device plugin" principles for making K8s hardware-aware, and learned the "image engineering" art of building lightweight, efficient images. At this point, we have a solid "foundation" for our cloud-native AI platform. Next, we will build a smarter, more efficient scheduling and resource management system on top of this foundation.