ARTICLE DETAIL

资讯详情

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

Windows 下用 subprocess 把 YOLO 训练搬上网页:实时日志、曲线、断点续训

Windows 下用 subprocess 把 YOLO 训练搬上网页:实时日志、曲线、断点续训 Windows 下用 subprocess 把 YOLO 训练搬上网页实时日志、曲线、断点续训系列第 3 篇想在内网 Web 页面上发起、停止、续训 YOLO 训练并看到实时日志和 loss/mAP 曲线本文给出一套基于subprocess调 ultralyticsyoloCLI 的完整方案串行队列、日志采集、进度跟踪、杀进程树、断点续训全部代码可抄。适合用 FastAPI/Flask 给命令行工具套 Web 壳、尤其是 Windows 单机单卡场景的开发者。本系列记录一个人开发 YOLO 训练管理平台的过程FastAPI SQLite Vue 3 EChartsWindows 部署。前两篇见文末导航。这篇是整个系统技术含量最高的一块怎么把一个命令行训练工具包装成网页上可排队、可停止、可续训、有实时曲线的训练系统。为什么用 subprocess而不是在进程内 import ultralyticsFastAPI 进程里直接from ultralytics import YOLO; model.train(...)当然也能跑但会很快遇到这些麻烦训练崩了CUDA 报错、OOM异常栈和 PyTorch 内部线程状态纠缠不清Web 进程跟着不稳定想停止训练PyTorch 没有可靠的半路中止接口服务重启后内存里的日志、进度全没了改用 subprocess spawn 独立的yoloCLI 进程后这些问题全部有了干净的解法崩溃隔离训练死的是子进程Web 服务毫发无损停止 杀进程树psutil 递归杀干净没有状态清理负担日志天然解耦子进程输出写日志文件接口按 offset 增量读服务重启不丢续训白送ultralytics 原生支持resumeTrue从 last.pt 恢复代价是要自己处理子进程输出解析、进度跟踪、竞态——这就是本文的内容。整体结构HTTP 请求线程 训练线程每任务一个 │ │ POST /tasks ──→ 建任务(pending) ──→ pump_pending() │ 队列锁一次只跑一个 ▼ staging 数据准备读版本快照 ▼ subprocess.Popen(yolo detect train) │ ┌──────────────────┼──────────────────┐ ▼ ▼ ▼ 日志线程 进度轮询2s 用户操作 逐字符读 stdout 数 results.csv 行数 停止→杀进程树 按 \r\n 切段写文件 更新 percent 续训→重新入队一、串行队列一把锁 一个泵公司机器就一张卡同时跑两个训练必然双双 OOM所以队列是串行的。实现上不需要 Celery 之类的重型队列一个锁加一个泵函数就够了_queue_lockthreading.Lock()_current_task_id:int|NoneNonedefpump_pending():启动最老的一个 pending 任务有正在跑的就什么都不做。可安全重复调用。global_current_task_idwith_queue_lock:if_current_task_idisnotNone:returntaskdb.query(TrainTask).filter(TrainTask.statuspending)\.order_by(TrainTask.id).first()iftaskisNone:return_current_task_idtask.idthreading.Thread(target_run_task,args(task.id,),daemonTrue).start()要点是**“检查空闲 占坑 启动全在一把锁里**——否则两个人同时点开始训练”都看到空闲同时启动显存爆炸。任务结束时释放占位再调一次pump_pending()拉起下一个。pump_pending设计成幂等的任何地方随手调都不会出问题。二、staging训练数据单独拷贝一份再训发起训练时从所选数据集版本快照里读出图片清单和标注合并拷贝到runs/task_id/dataset/图片重命名为{版本id}_{图片id}.jpg不同数据集的同名图不会互相覆盖标注从像素 xywh 转 YOLO 归一化class id 按合并后的类别表重写划分规则定死全部版本自带划分就用自带的test 集不进训练否则统一按比例随机分seed 固定为 42可复现staging 目录任务结束后保留——断点续训还要用这一步放在训练线程里做好处是训练用的数据和其他人正在进行的导入/标注操作完全隔离。三、启动子进程三个只有踩过才知道的细节procsubprocess.Popen(cmd,stdoutsubprocess.PIPE,stderrsubprocess.STDOUT,# 关键 1textTrue,encodingutf-8,# 关键 2errorsreplace,bufsize1,env{**os.environ,PYTHONIOENCODING:utf-8},)细节 1stderrsubprocess.STDOUT合并进 stdout。只读 stdout 不管 stderr 的话子进程 stderr 缓冲区写满后阻塞训练表面hang 住。要么合并要么单独开线程读。细节 2显式指定 UTF-8。Windows 上 Python 子进程默认按 GBK 解码输出yolo 的日志里有 emoji 和中文类别名直接 UnicodeDecodeError。父进程encodingutf-8 子进程环境变量PYTHONIOENCODINGutf-8双保险。细节 3找 yolo 可执行文件先找自己 venv 的。shutil.which(yolo)按 PATH 找可能命中系统 Python 装的另一个 ultralytics——我们就因此用 nightly 版 torch 起过训练CUDA 初始化直接卡死查了半天。改成先找当前解释器同目录venv_dirPath(sys.executable).parent exenext((str(p)forpin(venv_dir/yolo.exe,venv_dir/yolo)ifp.is_file()),None)\orshutil.which(yolo)四、日志逐字符读、按\r切段、写文件yolo 的进度条用\r回车刷新不按行输出。按行读会卡住所以要逐字符读按\r和\n都切分_ANSI_REre.compile(r\x1b\[[0-9;]*[A-Za-z])# 终端颜色/光标控制码def_log_reader(proc,log_path):withopen(log_path,a,encodingutf-8,errorsreplace,buffering1)asf:segmentforchiniter(lambda:proc.stdout.read(1),):ifchin\r\n:ifsegment:f.write(_ANSI_RE.sub(,segment)\n)segmentelse:segmentch顺手把 ANSI 转义码颜色、光标控制剥掉不然前端日志页一片乱码。日志只写文件不写数据库、不存内存。老项目曾把日志存内存 dict服务一重启全丢任务还在跑页面却一片空白。改成写run_dir/train.log后读日志的接口就是一个f.seek(offset)返回新增内容和next_offset前端轮询增量拉取刷新页面、重启服务都不丢日志。五、进度和实时曲线别解析日志去数 results.csv日志里的百分比是给人看的格式随 ultralytics 版本说变就变。结构化数据在results.csv里一行 一个 epoch行数就是进度whileproc.poll()isNone:time.sleep(2)done_epochs_epoch_count(results_csv)task.percentmin(99,int(done_epochs*100/max(task.epochs,1)))db.commit()实时曲线也一样前端 ECharts 的数据全部来自解析 results.csv。解析有两个小坑列名模糊匹配不同版本列名带不带(B)后缀、有没有空格都不一样统一 normalize 后用后缀匹配normcol.replace((B),).strip().lower()ifnorm.endswith(map50-95):keymAP50-95elifnorm.endswith(map50):keymAP50过滤 nan/inf指标列偶尔出现 nan直接入库后 JSON 序列化会把接口搞成 500入库前math.isfinite()检查六、停止psutil 杀整棵树和处理不完的竞态Windows 上proc.terminate()经常只杀了壳进程yolo 起的 dataloader 子进程还在跑。用 psutil 杀整棵进程树先礼后兵def_kill_tree(pid):procpsutil.Process(pid)procs[proc]proc.children(recursiveTrue)forpinprocs:try:p.terminate()exceptpsutil.Error:pass_,alivepsutil.wait_procs(procs,timeout1)forpinalive:try:p.kill()exceptpsutil.Error:pass真正麻烦的是竞态。“停止按钮可能在任何时刻按下排队中、staging 准备中、等 GPU 中、进程刚拉起 pid 还没落库……每一种都得处理否则会漏杀或状态错乱。解法是引入一个_stopping集合记录停止意图”训练线程在每个阶段边界复查_stopping:set[int]set()# stop_task无条件记录停止意图kill 得到就 killkill 不到由训练线程兜底defstop_task(db,task_id):..._stopping.add(task_id)iftask.pid:_kill_tree(task.pid)# _run_task 里三处复查staging 完成后、等 GPU 结束后、Popen 之后iftask_idin_stopping:# 不置 running、直接收尾或杀掉刚拉起的进程stop_task对 done/error/stopped 状态幂等不报错——前端连点两下也不会炸。七、断点续训状态清理 resumeTrue笔记本合盖、断电、服务重启后数据库里会残留一批statusrunning但进程早死了的任务。服务启动时扫一遍全部标记为 interrupted否则这些任务永远卡在运行中队列也被占死。续训 改回 pending 标记走 resume 重新入队defresume_task(db,task_id):iftask.statusnotin(interrupted,error,stopped):raiseValueError(只有 interrupted/error/stopped 状态的任务可以续训)last_ptrun_dir/weights/last.ptifnotlast_pt.exists():raiseValueError(找不到断点权重无法续训)task.statuspending_resuming.add(task_id)pump_pending()执行时命令换成modelrun_dir/weights/last.pt resumeTrue其余参数沿用原任务前端锁死只读。staging 目录因为保留了检测到dataset.yaml已存在直接复用几秒钟就接着跑。八、收尾入库的判定要严进程退出后的状态判定顺序很重要rcproc.returncode best_ptrun_dir/weights/best.ptifrc0andbest_pt.exists():# 正常完成自动入库best.pt 用于部署last.pt 归档供续训/微调eliftask_idin_stopping:# 用户主动停止elifrc0:# 退出码 0 但没产出 best.pt典型标注全空→ 明确标记失败并写清原因else:# 异常退出截取日志尾部 20 行入库并粗分错误类型数据/配置/资源两个值得说的点退出码 0 但 best.pt 不存在必须标记失败。全空标注就是训不出模型的如果显示成功但模型库里没有东西用户会以为出了 bug错误信息用正则粗分为 data / config / resource 三类前端可以给出检查数据/“检查配置”/显存不足的针对性提示比甩一屏 traceback 友好得多还有一个互斥设计训练前要先拿到GPU 门一个进程内互斥锁批量预标注、导出等重资源操作也走同一道门拿不到就等——避免训练和批量预标注抢显存双双 OOM。实际效果这套机制在一个真实内部平台上跑着的数字训练管理核心模块队列/staging/执行/停止/续训/入库共740 行 Python单文件不依赖 Celery、Redis 等任何外部组件进度轮询间隔2 秒失败时日志尾部20 行入库停止操作 terminate 后等1 秒再强杀——都是实测够用的值配套13 个 pytest 用例全部通过其中 4 个专测训练链路未知基座模型拦截、排队中停止、无 last.pt 禁止续训、服务重启后 running 任务自动标记 interrupted主观感受日志和曲线在页面上基本无延迟感断点续训从点击到训练恢复只需几秒staging 复用的功劳。什么时候不适用这套方案成立的前提是单机、单卡、任务以小时计超出这个范围就别照搬多卡或多机并发训练串行队列就是为单卡设计的。多卡应该上 Celery/RQ 这类真队列每卡一个 worker或者直接用 K8s 调度以天为单位的大训练任务单机后台线程没有分布式容错机器宕机只能靠 last.pt 恢复到 epoch 边界中间几个小时的进度照样丢需要 step 级而不是 epoch 级进度轮询 results.csv 的最小粒度是一个 epoch小数据集一个 epoch 十几秒还无所谓想画 iteration 级实时曲线得改用回调或解析日志团队已有成熟任务队列基础设施如果公司本来就有 Celery Redis 跑着直接往里加 worker 比另造一套维护成本更低小结把这堆东西串起来可带走的结论五条把命令行训练工具包装成 Web 服务subprocess 隔离比进程内 import 省心一个数量级崩溃隔离、杀进程即停止、重启不丢状态一切状态落盘日志写文件、进度写数据库、staging 留目录——服务随时重启都不怕结构化数据优先进度和指标从 results.csv 拿日志只给人看Windows 上跑子进程三件事别忘stderr 合并进 stdout、显式 UTF-8 双保险、可执行文件先找自己 venv 的所有竞态都假设会发生停止可以在任何时刻按下进程可以在任何时刻死掉每个阶段边界都复查一次意图这套模式不局限于 yolo——任何把命令行工具包装成 Web 服务的场景ffmpeg 转码、编译任务、批处理脚本都可以照搬。FAQQ为什么不用 Celery / multiprocessing 管训练任务单机单卡场景用不上。Celery 要多维护一个 brokerRedis/RabbitMQ和 worker 进程对一次只跑一个训练是杀鸡用牛刀multiprocessing 的子进程和 Web 进程耦合度高不如 subprocess CLI 干净而且 yolo 本身就有现成 CLI。Qsubprocess.Popen 读输出时程序卡死不动怎么回事两种典型原因一是子进程 stderr 没人读缓冲区写满后子进程阻塞解法stderrsubprocess.STDOUT合并或单独开线程读二是按行迭代读 stdout但对方用\r刷新进度条不输出换行解法逐字符读\r和\n都当分隔符。QWindows 上怎么彻底杀死 Python 子进程及其子进程proc.terminate()只杀直接子进程。用 psutilProcess(pid).children(recursiveTrue)拿到整棵树先全部terminate()wait_procs(timeout1)后对还活着的kill()。Qyolo 训练中断后用 resumeTrue为什么报找不到 last.pt 或从头开始resumeTrue要求model指向那次训练的last.pt且 run 目录里的优化器状态完整。如果换了模型路径、删了 run 目录或改了 epochs 等参数ultralytics 会报错或行为异常——所以续训要锁定原任务参数staging 目录也别删。Q断点续训能恢复到中断的那个 epoch 吗可以。last.pt 里存了 epoch 数、优化器状态和 AMP scaler 状态resumeTrue会从下一个 epoch 接着训学习率调度也连续。但当前 epoch 训练到一半的进度会丢粒度是 epoch。技术栈FastAPI · SQLite · Vue 3 · ECharts · ultralytics · psutil本系列共 6 篇系列第 1 篇开发总览——23 个实践教训系列第 2 篇需求设计与技术选型系列第 3 篇subprocess 训练进程管理本篇系列第 4 篇标注数据一致性的 4 个设计系列第 5 篇Windows 双击即用与 PyInstaller 打包系列第 6 篇业余时间做内部工具不烂尾的心得有问题欢迎评论区交流。
返回列表