feat: 初始化 GPU Monitor 项目
搭建基于 Flask + SocketIO 的 GPU 集群实时监控系统,包含远程采集、定时调度、Web 前端可视化及项目文档。
这个提交包含在:
+201
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>A100 GPU 实时监控系统</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
.gpu-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.gpu-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.status-dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-online { background-color: #10B981; }
|
||||
.status-offline { background-color: #EF4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-slate-100 min-h-screen p-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<header class="flex justify-between items-center mb-10">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">GPU Cluster Monitor</h1>
|
||||
<p class="text-slate-400">实时监控 NVIDIA A100 状态</p>
|
||||
</div>
|
||||
<div id="connection-status" class="flex items-center bg-slate-800 px-4 py-2 rounded-full text-sm">
|
||||
<span id="status-dot" class="status-dot status-offline"></span>
|
||||
<span id="status-text">连接中...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 服务器列表区域 -->
|
||||
<div id="servers-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- 动态生成服务器卡片 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPU 详情模态框 (用于展示图表) -->
|
||||
<div id="gpu-modal" class="fixed inset-0 bg-black/80 hidden flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-slate-800 rounded-2xl max-w-4xl w-full p-6 relative">
|
||||
<button id="close-modal" class="absolute top-4 right-4 text-slate-400 hover:text-white text-2xl">×</button>
|
||||
<h2 id="modal-title" class="text-2xl font-bold mb-6">GPU 详情</h2>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div id="modal-metrics" class="grid grid-cols-2 gap-4">
|
||||
<!-- 实时数值 -->
|
||||
</div>
|
||||
<div class="h-64">
|
||||
<canvas id="gpuChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const socket = io();
|
||||
let charts = {}; // 存储每个 GPU 的图表实例
|
||||
|
||||
// 更新连接状态
|
||||
socket.on('connect', () => {
|
||||
document.getElementById('status-dot').className = 'status-dot status-online';
|
||||
document.getElementById('status-text').textContent = '连接正常';
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
document.getElementById('status-dot').className = 'status-dot status-offline';
|
||||
document.getElementById('status-text').textContent = '连接中断';
|
||||
});
|
||||
|
||||
// 处理接收到的 GPU 数据
|
||||
socket.on('gpu_update', (data) => {
|
||||
const container = document.getElementById('servers-container');
|
||||
|
||||
for (const [serverAlias, gpus] of Object.entries(data)) {
|
||||
let serverCard = document.getElementById(`server-${serverAlias}`);
|
||||
if (!serverCard) {
|
||||
serverCard = createServerCard(serverAlias, gpus);
|
||||
container.appendChild(serverCard);
|
||||
}
|
||||
updateServerCard(serverCard, serverAlias, gpus);
|
||||
}
|
||||
});
|
||||
|
||||
function createServerCard(alias, gpus) {
|
||||
const div = document.createElement('div');
|
||||
div.id = `server-${alias}`;
|
||||
div.className = 'gpu-card bg-slate-800 rounded-2xl p-6 border border-slate-700';
|
||||
div.innerHTML = `
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold">${alias}</h3>
|
||||
<span class="text-xs text-slate-400">A100 Cluster</span>
|
||||
</div>
|
||||
<div id="gpus-${alias}" class="space-y-4"></div>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
function updateServerCard(card, alias, gpus) {
|
||||
const gpuContainer = document.getElementById(`gpus-${alias}`);
|
||||
if (!gpus) {
|
||||
gpuContainer.innerHTML = `
|
||||
<div class="flex items-center justify-center p-4 bg-slate-900/50 rounded-lg text-red-400 text-sm">
|
||||
<span class="status-dot status-offline"></span> 无法连接服务器
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
gpuContainer.innerHTML = '';
|
||||
gpus.forEach((gpu, index) => {
|
||||
const utilColor = gpu.util >= 80 ? 'text-red-400' : (gpu.util >= 50 ? 'text-yellow-400' : 'text-green-400');
|
||||
gpuContainer.innerHTML += `
|
||||
<div class="p-3 bg-slate-900/50 rounded-lg border border-slate-700 hover:bg-slate-700 cursor-pointer transition-colors"
|
||||
onclick="openGpuDetail('${alias}', ${index})">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-sm font-medium text-slate-300">GPU ${gpu.index} [${gpu.name}]</span>
|
||||
<span class="text-sm font-bold ${utilColor}">${gpu.util}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-700 h-2 rounded-full overflow-hidden">
|
||||
<div class="bg-green-500 h-full transition-all duration-500" style="width: ${gpu.util}%"></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-slate-400 mt-2">
|
||||
<span>Temp: ${gpu.temp}°C</span>
|
||||
<span>Mem: ${gpu.mem_used}/${gpu.mem_total} MiB</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function openGpuDetail(serverAlias, gpuIndex) {
|
||||
const modal = document.getElementById('gpu-modal');
|
||||
const title = document.getElementById('modal-title');
|
||||
const metrics = document.getElementById('modal-metrics');
|
||||
const chartCanvas = document.getElementById('gpuChart');
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
title.textContent = `Server: ${serverAlias} - GPU ${gpuIndex}`;
|
||||
|
||||
metrics.innerHTML = `
|
||||
<div class="p-4 bg-slate-900 rounded-lg text-center">
|
||||
<span class="text-slate-400 text-xs block mb-1">GPU 利用率</span>
|
||||
<span class="text-2xl font-bold text-green-400">实时更新中...</span>
|
||||
</div>
|
||||
<div class="p-4 bg-slate-900 rounded-lg text-center">
|
||||
<span class="text-slate-400 text-xs block mb-1">GPU 温度</span>
|
||||
<span class="text-2xl font-bold text-green-400">实时更新中...</span>
|
||||
</div>
|
||||
<div class="p-4 bg-slate-900 rounded-lg text-center">
|
||||
<span class="text-slate-400 text-xs block mb-1">显存占用</span>
|
||||
<span class="text-2xl font-bold text-green-400">实时更新中...</span>
|
||||
</div>
|
||||
<div class="p-4 bg-slate-900 rounded-lg text-center">
|
||||
<span class="text-slate-400 text-xs block mb-1">GPU 频率</span>
|
||||
<span class="text-green-400 text-2xl font-bold">实时更新中...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (charts.gpuChart) {
|
||||
charts.gpuChart.destroy();
|
||||
}
|
||||
|
||||
const ctx = chartCanvas.getContext('2d');
|
||||
Chart.defaults.color = '#94a3b8';
|
||||
charts.gpuChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'GPU Utilization (%)',
|
||||
borderColor: '#10B981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: { beginAtZero: true, max: 100 }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('close-modal').onclick = () => {
|
||||
document.getElementById('gpu-modal').classList.add('hidden');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
在新工单中引用
屏蔽一个用户