1
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>视野管理端</title>
|
||||
<script type="module" crossorigin src="/admin/assets/index-CIe9te1H.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-BaD1qigE.css">
|
||||
<script type="module" crossorigin src="/admin/assets/index-B_3W4J54.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-DZ3uy8VX.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -295,7 +295,10 @@ func (s *trainingService) datasetNameMap(ctx context.Context) map[int64]string {
|
||||
}
|
||||
|
||||
// AdminStartTraining 发起训练:并发度 1(已有 running 拒绝);先本地整理 yolo 训练集
|
||||
// (80/20 拆 train/val)再同步训练机 → 写任务参数 → 启动进程;任何一步失败置任务 failed。
|
||||
// (80/20 拆 train/val,有标注才可训练)落 running 记录,请求毫秒级返回。
|
||||
// 训练机侧准备(写任务参数 → 同步数据集 → 启动进程)耗时可达分钟级(ssh 同步整包),
|
||||
// 脱离请求 ctx 在后台协程执行(与预标注 runDetection 同模式),任何一步失败置任务 failed
|
||||
// 由列表/轮询呈现;并发检查在 Serial 内,双击/并发点发只落一条任务。
|
||||
func (s *trainingService) AdminStartTraining(ctx context.Context, req *dto.AdminTrainingStartReq) (*dto.AdminTrainingStartRes, error) {
|
||||
runner := common.Runner(ctx)
|
||||
if runner == nil {
|
||||
@@ -350,7 +353,9 @@ func (s *trainingService) AdminStartTraining(ctx context.Context, req *dto.Admin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 准备阶段(训练机侧)失败 → 任务置 failed(记录保留便于排查)
|
||||
// 训练机侧准备(写任务参数/同步数据集/启动进程)为生命周期任务,脱离请求 ctx 后台执行;
|
||||
// 失败置任务 failed(记录保留便于排查),请求本身不等待
|
||||
bgCtx := context.Background()
|
||||
job := &common.TrainingJob{
|
||||
TaskId: taskId,
|
||||
DatasetName: dataset.Name,
|
||||
@@ -358,40 +363,42 @@ func (s *trainingService) AdminStartTraining(ctx context.Context, req *dto.Admin
|
||||
Workdir: cfg.Workdir,
|
||||
DatasetDir: cfg.DatasetDir,
|
||||
}
|
||||
// data.yaml 的 path 指向训练机路径,随包一起同步
|
||||
trainPath := filepath.Join(cfg.Workdir, cfg.DatasetDir, "yolo", dataset.Name)
|
||||
pkg.Files = append(pkg.Files, common.YoloFile{
|
||||
Name: "dataset.yaml",
|
||||
Content: []byte(yoloYamlContent(trainPath, localAiClassNames(ctx))),
|
||||
})
|
||||
taskJSON, _ := json.Marshal(map[string]any{
|
||||
"workdir": cfg.Workdir,
|
||||
"yolo": filepath.ToSlash(filepath.Join(cfg.DatasetDir, "yolo", dataset.Name)),
|
||||
"imgsz": imgsz,
|
||||
"epochs": epochs,
|
||||
"batch": batch,
|
||||
"device": device,
|
||||
"project": filepath.ToSlash(filepath.Join("runs", "tasks", strconv.FormatInt(taskId, 10))),
|
||||
"log_file": filepath.ToSlash(filepath.Join("logs", strconv.FormatInt(taskId, 10)+".jsonl")),
|
||||
"result_file": filepath.ToSlash(filepath.Join("results", strconv.FormatInt(taskId, 10)+".json")),
|
||||
"artifact_zip": filepath.ToSlash(filepath.Join("artifacts", strconv.FormatInt(taskId, 10)+".zip")),
|
||||
})
|
||||
if err := runner.WriteTaskJson(ctx, job, string(taskJSON)); err != nil {
|
||||
_ = s.finishFailed(ctx, &entity.ModelTraining{Id: taskId}, "写任务参数失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := runner.SyncYoloDataset(ctx, job, pkg); err != nil {
|
||||
_ = s.finishFailed(ctx, &entity.ModelTraining{Id: taskId}, "同步数据集失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
pid, err := runner.Start(ctx, job)
|
||||
if err != nil {
|
||||
_ = s.finishFailed(ctx, &entity.ModelTraining{Id: taskId}, "启动训练失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := dao.Training.UpdatePid(ctx, taskId, pid); err != nil {
|
||||
g.Log().Errorf(ctx, "训练 %d 记录 pid 失败: %+v", taskId, err)
|
||||
}
|
||||
go func() {
|
||||
// data.yaml 的 path 指向训练机路径,随包一起同步
|
||||
trainPath := filepath.Join(cfg.Workdir, cfg.DatasetDir, "yolo", dataset.Name)
|
||||
pkg.Files = append(pkg.Files, common.YoloFile{
|
||||
Name: "dataset.yaml",
|
||||
Content: []byte(yoloYamlContent(trainPath, localAiClassNames(bgCtx))),
|
||||
})
|
||||
taskJSON, _ := json.Marshal(map[string]any{
|
||||
"workdir": cfg.Workdir,
|
||||
"yolo": filepath.ToSlash(filepath.Join(cfg.DatasetDir, "yolo", dataset.Name)),
|
||||
"imgsz": imgsz,
|
||||
"epochs": epochs,
|
||||
"batch": batch,
|
||||
"device": device,
|
||||
"project": filepath.ToSlash(filepath.Join("runs", "tasks", strconv.FormatInt(taskId, 10))),
|
||||
"log_file": filepath.ToSlash(filepath.Join("logs", strconv.FormatInt(taskId, 10)+".jsonl")),
|
||||
"result_file": filepath.ToSlash(filepath.Join("results", strconv.FormatInt(taskId, 10)+".json")),
|
||||
"artifact_zip": filepath.ToSlash(filepath.Join("artifacts", strconv.FormatInt(taskId, 10)+".zip")),
|
||||
})
|
||||
if err := runner.WriteTaskJson(bgCtx, job, string(taskJSON)); err != nil {
|
||||
_ = s.finishFailed(bgCtx, &entity.ModelTraining{Id: taskId}, "写任务参数失败: %v", err)
|
||||
return
|
||||
}
|
||||
if err := runner.SyncYoloDataset(bgCtx, job, pkg); err != nil {
|
||||
_ = s.finishFailed(bgCtx, &entity.ModelTraining{Id: taskId}, "同步数据集失败: %v", err)
|
||||
return
|
||||
}
|
||||
pid, err := runner.Start(bgCtx, job)
|
||||
if err != nil {
|
||||
_ = s.finishFailed(bgCtx, &entity.ModelTraining{Id: taskId}, "启动训练失败: %v", err)
|
||||
return
|
||||
}
|
||||
if err := dao.Training.UpdatePid(bgCtx, taskId, pid); err != nil {
|
||||
g.Log().Errorf(bgCtx, "训练 %d 记录 pid 失败: %+v", taskId, err)
|
||||
}
|
||||
}()
|
||||
return &dto.AdminTrainingStartRes{Id: taskId}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -362,6 +362,7 @@ training:
|
||||
- **训练脚本**(`server/training/train_server.py`,随项目迁移):支持 `--task-json <file>`(含 dataset/imgsz/epochs/batch/device/project 名),每 epoch 输出一行机器可读 JSON 到 `--log-file`(`{"epoch":1,"total":150,"metrics":{...}}`),结束写 `result.json`(最终指标)+ 自动打包 `artifact.zip`(best.pt + results.csv + 曲线);Go 侧解析日志行更新进度、轮询日志尾部截断 N KB 存 `model_training.log_tail`
|
||||
- **tflite 产物自检**(2026-08-26):`inspect_tflite.py` 的 flatbuffer 解析逻辑内嵌进 `train_server.py`(`check_tflite`),训练收尾定位 `best.tflite` 后自动校验并写 `result.json` 的 `tflite_check` 字段:`{"ok":bool,"reason":string,"inputs":[{"name","shape","type"}],"outputs":[...]}`;校验规则 = 输入恰 1 张且 4 维、元素总数 == imgsz²×3(兼容 NCHW/NHWC)、输出 ≥ 1 张且 batch 维 = 1;`ok=false`(如 shape 漂移、导出异常)时 Go 侧在拉产物前直接置训练失败并带出 reason,杜绝坏产物进入发布链路;`dump_graph.py` 保留作训练机人工深度调试
|
||||
- **任务生命周期**:`running → success/failed`;取消 = 杀进程(ssh 模式远程 kill pid);超时无心跳判死;**Go 服务重启后启动扫描** running 任务按 pid 存活探测(subprocess 本机、ssh 远程 `kill -0`),进程已死则置 failed
|
||||
- **发起训练异步化(2026-08-27)**:发起请求仅做校验(数据集存在 / `prepareYoloSet` 有标注 / Serial 内并发检查)+ 落 running 记录即返回(毫秒级);训练机侧准备(写任务参数 → ssh tar 同步数据集 → 启动进程,耗时可达分钟级)在后台协程执行(`context.Background()`,与预标注 `runDetection` 同模式),任何一步失败经 `finishFailed` 置任务 failed 由列表/轮询呈现——此前同步执行超过管理端 axios 10s 超时,出现「任务已落库但前端报 timeout」的不一致
|
||||
- **并发度 1**:发起训练时若已有 running 任务返回错误「训练进行中」;训练任务不排队(简化,管理端人工再点一次)
|
||||
- 产物拉取:成功后拉 `best.tflite` + `artifact.zip` 到服务器 `workspace/trainings/<taskId>/`,发布时引用
|
||||
- 写操作走 `common.Serial()` 单写者(SQLite 无 WAL,与既有链路一致);任务状态更新(进度轮询)为高频写,单独小事务
|
||||
|
||||
@@ -28,7 +28,7 @@ const coverFile = ref(null) // 新选择的封面文件(el-upload 单文件)
|
||||
const coverFileList = ref([]) // 封面回显:已有封面(服务端 url)或新选文件(本地预览)
|
||||
const coverDeleted = ref(false) // 用户删除了已有封面(保存时调删除接口)
|
||||
|
||||
const training = ref(false)
|
||||
const trainingIds = reactive(new Set()) // 训练发起中的数据集 id(请求期间按钮置灰防重复点击)
|
||||
|
||||
const statusMap = { building: '建设中', labeled: '已标注', synced: '已同步' }
|
||||
const statusTag = { building: 'info', labeled: 'success', synced: 'primary' }
|
||||
@@ -134,16 +134,19 @@ async function submitConfig() {
|
||||
// ---------- 开始训练(一键发起:任务名自动生成,参数走 config.yml training 节点) ----------
|
||||
|
||||
function startTrain(row) {
|
||||
training.value = true
|
||||
if (trainingIds.has(row.id)) return
|
||||
trainingIds.add(row.id)
|
||||
request
|
||||
.post('/trainings', { datasetId: row.id })
|
||||
// 后端已异步化(请求只做校验+落记录),60s 仅作兜底
|
||||
.post('/trainings', { datasetId: row.id }, { timeout: 60000 })
|
||||
.then(() => {
|
||||
ElMessage.success('训练任务已发起,完成后可发布为模型版本')
|
||||
load()
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
training.value = false
|
||||
trainingIds.delete(row.id)
|
||||
// 请求失败也可能已落 running 记录(如响应异常),一律刷新拿真实状态
|
||||
load()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -251,7 +254,14 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="ds-actions" @click.stop>
|
||||
<el-button type="primary" size="small" :icon="VideoPlay" :disabled="row.trainingStatus === 'running'" @click="startTrain(row)">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="VideoPlay"
|
||||
:loading="trainingIds.has(row.id)"
|
||||
:disabled="trainingIds.has(row.id) || row.trainingStatus === 'running'"
|
||||
@click="startTrain(row)"
|
||||
>
|
||||
{{ row.trainingStatus === 'running' ? '训练中' : '开始训练' }}
|
||||
</el-button>
|
||||
<el-button size="small" :icon="Setting" @click="openConfig(row)">配置</el-button>
|
||||
|
||||
Reference in New Issue
Block a user