赞
踩
Docker torchserve 部署模型流程——以WSL部署YOLO-FaceV2为例
WSL官方教程
1,https://learn.microsoft.com/zh-cn/windows/wsl/?source=recommendations
2,https://learn.microsoft.com/zh-cn/windows/wsl/setup/environment?source=recommendations
docker安装,这里注意选择合适的docker版本
https://docs.docker.com/
地址https://hub.docker.com/r/pytorch/torchserve/tags
docker pull pytorch/torchserve:0.7.0-gpu
左侧为docker镜像,右侧为拉取命令
docker镜像对应的dockerfile:
https://hub.docker.com/layers/pytorch/torchserve/0.7.0-gpu/images/sha256a8a5fb048b20fb71fed43d47caf370e5f4e15f27c219234734d8bb7d7870c158?context=explore
YOLO-FaceV2 Windows本地路径
YOLO-FaceV2 WSL路径
# pytorch/torchserve:0.7.0-gpu docker启动指令
docker run --rm -it --gpus all -p 8080:8080 -p 8081:8081 -v /mnt/c/data/CrowdCounting/serve/YOLO-FaceV2-master:/home/model-server/extra-files -v /mnt/c/data/CrowdCounting/serve/YOLO-FaceV2-master/model-store:/home/model-server/model-store pytorch/torchserve:0.7.0-gpu
# docker路径映射指令,根据自己的需要增加映射指令
-v /mnt/c/data/CrowdCounting/serve/YOLO-FaceV2-master:/home/model-server/extra-files
docker启动之后,查看镜像、容器以及进入容器需要打开新的terminal
# docker启动之后,查看镜像、容器以及进入容器需要打开新的terminal
# 查看所有镜像
docker images
# 查看已启动的镜像容器,一个镜像可以启动多个容器
docker ps
# 把cc4313027126修改为自己的容器ID
docker exec -it cc4313027126 /bin/bash
torch官网地址https://pytorch.org/get-started/previous-versions/
# 从官网安装torch
pip install torch==1.10.1+cu111 torchvision==0.11.2+cu111 torchaudio==0.10.1 -f https://download.pytorch.org/whl/cu111/torch_stable.html
# 从清华源安装其他依赖
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 50e43f173778为容器ID,yoloface:cu111为新镜像名字
docker commit 50e43f173778 yoloface:cu111
第一次配置docker,
特别是需要修改docker配置、需要根据requirements.txt文件安装python包的,
建议严格按照1~6的顺序,依次完成步骤,中间不要跳步!!!
否则可能无法成功启动docker,届时需要删掉已配置的docker容器,从头再来!!!
完整配置并保存docker镜像后,
不再需要修改docker配置、不再安装python包的,
按照2~4的步骤顺序,重新启动、进入docker容器,不再执行步骤1、5、6
注意,此时重新启动的docker应该为刚刚保存的yoloface:cu111,不再是最开始的pytorch/torchserve:0.7.0-gpu,因此将步骤2的启动指令更新为下面的指令
# yoloface:cu111 docker启动指令
docker run --rm -it --gpus all -p 8080:8080 -p 8081:8081 -v /mnt/c/data/CrowdCounting/serve/YOLO-FaceV2-master:/home/model-server/extra-files -v /mnt/c/data/CrowdCounting/serve/YOLO-FaceV2-master/model-store:/home/model-server/model-store yoloface:cu111
# 生成.mar文件指令示例,参数根据自己的情况重新设置
# 注意:正确设置路径,否则会产生一系列错误!!!!!
# 不要问我是怎么知道的,mar不相信眼泪
torch-model-archiver --model-name yolofacev2 --version 1.0 --model-file experimental.py --serialized-file best.pt --handler handler.py --extra-files "models.zip, utils.zip"
# 注意--extra-files "models.zip, utils.zip",引号中zip文件间的空格,如果有空格且出现“找不到zip文件”的错误,那就去掉空格
注意指令参数与文件相对路径对应
神经病啊,没有handler文件怎么生成mar?
这不就来了嘛!
接下来的修改过程:
请注意:正确设置路径,否则会产生一系列错误!!!!!
请注意:正确设置路径,否则会产生一系列错误!!!!!
请注意:正确设置路径,否则会产生一系列错误!!!!!
def initialize(self, context): properties = context.system_properties logger.info(f"Cuda available: {torch.cuda.is_available()}") logger.info(f"GPU available: {torch.cuda.device_count()}") use_cuda = torch.cuda.is_available() and torch.cuda.device_count() > 0 self.map_location = 'cuda' if use_cuda else 'cpu' self.device = torch.device(self.map_location + ':' + str(properties.get('gpu_id') ) if use_cuda else 'cpu') self.manifest = context.manifest model_dir = properties.get('model_dir') logger.info("==================model_dir===================" " %s loaded successfully", model_dir) self.model_pt_path = None if "serializedFile" in self.manifest["model"]: serialized_file = self.manifest["model"]["serializedFile"] self.model_pt_path = os.path.join(model_dir, serialized_file) model_file = self.manifest['model']['modelFile'] logger.info("Model file %s loaded successfully", self.model_pt_path)
上面的代码不要随意修改,它来自BaseHandler文件的initialize函数,它的作用主要是加载model_dir,model_file,与serialized_file,眼熟吗?看这里:
torch-model-archiver
--model-file experimental.py
--serialized-file best.pt
--model-name yolofacev2
--version 1.0
--handler handler.py
--extra-files "models.zip, utils.zip"
model_file,与serialized_file在mar文件生成的指令中出现过,它们负责加载模型文件与模型权重
model_dir是在handler文件执行过程中docker产生的临时路径,嘶~,它长这个亚子:
model_dir:/home/model-server/tmp/models/59324bc14e6c48d5821e157886545f1b
关键在model_dir里存放了“mar文件生成的指令”传入的所有文件
So,你可以自己找到临时路径,查看你想传入的文件是不是正确传入到model_dir,如果没有?你懂得!
突然感觉docker变得透明了
“mar文件生成的指令”中–extra-files可以传入压缩包,像这样:
torch-model-archiver
--extra-files "models.zip, utils.zip"
但需要在initialize导入压缩包并解压,解压后存放的位置就是model_dir,下图包含了解压后的结果
注意:压缩包里是这个样子的,zip里《必须》有完整的文件夹,解压之后才会有上面的效果!
导入压缩包并解压程序如下,放在handler文件initialize函数中
with zipfile.ZipFile(model_dir + '/models.zip', 'r') as zip_ref:
zip_ref.extractall(model_dir)
with zipfile.ZipFile(model_dir + '/utils.zip', 'r') as zip_ref:
zip_ref.extractall(model_dir)
self.load_yoloface_model()
注意看:这个load_yoloface_model函数叫小帅???重来!
在zip解压之后,才可以在load_yoloface_model函数导入之前被压缩的文件,比如模型文件、config文件等,注意导入文件的时机,否则?你懂得!
def load_yoloface_model(self):
from experimental import attempt_load
from utils.datasets import letterbox
from utils.general import check_img_size, non_max_suppression, scale_coords, xyxy2xywh
self.letterbox = letterbox
self.check_img_size = check_img_size
self.non_max_suppression = non_max_suppression
self.scale_coords = scale_coords
self.xyxy2xywh = xyxy2xywh
preprocess函数接收到的data是图片经过http转码的,需要转个圈圈,转回来,如下:
def preprocess(self, data): # Initialize # stride = int(model.stride.max()) # model stride print("debug--%d", len(data)) images = [] for row in data: image = row.get("data") or row.get("body") if isinstance(image, str): # if the image is a string of bytesarray. image = base64.b64decode(image) # If the image is sent as bytesarray elif isinstance(image, (bytearray, bytes)): image = Image.open(io.BytesIO(image)) image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR) else: # if the image is a list image = image.get('instances')[0] image = np.divide(torch.HalfTensor(image), 255) img0 = image
4,完整handler文件如下,仅供参考
什么?你觉得我代码写的烂,哎?,不知道为什么人家听不见,我觉得能跑就行!
# -*- coding: utf-8 -*- import datetime import os import cv2 import sys import zipfile import numpy as np import logging import base64 import torch import io from PIL import Image from ts.torch_handler.base_handler import BaseHandler logger = logging.getLogger(__name__) start_up_time = datetime.datetime.now() filename = ".//log_" + str(start_up_time).replace(':', '') + '.txt' logging.basicConfig(filename=filename, level=logging.INFO, format='[%(asctime)s.%(msecs)03d] %(message)s', datefmt='%H:%M:%S') logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) class FaceDetectHandler(BaseHandler): def __init__(self): super().__init__() self.imgsz = 640 self.iou_thres = 0.3 self.conf_thres = 0.1 self.xyxy2xywh = None self.scale_coords = None self.non_max_suppression = None self.check_img_size = None self.letterbox = None def load_yoloface_model(self): from experimental import attempt_load from utils.datasets import letterbox from utils.general import check_img_size, non_max_suppression, scale_coords, xyxy2xywh self.letterbox = letterbox self.check_img_size = check_img_size self.non_max_suppression = non_max_suppression self.scale_coords = scale_coords self.xyxy2xywh = xyxy2xywh with torch.no_grad(): self.model = attempt_load(self.model_pt_path, map_location=self.device) # load FP32 model def initialize(self, context): properties = context.system_properties logger.info(f"Cuda available: {torch.cuda.is_available()}") logger.info(f"GPU available: {torch.cuda.device_count()}") use_cuda = torch.cuda.is_available() and torch.cuda.device_count() > 0 self.map_location = 'cuda' if use_cuda else 'cpu' self.device = torch.device(self.map_location + ':' + str(properties.get('gpu_id') ) if use_cuda else 'cpu') self.manifest = context.manifest model_dir = properties.get('model_dir') logger.info("==================model_dir===========================" " %s loaded successfully", model_dir) self.model_pt_path = None if "serializedFile" in self.manifest["model"]: serialized_file = self.manifest["model"]["serializedFile"] self.model_pt_path = os.path.join(model_dir, serialized_file) model_file = self.manifest['model']['modelFile'] logger.info("Model file %s loaded successfully", self.model_pt_path) with zipfile.ZipFile(model_dir + '/models.zip', 'r') as zip_ref: zip_ref.extractall(model_dir) with zipfile.ZipFile(model_dir + '/utils.zip', 'r') as zip_ref: zip_ref.extractall(model_dir) self.load_yoloface_model() def dynamic_resize(self, shape, stride=64): max_size = max(shape[0], shape[1]) if max_size % stride != 0: max_size = (int(max_size / stride) + 1) * stride return max_size def preprocess(self, data): print("debug--%d", len(data)) images = [] for row in data: image = row.get("data") or row.get("body") if isinstance(image, str): # if the image is a string of bytesarray. image = base64.b64decode(image) # If the image is sent as bytesarray elif isinstance(image, (bytearray, bytes)): image = Image.open(io.BytesIO(image)) image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR) else: # if the image is a list image = image.get('instances')[0] image = np.divide(torch.HalfTensor(image), 255) img0 = image imgsz = self.imgsz if imgsz <= 0: # original size imgsz = self.dynamic_resize(image.shape) imgsz = self.check_img_size(imgsz, s=64) # check img_size # yolov5的resize,使用比例填充 # (683, 1024, 3) -> (448, 640, 3) img = self.letterbox(image, imgsz)[0] # Convert # (448, 640, 3) -> (3, 448, 640) img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416 img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(self.device) img = img.float() # uint8 to fp16/32 img /= 255.0 # 0 - 255 to 0.0 - 1.0 if img.ndimension() == 3: img = img.unsqueeze(0) images.append([img, img0]) return images def inference(self, data, *args, **kwargs): imgsz = self.imgsz model = self.model # Run inference # img(1,3,448,640)且归一化 bbox_sets = [] for img_4t, img0_3c in data: pred = model(img_4t)[0] bbox_sets.append([img_4t, img0_3c, pred]) return bbox_sets def postprocess(self, data): boxes = [[] for _ in range(len(data))] for i, bbox_sets in enumerate(data): img_4t, img0_3c, pred = bbox_sets[0], bbox_sets[1], bbox_sets[2] # Apply NMS pred = self.non_max_suppression(pred, self.conf_thres, self.iou_thres)[0] h, w, c = img0_3c.shape if pred is not None: pred[:, :4] = self.scale_coords(img_4t.shape[2:], pred[:, :4], img0_3c.shape).round() for j in range(pred.size()[0]): *xyxy, conf, cls = pred[j] xyxy = torch.Tensor(xyxy).to(self.device) # xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1) # normalized xywh xywh = (self.xyxy2xywh(torch.as_tensor(xyxy).view(1, 4)) / 1.0).view(-1) xywh = xywh.data.cpu().numpy() conf = pred[j, 4].cpu().numpy() # landmarks = (pred[j, 5:15].view(1, 10) / gn_lks).view(-1).tolist() # class_num = pred[j, 15].cpu().numpy() x1 = int(xywh[0] - 0.5 * xywh[2]) y1 = int(xywh[1] - 0.5 * xywh[3]) x2 = int(xywh[0] + 0.5 * xywh[2]) y2 = int(xywh[1] + 0.5 * xywh[3]) # boxes.append([x1, y1, x2 - x1, y2 - y1, conf]) boxes[i].append({ "x1": x1, "y1": y1, "x2": x2, "y2": y2, "confidence": conf.item() }) return boxes
根据mar文件存放地址重启torchserve,mar文件最好放在model-store里,为什么我忘了,后面找到了再补充
torchserve --stop
torchserve --start --ncs --model-store model-store --models yolofacev2.mar
重启torchserve后,打开新的terminal,可能需要再次进入docker容器(实际操作中,进不进容器需要尝试,有些WSL必须进容器才能进行连接测试,有些WSL必须不进入容器才能进行连接测试,根据实际情况做判断,怎么不报错,怎么来)
# 连接模型
curl http://localhost:8081/models/yolofacev2
# 测试命令
curl http://127.0.0.1:8080/predictions/yolofacev2 -T data/images/zidane.jpg
curl http://127.0.0.1:8080/predictions/yolofacev2 -T data/images/bus.jpg
样例测试结果如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。