# Ascend VNPU 实战：从原生 Docker 到 Ascend Docker Runtime


![Ascend vNPU 实战：从原生 Docker 到 Ascend Docker Runtime](https://img.lixueduan.com/kubernetes/cover/ascend-npu-docker-vnpu.jpg)

本文以 Ascend 310P3（Atlas 300I Pro）为例，介绍昇腾 NPU vNPU 的基本使用方法。
内容包括从原生 Docker 开始，演示手动创建 vNPU 并挂载到容器，以及 Ascend Docker Runtime 下的静态挂载和动态虚拟化。

<!--more-->

后续我们会继续把 vNPU 接入 Kubernetes，并进一步看 HAMi 如何对 vNPU 做统一的资源管理和调度。本文先绕开 Kubernetes 和 HAMi，从 Ascend 自身提供的 vNPU 能力开始，跑通 vNPU。

## vNPU 介绍

### 什么是 Ascend vNPU

昇腾的虚拟化实例叫 AVI（Ascend Virtual Instance）。它可以按照固定的虚拟化模板，把一个物理 NPU 划分成多个 vNPU，再把 vNPU 提供给容器使用。

从使用者的角度，原生 Docker 下主要需要完成三件事：

1. 选择虚拟化模式和 vNPU 模板；
2. 创建或指定要使用的 vNPU；
3. 把设备(vNPU)挂载到容器使用。

![昇腾 NPU 虚拟化架构](https://img.lixueduan.com/kubernetes/vnpu/hami-vnpu-arch.png)

### 和 NVIDIA MIG 的类比

Ascend vNPU 在使用方式上可以类比 NVIDIA MIG（Multi-Instance GPU）：两者都可以把一块物理加速卡的资源划分成更小的实例，再分别提供给容器或任务使用。

不过这只是使用方式上的类比。Ascend vNPU 与 NVIDIA MIG 在资源模型、虚拟化机制和具体实现上并不相同，因此不能简单把两者视为同一种底层技术。

| 对比维度 | Ascend vNPU | NVIDIA MIG |
|---|---|---|
| 虚拟实例 | AVI / vNPU | MIG Instance |
| 划分方式 | 根据 `vir01`、`vir02` 等模板划分 AICORE、内存、AICPU 和媒体处理资源 | 根据 GPU 支持的 profile 划分计算核心、显存和相关硬件资源 |
| 创建/注入方式 | 使用 `npu-smi` 创建静态 vNPU；Ascend Docker Runtime 可按 `ASCEND_VNPU_SPECS` 动态创建并注入，或挂载已有 vNPU | 使用 `nvidia-smi mig` / NVML 创建 MIG 实例；NVIDIA Container Toolkit 负责将已有 MIG 设备注入容器 |
| 容器视角 | 容器内看到一个 `310Pvir01` 等 vNPU 设备 | 容器内看到分配到的 MIG GPU |
| 共同点 | 都可以把物理加速器资源以更细粒度的实例提供给不同容器或任务使用 | 都可以把物理加速器资源以更细粒度的实例提供给不同容器或任务使用 |

本文后面的命令只针对 Ascend vNPU，NVIDIA MIG 仅作为帮助理解的参照。

### 不同场景下的使用方式

vNPU 的底层能力相同，但不同运行环境负责的事情不一样：

- **原生 Docker**：通过 `npu-smi` 手动创建 vNPU，再手动把 vNPU 设备和 Ascend 驱动环境挂载到容器中，下面会完整展开。

- **Ascend Docker Runtime**：支持静态和动态两种方式。静态方式仍然需要提前创建 vNPU，但 vNPU 设备注入由 Runtime 完成；动态方式则在启动容器时通过 `ASCEND_VISIBLE_DEVICES`、`ASCEND_VNPU_SPECS` 等参数指定物理设备和 vNPU 模板，由 Runtime 动态创建并注入 vNPU，不需要提前创建虚拟实例。

- **集群调度组件**：可以使用 Ascend Device Plugin 配合 Kubernetes Scheduler、Volcano 或其他调度组件。Device Plugin 负责设备发现、资源上报和节点侧设备分配，Scheduler 根据资源请求选择合适的节点；HAMi、Volcano 等增强方案还可以进一步参与资源调度和管理。

**简单说，原生 Docker 是“用户自己创建、自己挂载设备”，Ascend Docker Runtime 是“Runtime 帮你注入设备”，集群调度则是在此基础上进一步统一管理资源。**

![Ascend vNPU 三种 Docker 使用方式](https://img.lixueduan.com/kubernetes/vnpu/ascend-vnpu-docker-modes.jpg)

## 测试环境

本次使用环境关键信息如下：

| 项目 | 实测值 |
|---|---|
| 操作系统 | Huawei Cloud EulerOS 2.0（x86_64） |
| 主机名 | `lixd-test-npu` |
| NPU | Ascend 310P3 / Atlas 300I Pro，1 张 |
| 架构 | `x86_64` |
| Ascend Driver | 22.0.4 |
| CANN | 6.0.0 |
| Docker | docker-engine 18.09.0 |
| Ascend Docker Runtime | 6.0.0 |

### 查询 NPU 和 vNPU 模板

先确认 NPU 设备和芯片信息：

```bash
npu-smi info -l
npu-smi info -m
```

```text
	Total Count                    : 1

	NPU ID                         : 13
	Product Name                   : IT21PDDA01
	Serial Number                  : 2106030635ZENC0xxx94
	Chip Count                     : 1

	NPU ID                         Chip ID                        Chip Logic ID                  Chip Name
	13                             0                              0                              Ascend 310P3
	13                             1                              -                              Mcu
```

本机的关键结果是：

```text
NPU ID: 13
Chip ID: 0
Chip Name: Ascend 310P3
Product Name: Atlas 300I Pro
```

再确认产品类型：

```bash
npu-smi info -t product -i 13
```

```text
	Product Type                   : Atlas 300I Pro
```

再查询当前产品支持的 vNPU 模板：

```bash
npu-smi info -t template-info -i 13
```

```text
+------------------------------------------------------------------------------------------+
|NPU instance template info is:                                                            |
|Name                AICORE    Memory    AICPU     VPC            VENC           JPEGD     |
|                               GB                 PNGD           VDEC           JPEGE     |
|==========================================================================================|
|vir01               1         3         1         1              0              2         |
|                                                  0              1              1         |
+------------------------------------------------------------------------------------------+
|vir02               2         6         2         3              1              4         |
|                                                  0              3              2         |
+------------------------------------------------------------------------------------------+
|vir02_1c            2         6         1         3              0              4         |
|                                                  0              3              2         |
+------------------------------------------------------------------------------------------+
|vir04               4         12        4         6              2              8         |
|                                                  0              6              4         |
+------------------------------------------------------------------------------------------+
|vir04_3c            4         12        3         6              1              8         |
|                                                  0              6              4         |
+------------------------------------------------------------------------------------------+
|vir04_3c_ndvpp      4         12        3         0              0              0         |
|                                                  0              0              0         |
+------------------------------------------------------------------------------------------+
|vir04_4c_dvpp       4         12        4         12             3              16        |
|                                                  0              12             8         |
+------------------------------------------------------------------------------------------+
```

本机可用的模板包括 `vir01`、`vir02`、`vir02_1c`、`vir04`、`vir04_3c`、`vir04_3c_ndvpp` 和 `vir04_4c_dvpp`。本文选择 `vir01` 做最小验证。

## 原生 Docker 下的 vNPU Demo

原生 Docker 下使用 vNPU 分成两步：先用 `npu-smi` 创建 vNPU，再把生成的 vNPU 设备映射到容器。

### 安装 Docker

使用 `dnf` 安装 Docker。Huawei Cloud EulerOS 仓库里的包名是 `docker-engine`：

```bash
dnf install -y docker-engine
systemctl enable --now docker
```

确认 Docker 服务和服务端信息：

```bash
docker version
docker info --format 'server={{.ServerVersion}} arch={{.Architecture}} default-runtime={{.DefaultRuntime}}'
```

```text
server=18.09.0 arch=x86_64 default-runtime=runc
```

### 创建 vNPU

本机使用 NPU ID `13`、Chip ID `0` 和 `vir01` 模板：

```bash
npu-smi set -t create-vnpu -i 13 -c 0 -f vir01
npu-smi info -t info-vnpu -i 13 -c 0
```

输出如下：

```text
	Status                         : OK
	Message                        : Create vnpu success

+-------------------------------------------------------------------------------+
| NPU resource static info as follow:                                           |
| Format:Free/Total                   NA: Currently, query is not supported.    |
| AICORE    Memory    AICPU    VPC    VENC    VDEC    JPEGD    JPEGE    PNGD    |
|            GB                                                                 |
|===============================================================================|
| 7/8       18/21     6/7      11/12  3/3     11/12   14/16    7/8      NA/NA   |
+-------------------------------------------------------------------------------+
| Total number of vnpu: 1                                                       |
+-------------------------------------------------------------------------------+
|  Vnpu ID  |  Vgroup ID     |  Container ID  |  Status  |  Template Name       |
+-------------------------------------------------------------------------------+
|  100      |  0             |  000000000000  |  0       |  vir01               |
+-------------------------------------------------------------------------------+
```

创建成功后，本机得到 vNPU ID `100`，对应设备节点为 `/dev/vdavinci100`。

这里有几个容易混淆的设备编号：

* **`npu-smi` NPU ID**：宿主机通过 `npu-smi` 查询和管理设备时使用的编号，例如本文中的 `13`，创建 vNPU 时通过 `-i 13` 指定；
* **Runtime 物理设备 ID**：Ascend Docker Runtime 使用 `/dev/davinci*` 对应的设备序号选择物理设备。本文对应 `/dev/davinci0`，因此动态虚拟化时使用 `ASCEND_VISIBLE_DEVICES=0`；
* **vNPU ID**：创建 vNPU 后返回的虚拟实例编号，例如本文中的 `100`，对应 `/dev/vdavinci100`。使用 Runtime 挂载已有 vNPU 时，通过 `ASCEND_VISIBLE_DEVICES=100` 指定；
* **容器内显示编号**：设备注入容器后，`npu-smi` 还会显示容器视角下的设备编号，例如本文实测中的 `NPU 13`。

这些编号来自不同的设备管理视角，不应该直接混用。

### 启动容器挂载 vNPU

启动容器前，先确认 vNPU 设备节点和基础设备节点已经存在：

```bash
ls -l /dev/vdavinci100 \
  /dev/davinci_manager \
  /dev/devmm_svm \
  /dev/hisi_hdc
```

本次实测可以看到以下设备节点：

```text
crw-rw---- 1 HwHiAiUser HwHiAiUser 239,   0 Sep 17 17:42 /dev/davinci_manager
crw-rw---- 1 HwHiAiUser HwHiAiUser 234,   0 Sep 17 17:42 /dev/devmm_svm
crw-rw---- 1 HwHiAiUser HwHiAiUser 511,   0 Sep 17 17:42 /dev/hisi_hdc
crw------- 1 HwHiAiUser HwHiAiUser 236, 100 Sep 17 20:49 /dev/vdavinci100
```

然后我们启动容器，使用 `--device` 参数把 vNPU 挂载到容器：

```bash
IMG=ascendai/cann:7.0.1-310p-openeuler20.03-py3.8

docker run --rm \
  --device=/dev/vdavinci100:/dev/davinci100 \
  --device=/dev/davinci_manager \
  --device=/dev/devmm_svm \
  --device=/dev/hisi_hdc \
  -v /usr/local/sbin/npu-smi:/usr/local/sbin/npu-smi:ro \
  -v /usr/local/Ascend:/usr/local/Ascend:ro \
  -e LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/lib64:/usr/local/Ascend/driver/lib64/driver:/usr/local/Ascend/driver/lib64 \
  $IMG \
  npu-smi info
```

容器内输出如下：

```text
+--------------------------------------------------------------------------------------------------------+
| npu-smi 22.0.4                                   Version: 22.0.4                                       |
+-------------------------------+-----------------+------------------------------------------------------+
| NPU     Name                  | Health          | Power(W)     Temp(C)           Hugepages-Usage(page) |
| Chip    Device                | Bus-Id          | AICore(%)    Memory-Usage(MB)                        |
+===============================+=================+======================================================+
| 13      310Pvir01             | OK              | NA           56                0    / 0              |
| 0       0                     | 0000:00:0D.0    | 0            312  / 2690                             |
+===============================+=================+======================================================+
```

输出中的 `310Pvir01` 表明容器内已经看到 `vir01` 类型的 vNPU。

这里的参数可以简单分为两类。它们不是用来创建 vNPU 的，vNPU 已经在前一步创建完成：

- **设备映射（`--device`）**：把 `/dev/vdavinci100` 以及
  `/dev/davinci_manager`、`/dev/devmm_svm`、`/dev/hisi_hdc` 等 Ascend 设备节点交给容器，
  这样容器才能访问已经创建的 vNPU。
- **工具和运行库（`-v`、`LD_LIBRARY_PATH`）**：把宿主机的 `npu-smi` 和
  `/usr/local/Ascend` 驱动/工具库以只读方式提供给容器，并让动态链接器能找到这些库。

## Ascend Docker Runtime Demo

### 安装 Ascend Docker Runtime

原生 Docker 路径验证完成后，再安装 Ascend Docker Runtime，继续验证 Runtime 的静态挂载和动态虚拟化。

本次使用 MindCluster 6.0.0 提供的 x86_64 安装包。官方安装包地址见 [MindCluster v6.0.0 Release](https://gitee.com/ascend/mind-cluster/releases/tag/v6.0.0)。

```bash
mkdir -p /tmp/ascend-docker-runtime
cd /tmp/ascend-docker-runtime

wget -O Ascend-docker-runtime_6.0.0_linux-x86_64.run \
  https://gitee.com/ascend/mind-cluster/releases/download/v6.0.0/Ascend-docker-runtime_6.0.0_linux-x86_64.run

sha256sum Ascend-docker-runtime_6.0.0_linux-x86_64.run
chmod u+x Ascend-docker-runtime_6.0.0_linux-x86_64.run
./Ascend-docker-runtime_6.0.0_linux-x86_64.run --install
```

命令输出如下：

```text
2e3800050d4203daa87937748a436ba0c32fabbc8693ac597ee40ba5b6f7c6af  Ascend-docker-runtime_6.0.0_linux-x86_64.run
Uncompressing ascend-docker-runtime     0%    57%  100%
[INFO] installing ascend docker runtime
[INFO] platform(x86_64) matched!
[INFO] install executable files success
[INFO] install scene is 'docker'.
[INFO] /etc/docker/daemon.json modify success
[INFO] Ascend Docker Runtime has been installed in: /usr/local/Ascend/Ascend-Docker-Runtime
[INFO] The version of Ascend Docker Runtime is: 6.0.0
[INFO] please reboot daemon and container engine to take effect
[INFO] Ascend Docker Runtime install success
```

安装完成后重载并重启 Docker：

```bash
systemctl daemon-reload
systemctl restart docker
docker info --format 'server={{.ServerVersion}} arch={{.Architecture}} default-runtime={{.DefaultRuntime}}'
```

```text
server=18.09.0 arch=x86_64 default-runtime=ascend
```

Runtime 安装时会把 `ascend` 注册到 `/etc/docker/daemon.json` 并设为默认 runtime：

```json
{
        "default-runtime": "ascend",
        "runtimes": {
                "ascend": {
                        "path": "/usr/local/Ascend/Ascend-Docker-Runtime/ascend-docker-runtime",
                        "runtimeArgs": []
                }
        }
}
```

### Ascend Docker Runtime：静态 vNPU

安装 Runtime 后，可以复用前面通过 `npu-smi` 创建的静态 vNPU。与原生 Docker 的区别是：不再手工写 `--device`，而是把 vNPU ID 交给 Runtime，由 Runtime 完成设备注入。

本机前面创建的 vNPU ID 是 `100`。设置 `ASCEND_RUNTIME_OPTIONS=VIRTUAL`，告诉 Runtime `ASCEND_VISIBLE_DEVICES` 指向的是已经存在的 vNPU：

```bash
IMG=ascendai/cann:7.0.1-310p-openeuler20.03-py3.8

docker run --rm --runtime=ascend \
  -e ASCEND_VISIBLE_DEVICES=100 \
  -e ASCEND_RUNTIME_OPTIONS=VIRTUAL \
  $IMG \
  npu-smi info
```

容器内输出如下：

```text
+--------------------------------------------------------------------------------------------------------+
| npu-smi 22.0.4                                   Version: 22.0.4                                       |
+-------------------------------+-----------------+------------------------------------------------------+
| NPU     Name                  | Health          | Power(W)     Temp(C)           Hugepages-Usage(page) |
| Chip    Device                | Bus-Id          | AICore(%)    Memory-Usage(MB)                        |
+===============================+=================+======================================================+
| 13      310Pvir01             | OK              | NA           56                0    / 0              |
| 0       0                     | 0000:00:0D.0    | 0            312  / 2690                             |
+===============================+=================+======================================================+
```

相比原生 Docker，不再需要手工声明 --device、挂载宿主机驱动目录或配置 LD_LIBRARY_PATH。在使用 Ascend Runtime 后，只需要通过 ASCEND_VISIBLE_DEVICES 和 ASCEND_RUNTIME_OPTIONS 描述需要注入的 vNPU。Runtime 在 prestart hook 阶段自动完成三件事：

- 把 `ASCEND_VISIBLE_DEVICES` 对应的 NPU 设备挂载到容器；
- 在宿主机上配置该容器的 device cgroup，保证设备隔离；
- 将宿主机相关的 Ascend 驱动运行库和工具按 Runtime 配置注入容器。

也就是说，Runtime 已经解决了原生 Docker 里最麻烦的那部分。

### Ascend Docker Runtime：动态 vNPU

Ascend Docker Runtime 可以在容器启动时根据环境变量自动创建 vNPU，并在容器退出后释放动态实例。动态验证和上面的静态验证是两条独立路径，动态启动时设备从没有预创建 vNPU 的状态开始。

因此，先确认前面的静态 vNPU 容器已经退出，再销毁 vNPU `100`，并确认物理 NPU 上已经没有预创建实例：

```bash
npu-smi set -t destroy-vnpu -i 13 -c 0 -v 100
npu-smi info -t info-vnpu -i 13 -c 0
```

#### 关闭 vNPU 配置恢复

动态 Runtime 需要关闭 vNPU 配置恢复功能：

```bash
npu-smi set -t vnpu-cfg-recover -d 0
```

输出如下：

```text
	Status                         : OK
	Message                        : The VNPU config recover mode Disable is set successfully.
```

#### 启动动态 vNPU 容器

通过 ASCEND_VISIBLE_DEVICES 选择物理设备，通过 ASCEND_VNPU_SPECS 指定 vNPU 模板。
> 这里的 ASCEND_VISIBLE_DEVICES=0 对应宿主机 /dev/davinci0，与前面 npu-smi -i 13 使用的 NPU ID 属于不同的设备编号视角。

```bash
docker run --rm --runtime=ascend \
  -e ASCEND_VISIBLE_DEVICES=0 \
  -e ASCEND_VNPU_SPECS=vir01 \
  $IMG \
  npu-smi info
```

容器内输出如下：

```text
+--------------------------------------------------------------------------------------------------------+
| npu-smi 22.0.4                                   Version: 22.0.4                                       |
+-------------------------------+-----------------+------------------------------------------------------+
| NPU     Name                  | Health          | Power(W)     Temp(C)           Hugepages-Usage(page) |
| Chip    Device                | Bus-Id          | AICore(%)    Memory-Usage(MB)                        |
+===============================+=================+======================================================+
| 13      310Pvir01             | OK              | NA           56                0    / 0              |
| 0       0                     | 0000:00:0D.0    | 0            312  / 2690                             |
+===============================+=================+======================================================+
```

输出中的 `310Pvir01` 表明 Runtime 已经在容器内注入动态创建的 `vir01` vNPU。

容器退出后再次查询：

```bash
npu-smi info -t info-vnpu -i 13 -c 0
```

输出如下：

```text
+-------------------------------------------------------------------------------+
| NPU resource static info as follow:                                           |
| Format:Free/Total                   NA: Currently, query is not supported.    |
| AICORE    Memory    AICPU    VPC    VENC    VDEC    JPEGD    JPEGE    PNGD    |
|            GB                                                                 |
+===============================================================================+
| 8/8       21/21     7/7      12/12  3/3     12/12   16/16    8/8      NA/NA   |
+-------------------------------------------------------------------------------+
| Total number of vnpu: 0                                                       |
+-------------------------------------------------------------------------------+
|  Vnpu ID  |  Vgroup ID     |  Container ID  |  Status  |  Template Name       |
+-------------------------------------------------------------------------------+
```

结果显示当前 vNPU 数量为 `0`，说明动态实例随容器生命周期自动释放，AICORE 和内存也都恢复到 `8/8`、`21/21`。

## 小结

Docker 方式的职责可以概括为：

| 方式 | 设备和模板 | 容器内注入 |
|---|---|---|
| 原生 Docker 静态 vNPU | 用户执行 `npu-smi` 创建 | 用户手工 `--device`，并挂载 `npu-smi` 和驱动库 |
| Ascend Docker Runtime 静态 vNPU | 用户执行 `npu-smi` 创建，Runtime 接收 vNPU ID | Runtime 根据 `ASCEND_VISIBLE_DEVICES` 和 `ASCEND_RUNTIME_OPTIONS=VIRTUAL` 自动注入 |
| Ascend Docker Runtime 动态 vNPU | 启动容器时设置 `ASCEND_VISIBLE_DEVICES`、`ASCEND_VNPU_SPECS` | Runtime 自动创建实例并注入，容器退出后释放 |

两者的核心差别在于**谁负责把设备塞进容器**：原生 Docker 靠用户手写 `--device` 和挂载，Ascend Docker Runtime 把这些交给 prestart hook 自动完成。这两种方式使用的是同一套底层 vNPU 能力。

到这里，我们先把 Ascend 自身提供的 vNPU 能力和两种 Docker 使用方式跑通了。下一步再把这套能力放进 Kubernetes，看看 Ascend Device Plugin 和 HAMi 分别解决什么问题。


---

> 作者: [意琦行](https://github.com/lixd)  
> URL: https://www.lixueduan.com/posts/kubernetes/67-ascend-npu-docker-vnpu/  

