Files
ModelTrainEval/train.py
T
Shaun ZhouandClaude Opus 5 141cb907f2 feat: 支持 cuda / mps / cpu 三后端,Mac 上可用 MPS 训练
移除"训练必须有 Nvidia GPU"的硬性守卫,改为按后端能力分派。

- utils/device: resolve_device("auto") 按 cuda > mps > cpu 挑最快的可用后端;
  显式指定不可用的后端时直接报错,不会悄悄退回 CPU——训练慢几十倍却不吭声是最坏的情况
- AMP 随后端取默认值:CUDA 开、MPS 关。MPS 上 autocast 数值正常但实测更慢
  (tiny_conv @128²:fp32 76 ms/步、fp16 90 ms、bf16 108 ms),需要时 amp: true 可强开
- PeakMemory 统一显存峰值统计:CUDA 读内置计数器,MPS 无峰值 API 改用逐 step 采样
  (是下界,日志与产出里已标注)
- profile_inference 支持 MPS,并提示 torch.profiler 没有 MPS 的 ProfilerActivity,
  算子明细只反映 CPU 侧下发耗时
- train.py / eval.py 新增 --device,配置默认改为 auto
- check_env 改为列出全部可用后端与 auto 的选择结果

实测:tiny_conv 训练在 MPS 上比 CPU 快约 32 倍。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:43:35 +08:00

105 lines
3.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
"""训练入口。
用法:
python train.py --config configs/train/baseline.yaml
python train.py --config configs/train/baseline.yaml --set train.epochs=10 train.optimizer.lr=5e-4
python train.py --config configs/train/baseline.yaml --device mps
设备由配置的 `device` 决定,默认 `auto`:按 cuda > mps > cpu 挑最快的可用后端。
显式指定但不可用时直接报错,不会悄悄退回 CPU。
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from datasets import build_dataloader, build_dataset, load_classes
from engine import Trainer
from models import build_model
from utils import check_label_mode, load_config, seed_everything, setup_logging
from utils.device import DeviceNotAvailableError, describe_device, resolve_device
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="薄膜缺陷分类定位模型训练")
parser.add_argument("--config", required=True, help="yaml 配置路径")
parser.add_argument("--set", nargs="*", default=[], metavar="KEY=VALUE", help="覆盖配置项")
parser.add_argument("--work-dir", default=None, help="覆盖配置中的 work_dir")
parser.add_argument("--device", default=None, help="覆盖配置中的 device:auto / cuda / mps / cpu")
parser.add_argument("--resume", default=None, help="从检查点恢复训练")
return parser.parse_args()
def main() -> int:
args = parse_args()
cfg = load_config(args.config, args.set)
work_dir = Path(args.work_dir or cfg.get("work_dir", "runs/default"))
work_dir.mkdir(parents=True, exist_ok=True)
logger = setup_logging(work_dir / "train.log")
try:
device = resolve_device(args.device or cfg.get("device", "auto"))
except DeviceNotAvailableError as exc:
logger.error("%s", exc)
return 1
logger.info("训练设备: %s", describe_device(device))
seed_everything(int(cfg.get("seed", 42)), bool(cfg.get("deterministic", False)))
classes = load_classes(cfg["classes"])
# label_mode 由 yaml 决定,模型与数据集必须一致,这里统一下发。
label_mode = check_label_mode(str(cfg.get("label_mode", "multilabel")))
model_cfg = dict(cfg["model"])
model_cfg.setdefault("num_classes", len(classes))
model_cfg.setdefault("label_mode", label_mode)
model = build_model(model_cfg).to(device)
logger.info(
"模型 %s 输出 stride=%d,参数量 %.2fM",
model_cfg["name"],
model.out_stride,
sum(p.numel() for p in model.parameters()) / 1e6,
)
loaders = {}
for split in ("train", "val"):
split_cfg = cfg["data"].get(split)
if not split_cfg:
loaders[split] = None
continue
dataset = build_dataset(
split_cfg["dataset"], classes=classes, stride=model.out_stride, label_mode=label_mode
)
loaders[split] = build_dataloader(dataset, **dict(split_cfg.get("loader", {})))
logger.info("%s 集样本数: %d", split, len(dataset))
trainer = Trainer(
model=model,
train_loader=loaders["train"],
val_loader=loaders["val"],
cfg=cfg.get("train", {}),
device=device,
work_dir=work_dir,
full_config=cfg.to_dict(),
)
if args.resume:
from engine import load_checkpoint
state = load_checkpoint(args.resume, model, trainer.optimizer, map_location=device)
trainer.start_epoch = int(state.get("epoch", 0)) + 1
trainer.best_metric = float(state.get("best_metric") or float("-inf"))
logger.info("已从 %s 恢复,起始 epoch=%d", args.resume, trainer.start_epoch)
cfg.dump(work_dir / "config.yaml")
best = trainer.fit()
logger.info("训练结束,最佳 %s=%.4f", trainer.monitor, best)
return 0
if __name__ == "__main__":
sys.exit(main())