CANN容器化部署与云原生实践

CANN组织链接:https://atomgit.com/cann
CANN community仓库链接:https://atomgit.com/cann/community

一、容器化部署概述

1.1 容器化优势

容器化技术为CANN应用部署提供了标准化和可移植的解决方案。

1.1.1 主要优势
  • 环境一致性:开发、测试、生产环境一致
  • 快速部署:秒级启动应用
  • 资源隔离:CPU、内存、GPU隔离
  • 易于扩展:水平扩展方便
1.1.2 容器技术栈
  • Docker:容器运行时
  • Kubernetes:容器编排
  • Helm:应用包管理
  • Prometheus:监控告警

1.2 CANN容器化挑战

  • NPU设备直通
  • 驱动兼容性
  • 性能优化
  • 存储管理

二、Docker容器化

2.1 基础镜像构建

# CANN基础镜像
FROM ubuntu:20.04

# 设置环境变量
ENV ASCEND_HOME=/usr/local/Ascend
ENV PATH=${ASCEND_HOME}/ascend-toolkit/latest/bin:${PATH}
ENV LD_LIBRARY_PATH=${ASCEND_HOME}/ascend-toolkit/latest/lib64:${LD_LIBRARY_PATH}

# 安装基础依赖
RUN apt-get update && apt-get install -y \
    python3.8 \
    python3-pip \
    python3-dev \
    gcc \
    g++ \
    cmake \
    wget \
    && rm -rf /var/lib/apt/lists/*

# 安装CANN运行时
COPY cann-runtime_*.run /tmp/
RUN chmod +x /tmp/cann-runtime_*.run && \
    /tmp/cann-runtime_*.run --install --quiet && \
    rm /tmp/cann-runtime_*.run

# 安装Python依赖
COPY requirements.txt /tmp/
RUN pip3 install --no-cache-dir -r /tmp/requirements.txt

# 设置工作目录
WORKDIR /app

# 复制应用代码
COPY . /app/

# 设置入口点
ENTRYPOINT ["python3", "app.py"]

2.2 应用镜像构建

# CANN应用镜像
FROM cann-base:latest

# 安装应用依赖
RUN pip3 install --no-cache-dir \
    flask==2.0.0 \
    gunicorn==20.1.0 \
    numpy==1.21.0 \
    opencv-python==4.5.0

# 复制模型文件
COPY models/ /app/models/

# 复制应用代码
COPY app/ /app/app/

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "4", "app:app"]

2.3 构建脚本

#!/bin/bash
# build.sh

set -e

# 镜像名称和标签
IMAGE_NAME="cann-app"
IMAGE_TAG="v1.0"

# 构建基础镜像
echo "Building base image..."
docker build -t cann-base:latest -f Dockerfile.base .

# 构建应用镜像
echo "Building application image..."
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} -f Dockerfile .

# 打标签
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest

echo "Build completed successfully!"
echo "Images:"
docker images | grep cann

三、NPU设备管理

3.1 NPU直通配置

# 启用NPU直通
#!/bin/bash

# 检查NPU设备
npu-smi info

# 配置Docker使用NPU
# 方法1:使用设备直通
docker run -it \
  --device=/dev/davinci0 \
  --device=/dev/davinci_manager \
  --device=/dev/devmm_svm \
  --device=/dev/hisi_hdc \
  -v /usr/local/Ascend:/usr/local/Ascend \
  cann-app:latest

# 方法2:使用--privileged(不推荐)
docker run -it \
  --privileged \
  -v /usr/local/Ascend:/usr/local/Ascend \
  cann-app:latest

3.2 多NPU配置

#!/bin/bash
# multi_npu_run.sh

# 可用的NPU数量
NUM_NPUS=$(npu-smi info | grep "NPU" | wc -l)

echo "Available NPUs: ${NUM_NPUS}"

# 为每个NPU启动容器
for i in $(seq 0 $((${NUM_NPUS} - 1))); do
  echo "Starting container for NPU ${i}..."

  docker run -d \
    --name cann-app-npu-${i} \
    --device=/dev/davinci${i} \
    --device=/dev/davinci_manager \
    --device=/dev/devmm_svm \
    --device=/dev/hisi_hdc \
    -v /usr/local/Ascend:/usr/local/Ascend \
    -e ASCEND_VISIBLE_DEVICES=${i} \
    -p 808${i}:8080 \
    cann-app:latest
done

echo "Started containers for all NPUs"
docker ps | grep cann-app

3.3 NPU资源限制

#!/bin/bash
# 限制NPU使用

# 使用cgroup限制NPU计算资源
docker run -it \
  --device=/dev/davinci0 \
  --device-read-bps=/dev/davinci0:1000000000 \
  --device-write-bps=/dev/davinci0:1000000000 \
  -v /usr/local/Ascend:/usr/local/Ascend \
  cann-app:latest

四、Kubernetes部署

4.1 NPU设备插件

# npu-device-plugin.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: ascend-npu-device-plugin
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: ascend-npu-device-plugin
  template:
    metadata:
      labels:
        name: ascend-npu-device-plugin
    spec:
      hostNetwork: true
      containers:
      - name: ascend-npu-device-plugin
        image: ascend-npu/k8s-device-plugin:latest
        command: ["./npu-device-plugin"]
        args:
        - "--v=5"
        volumeMounts:
        - name: device-dir
          mountPath: /dev
        - name: sys-dir
          mountPath: /sys
        resources:
          requests:
            memory: "100Mi"
            cpu: "100m"
          limits:
            memory: "500Mi"
            cpu: "500m"
      volumes:
      - name: device-dir
        hostPath:
          path: /dev
      - name: sys-dir
        hostPath:
          path: /sys

4.2 应用部署配置

# cann-app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cann-app
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: cann-app
  template:
    metadata:
      labels:
        app: cann-app
    version: v1.0
    spec:
      containers:
      - name: cann-app
        image: cann-app:v1.0
        ports:
        - containerPort: 8080
          name: http
        resources:
          requests:
            ascend.com/npu: 1
            memory: "4Gi"
            cpu: "2"
          limits:
            ascend.com/npu: 1
            memory: "8Gi"
            cpu: "4"
        env:
        - name: ASCEND_VISIBLE_DEVICES
          value: "0,1,2,3"
        volumeMounts:
        - name: cann-software
          mountPath: /usr/local/Ascend
          readOnly: true
        - name: model-storage
          mountPath: /app/models
        - name: data-storage
          mountPath: /app/data
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
      volumes:
      - name: cann-software
        hostPath:
          path: /usr/local/Ascend
      - name: model-storage
        persistentVolumeClaim:
          claimName: model-pvc
      - name: data-storage
        emptyDir: {}
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: ascend.com/npu.count
                operator: Gt
                values:
                - "0"

4.3 Service配置

# cann-app-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: cann-app
  namespace: default
  labels:
    app: cann-app
spec:
  type: ClusterIP
  ports:
  - port: 8080
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: cann-app

---
apiVersion: v1
kind: Service
metadata:
  name: cann-app-lb
  namespace: default
spec:
  type: LoadBalancer
  ports:
  - port: 8080
    targetPort: 8080
    protocol: TCP
  selector:
    app: cann-app
  sessionAffinity: ClientIP

五、Helm Chart

5.1 Chart结构

cann-app/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   └── ingress.yaml
└── README.md

5.2 Chart.yaml

apiVersion: v2
name: cann-app
description: A Helm chart for CANN application
version: 1.0.0
appVersion: "1.0"
keywords:
  - cann
  - ai
  - inference
maintainers:
  - name: CANN Team
    email: support@example.com

5.3 values.yaml

replicaCount: 3

image:
  repository: cann-app
  tag: v1.0
  pullPolicy: IfNotPresent

npu:
  count: 1
  devices: "0,1,2,3"

service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: nginx
  hosts:
    - host: cann-app.example.com
      paths:
      - path: /
        pathType: Prefix

resources:
  requests:
    ascend.com/npu: 1
    memory: 4Gi
    cpu: 2
  limits:
    ascend.com/npu: 1
    memory: 8Gi
    cpu: 4

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
  targetMemoryUtilizationPercentage: 80

config:
  logLevel: info
  batchSize: 32
  maxConcurrency: 100

5.4 deployment模板

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "cann-app.fullname" . }}
  labels:
    {{- include "cann-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "cann-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "cann-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - containerPort: {{ .Values.service.port }}
        env:
        - name: ASCEND_VISIBLE_DEVICES
          value: {{ .Values.npu.devices | quote }}
        envFrom:
        - configMapRef:
            name: {{ include "cann-app.fullname" . }}-config
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
        volumeMounts:
        - name: cann-software
          mountPath: /usr/local/Ascend
      volumes:
      - name: cann-software
        hostPath:
          path: /usr/local/Ascend
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: ascend.com/npu.count
                operator: Gt
                values:
                - {{ .Values.npu.count | quote }}

六、监控与日志

6.1 Prometheus监控

# prometheus-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s

    scrape_configs:
    - job_name: 'cann-app'
      kubernetes_sd_configs:
      - role: pod
      relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: cann-app
        action: keep
      - source_labels: [__meta_kubernetes_pod_ip]
        target_label: __address__
        replacement: $1:8080
      metric_relabel_configs:
      - source_labels: [__name__]
        regex: 'npu_.*'
        action: keep

6.2 自定义监控指标

# app/metrics.py
from prometheus_client import Counter, Histogram, Gauge, start_http_server

# 定义指标
inference_requests = Counter(
    'inference_requests_total',
    'Total inference requests',
    ['model', 'status']
)

inference_duration = Histogram(
    'inference_duration_seconds',
    'Inference duration',
    ['model']
)

npu_utilization = Gauge(
    'npu_utilization_percent',
    'NPU utilization',
    ['device_id']
)

def setup_metrics(port=8000):
    """启动指标服务"""
    start_http_server(port)

def record_inference(model_name, duration, status):
    """记录推理指标"""
    inference_requests.labels(model=model_name, status=status).inc()
    inference_duration.labels(model=model_name).observe(duration)

def update_npu_utilization():
    """更新NPU利用率"""
    import subprocess
    import re

    try:
        result = subprocess.run(
            ['npu-smi', 'info'],
            capture_output=True,
            text=True
        )

        # 解析NPU利用率
        for line in result.stdout.split('\n'):
            if 'NPU' in line and '%' in line:
                match = re.search(r'NPU\s+(\d+).*?(\d+)%', line)
                if match:
                    device_id = match.group(1)
                    utilization = match.group(2)
                    npu_utilization.labels(device_id=device_id).set(utilization)

    except Exception as e:
        print(f"Failed to update NPU utilization: {e}")

6.3 日志收集

# fluentd-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  namespace: logging
data:
  fluent.conf: |
    # CANN应用日志收集
    <source>
      @type tail
      @id cann_app_log
      path /var/log/containers/*cann-app*.log
      pos_file /var/log/fluentd-cann-app.log.pos
      tag cann.*
      <parse>
        @type json
      </parse>
    </source>

    # 过滤和解析
    <filter cann.**>
      @type parser
      key_name log
      <parse>
        @type grok
        expression %{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:message}
      </parse>
    </filter>

    # 输出到Elasticsearch
    <match cann.**>
      @type elasticsearch
      host elasticsearch
      port 9200
      logstash_format true
      logstash_prefix cann-app
    </match>

七、持续集成/持续部署

7.1 CI/CD流水线

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

variables:
  IMAGE_NAME: cann-app
  REGISTRY: registry.example.com

build:
  stage: build
  script:
    - docker build -t ${REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} .
    - docker push ${REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}
  tags:
    - docker

test:
  stage: test
  script:
    - docker run --rm --device=/dev/davinci0 ${REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} pytest tests/
  tags:
    - npu

deploy:dev:
  stage: deploy
  script:
    - helm upgrade --install cann-app ./helm-chart --set image.tag=${CI_COMMIT_SHA} --namespace dev
  environment:
    name: development
  only:
    - develop

deploy:prod:
  stage: deploy
  script:
    - helm upgrade --install cann-app ./helm-chart --set image.tag=${CI_COMMIT_SHA} --namespace production
  environment:
    name: production
  when: manual
  only:
    - main

7.2 自动化测试

# tests/test_cann_app.py
import pytest
import requests
import numpy as np

class TestCANNApp:
    """CANN应用测试"""

    @pytest.fixture
    def client(self):
        """测试客户端"""
        base_url = "http://cann-app:8080"
        return base_url

    def test_health_check(self, client):
        """健康检查"""
        response = requests.get(f"{client}/health")
        assert response.status_code == 200
        assert response.json()["status"] == "healthy"

    def test_inference(self, client):
        """推理测试"""
        # 准备测试数据
        test_data = {
            "image": np.random.rand(224, 224, 3).tolist()
        }

        # 发送推理请求
        response = requests.post(
            f"{client}/predict",
            json=test_data
        )

        assert response.status_code == 200
        result = response.json()
        assert "prediction" in result
        assert "confidence" in result

    def test_performance(self, client):
        """性能测试"""
        import time

        # 准备测试数据
        test_data = {
            "image": np.random.rand(224, 224, 3).tolist()
        }

        # 测量推理时间
        start_time = time.time()
        response = requests.post(
            f"{client}/predict",
            json=test_data
        )
        latency = time.time() - start_time

        assert response.status_code == 200
        assert latency < 0.1  # 延迟小于100ms

八、最佳实践

8.1 镜像优化

# 多阶段构建优化
FROM python:3.8-slim as builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM cann-base:latest

COPY --from=builder /root/.local /root/.local
COPY . /app

ENV PATH=/root/.local/bin:$PATH

WORKDIR /app
CMD ["python", "app.py"]

8.2 安全配置

# security-context.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cann-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: cann-app
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
        - ALL
      readOnlyRootFilesystem: true

8.3 资源配额

# resource-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: cann-app-quota
  namespace: default
spec:
  hard:
    requests.ascend.com/npu: "16"
    requests.cpu: "32"
    requests.memory: 64Gi
    limits.ascend.com/npu: "16"
    limits.cpu: "64"
    limits.memory: 128Gi
    persistentvolumeclaims: "10"

九、总结

CANN容器化部署结合云原生技术,为AI应用提供了灵活、可扩展的部署方案。通过合理的容器编排和资源配置,可以构建高效的AI推理服务。

关键点:

  • NPU设备直通配置
  • Kubernetes资源管理
  • 监控日志体系
  • CI/CD自动化流程

参考资料

更多推荐