365 lines
15 KiB
JavaScript
365 lines
15 KiB
JavaScript
// 标签切换功能
|
||
function openTab(evt, tabName) {
|
||
var i, tabContent, tabLinks;
|
||
tabContent = document.getElementsByClassName("tab-content");
|
||
for (i = 0; i < tabContent.length; i++) {
|
||
tabContent[i].classList.add("hidden");
|
||
}
|
||
tabLinks = document.getElementsByClassName("tab");
|
||
for (i = 0; i < tabLinks.length; i++) {
|
||
tabLinks[i].classList.remove("active");
|
||
}
|
||
document.getElementById(tabName).classList.remove("hidden");
|
||
evt.currentTarget.classList.add("active");
|
||
|
||
// 当切换到其他标签页时,清除所有日志自动更新定时器
|
||
if (tabName !== 'processes') {
|
||
for (let processId in logTimers) {
|
||
clearInterval(logTimers[processId]);
|
||
delete logTimers[processId];
|
||
}
|
||
}
|
||
|
||
// 如果切换到进程页面,刷新进程列表
|
||
if (tabName === 'processes') {
|
||
loadProcesses();
|
||
} else if (tabName === 'logfiles') {
|
||
loadLogFiles();
|
||
}
|
||
}
|
||
|
||
// 根据训练类型显示/隐藏特定参数
|
||
document.getElementById('train_type').addEventListener('change', function() {
|
||
const trainType = this.value;
|
||
const pretrainSftFields = document.querySelectorAll('.pretrain-sft');
|
||
const loraFields = document.querySelectorAll('.lora');
|
||
const dpoFields = document.querySelectorAll('.dpo');
|
||
const dpoParamCard = document.querySelector('.parameter-card.dpo');
|
||
|
||
pretrainSftFields.forEach(field => {
|
||
field.style.display = (trainType === 'pretrain' || trainType === 'sft') ? 'block' : 'none';
|
||
});
|
||
|
||
loraFields.forEach(field => {
|
||
field.style.display = trainType === 'lora' ? 'block' : 'none';
|
||
});
|
||
|
||
dpoFields.forEach(field => {
|
||
field.style.display = trainType === 'dpo' ? 'block' : 'none';
|
||
});
|
||
|
||
if (dpoParamCard) {
|
||
dpoParamCard.style.display = trainType === 'dpo' ? 'block' : 'none';
|
||
}
|
||
|
||
// 设置默认值
|
||
if (trainType === 'pretrain') {
|
||
document.getElementById('save_dir').value = '../out';
|
||
document.getElementById('save_weight').value = 'pretrain';
|
||
document.getElementById('epochs').value = '1';
|
||
document.getElementById('batch_size').value = '32';
|
||
document.getElementById('learning_rate').value = '5e-4';
|
||
document.getElementById('data_path').value = '../dataset/pretrain_hq.jsonl';
|
||
document.getElementById('from_weight').value = 'none';
|
||
document.getElementById('log_interval').value = '100';
|
||
document.getElementById('save_interval').value = '100';
|
||
} else if (trainType === 'sft') {
|
||
document.getElementById('save_dir').value = '../out';
|
||
document.getElementById('save_weight').value = 'full_sft';
|
||
document.getElementById('epochs').value = '2';
|
||
document.getElementById('batch_size').value = '16';
|
||
document.getElementById('learning_rate').value = '5e-7';
|
||
document.getElementById('data_path').value = '../dataset/sft_mini_512.jsonl';
|
||
document.getElementById('from_weight').value = 'pretrain';
|
||
document.getElementById('log_interval').value = '100';
|
||
document.getElementById('save_interval').value = '100';
|
||
} else if (trainType === 'lora') {
|
||
document.getElementById('save_dir').value = '../out/lora';
|
||
document.getElementById('lora_name').value = 'lora_identity';
|
||
document.getElementById('epochs').value = '50';
|
||
document.getElementById('batch_size').value = '32';
|
||
document.getElementById('learning_rate').value = '1e-4';
|
||
document.getElementById('data_path').value = '../dataset/lora_identity.jsonl';
|
||
document.getElementById('from_weight').value = 'full_sft';
|
||
document.getElementById('log_interval').value = '10';
|
||
document.getElementById('save_interval').value = '1';
|
||
} else if (trainType === 'dpo') {
|
||
document.getElementById('save_dir').value = '../out';
|
||
document.getElementById('save_weight').value = 'dpo';
|
||
document.getElementById('epochs').value = '1';
|
||
document.getElementById('batch_size').value = '4';
|
||
document.getElementById('learning_rate').value = '4e-8';
|
||
document.getElementById('data_path').value = '../dataset/dpo.jsonl';
|
||
document.getElementById('from_weight').value = 'full_sft';
|
||
document.getElementById('log_interval').value = '100';
|
||
document.getElementById('save_interval').value = '100';
|
||
document.getElementById('beta').value = '0.1';
|
||
}
|
||
});
|
||
|
||
// 初始触发一次change事件以设置默认值
|
||
document.getElementById('train_type').dispatchEvent(new Event('change'));
|
||
|
||
// 加载进程列表
|
||
function loadProcesses() {
|
||
fetch('/processes')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const processList = document.getElementById('process-list');
|
||
processList.innerHTML = '';
|
||
|
||
if (data.length === 0) {
|
||
processList.innerHTML = '<p>暂无训练进程</p>';
|
||
return;
|
||
}
|
||
|
||
data.forEach(process => {
|
||
const processItem = document.createElement('div');
|
||
processItem.className = 'process-item';
|
||
|
||
let statusClass = '';
|
||
// 根据后端返回的status字段设置状态类
|
||
if (process.status === '运行中') {
|
||
statusClass = 'status-running';
|
||
} else if (process.status === '手动停止') {
|
||
statusClass = 'status-manual-stop';
|
||
} else if (process.status === '出错') {
|
||
statusClass = 'status-error';
|
||
} else {
|
||
statusClass = 'status-completed';
|
||
}
|
||
|
||
// 设置进程数据属性,用于后续检查状态
|
||
processItem.dataset.processId = process.id;
|
||
processItem.dataset.processStatus = process.status;
|
||
|
||
processItem.innerHTML = `
|
||
<div class="process-info">
|
||
<div>
|
||
<strong>${process.train_type}</strong> - ${process.start_time}
|
||
</div>
|
||
<div>
|
||
<span class="process-status ${statusClass}">${process.status}</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<button class="btn-logs" onclick="showLogs('${process.id}')">查看日志</button>
|
||
<button class="btn-logs" onclick="refreshLog('${process.id}')">刷新日志</button>
|
||
${process.running ? `<button class="btn-stop" onclick="stopProcess('${process.id}')">停止训练</button>` : ''}
|
||
</div>
|
||
<div id="logs-${process.id}" class="logs-container hidden"></div>
|
||
`;
|
||
|
||
processList.appendChild(processItem);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 存储训练日志定时器的ID
|
||
let logTimers = {};
|
||
|
||
// 显示日志
|
||
function showLogs(processId) {
|
||
const logsContainer = document.getElementById(`logs-${processId}`);
|
||
logsContainer.classList.toggle('hidden');
|
||
|
||
if (!logsContainer.classList.contains('hidden')) {
|
||
// 加载日志内容
|
||
loadLogContent(processId, logsContainer);
|
||
|
||
// 清除可能存在的旧定时器
|
||
if (logTimers[processId]) {
|
||
clearInterval(logTimers[processId]);
|
||
}
|
||
|
||
// 查找进程元素,获取其状态
|
||
const processItem = document.querySelector(`[data-process-id="${processId}"]`);
|
||
const isRunning = processItem && processItem.dataset.processStatus === '运行中';
|
||
|
||
// 只有运行中的进程才设置自动刷新定时器
|
||
if (isRunning) {
|
||
logTimers[processId] = setInterval(() => {
|
||
// 检查日志容器是否可见
|
||
if (!logsContainer.classList.contains('hidden')) {
|
||
// 再次检查进程状态,确保仍然是运行中
|
||
const currentProcessItem = document.querySelector(`[data-process-id="${processId}"]`);
|
||
const stillRunning = currentProcessItem && currentProcessItem.dataset.processStatus === '运行中';
|
||
|
||
if (stillRunning) {
|
||
loadLogContent(processId, logsContainer);
|
||
} else {
|
||
// 如果进程不再运行中,清除定时器
|
||
clearInterval(logTimers[processId]);
|
||
delete logTimers[processId];
|
||
}
|
||
}
|
||
}, 1000);
|
||
}
|
||
} else {
|
||
// 隐藏日志时清除定时器
|
||
if (logTimers[processId]) {
|
||
clearInterval(logTimers[processId]);
|
||
delete logTimers[processId];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 刷新日志
|
||
function refreshLog(processId) {
|
||
const logsContainer = document.getElementById(`logs-${processId}`);
|
||
if (!logsContainer.classList.contains('hidden')) {
|
||
loadLogContent(processId, logsContainer);
|
||
|
||
// 查找进程元素,获取其状态
|
||
const processItem = document.querySelector(`[data-process-id="${processId}"]`);
|
||
const isRunning = processItem && processItem.dataset.processStatus === '运行中';
|
||
|
||
// 重置定时器,确保从现在开始每1秒更新一次
|
||
if (logTimers[processId]) {
|
||
clearInterval(logTimers[processId]);
|
||
}
|
||
|
||
// 只有运行中的进程才设置定时器
|
||
if (isRunning) {
|
||
logTimers[processId] = setInterval(() => {
|
||
if (!logsContainer.classList.contains('hidden')) {
|
||
// 再次检查进程状态
|
||
const currentProcessItem = document.querySelector(`[data-process-id="${processId}"]`);
|
||
const stillRunning = currentProcessItem && currentProcessItem.dataset.processStatus === '运行中';
|
||
|
||
if (stillRunning) {
|
||
loadLogContent(processId, logsContainer);
|
||
} else {
|
||
// 如果进程不再运行中,清除定时器
|
||
clearInterval(logTimers[processId]);
|
||
delete logTimers[processId];
|
||
}
|
||
}
|
||
}, 1000);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载日志内容
|
||
function loadLogContent(processId, logsContainer) {
|
||
fetch(`/logs/${processId}`)
|
||
.then(response => response.text())
|
||
.then(logs => {
|
||
logsContainer.textContent = logs;
|
||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||
});
|
||
}
|
||
|
||
// 停止进程
|
||
function stopProcess(processId) {
|
||
if (confirm('确定要停止这个训练进程吗?')) {
|
||
fetch(`/stop/${processId}`, {
|
||
method: 'POST'
|
||
})
|
||
.then(() => {
|
||
loadProcesses();
|
||
});
|
||
}
|
||
}
|
||
|
||
// 表单提交处理
|
||
document.getElementById('train-form').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
const formData = new FormData(this);
|
||
const data = Object.fromEntries(formData.entries());
|
||
|
||
fetch('/train', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
alert('训练已开始!');
|
||
openTab(event, 'processes');
|
||
} else {
|
||
alert('训练启动失败:' + result.error);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 加载日志文件列表
|
||
function loadLogFiles() {
|
||
fetch('/logfiles')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const logfilesList = document.getElementById('logfiles-list');
|
||
logfilesList.innerHTML = '';
|
||
|
||
if (data.length === 0) {
|
||
logfilesList.innerHTML = '<p>暂无日志文件</p>';
|
||
return;
|
||
}
|
||
|
||
// 按日期倒序排序
|
||
data.sort((a, b) => new Date(b.modified_time) - new Date(a.modified_time));
|
||
|
||
data.forEach(logfile => {
|
||
const fileItem = document.createElement('div');
|
||
fileItem.className = 'process-item';
|
||
|
||
// 从文件名提取训练类型
|
||
let trainType = '未知';
|
||
if (logfile.filename.includes('train_pretrain_')) {
|
||
trainType = 'pretrain';
|
||
} else if (logfile.filename.includes('train_sft_')) {
|
||
trainType = 'sft';
|
||
} else if (logfile.filename.includes('train_lora_')) {
|
||
trainType = 'lora';
|
||
} else if (logfile.filename.includes('train_dpo_')) {
|
||
trainType = 'dpo';
|
||
} else if (logfile.filename.includes('train_ppo_')) {
|
||
trainType = 'ppo';
|
||
} else if (logfile.filename.includes('train_grpo_')) {
|
||
trainType = 'grpo';
|
||
} else if (logfile.filename.includes('train_spo_')) {
|
||
trainType = 'spo';
|
||
}
|
||
|
||
|
||
fileItem.innerHTML = `
|
||
<div class="process-info">
|
||
<div>
|
||
<strong>${trainType}</strong> - ${logfile.filename}
|
||
</div>
|
||
<div>
|
||
<span class="process-status status-completed">已保存</span>
|
||
<span style="margin-left: 10px; color: #999; font-size: 0.9em;">${logfile.modified_time}</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<button class="btn-logs" onclick="viewLogFile('${logfile.filename}', this)">查看日志</button>
|
||
</div>
|
||
<div id="log-content-${logfile.filename.replace(/\./g, '-')}" class="logs-container hidden"></div>
|
||
`;
|
||
|
||
logfilesList.appendChild(fileItem);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 查看日志文件内容
|
||
function viewLogFile(filename, button) {
|
||
const safeFilename = filename.replace(/\./g, '-');
|
||
const logContainer = button.closest('.process-item').querySelector(`#log-content-${safeFilename}`);
|
||
logContainer.classList.toggle('hidden');
|
||
|
||
if (!logContainer.classList.contains('hidden') && logContainer.textContent === '') {
|
||
logContainer.textContent = '加载日志中...';
|
||
|
||
fetch(`/logfile-content/${encodeURIComponent(filename)}`)
|
||
.then(response => response.text())
|
||
.then(logs => {
|
||
logContainer.textContent = logs;
|
||
logContainer.scrollTop = 0;
|
||
});
|
||
}
|
||
} |