三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从零部署AI模型服务:Flask+ONNX Runtime实战指南

从零部署AI模型服务:Flask+ONNX Runtime实战指南

在实际工程实践中,AI模型从训练、评估到最终部署上线,是一个环环相扣的系统性工程。很多开发者,尤其是初学者,常常在本地环境跑通模型后,面对生产部署时却感到无从下手,不知道如何将模型封装成服务、如何管理版本、如何保证性能和稳定性。本文将围绕一个典型的AI模型部署场景,以Python Flask框架和ONNX Runtime推理引擎为例,带你从零开始,将一个训练好的图像分类模型部署为可对外提供HTTP API的Web服务。我们将重点关注环境隔离、服务封装、性能优化和基础监控,确保你不仅能跑通Demo,更能理解生产级AI服务部署的核心要素。

1. 理解AI模型部署的核心链路与挑战

将AI模型投入实际使用,远不止运行一个Python脚本那么简单。一个完整的部署链路通常包含模型准备、服务封装、资源管理和运维监控四个阶段。

1.1 从训练到推理:模型格式的转换

在训练阶段,我们使用PyTorch、TensorFlow等框架,其保存的模型文件(如.pth.h5)包含了完整的训练计算图、参数和优化器状态,适合继续训练或微调。但在生产推理环境中,我们追求的是高效率、低延迟和跨平台兼容性。因此,通常需要将训练框架的模型转换为专用的推理格式。

  • ONNX(Open Neural Network Exchange):一种开放的模型表示格式,旨在让不同框架训练的模型能在各种硬件和推理引擎上运行。它剥离了训练特有的操作,只保留前向推理的计算图,是模型部署中常用的中间格式。
  • TensorRT、OpenVINO等:针对NVIDIA GPU或Intel CPU等特定硬件进一步优化的推理引擎,能对计算图进行层融合、精度校准等深度优化,获得极致的推理速度。

在本实践中,我们选择ONNX作为中间格式,并使用ONNX Runtime作为推理引擎,它在CPU和GPU上都有良好的支持,且易于集成。

1.2 服务化封装:从脚本到API

模型本身只是一个计算函数。要让外部系统(如Web应用、移动App)调用,必须将其封装成服务。最常见的方式是提供HTTP RESTful API

  • Web框架选择:轻量级的如Flask、FastAPI,重量级的如Django。对于单纯的模型推理服务,FastAPI(异步)和Flask(同步)是更常见的选择,它们简洁、高效。
  • API设计:通常至少需要两个端点。一个用于健康检查(如GET /health),供运维系统探活;另一个用于模型推理(如POST /predict),接收输入数据并返回预测结果。

1.3 生产环境考量:超越“能跑通”

在本地开发环境能运行,只是第一步。生产环境要求服务具备:

  • 稳定性:7x24小时不间断运行,处理并发请求。
  • 性能:低延迟、高吞吐,合理利用硬件资源(CPU/GPU)。
  • 可观测性:有日志、有监控,能快速定位问题。
  • 可维护性:配置与代码分离,易于更新和回滚。

2. 环境准备与项目结构

我们假设你已经在本地使用PyTorch训练好了一个简单的图像分类模型(例如ResNet18,用于识别猫狗)。现在,我们要为其打造一个部署环境。

2.1 创建隔离的Python环境

使用Conda或venv创建独立环境,避免包冲突。

# 使用 conda conda create -n model_serving python=3.9 conda activate model_serving # 或使用 venv python -m venv venv_model_serving # Linux/Mac source venv_model_serving/bin/activate # Windows venv_model_serving\Scripts\activate

2.2 初始化项目目录

一个清晰的项目结构是良好工程实践的起点。

model_serving_project/ ├── app/ │ ├── __init__.py │ ├── main.py # Flask应用主入口 │ ├── predictor.py # 模型加载与推理类 │ └── utils.py # 图像预处理等工具函数 ├── models/ │ ├── resnet18.onnx # 转换后的ONNX模型文件 │ └── labels.txt # 类别标签文件 ├── requirements.txt # 项目依赖 ├── config.yaml # 配置文件 ├── Dockerfile # Docker镜像构建文件 └── README.md

2.3 安装核心依赖

创建requirements.txt文件,内容如下:

# Web框架 flask>=2.0.0 # 推理引擎 onnxruntime>=1.10.0 # 图像处理 Pillow>=9.0.0 opencv-python-headless>=4.5.0 # 配置文件解析 pyyaml>=6.0 # 进程管理(用于生产环境,可选) gunicorn>=20.0.0

使用pip安装:

pip install -r requirements.txt

3. 模型转换与核心推理类实现

3.1 将PyTorch模型转换为ONNX格式

