3. 容器化基础与医学影像AI推理服务封装
医学影像AI推理服务要在医院内网、边缘服务器或云主机上稳定运行,最大的障碍往往不是模型精度,而是运行环境。同一份模型权重,在开发机上用 PyTorch 2.1 + CUDA 12.1 能跑通,换到另一台装有旧驱动的 PACS 服务器上就可能报 CUDA driver version is insufficient for CUDA runtime version。容器化的价值,正是把“模型 + 依赖 + 运行时”打包成一个可复现的镜像,让推理服务在不同机器上表现一致。
一、镜像分层:把变化频率不同的内容分开
Docker 镜像由只读层叠加而成,每一层对应 Dockerfile 中的一条指令。构建医学影像推理镜像时,应按“变化频率从低到高”排列层,才能最大化缓存命中率。典型顺序是:基础镜像(CUDA runtime)→ 系统依赖(libgl1、libgomp1 等)→ Python 依赖(torch、monai、nibabel)→ 应用代码 → 模型权重。模型权重通常几百 MB 到数 GB,且每次更新模型都会变,放在最上层可避免重装依赖。
基础镜像选择上,若推理只用 GPU 且不需要编译 CUDA 算子,用 nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04 即可,它比 devel 版小很多;若需在镜像内编译自定义算子,才用 devel 版并在多阶段构建中把产物复制到 runtime 阶段。下面是一个面向 CT 肺结节分割推理服务的 Dockerfile 骨架:
FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
RUN apt-get update && apt-get install -y --no-install-recommends \
python3.10 python3-pip libgl1 libglib2.0-0 libgomp1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app/ /app/app/
COPY weights/lung_nodule_v3.pt /app/weights/
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
二、CUDA 与驱动的兼容关系
这是医学影像 AI 部署中最容易踩坑的地方。需要区分三个版本:宿主机 NVIDIA 驱动版本、镜像内 CUDA Runtime 版本、以及 PyTorch 编译时使用的 CUDA 版本。关键规则是:驱动向下兼容 CUDA Runtime。即宿主机驱动版本必须大于等于镜像内 CUDA Runtime 所要求的最低驱动版本,反之则不行。
例如 CUDA 12.1 要求 Linux 驱动 ≥ 530.30.02;若医院服务器驱动是 525.x,则只能使用 CUDA 11.8 或更低的镜像。此时若强行运行 CUDA 12.1 镜像,容器启动后 nvidia-smi 可能仍能显示 GPU,但 PyTorch 调用 torch.cuda.is_available() 会返回 False,或首次推理时报驱动版本不足。排查时应先在宿主机执行 nvidia-smi 查看 Driver Version,再对照 NVIDIA 官方兼容表选择镜像标签。
另一个常见问题是容器内缺少 NVIDIA Container Toolkit 的运行时注入。若 docker run 时未加 --gpus all,容器内看不到 GPU 设备。生产环境建议在 docker-compose.yml 或 K8s 的 resources.limits 中显式声明 GPU 资源,避免调度到无 GPU 节点。
三、模型权重与依赖打包策略
模型权重是否打进镜像,取决于交付方式。若镜像通过私有 Registry 分发,且权重文件在 2 GB 以内,直接 COPY 进镜像最省事,启动时无需额外下载。若权重超过 5 GB,或需要频繁更新模型而不想重建镜像,则应把权重放在宿主机卷或对象存储中,容器启动时挂载或下载。此时需在启动脚本中校验权重文件的 SHA256,防止加载到损坏或不匹配的权重。
依赖打包要特别注意两点:一是 requirements.txt 中应锁定精确版本,如 torch==2.1.2+cu121,避免 pip install 时解析到不兼容的新版本;二是医学影像常用的 SimpleITK、pydicom、nibabel 等库对系统库有依赖,需在 apt 层提前安装。建议在 CI 中执行一次 docker build 后运行 python -c "import torch; print(torch.cuda.is_available())" 作为冒烟测试,确保镜像内 GPU 可用。
四、推理服务 API 设计
医学影像推理服务的 API 与普通图像分类不同:输入通常是 DICOM 序列或 NIfTI 体积,单次请求可能包含数百张切片,响应时间从数秒到数十秒不等。因此 API 设计要兼顾同步与异步两种模式。
同步接口适合小体积、低延迟场景,例如单张 X 光片分类。请求体用 multipart/form-data 上传文件,响应返回 JSON 格式的标签与置信度。异步接口适合 CT/MR 体积分割:客户端先 POST 创建任务,服务端返回 task_id,客户端再轮询 GET /tasks/{task_id} 获取状态与结果下载链接。这样可避免长连接超时,也便于服务端做队列限流。
无论同步还是异步,响应中都应包含 model_version、inference_time_ms 和 preprocessing 字段,便于临床回溯时确认是哪个模型、用了什么窗宽窗位。对于分割任务,结果建议以 NIfTI 或 DICOM-SEG 格式返回,并附带与原始影像对齐的仿射矩阵信息。
HEALTHCHECK,定期调用 /health 接口,该接口应同时检查 GPU 可用性与模型是否已加载。K8s 的 livenessProbe 可据此判断容器是否需要重启,避免模型加载失败后容器仍处于 Running 状态却无法提供推理。
最后,镜像构建完成后应记录镜像的 digest(如 sha256:abc123...),而非仅记录 tag。tag 可被覆盖,digest 不可变,在交付验收和问题追溯时,digest 才是唯一可靠的镜像标识。