ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv5+Flask实战:从模型导出到Web推理服务部署全指南

YOLOv5+Flask实战:从模型导出到Web推理服务部署全指南 简介压缩包内提供 Yolov5 目标检测模型与 Flask 框架结合的 Web 部署工程代码面向需要将深度学习模型快速包装成在线服务的开发者也可用于课程设计或毕设原型展示。项目共 15 个文件核心为 Python 脚本app.py、restapi.py、test_request.py 等分别承担 Flask 应用、API 路由、请求测试等职责搭配前端页面index.html、style.css实现图片上传与展示requirements.txt 与 Dockerfile 帮助一键复现环境两份 Markdown 文档说明使用流程配图便于预览效果整体压缩后仅 187KB结构精炼适合快速阅读与二次开发。该资源已被 713 人学习下载验证了其在轻量级目标检测 Web 化方向上的实际参考价值。从代码中可以完整理解 Yolov5 模型加载、图片预处理、推理输出解析以及 JSON 结果返回的关键流程围绕 /detect 接口形成可运行的最小闭环借助测试脚本可直接发起请求验证服务规避常见的环境依赖和接口联调问题是快速搭建实时目标检测 Web 应用的高效起点。1. YOLOv5 Web部署到底在做什么从本地模型到可用的HTTP服务把训练好的YOLOv5权重文件挂到Flask上让浏览器能上传一张图、回传一个带标注框的检测结果这件事听起来像“加个接口就行”但真做起来会撞上模型加载、推理效率、视频流、并发冲突这些连环坑。很多人第一次做Web部署时把模型直接塞进视图函数里结果一个请求把GPU显存占满第二个请求直接卡死。这篇文章我会用一个完整的Flask项目讲清楚怎么把YOLOv5封装成可被HTTP调用的推理服务怎么设计路由和数据格式怎么处理视频流与并发以及那些让新手翻车的玄学问题到底出在哪里。这套方案适合两类人一类是刚训练完YOLOv5自定义数据集、想让同事或客户通过网页快速体验效果的算法工程师另一类是后端开发者需要把别人训练好的权重落成一个能接入内部系统的服务接口。无论你是哪种这篇文章给的代码和参数调法都能直接复制到你的项目里跑通。2. 导出与封装把YOLOv5权重转成可部署的服务核心2.1 模型导出pt转TorchScript或ONNX权衡的是灵活性与速度训练好的YOLOv5通常是一个.pt文件里面除了权重还存了模型结构、优化器状态、训练超参数等冗余信息。直接拿它做Web推理当然能跑但每次加载都要重新解析整个模型结构首次请求的延迟可能高达十几秒。常见做法是先导出成更适合推理的格式。我给不同项目用得最多的是两种TorchScript和ONNX。TorchScript是PyTorch官方推荐的部署格式和PyTorch版本绑定紧加载后就是一个已编译的计算图推理速度比原版.pt快不少而且不依赖原始模型定义代码。ONNX则更通用一些如果后续要切换到TensorRT、ONNX Runtime或者CPU推理它是更好的中间格式。我的建议是如果Web服务就是Flask PyTorch这个大环境直接用TorchScript就够如果考虑将来换推理引擎先导出ONNX。YOLOv5官方仓库的export.py可以直接做这件事。跑通一个最小导出命令很快python export.py --weights ./runs/train/exp/weights/best.pt --include torchscript --img-size 640参数说明--include指定导出格式这里只导出TorchScript--img-size要和训练时一致YOLOv5默认训练是640导出完成后会在权重文件夹下生成best.torchscript文件。如果想导出ONNX就把--include改成onnx。注意导出的设备和训练设备尽量一致不然有时候会因为设备不匹配导致加载报错。导出之后还要验证一下加载导出的模型对一张测试图跑一遍推理对比原.pt模型的前5个检测框是否一致。这个步骤别跳很多人导完就直接部署结果检测框乱飞最后才发现是导出的预处理和后处理参数没对齐。2.2 封装推理类把预处理、推理、后处理做成稳定的接口不管用什么框架部署我都习惯先把模型包装成一个独立的推理类而不是在Flask路由里直接写model(img)。原因很简单Flask路由层只负责处理HTTP请求和响应模型推理的预处理、前向传播、NMS、坐标换算这些逻辑应该被隔离成一个可单测的单元。这样以后换FastAPI、换ONNX Runtime只需要改这个类内部实现路由代码一行不动。下面是一个最精简的推理类封装假设你已经用YOLOv5官方仓库的detect.py跑通了检测流程import torch import cv2 import numpy as np from pathlib import Path class YOLOv5Detector: def __init__(self, weights_path: str, device: str cuda:0): # 用 torch.jit.load 加载 TorchScript 模型 self.model torch.jit.load(weights_path, map_locationdevice) self.model self.model.to(device) self.model.eval() self.device device self.img_size 640 self.conf_thres 0.25 self.iou_thres 0.45 # YOLOv5 的类别名按训练配置文件顺序 self.names [person, car, dog, cat] # 替换成你的类别 def preprocess(self, img_bgr: np.ndarray) - torch.Tensor: # YOLOv5 用的是 BGR 输入letterbox 缩放然后归一化到 [0,1] h, w img_bgr.shape[:2] r min(self.img_size / h, self.img_size / w) new_w, new_h int(round(w * r)), int(round(h * r)) resized cv2.resize(img_bgr, (new_w, new_h), interpolationcv2.INTER_LINEAR) canvas np.full((self.img_size, self.img_size, 3), 114, dtypenp.float32) canvas[:new_h, :new_w] resized # HWC - CHW, BGR - RGB模型内部用 RGB 顺序 x canvas[:, :, ::-1].transpose(2, 0, 1) x np.ascontiguousarray(x) x torch.from_numpy(x).float() / 255.0 x x.unsqueeze(0).to(self.device) return x, r, new_w, new_h def postprocess(self, pred, r, new_w, new_h, orig_shape): # 模型输出 shape: [1, 25200, 85]85 4(框) 1(置信度) 80(类别) # 这里做简单过滤先按置信度过滤再按类别取最大值 pred pred[0].cpu().numpy() boxes, scores, class_ids [], [], [] for row in pred: score row[4] if score self.conf_thres: continue cls_id int(np.argmax(row[5:])) cls_score row[5 cls_id] if cls_score * score self.conf_thres: continue x1, y1, x2, y2 row[:4] # 还原到 letterbox 前坐标 x1 / r; y1 / r; x2 / r; y2 / r boxes.append([x1, y1, x2, y2]) scores.append(score * cls_score) class_ids.append(cls_id) return boxes, scores, class_ids def detect(self, img_bgr: np.ndarray): x, r, new_w, new_h self.preprocess(img_bgr) with torch.no_grad(): pred self.model(x) boxes, scores, class_ids self.postprocess(pred[0], r, new_w, new_h) return boxes, scores, class_ids这里的postprocess刻意没有实现完整NMS正式使用建议直接用官方utils/general.py里的non_max_suppression函数代码会更稳定。我这样写是为了让你看清坐标还原的步骤YOLOv5输出的xyxy是相对输入图像即letterbox填充后的640×640的坐标要除以缩放比例r得到原始图像坐标。这一步做错检测框就会整体偏移。初始化这个类时要注意torch.jit.load之后一定要调用.eval()否则模型里某些在训练时才有行为的模块如dropout或BN统计会干扰推理结果。另外如果模型是在GPU训练的而服务器没有GPU请用map_locationcpu加载然后把推理时的self.device也设成CPU否则会报device mismatch。3. Flask接口设计与前后端交互POST JSON、上传文件、视频流3.1 一个最小但完整的Flask应用路由设计与参数模型封装好之后Flask端就清晰了。我一般会在Flask应用初始化时就把检测器实例化好而不是在每个请求里重新加载模型。这听起来理所当然但很多初学者会把YOLOv5Detector(...)写到某个路由函数内部结果每次请求都要花几秒加载模型服务基本不可用。from flask import Flask, request, jsonify, render_template, Response import cv2 import numpy as np import base64 from detector import YOLOv5Detector app Flask(__name__) detector YOLOv5Detector(best.torchscript, devicecuda:0) app.route(/, methods[GET]) def index(): return render_template(index.html) app.route(/detect, methods[POST]) def detect(): if file not in request.files: return jsonify({error: no file uploaded}), 400 file request.files[file] img_bytes file.read() # 用一个便捷函数把字节流转成 OpenCV 矩阵 img_bgr cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) if img_bgr is None: return jsonify({error: invalid image}), 400 boxes, scores, class_ids detector.detect(img_bgr) # 把结果画到图上转成 base64 回传省得前端再调一次绘图接口 for box, score, cls_id in zip(boxes, scores, class_ids): x1, y1, x2, y2 [int(v) for v in box] label f{detector.names[cls_id]} {score:.2f} cv2.rectangle(img_bgr, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img_bgr, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) _, encoded cv2.imencode(.jpg, img_bgr) img_base64 base64.b64encode(encoded).decode(utf-8) return jsonify({ image_base64: img_base64, count: len(boxes), boxes: boxes, scores: scores, class_ids: class_ids }) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)这段代码的核心设计是detector在模块加载时创建一次所有请求共享同一个实例。threadedTrue允许Flask同时处理多个请求但要注意OpenCV的imdecode在并发环境下是安全的而PyTorch的模型forward也是线程安全的只是如果多个请求同时推入显存显存占用会成倍增加。这个问题后面避坑章节再细说。cv2.imdecode读取的是文件字节流不能用cv2.imread直接读request.files里的路径因为Flask拿到的是内存中的文件对象不是真实文件路径。这个细节很多人第一次写都会踩。另外返回的boxes是Python list不是numpy数组jsonify才能正常序列化。3.2 前端页面上传图片与显示结果前端不需要复杂一个HTML文件加一点原生JavaScript就够不需要引入axios或jQuery。核心逻辑是input typefile选择图片后用FormData把文件POST到/detect接口拿到base64图片后直接塞到img的src属性里。!DOCTYPE html html head meta charsetutf-8 titleYOLOv5 Web Demo/title /head body h2YOLOv5 目标检测/h2 input typefile idupload acceptimage/* button onclickuploadImage()检测/button brbr img idresult width640 alt检测结果 script async function uploadImage() { const fileInput document.getElementById(upload); if (!fileInput.files.length) { alert(请先选择图片); return; } const formData new FormData(); formData.append(file, fileInput.files[0]); const resp await fetch(/detect, { method: POST, body: formData }); const data await resp.json(); if (data.error) { alert(data.error); return; } document.getElementById(result).src data:image/jpeg;base64, data.image_base64; } /script /body /html这段代码的要点是acceptimage/*限制了文件选择器只显示图片避免用户传一个.txt上来然后后端解析失败。fetch的body直接传FormData不要手动设置Content-Type浏览器会自动加上带boundary的multipart/form-data否则Flask的request.files拿不到文件。这种方式足以支撑内部测试和demo演示但生产环境建议加一个上传文件大小限制比如MAX_CONTENT_LENGTH 16 * 1024 * 1024防止大图把服务端内存打爆。3.3 视频流与实时检测如何把帧送到模型再回推图片检测是最基础的需求但实际业务里更多人问的是“能不能直接检测视频流”比如摄像头画面实时检测。这个场景用Flask YOLOv5实现的方式是Flask后端不断从视频源读帧送入检测器把标注后的帧以MJPEG流格式推给前端。前端只需要一个img标签指向/video_feed接口浏览器就能持续刷新画面。app.route(/video_feed) def video_feed(): # 这个生成器函数会被 Flask 持续调用不断 yield JPEG 帧 def generate(): # cap 在请求内部创建请求结束自动释放 cap cv2.VideoCapture(0) # 摄像头索引也可以是 RTSP 地址 while True: ok, frame cap.read() if not ok: break boxes, scores, class_ids detector.detect(frame) for box, score, cls_id in zip(boxes, scores, class_ids): x1, y1, x2, y2 [int(v) for v in box] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) label f{detector.names[cls_id]} {score:.2f} cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 编码成 JPEG按 multipart 协议输出 ok, jpeg cv2.imencode(.jpg, frame) if not ok: continue frame_bytes jpeg.tobytes() yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n frame_bytes b\r\n) return Response(generate(), mimetypemultipart/x-mixed-replace; boundaryframe)这里有三个参数值得关注。第一个是cv2.VideoCapture的输入源摄像头索引在OpenCV里从0开始如果是网络摄像头或RTSP流直接传rtsp://...字符串即可。第二个是mimetype必须是multipart/x-mixed-replace前端才能持续收到新帧如果写错浏览器会直接下载而不是播放。第三个是帧率控制generate()循环里没有time.sleep如果模型检测速度只有5 FPS那推流也只有5 FPS这是正常的但如果摄像头帧率本身就高模型跟不上就要在循环里加一个等待或跳帧不然CPU会被读帧操作占满后面避坑里会细讲。4. 性能与并发卡顿、超时、GPU占用异常怎么调4.1 单进程到多线程模型加载的位置决定了并发上限很多人在Flask里跑YOLOv5一压测就发现并发请求一多GPU利用率上不去但响应时间暴涨甚至会内存溢出。问题出在对GIL和PyTorch推理方式的理解不到位。Flask默认是单进程多线程Python线程共享一个解释器但PyTorch的CPU推理会释放GIL所以CPU推理时多线程确实能提升吞吐。然而GPU推理时多个线程同时调用model(x)CUDA会为每个请求分配独立的上下文显存开销成倍上涨。常见的解法有两种。第一种是串行化推理给检测器加一个线程锁同一时间只允许一个请求进入模型前向传播。这种方式简单粗暴但会损失并发能力。第二种是请求排队把推理任务丢进队列由单独的后台线程消费Flask路由只负责把结果取回。我一般用后者因为这样GPU利用率稳定不会出现多个请求同时抢显存导致OOM。import queue import threading import uuid task_queue queue.Queue(maxsize100) def inference_worker(): while True: task_id, img_bgr task_queue.get() try: boxes, scores, class_ids detector.detect(img_bgr) task_results[task_id] (boxes, scores, class_ids) except Exception as e: task_results[task_id] None finally: task_queue.task_done() threading.Thread(targetinference_worker, daemonTrue).start()这个写法把「接收请求」和「执行推理」解耦。task_queue.maxsize100是队列积压上限超过这个数的请求会立刻阻塞等待相当于一个天然限流器防止系统被请求风暴打垮。每个请求进来时生成唯一task_id然后把(task_id, img_bgr)丢入队列循环等待自己的结果出现在task_results里。这个方案适合GPU推理因为后台只有一个线程在调用model显存占用是稳定的不会随并发升高而膨胀。4.2 模型加载缓存、半精度与批量推理三个立竿见影的参数模型推理性能的瓶颈往往不在模型本身而在三个周边参数输入尺寸、精度、以及是否复用Tensor Core。第一个是输入尺寸YOLOv5默认是640×640如果检测目标较小或精度要求高可以推高到1280但推理时间会成倍增长大约增长4倍如果只是内网demo建议保持640优先保证实时性。第二个是半精度PyTorch里用model.half()可以把模型和输入张量都转成FP16在支持Tensor Core的GPU上推理速度提升明显显存占用也减半。但要注意如果你的模型权重是FP32导出的TorchScriptmodel.half()转换可能会让部分层的输出精度下降检测框略微偏移。我一般会在导出时直接用--half参数导出FP16模型这样不需要在运行时转换。第三个是批量推理。单个请求一张图是常规用法但如果在特殊场景下比如巡检系统同时上传10张图可以一次请求传入多张图用torch.cat拼接成batch输入YOLOv5对batch推理有优化吞吐比单张循环高很多。批量推理要注意内存管理batch size每增加1显存占用约线性增加。我给一个参考经验一张640×640的图在FP16下约占1.2GB显存含中间激活值如果你的GPU是8GB显存batch size最多开4。下面是一段批量推理的伪代码思路参数值得逐行看def detect_batch(self, img_list: list): # 预处理所有图stack成batch xs [] r_list [] for img in img_list: x, r, _, _ self.preprocess(img) xs.append(x) r_list.append(r) x_batch torch.cat(xs, dim0) # shape: [B, 3, 640, 640] with torch.no_grad(): preds self.model(x_batch) # [B, 25200, 85] results [] for i, pred in enumerate(preds): boxes, scores, class_ids self.postprocess(pred.unsqueeze(0), r_list[i]) results.append((boxes, scores, class_ids)) return results注意torch.cat之后模型输出的第一个维度就是batch索引后处理时按索引取回对应结果。r_list要保存每张图的缩放比例后处理做坐标还原时对不同图用不同比例这个细节很容易错错的结果是第2张图之后的检测框都偏移。4.3 并发线程数、超时与队列深度经验参数表这里给一套我在多台服务器上调过的参数基线你可以直接作为起点再根据压测结果微调参数默认值/建议值说明FlaskthreadedTrue接收HTTP请求的线程数不对推理并发负责task_queue.maxsize50~100队列满了之后再来的请求会阻塞起到限流作用推理线程数1GPU推理建议单线程CPU推理可尝试2~4个conf_thres0.25太低会有大量误检太高会漏检小目标iou_thres0.45NMS去重阈值多目标重叠场景适当调高到0.5图片最大尺寸640超过建议宽度按比例缩放防止内存暴涨单张请求超时30sFlask默认无超时需在Nginx层设置上传文件大小限制16MB在Flask配置里设MAX_CONTENT_LENGTH队列深度调得越小系统在高并发下越稳定但用户等待时间会变长调得越大吞吐越高但极端尖峰可能导致系统内存暴涨。我一般先用wrk或ab做一次100并发、持续1分钟的压测观察显存和内存曲线再反向调整队列深度。如果显存已经到80%以上就减小maxsize或降低img_size。5. 避坑我把YOLOv5部署到Flask时踩过的几个坑5.1 首次请求特别慢浏览器转圈十几秒现象Flask服务启动后第一次发送检测请求等了十几秒才出结果第二次之后就快了。原因模型加载是惰性的。YOLOv5Detector.__init__里虽然调用了torch.jit.load但真正的权重载入和CUDA上下文初始化可能延迟到第一次推理才触发。而且第一次CUDA调用会做cuDNN预热花费几秒是正常的。解决在服务启动后主动跑一次“空推理”预热用一张全黑的640×640图片调用一次detect()。这会把CUDA kernel、cuDNN context全部初始化完毕之后普通请求延迟就能稳定在几十毫秒。我习惯在__main__里加一行warmup_img np.zeros((640, 640, 3), dtypenp.uint8) detector.detect(warmup_img)5.2 并发一高显存直接OOM现象并发请求从5增加到10GPU显存从4GB飙升到12GB然后报CUDA out of memory。原因如果没有做请求排队或锁多个线程同时进入模型前向传播Pytorch会为每个调用分配独立的CUDA context和激活值缓存显存占用是并发数量的线性函数。这是单进程多线程GPU推理最容易踩的雷。解决按思路4.1的方式把推理放进队列由单线程消费。或者给detect方法加一个threading.Lock()。锁的缺点是会阻塞请求线程让调用方等待队列则更平滑。如果实在需要同时推理多路视频流建议用多个进程每个进程持有独立模型进程之间用multiprocessing共享队列。5.3 前端收到结果但图片显示空白现象接口返回了JSONimage_base64字段也有值但图片在浏览器里显示为破碎图标。原因cv2.imencode(.jpg, img_bgr)返回的是(retval, buffer)buffer是numpy.ndarray如果直接把这个数组交给base64.b64encode会报错因为b64encode需要字节串。如果代码里用了base64.b64encode(encoded)而encoded是ndarrayPython会悄悄转成字符串再编码结果就是无效的base64数据浏览器解析失败。解决先调用encoded.tobytes()转成字节串再编码。文章3.1节的代码已经写了正确写法_, encoded cv2.imencode(.jpg, img_bgr) img_base64 base64.b64encode(encoded.tobytes()).decode(utf-8)5.4 视频流延迟越来越大直到画面变成幻灯片现象推流地址打开后前几秒流畅过了一分钟开始卡顿延迟持续累积。原因generate()生成器里cv2.VideoCapture.read()从摄像头或RTSP源读取帧的速度远快于模型推理速度如果不对输入帧做丢弃或延时生成器内部积压的帧会越来越多每帧处理耗时不断叠加最终延迟不可控。解决在生成器里加一个帧率控制逻辑比如目标推流15FPS检测一次耗时0.1s那么每帧之间sleep(max(0, 1/15 - process_time))并且如果模型速度跟不上直接丢弃下一帧只检测最新帧。一个简单实现是import time prev_time time.time() while True: ok, frame cap.read() if not ok: break # 如果距离上一帧处理时间不足 1/15 秒跳过这一帧 now time.time() if now - prev_time 1.0 / 15.0: continue prev_time now # 推理与推流这种“跳帧机制”能保证推流稳定在目标帧率且不会因为输入源帧率过高而堆积。对于RTSP流cap.read()本身会阻塞等待新帧不需要额外sleep但如果是本地视频文件或USB摄像头一定要加这个控制。5.5 模型在CPU服务器上跑延迟高到没法用现象没有GPU的服务器上单张图推理耗时2~3秒完全达不到实时。原因YOLOv5在CPU上的推理确实慢特别是800万像素照片缩放成640后模型计算量大。但还有一个常被忽略的点如果TorchScript模型是用FP16导出的CPU加载会报错或不支持最终退回FP32反而比直接FP32导出更慢。解决CPU部署用ONNX Runtime替代PyTorch推理是常见做法。用onnxruntime加载同一个best.onnx模型推理速度能比PyTorch CPU快2~5倍。这个方案的具体做法不展开但有一条经验CPU服务器就别用TorchScript直接用ONNX ONNX Runtimeproviders[CPUExecutionProvider]输入图像预处理保持一致代码结构和我给的推理类几乎一样只是把model(x)换成了session.run(None, {images: x_numpy})。如果你的需求就是要在廉价服务器上跑这个方向值得尝试。6. 进阶把服务变得可观测、可维护、可升级6.1 为推理接口添加日志与耗时监控一把不起眼但必要的尺子部署不是“能跑就行”尤其是要给业务方交付时你需要能回答“为什么这个请求这么慢”“失败率是多少”。我给所有Flask推理服务都加一个装饰器来记录每个请求的耗时、返回码和实体数量。代码量很少但排查问题时价值极高import time import logging from functools import wraps logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) logger logging.getLogger(yolov5-server) def log_request(fn): wraps(fn) def wrapper(*args, **kwargs): start time.perf_counter() resp fn(*args, **kwargs) duration_ms (time.perf_counter() - start) * 1000 # 如果响应是 jsonify 对象能取到 status_code if isinstance(resp, tuple): status resp[1] else: status resp.status_code logger.info(f{request.path} {status} {duration_ms:.1f}ms) return resp return wrapper这个装饰器直接放在/detect路由函数上每次请求结束都会打一行日志。生产环境可以追加到QueueHandler写入日志文件或ELK这里不展开。另一个实操技巧是把推理的耗时单独打点比如在detector.detect里用time.perf_counter分别记录预处理、前向传播、后处理三段耗时这样你就知道瓶颈到底是模型本身还是图像解码还是坐标转换的Python循环。我之前遇到一个“慢请求”问题排查后发现90%时间花在cv2.imdecode解码一张4000万像素原图上和模型推理没关系。6.2 模型热更新不重启服务切换模型权重训练新模型后传统做法是替换文件、重启服务这会中断线上检测。稍微聪明一点的做法是模型路径派发到请求参数里服务内部维护一个模型管理字典当某个请求指定model_version时先判断当前内存里有没有这个版本如果不存在就从磁盘加载并加入字典这样可以在不重启情况下随时切换模型。MODEL_REGISTRY {} MODEL_LOCK threading.Lock() def get_model(version: str): if version not in MODEL_REGISTRY: with MODEL_LOCK: if version not in MODEL_REGISTRY: # 双检锁 MODEL_REGISTRY[version] YOLOv5Detector(f{version}.torchscript) return MODEL_REGISTRY[version]这个实现适合模型文件不大小于500MB的场景。如果模型数量多且单个体积大内存消耗会是问题那时可以改成LRU淘汰机制。热更新能提升运维体验但也要注意多版本并存时的显存管理加载新模型前最好先看显存剩余不够就torch.cuda.empty_cache()后再加载。6.3 从Flask到生产级什么时候该换FastAPI或加GunicornFlask自带的开发服务器Werkzeug不适合直接暴露公网这是老生常谈但很多人直到上线才意识到。生产环境至少要在Flask前面加一层Nginx做反向代理和静态文件服务用Gunicorn替代app.run()来启动Flask。如果项目后续接口数量变多、需要请求参数校验、自动生成API文档那么换FastAPI是值得的代码迁移成本并不高——因为我已经把推理逻辑全部封装在YOLOv5Detector类里路由层换一个框架内部代码一行不用改。以下是一份Gunicorn启动命令的参考参数按我的经验配置gunicorn -w 1 -b 0.0.0.0:5000 --timeout 120 app:app-w设置worker进程数对于GPU推理服务建议设为1避免多进程争用GPU如果使用CPU推理可以设为CPU核心数。--timeout 120是必须设置的因为首次请求如果包含模型预热耗时会超过默认的30秒。如果你有多个GPU可以用-w 2并分别在两个worker里通过CUDA_VISIBLE_DEVICES指定不同GPU这样单机吞吐能翻倍。怎么说呢部署这件事表面上是把模型“挂到网上”实际上是一次对工程化能力的检验。我自己做过几个版本才总结出一条经验模型性能决定天花板但工程细节决定你能不能摸到天花板。那些看似不起眼的预热、队列、日志恰恰是线上服务稳定性的根基。希望这篇笔记里的代码和坑位记录能帮到你让你少走几段我走过的弯路。本文还有配套的精品资源点击获取
返回列表