Files
YG_FT/frontend/src/views/model/MergeWeightsView.vue
wuyongtao 0271942ba5 feat: 模型推理异步加载与对话链路修复,同步基线
模型推理全异步化改造:
- 计算节点 InferenceSession 改为后台线程异步加载模型,load 立即返回,
  加载期间事件循环保持响应(/inference/status 与 /health 不阻塞)
- 后端模型加载改为异步派发 + 轮询对账器(reconcile_inference_loads),
  任务状态由 starting 自动推进到 ready/error,解决多节点启动超时
  (timeout of 120000ms exceeded)
- 推理删除/卸载改为任务感知 + 短超时,删除先删记录再 best-effort 卸载,
  不再被不可达节点阻塞;同节点新模型替换旧任务标记失效
- 流式对话透传 task_id/node_id 路由到真正加载模型的算力节点,
  useStreamChat 解析 SSE 错误帧以干净文案展示
- 对话历史按任务 id 本地持久化,退出重进可恢复;移除页脚提示文本
- 新增后端推理异步加载与计算节点异步状态机单元测试

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 16:59:34 +08:00

120 lines
3.7 KiB
Vue

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import PageCard from '@/components/PageCard.vue'
import { getTrainedModels, mergeModel } from '@/api/modules/model'
import { TRAIN_METHOD_MAP } from '@/constants'
import type { TrainedModel } from '@/types'
const route = useRoute()
const router = useRouter()
const merging = ref(false)
const modelName = computed(() => (route.query.model as string) || '')
const method = computed(() => (route.query.method as string) || 'lora')
const trainedModels = ref<TrainedModel[]>([])
const currentModel = computed(() => trainedModels.value.find((m) => m.name === modelName.value))
const form = reactive({
trained_model_id: '',
model_name: modelName.value,
train_method: method.value,
base_model_path: '',
adapter_path: '',
compute_node_id: '',
})
async function loadModel() {
try {
const res = await getTrainedModels()
trainedModels.value = res?.models || []
const target = trainedModels.value.find((m) => m.name === modelName.value)
form.trained_model_id = target?.id == null ? '' : String(target.id)
form.base_model_path = target?.base_model_path || ''
form.adapter_path = target?.artifact_dir || target?.adapter_path || target?.merged_path || ''
form.compute_node_id = target?.compute_node_id || ''
} catch {
// ignore
}
}
async function handleMerge() {
if (!form.model_name || !form.base_model_path || !form.adapter_path) {
ElMessage.warning('缺少模型信息')
return
}
merging.value = true
try {
await mergeModel({
trained_model_id: form.trained_model_id || form.model_name,
model_name: form.model_name,
train_method: form.train_method,
base_model_path: form.base_model_path,
adapter_path: form.adapter_path,
compute_node_id: form.compute_node_id,
output_model_name: `${form.model_name}-merged`,
})
ElMessage.success('合并成功')
router.push('/model-manage')
} catch {
// ignore
} finally {
merging.value = false
}
}
function handleCancel() {
router.back()
}
onMounted(loadModel)
</script>
<template>
<PageCard title="合并权重">
<el-alert
type="info"
:closable="false"
show-icon
title="将 LoRA 适配器权重合并到基座模型,合并后可直接用于推理部署。"
style="margin-bottom: 20px"
/>
<el-form label-width="120px" style="max-width: 600px">
<el-form-item label="模型名称">
<el-input v-model="form.model_name" disabled />
</el-form-item>
<el-form-item label="训练方法">
<el-input :model-value="TRAIN_METHOD_MAP[form.train_method] || form.train_method" disabled />
</el-form-item>
<el-form-item label="基座模型路径">
<el-input v-model="form.base_model_path" placeholder="基座模型路径" />
</el-form-item>
<el-form-item label="Adapter 路径">
<el-input v-model="form.adapter_path" placeholder="LoRA Adapter 权重目录" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="merging" @click="handleMerge">
{{ merging ? '合并中...' : '开始合并' }}
</el-button>
<el-button @click="handleCancel">取消</el-button>
</el-form-item>
</el-form>
<div v-if="currentModel" class="model-status">
<el-tag v-if="currentModel.merging" type="warning">合并中</el-tag>
<el-tag v-else-if="currentModel.merged" type="success">已合并</el-tag>
<el-tag v-else type="info">未合并</el-tag>
</div>
</PageCard>
</template>
<style scoped>
.model-status {
margin-top: 20px;
}
</style>