首先,你需要一个脚本将训练好的PyTorch模型(.pth)转换为ONNX格式。假设你的模型定义在model.py中。

# export_to_onnx.py import torch import torchvision.models as models from model import YourModelClass # 导入你的自定义模型类 # 1. 加载训练好的模型权重 model = YourModelClass(num_classes=2) # 例如2分类 model.load_state_dict(torch.load('best_model.pth', map_location='cpu')) model.eval() # 切换到评估模式 # 2. 创建示例输入张量(维度需与模型训练时一致) # 假设输入是3通道,224x224的图片 dummy_input = torch.randn(1, 3, 224, 224) # 3. 导出为ONNX onnx_model_path = './models/resnet18.onnx' torch.onnx.export( model, dummy_input, onnx_model_path, export_params=True, # 存储训练好的参数 opset_version=12, # ONNX算子集版本 do_constant_folding=True, # 优化常量折叠 input_names=['input'], # 输入名 output_names=['output'], # 输出名 dynamic_axes={'input': {0: 'batch_size'}, # 支持动态batch 'output': {0: 'batch_size'}} ) print(f'Model exported to {onnx_model_path}')

运行此脚本,得到resnet18.onnx文件。

3.2 实现模型预测器(Predictor)

app/predictor.py中,我们创建一个类来封装ONNX Runtime的加载和推理逻辑。

# app/predictor.py import onnxruntime as ort import numpy as np from PIL import Image import cv2 class ModelPredictor: def __init__(self, model_path, label_path): """ 初始化预测器 Args: model_path: ONNX模型文件路径 label_path: 类别标签文件路径 """ # 创建ONNX Runtime会话 # 对于GPU推理,可以指定 providers=['CUDAExecutionProvider'] self.session = ort.InferenceSession(model_path, providers=['CPUExecutionProvider']) self.input_name = self.session.get_inputs()[0].name self.output_name = self.session.get_outputs()[0].name # 加载标签 with open(label_path, 'r', encoding='utf-8') as f: self.labels = [line.strip() for line in f.readlines()] # 从模型获取输入尺寸 (假设是 [batch, channel, height, width]) self.input_shape = self.session.get_inputs()[0].shape self.input_height = self.input_shape[2] self.input_width = self.input_shape[3] print(f"Model loaded. Input shape: {self.input_shape}, Labels: {self.labels}") def preprocess(self, image): """ 图像预处理:调整大小、归一化、转换维度 Args: image: PIL.Image对象 Returns: numpy.ndarray: 预处理后的张量 """ # 调整大小 img = image.resize((self.input_width, self.input_height)) # 转换为numpy数组并归一化到[0,1] img_array = np.array(img).astype(np.float32) / 255.0 # 如果模型需要BGR输入(如OpenCV训练),则转换通道 # img_array = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR) # 归一化到ImageNet的均值和标准差(根据你的模型训练方式调整) mean = np.array([0.485, 0.456, 0.406]).reshape(1, 1, 3) std = np.array([0.229, 0.224, 0.225]).reshape(1, 1, 3) img_array = (img_array - mean) / std # 转换维度顺序为 NCHW (Batch, Channel, Height, Width) img_array = np.transpose(img_array, (2, 0, 1)) # 添加batch维度 img_array = np.expand_dims(img_array, axis=0) return img_array def predict(self, image): """ 执行模型推理 Args: image: PIL.Image对象 Returns: dict: 包含预测结果,如类别、置信度等 """ # 预处理 input_tensor = self.preprocess(image) # 运行推理 outputs = self.session.run([self.output_name], {self.input_name: input_tensor}) predictions = outputs[0][0] # 取第一个batch的结果 # 应用softmax获取概率 probabilities = np.exp(predictions) / np.sum(np.exp(predictions)) # 获取最高概率的索引和值 top_index = np.argmax(probabilities) top_label = self.labels[top_index] top_prob = float(probabilities[top_index]) return { 'class_id': int(top_index), 'class_name': top_label, 'confidence': top_prob, 'all_probabilities': probabilities.tolist() # 可选,返回所有类别概率 }

4. 构建Flask Web服务与API

4.1 创建Flask应用主文件

app/main.py中,我们创建Flask应用,并定义API端点。

