TorchServe 推理服务
本文介绍如何把 PyTorch 模型通过 TorchServe 部署为在线推理服务。在推理服务列表中新建服务时,service_type 选择 torch-server,平台会自动完成模型打包、配置文件生成和启动命令编排。下文标注"(系统完成)"的步骤通常由平台自动处理,用户只需准备好模型文件。
事实核对依据:服务类型、镜像、配置、端口、命令等来自
myapp/views/view_inferenceserving.py与myapp/config.py(INFERENCE_IMAGES段)。
镜像
平台内置可选的 TorchServe 镜像(myapp/config.py:1327 torch-server):
ccr.ccs.tencentyun.com/cube-studio/torchserve:0.9.0-gpu
ccr.ccs.tencentyun.com/cube-studio/torchserve:0.9.0-cpu
ccr.ccs.tencentyun.com/cube-studio/torchserve:0.8.2-gpu
ccr.ccs.tencentyun.com/cube-studio/torchserve:0.8.2-cpu
ccr.ccs.tencentyun.com/cube-studio/torchserve:0.7.1-gpu
ccr.ccs.tencentyun.com/cube-studio/torchserve:0.7.1-cpu
CPU 镜像用于无 GPU 场景,GPU 镜像需配合资源里的 resource_gpu。各版本对 Torch / CUDA 的兼容关系以 TorchServe 官方发布说明为准:https://github.com/pytorch/serve/releases
模型导出
一定要导出完整的模型,包括模型结构和模型参数。 只保存 state_dict(权重)而不保存结构,TorchServe 无法直接加载。
# 仅保存/加载权重(不推荐单独用于部署)
# torch.save(model.state_dict(), PATH)
# model.load_state_dict(torch.load(PATH))
# 保存完整模型
torch.save(model, PATH)
model = torch.load(PATH)
model.eval() # 转为推理模式
example_input = torch.rand(1, 3, 224, 224)
traced_model = torch.jit.trace(model, example_input) # TorchScript 序列化优化
traced_model.save('./model.pt') # 保存完整模型
pt 模型打包(系统完成)
平台在部署时会自动调用 torch-model-archiver 把 .pt 等模型文件打包成 .mar(view_inferenceserving.py:695):
pip3 install torch-model-archiver
torch-model-archiver \
--model-name $model_name \
--version $model_version \
--handler $handler \
--serialized-file $model_path \
--export-path /models -f
其中 --handler 在平台中取自服务的 transformer 字段,未填写时回退到 model_type(view_inferenceserving.py:695 的 item.transformer or item.model_type)。TorchServe 内置 handler 包括 image_classifier、image_segmenter、object_detector、text_classifier 等;其他类型见 https://pytorch.org/serve/default_handlers.html 。自定义 handler 见文末「自定义 handler」。
如果模型文件本身已经是 .mar,平台不再重复打包,直接拷贝到 /models/ 后启动。在「模型地址」(model_path)中填写的规则:torch-model-archiver 编译后的 .mar 文件,或 TorchScript 保存的模型文件(view_inferenceserving.py:228)。
配置文件 config.properties(系统完成)
平台自动生成 config.properties(view_inferenceserving.py:543 torch_config()),内容如下:
inference_address=http://0.0.0.0:8080
management_address=http://0.0.0.0:8081
metrics_address=http://0.0.0.0:8082
cors_allowed_origin=*
cors_allowed_methods=GET, POST, PUT, OPTIONS
cors_allowed_headers=X-Custom-Header
number_of_netty_threads=32
enable_metrics_api=true
job_queue_size=1000
enable_envvars_config=true
async_logging=true
default_response_timeout=120
max_request_size=6553500
说明:
inference_address/management_address/metrics_address分别对应推理、管理、监控三个端口(8080 / 8081 / 8082)。- 这些属性也可通过环境变量
TS_<PROPERTY_NAME>注入(view_inferenceserving.py:542注释)。 - 与早期版本相比,当前配置启用了
enable_envvars_config=true、async_logging=true,并新增default_response_timeout=120、max_request_size=6553500。
注意:早期文档中的
log4j.properties日志配置文件与启动命令里的---log-config参数,在当前平台自动生成的配置与启动命令中已不存在(view_inferenceserving.py:704的启动命令只挂载--ts-config=/config/config.properties,未含--log-config)。如需自定义日志,请参考 TorchServe 原生 logging 配置自行加入。
部署服务(系统完成)
平台自动编排的启动命令大致为(view_inferenceserving.py:704):
# 工作目录:/models(系统自动设置)
cp /config/* /models/ \
&& <torch-model-archiver 打包,或 cp 已有 .mar> \
&& torchserve --start --model-store /models \
--models $model_name=$model_name.mar \
--foreground \
--ts-config=/config/config.properties
无需手动填写启动命令;如需自定义,可在服务的「启动命令」字段覆盖默认值。
API 访问
端口(view_inferenceserving.py:106-127):
- 推理:
8080 - 管理:
8081(暴露端口为8080,8081) - 监控:
8082(指标8082:/metrics) - 健康检查:
8080:/ping
推理接口(8080 端口):
POST /v1/models/{model_name}:predict 推荐接口,兼容 KServe(kfserving)
POST /predictions/{model_name} TorchServe 原生接口
POST /predictions/{model_name}/{version}
管理接口(8081):GET /models(平台的访问域名 host 默认指向 :8081/models,view_inferenceserving.py:69)。
监控接口(8082):/metrics。
平台在服务列表的「推理测试」会打开 Notebook 示例 pipeline/example/inference/torch-client.ipynb(view_inferenceserving.py:2202)。
image_classifier 调用示例:
# pytorch
SERVER_URL = 'http://<推理服务域名>/predictions/resnet50'
IMAGE_PATH = 'smallcat.jpg'
files = {'data': open(IMAGE_PATH, 'rb')}
response = requests.post(SERVER_URL, files=files)
print(response.json())
print(response.status_code)
response.raise_for_status()
# curl http://<推理服务域名>/predictions/resnet50 -T smallcat.jpg
# curl http://<推理服务域名>/predictions/resnet50 -F "data=@smallcat.jpg"
其他类型接口参考 https://pytorch.org/serve/default_handlers.html 。
自定义 handler
示例 MNISTDigitClassifier:根据用户输入的图片地址先下载图片再推理,并回传请求中的 image_id。
生成 handler.py:
from torchvision import transforms
from ts.torch_handler.image_classifier import ImageClassifier
import base64, time, json
import torch
from PIL import Image
import io
import requests
import pysnooper
class MNISTDigitClassifier(ImageClassifier):
image_processing = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
# @pysnooper.snoop()
def handle(self, data, context):
# 输入输出为 list
start_time = time.time()
self.context = context
metrics = self.context.metrics
data_preprocess = self.preprocess(data)
if not self._is_explain():
output = self.inference(data_preprocess)
output = self.postprocess(output)
else:
output = self.explain_handle(data_preprocess, data)
stop_time = time.time()
metrics.add_time('HandlerTime', round(
(stop_time - start_time) * 1000, 2), None, 'ms')
back = []
for index in range(len(output)):
back.append({
"predict": output[index],
"image_id": data[index]['body']['image_id']
})
return back
# @pysnooper.snoop()
def preprocess(self, data):
des_images = []
for row in data:
image_url = row.get("body")['image_url']
image = requests.get(image_url).content
if isinstance(image, (bytearray, bytes)):
image = Image.open(io.BytesIO(image))
image = self.image_processing(image)
else:
# 如果传入的是 list 形式的图片数据
image = torch.FloatTensor(image)
des_images.append(image)
return torch.stack(des_images).to(self.device)
# @pysnooper.snoop()
def postprocess(self, data):
return data.argmax(1).tolist()
将 handler.py 一起打入 .mar(系统可自动完成),再把生成的 .mar 文件路径填入界面:
torch-model-archiver --model-name xx --version 1.0 --handler handler.py --serialized-file xx.pt --export-path ./ -f
client.py 调用:
data = {
"image_url": "http://xx.xx.xx/xx",
"image_id": "11111"
}
url = 'http://127.0.0.1:8080/v1/models/mnist:predict'
res = requests.post(url, json=data)
print(res.status_code)
result = res.content.decode()
print(result)