Files
YG_FT_Platform/web/pages/fine-tune-create.html
WIN-JHFT4D3SIVT\caoxiaozhu 40ca89fad5 1. 修改了应用端口的问题
2. 增加了创建虚拟环境的脚本和删除虚拟环境的脚本
2026-01-26 14:33:00 +08:00

1038 lines
63 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>创建训练任务 / 远光软件微调平台</title>
<script src="../lib/tailwindcss/tailwind.js"></script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<style>
.sidebar-section-title {
padding: 0.5rem 1rem;
font-size: 0.75rem;
color: rgba(191, 203, 217, 0.7);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.nav-link:hover {
background-color: rgba(0, 21, 41, 0.2);
}
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s, outline 0.2s;
}
.form-input:focus {
border-color: #1890ff;
outline: none;
}
.form-select {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s, outline 0.2s;
appearance: none;
background-color: white;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.form-select:focus {
border-color: #1890ff;
outline: none;
}
.card-radio {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.card-radio.active {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
.card-radio:hover {
border-color: #d1d5db;
}
.bg-primary { background-color: #1890ff; }
.text-primary { color: #1890ff; }
.border-primary { border-color: #1890ff; }
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
</style>
</head>
<body class="antialiased bg-gray-50 flex h-screen overflow-hidden">
<!-- 侧边导航 -->
<aside class="w-64 text-[#bfcbd9] flex-shrink-0 hidden md:block flex flex-col h-full" style="background-color: #001529;">
<!-- 平台LOGO区域 -->
<div class="pt-5 pb-3 border-b border-[#001529]/30 flex items-center justify-center pl-2">
<img src="../assets/logo/logo.png" alt="Logo" class="w-8 h-8 object-contain mr-2">
<span class="text-white font-medium text-base">远光软件微调平台</span>
</div>
<!-- 导航主区域 -->
<nav class="flex-1 overflow-y-auto py-2 relative">
<!-- 滑块指示器 -->
<div class="sidebar-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-cogs w-5 text-center"></i>
<span class="ml-2">模型调优</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=my-models" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database w-5 text-center"></i>
<span class="ml-2">我的模型</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=model-eval" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-line-chart w-5 text-center"></i>
<span class="ml-2">模型评测</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=model-compare" data-page="model-compare" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-server w-5 text-center"></i>
<span class="ml-2">模型对比</span>
</a>
</div>
<!-- 第二分区:资源管理 -->
<div class="sidebar-section-title mt-6">资源管理</div>
<div class="nav-item-wrapper">
<a href="main.html?page=model-manage" data-page="model-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-cube w-5 text-center"></i>
<span class="ml-2">模型管理</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=dataset-manage" data-page="dataset-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-file-text w-5 text-center"></i>
<span class="ml-2">数据集管理</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=data-generate" data-page="data-generate" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database w-5 text-center"></i>
<span class="ml-2">其他工具</span>
</a>
</div>
<!-- 第三分区:系统设置 -->
<div class="sidebar-section-title mt-6">系统设置</div>
<div class="nav-item-wrapper">
<a href="main.html?page=config" data-page="config" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-bar-chart w-5 text-center"></i>
<span class="ml-2">平台性能</span>
</a>
</div>
</nav>
<!-- 底部信息区域 -->
<div class="p-4 border-t border-[#001529]/30 text-xs mt-auto">
<div class="mb-2 text-[#bfcbd9]/80">默认业务空间</div>
<div class="flex items-center justify-between">
<span class="text-[#bfcbd9]">版本 v1.0.0</span>
<i class="fa fa-question-circle-o text-[#bfcbd9]/70"></i>
</div>
</div>
</aside>
<!-- 主内容区 -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">
<div class="flex items-center justify-between px-6 h-14">
<div class="flex items-center space-x-4">
<a href="main.html" class="text-gray-500 hover:text-gray-700 flex items-center">
<i class="fa fa-arrow-left"></i>
<span class="ml-1">上一步</span>
</a>
</div>
<div class="flex items-center space-x-4">
<div class="relative group">
<img src="https://picsum.photos/id/1005/32/32" class="w-8 h-8 rounded-full cursor-pointer" alt="用户头像">
<div class="absolute right-0 top-full mt-2 bg-white rounded shadow-lg py-1 hidden group-hover:block border border-gray-100 min-w-[140px]">
<a href="login.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap">
<i class="fa fa-sign-out mr-1"></i>退出登录
</a>
</div>
</div>
</div>
</div>
</header>
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
<!-- 页面标题 -->
<div class="bg-white rounded-lg shadow-sm w-full p-4 border-b border-gray-100 mb-4">
<div class="flex items-center text-sm">
<span class="text-primary cursor-pointer hover:underline" onclick="window.location.href='main.html'">模型调优</span>
<span class="mx-2 text-gray-300">/</span>
<span class="text-gray-800 font-medium">创建训练任务</span>
</div>
</div>
<!-- 表单内容 -->
<div class="bg-white rounded-lg shadow-sm w-full">
<div class="p-6">
<form id="createForm">
<!-- 基本信息 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">基本信息</h3>
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-3">任务名称</label>
<div>
<input type="text" name="name" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入任务名称" maxlength="50">
<p class="text-xs text-gray-400 mt-1"><span id="nameCount">0</span> / 50</p>
</div>
</div>
</div>
<!-- 训练配置 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练配置</h3>
<!-- GPU硬件选择 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">
<span class="text-red-500 mr-1">*</span>GPU硬件选择
</label>
<div id="gpuSelectionArea" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- GPU列表将通过JS动态加载 -->
<div class="text-sm text-gray-400 col-span-full">正在加载GPU设备...</div>
</div>
<p class="text-xs text-gray-400 mt-2">支持选择多个GPU进行训练多卡训练</p>
</div>
<!-- 训练方式 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方式</label>
<div class="grid grid-cols-3 gap-4">
<div class="card-radio active" data-value="SFT">
<div class="flex items-start">
<input type="radio" name="train_type" value="SFT" checked class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">SFT 微调训练</div>
<div class="text-xs text-gray-400 mt-1">在监督指令下,增强模型指令跟随的能力,提升全参数微调训练方式</div>
</div>
</div>
</div>
<div class="card-radio" data-value="DPO">
<div class="flex items-start">
<input type="radio" name="train_type" value="DPO" class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">DPO 偏好训练</div>
<div class="text-xs text-gray-400 mt-1">引入人类反馈,降低幻觉,使得模型输出更符合人类偏好</div>
</div>
</div>
</div>
<div class="card-radio" data-value="CPT">
<div class="flex items-start">
<input type="radio" name="train_type" value="CPT" class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">CPT 继续预训练</div>
<div class="text-xs text-gray-400 mt-1">通过无标注数据进行无监督继续训练,强化或新增模型特定能力</div>
</div>
</div>
</div>
</div>
</div>
<!-- 选择模型 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">选择模型</label>
<div class="flex items-center">
<select name="base_model" id="baseModelSelect" class="form-select flex-1 max-w-md">
<option value="">请选择模型</option>
</select>
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadModels()">
<i class="fa fa-refresh"></i>
</button>
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='model-manage-create.html?from=fine-tune'">
+ 新增模型
</button>
</div>
</div>
<!-- 训练方法 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方法</label>
<div class="flex items-center space-x-6">
<label class="flex items-center cursor-pointer">
<input type="radio" name="train_method" value="lora" class="mr-2" onchange="toggleTrainMethod()" checked>
<span class="text-sm">高效训练</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="radio" name="train_method" value="full" class="mr-2" onchange="toggleTrainMethod()">
<span class="text-sm">全参训练</span>
</label>
</div>
</div>
<!-- 训练方法参数配置 -->
<div id="trainMethodParams" class="mb-6">
<div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 bg-gray-50 border-b border-gray-200">
<div class="flex items-center">
<i class="fa fa-sliders text-primary mr-2"></i>
<label class="block text-sm font-medium text-gray-700">训练参数配置</label>
</div>
<div class="flex items-center space-x-3">
<button type="button" onclick="resetParams()" class="text-gray-500 hover:text-primary text-sm flex items-center transition-colors" title="恢复默认配置">
<i class="fa fa-rotate-left mr-1"></i>
<span>重置</span>
</button>
<button type="button" onclick="toggleParamsCollapse()" class="text-gray-500 hover:text-primary text-sm flex items-center transition-colors">
<span id="paramsToggleText">收起</span>
<i id="paramsToggleIcon" class="fa fa-chevron-up ml-1 text-xs transition-transform"></i>
</button>
</div>
</div>
<div id="paramsContent" class="overflow-x-auto">
<table class="w-full text-sm divide-y divide-gray-100">
<thead class="bg-gradient-to-r from-blue-50 to-indigo-50">
<tr>
<th class="text-left py-3 px-4 font-medium text-gray-600">参数名称</th>
<th class="text-left py-3 px-4 font-medium text-gray-600">参数值</th>
<th class="text-left py-3 px-4 font-medium text-gray-600">取值范围</th>
<th class="text-left py-3 px-4 font-medium text-gray-600">参数说明</th>
</tr>
</thead>
<tbody id="commonParamsBody" class="divide-y divide-gray-100">
<!-- 公共参数 -->
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">batch_size</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="batch_size" value="1" min="1" max="64" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[1, 64]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">单步训练样本数量,数值越大训练速度越快,但显存占用越高</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">learning_rate</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="learning_rate" value="0.0001" step="0.00001" min="0.000001" max="1" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0.000001, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">模型参数更新步长,过大可能导致训练不稳定,过小收敛速度慢</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">n_epochs</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="n_epochs" value="1" min="1" max="100" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[1, 100]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">完整遍历训练数据集的次数建议设置在1-10之间</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">eval_steps</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="eval_steps" value="100" min="10" max="10000" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[10, 10000]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">每训练多少步进行一次模型评估建议设置为100的倍数</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lr_scheduler_type</span>
</td>
<td class="py-3 px-4">
<select name="lr_scheduler_type" class="w-28 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
<option value="cosine">cosine</option>
<option value="linear">linear</option>
<option value="constant">constant</option>
</select>
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">3种可选</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">学习率变化策略cosine为余弦退火linear为线性下降constant为保持不变</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">max_length</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="max_length" value="512" min="64" max="4096" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[64, 4096]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">单条训练数据的最大token数超出部分将被截断</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">warmup_ratio</span>
</td>
<td class="py-3 px-4">
<input type="number" name="warmup_ratio" value="0.05" step="0.01" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">学习率预热步数占总步数的比例设置为0则不预热</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">weight_decay</span>
</td>
<td class="py-3 px-4">
<input type="number" name="weight_decay" value="0.01" step="0.001" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">防止过拟合的正则化技术,值越大对模型参数约束越强</td>
</tr>
</tbody>
<tbody id="loraParamsBody" class="hidden divide-y divide-gray-100">
<!-- LoRA参数 -->
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lora_alpha</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<select name="lora_alpha" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
<option value="16" selected>16</option>
<option value="32">32</option>
<option value="64">64</option>
<option value="128">128</option>
</select>
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">4种可选</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA缩放系数用于控制低秩适配矩阵的权重影响模型对微调数据的敏感度</td>
</tr>
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lora_dropout</span>
</td>
<td class="py-3 px-4">
<input type="number" name="lora_dropout" value="0.1" step="0.01" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA层 dropout 概率,在低秩适配矩阵中随机丢弃部分神经元以防止过拟合</td>
</tr>
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lora_rank</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<select name="lora_rank" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
<option value="8" selected>8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
</select>
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">4种可选</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA低秩矩阵的秩值越大表示低秩矩阵的维度越高微调能力越强</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 数据配置 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">数据配置</h3>
<!-- 训练集 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-1">
<span class="text-red-500 mr-1">*</span>训练集
</label>
<div class="flex items-center">
<select name="dataset_id" id="trainDatasetSelect" class="form-select flex-1 max-w-md">
<option value="">请选择训练数据集</option>
</select>
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadDatasets()">
<i class="fa fa-refresh"></i>
</button>
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='dataset-create.html?from=fine-tune'">
+ 新增数据集
</button>
</div>
</div>
<!-- 验证集 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">验证集 <span class="text-red-500">*</span></label>
<div class="flex items-center space-x-6 mb-3">
<label class="flex items-center">
<input type="radio" name="valid_split" value="auto" checked class="mr-2" onchange="toggleValidSplit()">
<span class="text-sm">自动切分</span>
</label>
<label class="flex items-center">
<input type="radio" name="valid_split" value="custom" class="mr-2" onchange="toggleValidSplit()">
<span class="text-sm">选择数据集</span>
</label>
</div>
<div id="autoSplitSection" class="flex items-center">
<span class="text-sm text-gray-600 mr-2">从当前训练集随机分割</span>
<input type="number" name="valid_ratio" value="10" class="w-16 px-2 py-1 border border-gray-300 rounded text-sm text-center focus:border-primary focus:outline-none">
<span class="text-sm text-gray-600 ml-2">% 作为验证集</span>
</div>
<div id="customSplitSection" class="hidden">
<div class="flex items-center">
<select name="valid_dataset_id" id="validDatasetSelect" class="form-select flex-1 max-w-md">
<option value="">请选择验证数据集</option>
</select>
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadDatasets()">
<i class="fa fa-refresh"></i>
</button>
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='dataset-create.html?from=fine-tune'">
+ 新增数据集
</button>
</div>
</div>
</div>
</div>
<!-- 训练产出 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练产出</h3>
<!-- 模型名称 -->
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-3">模型名称</label>
<div>
<input type="text" name="output_model_name" class="w-64 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入模型名称" maxlength="50">
<p class="text-xs text-gray-400 mt-1"><span id="modelNameCount">0</span> / 50</p>
</div>
</div>
<!-- 模型加密 -->
<div class="mb-4">
<div class="flex items-center">
<span class="text-sm text-gray-600 mr-2">模型加密</span>
<span class="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded">安全升级</span>
</div>
<p class="text-xs text-gray-400 mt-1">为保障您的数据安全,平台会为导出的模型文件开启 OSS 服务端加密</p>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
<div class="flex items-center space-x-3">
<button type="button" onclick="submitForm()" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90">
开始训练
</button>
<a href="main.html" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300">
取消
</a>
</div>
<div class="flex items-center text-sm">
<a href="#" class="text-primary hover:underline">训练费用 (预估)</a>
<span class="mx-2 text-gray-300">|</span>
<a href="#" class="text-primary hover:underline">计算详情</a>
</div>
</div>
</form>
</div>
</div>
</main>
</div>
<script>
// API 基础地址 - 使用 config.yaml 中的 app.port (7861)
const getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:7861/api`;
};
const API_BASE = getApiBase();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 卡片式单选框
document.querySelectorAll('.card-radio').forEach(card => {
card.addEventListener('click', () => {
const parent = card.parentElement;
parent.querySelectorAll('.card-radio').forEach(c => c.classList.remove('active'));
card.classList.add('active');
card.querySelector('input').checked = true;
});
});
// 任务名称字数统计
const nameInput = document.querySelector('input[name="name"]');
nameInput.addEventListener('input', () => {
document.getElementById('nameCount').textContent = nameInput.value.length;
});
// 模型名称字数统计
const modelNameInput = document.querySelector('input[name="output_model_name"]');
modelNameInput.addEventListener('input', () => {
document.getElementById('modelNameCount').textContent = modelNameInput.value.length;
});
// 加载数据集列表
loadDatasets();
// 加载模型列表
loadModels();
// 初始化训练方法参数显示
toggleTrainMethod();
// 加载GPU列表
loadGPUList();
// 设置侧边栏当前页高亮
const currentPage = 'fine-tune';
document.querySelectorAll('.nav-link').forEach(link => {
if (link.dataset.page === currentPage) {
link.classList.add('bg-[#1890ff]/10', 'text-[#1890ff]');
link.classList.remove('hover:bg-[#001529]/20', 'transition-colors');
}
});
updateSidebarSlider();
// 绑定导航点击事件
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
if (!this.href.includes('fine-tune-create')) {
e.preventDefault();
window.location.href = this.href;
}
});
});
});
// 更新侧边栏滑块位置
function updateSidebarSlider() {
const slider = document.getElementById('sidebar-slider');
if (!slider) return;
const activeLink = document.querySelector('.nav-link.bg-\\[\\#1890ff\\]\\/10');
if (activeLink) {
const wrapper = activeLink.closest('.nav-item-wrapper');
if (wrapper) {
slider.style.top = wrapper.offsetTop + 'px';
slider.style.height = wrapper.offsetHeight + 'px';
}
}
}
// 切换验证集切分方式
function toggleValidSplit() {
const validSplit = document.querySelector('input[name="valid_split"]:checked').value;
const autoSection = document.getElementById('autoSplitSection');
const customSection = document.getElementById('customSplitSection');
if (validSplit === 'auto') {
autoSection.classList.remove('hidden');
customSection.classList.add('hidden');
} else {
autoSection.classList.add('hidden');
customSection.classList.remove('hidden');
}
}
// 切换训练方法 - 显示/隐藏LoRA参数
function toggleTrainMethod() {
const trainMethod = document.querySelector('input[name="train_method"]:checked').value;
const loraParamsBody = document.getElementById('loraParamsBody');
if (trainMethod === 'lora') {
loraParamsBody.classList.remove('hidden');
} else {
loraParamsBody.classList.add('hidden');
}
}
// 切换参数配置展开/收起
function toggleParamsCollapse() {
const content = document.getElementById('paramsContent');
const toggleText = document.getElementById('paramsToggleText');
const toggleIcon = document.getElementById('paramsToggleIcon');
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
toggleText.textContent = '收起';
toggleIcon.className = 'fa fa-chevron-up ml-1 text-xs';
} else {
content.classList.add('hidden');
toggleText.textContent = '展开';
toggleIcon.className = 'fa fa-chevron-down ml-1 text-xs';
}
}
// 重置参数为默认值
function resetParams() {
const defaults = {
'batch_size': 1,
'learning_rate': 0.0001,
'n_epochs': 1,
'eval_steps': 100,
'lr_scheduler_type': 'cosine',
'max_length': 512,
'warmup_ratio': 0.05,
'weight_decay': 0.01,
'lora_alpha': '32',
'lora_dropout': 0.1,
'lora_rank': '8'
};
for (const [name, value] of Object.entries(defaults)) {
const input = document.querySelector(`input[name="${name}"]`);
if (input) {
if (input.type === 'number') {
input.value = value;
} else if (input.type === 'select-one') {
input.value = value;
}
}
const select = document.querySelector(`select[name="${name}"]`);
if (select) select.value = value;
}
}
// 渲染数据集单选列表
function renderDatasetRadio(datasets, containerId, name, selectedId) {
const container = document.getElementById(containerId);
if (!datasets || datasets.length === 0) {
container.innerHTML = '<span class="text-sm text-gray-400">暂无可用数据集</span>';
return;
}
container.innerHTML = datasets.map(d => `
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="${name}" value="${d.id}" ${d.id === selectedId ? 'checked' : ''} class="text-primary focus:ring-primary">
<span class="text-sm text-gray-700">${d.name}</span>
</label>
`).join('');
}
// 加载数据集列表
async function loadDatasets() {
try {
const response = await fetch(`${API_BASE}/dataset-manage`);
const result = await response.json();
if (result.code === 0) {
// 更新训练集下拉框
const trainSelect = document.getElementById('trainDatasetSelect');
if (trainSelect) {
trainSelect.innerHTML = '<option value="">请选择训练数据集</option>' +
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
}
// 更新验证集下拉框
const validSelect = document.getElementById('validDatasetSelect');
if (validSelect) {
validSelect.innerHTML = '<option value="">请选择验证数据集</option>' +
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
}
}
} catch (e) {
console.error('加载数据集失败:', e);
}
}
// 加载模型列表
async function loadModels() {
try {
const response = await fetch(`${API_BASE}/model-manage`);
const result = await response.json();
if (result.code === 0) {
const modelSelect = document.getElementById('baseModelSelect');
if (modelSelect) {
modelSelect.innerHTML = '<option value="">请选择模型</option>' +
result.data.map(m => `<option value="${m.id}">${m.name}</option>`).join('');
}
}
} catch (e) {
console.error('加载模型失败:', e);
}
}
// 加载GPU列表
async function loadGPUList() {
const container = document.getElementById('gpuSelectionArea');
if (!container) return;
try {
// 模拟GPU数据 - 实际项目中可以从API获取
const gpuData = await fetchGPUs();
renderGPUList(gpuData);
} catch (error) {
console.error('加载GPU列表失败:', error);
container.innerHTML = '<div class="text-sm text-red-500 col-span-full">加载GPU设备失败请刷新重试</div>';
}
}
// 获取GPU数据模拟数据实际可从API获取
async function fetchGPUs() {
// 实际项目中可以调用后端API获取GPU信息
// const response = await fetch(`${API_BASE}/gpus`);
// return await response.json();
// 模拟GPU数据
return [
{ id: 'gpu0', name: 'NVIDIA A100 80GB', memory: '80GB', cuda_cores: 6912, available: true },
{ id: 'gpu1', name: 'NVIDIA A100 80GB', memory: '80GB', cuda_cores: 6912, available: true },
{ id: 'gpu2', name: 'NVIDIA A100 40GB', memory: '40GB', cuda_cores: 6912, available: true },
{ id: 'gpu3', name: 'NVIDIA A100 40GB', memory: '40GB', cuda_cores: 6912, available: false },
{ id: 'gpu4', name: 'NVIDIA V100 32GB', memory: '32GB', cuda_cores: 5120, available: true },
{ id: 'gpu5', name: 'NVIDIA V100 16GB', memory: '16GB', cuda_cores: 5120, available: false },
{ id: 'gpu6', name: 'NVIDIA RTX 3090', memory: '24GB', cuda_cores: 10496, available: true },
{ id: 'gpu7', name: 'NVIDIA RTX 4090', memory: '24GB', cuda_cores: 16384, available: true }
];
}
// 渲染GPU列表点击卡片选中无需复选框
function renderGPUList(gpus) {
const container = document.getElementById('gpuSelectionArea');
if (!container) return;
container.innerHTML = gpus.map(gpu => `
<div id="gpu_card_${gpu.id}"
class="gpu-card ${!gpu.available ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:border-primary'} border rounded-lg p-3 transition-all"
onclick="toggleGPUSelection('${gpu.id}')"
${!gpu.available ? 'title="该GPU不可用"' : ''}
data-gpu-id="${gpu.id}">
<div class="flex items-center">
<i class="fa fa-microchip text-primary mr-2"></i>
<div class="flex-1">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">${gpu.name}</span>
${gpu.available
? '<i class="fa fa-check-circle text-green-600 text-xs"></i>'
: '<i class="fa fa-times-circle text-red-500 text-xs"></i>'}
</div>
<div class="text-xs text-gray-500 mt-1">
<span class="mr-2"><i class="fa fa-floppy-o mr-1"></i>${gpu.memory}</span>
<span><i class="fa fa-cog mr-1"></i>${gpu.cuda_cores} CUDA</span>
</div>
</div>
</div>
</div>
`).join('');
}
// 切换GPU选择状态
function toggleGPUSelection(gpuId) {
const card = document.getElementById(`gpu_card_${gpuId}`);
if (!card || card.classList.contains('opacity-50')) return;
// 切换选中状态
if (card.classList.contains('border-primary')) {
// 取消选中
card.classList.remove('border-primary', 'bg-blue-50');
card.querySelector('.fa-check-circle').classList.replace('text-primary', 'text-green-600');
} else {
// 选中
card.classList.add('border-primary', 'bg-blue-50');
// 移除检查图标,添加选中标记
const icon = card.querySelector('.fa-check-circle');
if (icon) {
icon.classList.remove('fa-check-circle', 'text-green-600');
icon.classList.add('fa-check', 'text-primary');
}
}
}
// 获取选中的GPU列表
function getSelectedGPUs() {
const cards = document.querySelectorAll('.gpu-card.border-primary');
return Array.from(cards).map(card => card.dataset.gpuId);
}
// 提交表单
async function submitForm() {
const form = document.getElementById('createForm');
const formData = new FormData(form);
const validSplit = formData.get('valid_split');
// 获取选中的GPU
const selectedGPUs = getSelectedGPUs();
const data = {
name: formData.get('name'),
base_model: formData.get('base_model'),
train_type: formData.get('train_type'),
train_method: formData.get('train_method'),
gpus: selectedGPUs, // 添加GPU选择
train_dataset_id: formData.get('train_dataset_id'),
valid_split: validSplit,
valid_ratio: parseInt(formData.get('valid_ratio')) || 10,
valid_dataset_id: validSplit === 'custom' ? formData.get('valid_dataset_id') : null,
output_model_name: formData.get('output_model_name'),
status: 'pending',
progress: 0
};
if (!data.name) {
showMessage('提示', '请输入任务名称', 'warning');
return;
}
if (selectedGPUs.length === 0) {
showMessage('提示', '请选择至少一个GPU硬件', 'warning');
return;
}
if (!data.base_model) {
showMessage('提示', '请选择基础模型', 'warning');
return;
}
if (!data.train_dataset_id) {
showMessage('提示', '请选择训练集', 'warning');
return;
}
if (validSplit === 'custom' && !data.valid_dataset_id) {
showMessage('提示', '请选择验证集', 'warning');
return;
}
try {
const response = await fetch(`${API_BASE}/fine-tune`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.code === 0) {
showMessage('成功', '创建成功!', 'success', () => {
window.location.href = 'main.html';
});
} else {
showMessage('错误', result.message || '创建失败', 'error');
}
} catch (error) {
showMessage('错误', '创建失败: ' + error.message, 'error');
}
}
</script>
<!-- 自定义消息弹窗 -->
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="if(event.target === this) closeModal();">
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden transform transition-all">
<div class="flex flex-col items-center justify-center min-h-[160px] py-6">
<div id="modalIcon"></div>
<h3 id="modalTitle" class="text-base font-medium text-gray-800 mb-2"></h3>
<p id="modalMessage" class="text-gray-600 text-sm"></p>
</div>
<div id="modalBtnGroup" class="hidden px-6 pb-6 flex flex-col space-y-2 mx-4">
<button id="modalConfirmBtn" class="px-4 py-2 w-full text-white rounded transition-colors text-sm">确定</button>
<button id="modalCancelBtn" class="px-4 py-2 w-full border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors text-sm">取消</button>
</div>
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
<button id="modalConfirmBtn2" class="px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]">确定</button>
</div>
</div>
</div>
<script>
// 显示消息弹窗
function showMessage(title, message, type = 'info', onConfirm) {
const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
const modalConfirmBtn2 = document.getElementById('modalConfirmBtn2');
const modalBtnGroup = document.getElementById('modalBtnGroup');
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
modalTitle.textContent = title;
modalTitle.className = 'text-lg font-medium text-gray-800 mb-2';
if (type === 'success') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-xl text-green-600"></i></div>';
} else if (type === 'error') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-xl text-red-600"></i></div>';
} else if (type === 'warning') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
} else {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-xl text-blue-600"></i></div>';
}
modalMessage.innerHTML = message;
modalBtnGroup.classList.add('hidden');
modalSingleBtnGroup.classList.remove('hidden');
const confirmBtn = modalConfirmBtn2;
confirmBtn.className = 'px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]';
if (type === 'success') {
confirmBtn.classList.add('bg-primary');
} else if (type === 'error') {
confirmBtn.classList.add('bg-red-500');
} else if (type === 'warning') {
confirmBtn.classList.add('bg-yellow-500');
} else {
confirmBtn.classList.add('bg-primary');
}
confirmBtn.onclick = () => {
closeModal();
if (onConfirm) onConfirm();
};
modal.classList.remove('hidden');
}
function closeModal() {
const modal = document.getElementById('customModal');
modal.classList.add('hidden');
}
</script>
</body>
</html>