# app/main.py from flask import Flask, request, jsonify from .predictor import ModelPredictor import yaml import logging from PIL import Image import io # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = Flask(__name__) # 从配置文件加载路径 with open('config.yaml', 'r') as f: config = yaml.safe_load(f) MODEL_PATH = config['model']['path'] LABEL_PATH = config['model']['label_path'] # 全局预测器实例(注意:在生产部署中,需考虑多进程/多线程安全) predictor = None def load_predictor(): """加载模型预测器(懒加载或应用启动时加载)""" global predictor if predictor is None: logger.info(f"Loading model from {MODEL_PATH}") predictor = ModelPredictor(MODEL_PATH, LABEL_PATH) logger.info("Model loaded successfully.") return predictor @app.route('/health', methods=['GET']) def health_check(): """健康检查端点""" return jsonify({'status': 'healthy', 'service': 'model-serving-api'}), 200 @app.route('/predict', methods=['POST']) def predict(): """模型预测端点""" # 检查请求中是否包含文件 if 'file' not in request.files: return jsonify({'error': 'No file part in the request'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No selected file'}), 400 try: # 读取图像文件 image_bytes = file.read() image = Image.open(io.BytesIO(image_bytes)).convert('RGB') # 获取预测器并推理 model_predictor = load_predictor() result = model_predictor.predict(image) logger.info(f"Prediction result: {result}") return jsonify(result), 200 except Exception as e: logger.error(f"Prediction error: {str(e)}", exc_info=True) return jsonify({'error': 'Internal server error during prediction'}), 500 if __name__ == '__main__': # 开发环境直接运行 app.run(host='0.0.0.0', port=5000, debug=True) else: # 被Gunicorn等WSGI服务器导入时,预加载模型 load_predictor()

4.2 编写配置文件

创建config.yaml,将配置与代码分离。

# config.yaml server: host: "0.0.0.0" port: 5000 debug: false # 生产环境务必设为false model: path: "./models/resnet18.onnx" label_path: "./models/labels.txt" logging: level: "INFO" file: "./logs/app.log"

4.3 创建标签文件

models/labels.txt中按行写入类别名称,顺序与模型输出层一致。

cat dog

5. 运行、测试与验证

5.1 启动开发服务器

在项目根目录下运行:

export FLASK_APP=app.main export FLASK_ENV=development flask run --host=0.0.0.0 --port=5000

或直接运行:

python -m app.main

看到输出提示服务在http://0.0.0.0:5000启动。

5.2 测试API接口

使用curl或Python的requests库进行测试。

测试健康检查:

curl http://localhost:5000/health

预期返回:{"status":"healthy","service":"model-serving-api"}

测试预测接口:准备一张猫或狗的图片test.jpg

curl -X POST -F "file=@./test.jpg" http://localhost:5000/predict

预期返回一个JSON对象,包含预测的类别、置信度等信息,例如:

{ "class_id": 0, "class_name": "cat", "confidence": 0.987, "all_probabilities": [0.987, 0.013] }

5.3 使用Python脚本进行更全面的测试

创建一个测试脚本test_api.py

import requests import json url = 'http://localhost:5000/predict' image_path = './test.jpg' with open(image_path, 'rb') as img: files = {'file': img} response = requests.post(url, files=files) if response.status_code == 200: print("Prediction successful:") print(json.dumps(response.json(), indent=2)) else: print(f"Request failed with status {response.status_code}: {response.text}")

6. 生产环境部署与优化

在开发环境跑通后,我们需要为生产环境做准备。

6.1 使用Gunicorn作为WSGI服务器

Flask自带的开发服务器性能弱,不支持并发,仅用于开发。生产环境应使用WSGI服务器,如Gunicorn(Linux)或Waitress(Windows)。

安装Gunicorn后,使用以下命令启动:

gunicorn -w 4 -b 0.0.0.0:5000 'app.main:app'
  • -w 4:启动4个工作进程(通常建议为CPU核心数的1-2倍)。
  • -b:绑定地址和端口。
  • 'app.main:app':指定Flask应用实例。

6.2 编写Dockerfile进行容器化部署

容器化能保证环境一致性,是生产部署的标准做法。

# Dockerfile # 使用官方Python轻量级镜像 FROM python:3.9-slim # 设置工作目录 WORKDIR /app # 复制依赖文件并安装 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码和模型文件 COPY app ./app COPY models ./models COPY config.yaml . # 创建日志目录 RUN mkdir -p logs # 暴露端口 EXPOSE 5000 # 设置环境变量,关闭Flask调试模式 ENV FLASK_APP=app.main ENV FLASK_ENV=production # 使用Gunicorn启动应用 CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--access-logfile", "./logs/access.log", "--error-logfile", "./logs/error.log", "app.main:app"]

构建并运行Docker镜像:

# 构建镜像 docker build -t model-serving-api . # 运行容器 docker run -d -p 5000:5000 --name my-model-server model-serving-api

6.3 性能优化与监控要点

  1. 模型预热:在服务启动后,先使用一张示例图片进行一次推理,触发运行时优化,避免第一个请求延迟过高。可以在load_predictor函数末尾添加预热逻辑。
  2. 批处理支持:如果请求量大,可以考虑在/predict端点支持批量图片上传,并在predictor.py中实现批处理推理,能显著提升GPU利用率。
  3. 异步处理:对于推理耗时较长的模型,可以考虑使用异步框架(如FastAPI +async/await)或消息队列(如Celery + Redis),将推理任务放入后台,通过轮询或WebSocket返回结果。
  4. 监控与日志
    • 应用日志:使用Python的logging模块,将不同级别的日志输出到文件,并接入ELK等日志系统。
    • 性能监控:记录每个预测请求的耗时、成功率。可以集成Prometheus客户端(如prometheus-flask-exporter)暴露指标。
    • 健康检查:除了/health,可以增加/metrics端点暴露监控指标。
  5. 配置管理:将config.yaml中的敏感信息(如模型路径)通过环境变量注入,或在Kubernetes中使用ConfigMap。

7. 常见问题排查清单

在部署和运行过程中,你可能会遇到以下问题。这里提供排查思路。

问题现象可能原因检查方式与解决方案
服务启动失败,提示ImportError依赖未安装或环境错误1. 确认已激活正确的虚拟环境。
2. 运行pip install -r requirements.txt确保所有依赖已安装。
3. 检查Python版本是否匹配。
访问/predict返回400错误请求格式不正确1. 检查请求是否为POST方法。
2. 检查表单中文件字段名是否为file
3. 使用curl -v或Postman查看完整的请求头和数据。
访问/predict返回500内部错误服务器端处理异常1. 查看Flask或Gunicorn的错误日志。
2. 检查模型文件路径config.yaml中配置是否正确。
3. 检查labels.txt文件是否存在,且行数与模型输出类别数一致。
4. 检查输入图片格式,确保能被PIL正常打开。
推理速度非常慢1. 使用了CPU进行推理。
2. 图片预处理耗时过长。
3. 模型本身较大。
1. 确认ONNX Runtime使用了正确的Execution Provider(如GPU)。
2. 优化预处理代码,避免在循环中重复初始化。
3. 考虑对模型进行量化、剪枝等优化,或使用TensorRT等专用推理引擎。
并发请求下内存持续增长内存泄漏1. 检查预测器ModelPredictor是否被重复创建。
2. 确保图像处理对象(如PIL Image, numpy array)在使用后被正确释放。
3. 使用tracemalloc等工具定位内存分配热点。
Docker容器启动后立即退出Dockerfile中CMD命令错误或应用启动失败1. 使用docker logs <container_id>查看容器日志。
2. 检查Dockerfile中CMD命令格式是否正确。
3. 尝试在Dockerfile的CMD前加一个sleep命令,或使用docker run -it交互模式启动,手动检查错误。

8. 最佳实践与扩展方向

8.1 模型服务化最佳实践

  1. 版本化:模型文件应带有版本号(如model_v1.onnx),API端点也应支持版本(如/api/v1/predict)。这样可以在不中断服务的情况下部署新模型。
  2. 输入验证:在API端点中对输入数据进行严格验证,如图片尺寸、格式、文件大小限制,防止恶意请求。
  3. 限流与熔断:使用Nginx、API网关或应用级中间件(如Flask-Limiter)对API进行限流,防止服务被压垮。实现简单的熔断机制,在连续失败后暂时拒绝请求。
  4. 优雅退出:在收到终止信号(如SIGTERM)时,让服务完成正在处理的请求后再关闭,避免数据丢失。
  5. 配置外部化:所有配置(模型路径、服务器端口、日志级别)都应通过环境变量或外部配置中心管理,而不是硬编码在代码中。

8.2 后续扩展方向

  • 模型管理平台:当模型数量增多时,需要平台来管理模型的注册、存储、版本和部署。可以了解MLflow、Kubeflow等工具。
  • A/B测试与流量切分:部署多个模型版本,并通过API网关将一定比例的流量导向新版本,以评估模型效果。
  • 自动化CI/CD流水线:将模型训练、验证、转换、部署和测试流程自动化。当新模型训练达标后,自动触发部署流程。
  • 服务网格与Kubernetes:在K8s中部署模型服务,利用其服务发现、负载均衡、自动扩缩容和滚动更新能力,构建高可用的推理服务集群。
  • 边缘部署:对于延迟敏感或数据隐私要求高的场景,考虑使用TensorFlow Lite、PyTorch Mobile或ONNX Runtime移动端版本,将模型部署到手机或IoT设备上。

通过以上步骤,你不仅完成了一个AI模型服务从本地到生产环境的部署,更重要的是理解了其中每个环节的设计考量与潜在风险。实际项目中,还需要根据具体的业务需求、流量规模和运维体系,对上述方案进行裁剪和增强。建议先从单机容器化部署开始,逐步引入更复杂的架构组件。

← 返回列表