Llama 模型管理器 — 开源本地 AI 模型启动管理工具
> 一键启动 / 停止本地 LLM 模型,实时监控系统资源,支持模糊匹配自动发现 BAT 文件。
---
## 项目简介
**Llama 模型管理器** 是一个基于 WinForms (.NET Framework 4.8) 的桌面应用程序,专门用于管理本地 AI 推理服务器。如果你手头有多个 GGUF/GGML 格式的本地模型,每个模型对应一个 `.bat` 启动脚本,那么这个工具就是你的"一站式控制台"。
### 核心功能
| 功能 | 说明 |
|:---|:---|
| 模型卡片列表 | 每个模型一张卡片,显示名称、端口号、运行状态 |
| 一键启动/停止 | 点击 RUN/HALT 按钮,自动查找 BAT 文件并启动进程 |
| 模糊匹配 | 文件名不精确匹配时自动按模型名关键词查找 |
| 智能滚动条 | 卡片超出可视区时自动出现滚动条 |
| 系统监控 | 实时显示 CPU / GPU / 内存 / 磁盘占用率 |
| 日志面板 | 颜色区分的运行日志,自动滚动,上限 1000 条 |
| 进程树终止 | 停止模型时递归杀死所有子进程 |
| 端口检测 | 启动前检查端口是否已被占用 |
| 内嵌资源 | Logo 和图标已嵌入 EXE,单文件运行无需额外依赖 |
---
## 项目结构
```
D:\Qwen3XL\
├── LlamaManager.exe ← 主程序(双击运行,64KB)
├── MainForm.cs ← 完整源码(约 24KB,700 行)
├── app.ico ← 图标源文件
├── logo.png ← Logo 图片源文件
├── DeepSeek-R1-14B服务器.bat
├── Gemma-4-26B 推理服务器.bat
├── Gemma-4-31B服务器.bat
├── ... ← 各模型的启动脚本(共 13 个)
├── Qwopus-9B-Coder服务器.bat
└── Llama模型管理器.lnk ← 桌面快捷方式
```
---
## 技术架构
```
┌─────────────────────────────┐
│ Llama 模型管理器 │
│ (WinForms 窗体) │
├──────────────┬──────────────┤
│ 左侧面板 │ 右侧面板 │
│ ┌────────┐ │ ┌────────┐ │
│ │ MODELS │ │ │ LOG │ │
│ │ (卡片) │ │ │(日志) │ │
│ │ 手动 │ │ │ 颜色 │ │
│ │ 滚动条 │ │ │ 标记 │ │
│ └────────┘ │ └────────┘ │
├──────────────┴──────────────┤
│ 状态栏: 活跃数 / 路径 │
└─────────────────────────────┘
```
- **.NET Framework 4.8** — 利用 WinForms 原生控件
- **System.Management** — WMI 查询 CPU/内存/进程树
- **nvidia-smi** — 获取 GPU 利用率和温度
- **PerformanceCounter** — 实时 CPU 占用率
- **手动 Panel 布局** — 替代 FlowLayoutPanel,消除渲染偏差
---
## 完整代码
```csharp
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Management;
using System.Net.NetworkInformation;
using System.Windows.Forms;
class ModelInfo
{
public string Name { get; set; }
public string BatName { get; set; }
public string DisplayName { get; set; }
public int Port { get; set; }
public Process Proc { get; set; }
public DateTime StartTime { get; set; }
public Label StatusLabel { get; set; }
public Button Btn { get; set; }
public Panel Card { get; set; }
}
class MainForm : Form
{
private ListBox globalLog;
private Timer statsTimer;
private Label cpuVal, gpuVal, memVal, diskVal;
private Label upTime, statusLabel, deckCount;
private Panel cardContainer;
private VScrollBar cardScroll;
private int scrollOffset = 0;
private Label cardCountLabel;
private readonly string baseDir = @"D:\Qwen3XL";
private ModelInfo[] models;
private PerformanceCounter cpuCounter, memCounter;
private readonly Stopwatch uptimeWatch = Stopwatch.StartNew();
private const int CardWidth = 297;
private const int CardHeight = 52;
private const int CardGap = 8;
private static class Colors
{
public static readonly Color Bg = Color.FromArgb(12, 12, 20);
public static readonly Color PanelBg = Color.FromArgb(22, 22, 38);
public static readonly Color CardBg = Color.FromArgb(28, 28, 48);
public static readonly Color CardHover = Color.FromArgb(38, 38, 68);
public static readonly Color Green = Color.FromArgb(0, 220, 150);
public static readonly Color GreenDk = Color.FromArgb(0, 180, 120);
public static readonly Color Red = Color.FromArgb(240, 80, 80);
public static readonly Color Yellow = Color.FromArgb(255, 180, 80);
public static readonly Color Text = Color.FromArgb(240, 240, 255);
public static readonly Color Sub = Color.FromArgb(140, 140, 160);
public static readonly Color Dim = Color.FromArgb(90, 90, 110);
public static readonly Color Border = Color.FromArgb(45, 45, 70);
public const int LeftWidth = 355;
}
public MainForm()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
Text = "llama\u6a21\u578b\u7ba1\u7406\u5668";
Size = new Size(1150, 920);
MinimumSize = new Size(850, 760);
StartPosition = FormStartPosition.CenterScreen;
BackColor = Colors.Bg;
Font = new Font("Segoe UI", 10, FontStyle.Regular);
InitCounters();
LoadModels();
BuildUI();
statsTimer = new Timer { Interval = 2000 };
statsTimer.Tick += (st, et) => UpdateStats();
statsTimer.Start();
Shown += (st, et) => { UpdateScroll(); LayoutCards(); };
FormClosing += (st, et) => { StopAll(); statsTimer.Stop(); };
Resize += (st, et) => { if (WindowState == FormWindowState.Minimized) statsTimer.Stop(); else if (!statsTimer.Enabled) statsTimer.Start(); };
}
private void InitCounters()
{
try
{
cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); cpuCounter.NextValue();
memCounter = new PerformanceCounter("Memory", "% Committed Bytes In Use"); memCounter.NextValue();
}
catch { }
}
private void LoadModels()
{
models = new ModelInfo[] {
NewModel("Gemma-4-26B", "Gemma-4-26B 推理服务器.bat", "Gemma 4 26B", 8082),
NewModel("Gemma-4-31B", "Gemma-4-31B服务器.bat", "Gemma 4 31B", 8083),
NewModel("Gemma-4-E2B", "Gemma-4-E2B服务器.bat", "Gemma 4 E2B", 8084),
NewModel("Gemma-4-E4B", "Gemma-4-E4B服务器.bat", "Gemma 4 E4B", 8085),
NewModel("Granite-4.1-3B-Q4", "Granite-4.1-3B-Q4服务器.bat", "Granite 4.1 3B Q4", 8086),
NewModel("Granite-4.1-8B", "Granite-4.1-8B服务器.bat", "Granite 4.1 8B", 8087),
NewModel("Qwen3.5-0.8B-BF16", "Qwen3.5-0.8B-BF16服务器.bat", "Qwen 3.5 0.8B BF16", 8088),
NewModel("Qwen3.5-2B-BF16", "Qwen3.5-2B-BF16服务器.bat", "Qwen 3.5 2B BF16", 8089),
NewModel("Qwen3.5-4B-Q4", "Qwen3.5-4B-Q4服务器.bat", "Qwen 3.5 4B Q4", 8090),
NewModel("Qwen3.5-4B-Q8", "Qwen3.5-4B-Q8服务器.bat", "Qwen 3.5 4B Q8", 8091),
NewModel("Qwen3.6-27B-Q4", "Qwen3.6-27B-Q4服务器.bat", "Qwen 3.6 27B Q4", 8092),
NewModel("Qwen3.6-35B-Uncensored", "Qwen3.6-35B-Uncensored服务器.bat", "Qwen 3.6 35B Uncensored", 8093),
NewModel("Qwopus-9B-Coder", "Qwopus-9B-Coder服务器.bat", "Qwopus 9B Coder", 8094),
};
}
private ModelInfo NewModel(string name, string bat, string display, int port)
=> new ModelInfo { Name = name, BatName = bat, DisplayName = display, Port = port };
private string FindBat(ModelInfo m)
{
string exact = Path.Combine(baseDir, m.BatName);
if (File.Exists(exact)) return exact;
if (Directory.Exists(baseDir))
{
try
{
foreach (string f in Directory.GetFiles(baseDir, "*.bat"))
if (Path.GetFileName(f).IndexOf(m.Name, StringComparison.OrdinalIgnoreCase) >= 0)
return f;
}
catch { }
}
return null;
}
private void BuildUI()
{
BuildTop();
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1, BackColor = Colors.Bg };
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, Colors.LeftWidth));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
var left = new Panel { Dock = DockStyle.Fill, Padding = new Padding(6, 4, 6, 4) };
var right = new Panel { Dock = DockStyle.Fill, Padding = new Padding(6, 4, 6, 4) };
BuildRight(right);
BuildLeft(left);
layout.Controls.Add(left, 0, 0);
layout.Controls.Add(right, 1, 0);
Controls.Add(layout);
BuildBottom();
}
private void BuildTop()
{
var top = new Panel { Dock = DockStyle.Top, Height = 64, BackColor = Colors.PanelBg, Padding = new Padding(12, 6, 12, 6) };
top.Paint += (st, et) => { using (var p = new Pen(Color.FromArgb(100, Colors.Green), 2)) et.Graphics.DrawLine(p, 0, top.Height - 1, top.Width, top.Height - 1); };
try
{
var asm = System.Reflection.Assembly.GetExecutingAssembly();
using (var s = asm.GetManifestResourceStream("logo.png"))
if (s != null) top.Controls.Add(new PictureBox { Image = Image.FromStream(s), SizeMode = PictureBoxSizeMode.Zoom, Location = new Point(4, 2), Size = new Size(170, 54), BackColor = Color.Transparent });
}
catch { }
deckCount = new Label { Text = "", ForeColor = Colors.Sub, Font = new Font("Consolas", 8), Location = new Point(182, 42), AutoSize = true };
top.Controls.Add(deckCount);
var sf = new FlowLayoutPanel { Anchor = AnchorStyles.Top | AnchorStyles.Right, Location = new Point(360, 6), Size = new Size(460, 46), BackColor = Color.Transparent, WrapContents = false };
sf.Controls.AddRange(new Control[] { StatBox("CPU", out cpuVal), StatBox("GPU", out gpuVal), StatBox("MEM", out memVal), StatBox("DSK", out diskVal) });
top.Controls.Add(sf);
upTime = new Label { Text = "UPTIME: --", ForeColor = Colors.Sub, Font = new Font("Consolas", 8), Location = new Point(360, 46), AutoSize = true };
top.Controls.Add(upTime);
Controls.Add(top);
}
private Panel StatBox(string title, out Label val)
{
val = new Label { Text = "--", ForeColor = Colors.Green, Font = new Font("Consolas", 12, FontStyle.Bold), Location = new Point(8, 22), AutoSize = true };
var box = new Panel { Size = new Size(130, 42), Margin = new Padding(4, 0, 4, 0), BackColor = Color.FromArgb(35, 35, 58) };
box.Paint += (st, et) => { using (var p = new Pen(Colors.Border, 1)) et.Graphics.DrawRectangle(p, 0, 0, box.Width - 1, box.Height - 1); };
box.Controls.Add(new Label { Text = title, ForeColor = Colors.Dim, Font = new Font("Consolas", 8), Location = new Point(8, 4), AutoSize = true });
box.Controls.Add(val);
return box;
}
private void BuildLeft(Control panel)
{
panel.BackColor = Colors.Bg;
var hdr = new Panel { Dock = DockStyle.Top, Height = 26, BackColor = Colors.Bg };
hdr.Controls.Add(new Label { Text = "MODELS", Font = new Font("Consolas", 10, FontStyle.Bold), ForeColor = Colors.Green, Location = new Point(4, 2), AutoSize = true });
cardCountLabel = new Label { Text = "", ForeColor = Colors.Yellow, Font = new Font("Consolas", 8), Location = new Point(100, 5), AutoSize = true };
hdr.Controls.Add(cardCountLabel);
panel.Controls.Add(hdr);
cardScroll = new VScrollBar { Dock = DockStyle.Right, Visible = false };
cardScroll.Scroll += (sv, ev) => { scrollOffset = ev.NewValue; LayoutCards(); };
panel.Controls.Add(cardScroll);
cardContainer = new Panel { Dock = DockStyle.Fill, BackColor = Colors.Bg };
panel.Controls.Add(cardContainer);
panel.Resize += (sp, ep) => { UpdateScroll(); LayoutCards(); };
BuildCards();
}
private void UpdateScroll()
{
int total = models.Length * (CardHeight + CardGap) + CardGap;
int view = cardContainer.ClientSize.Height;
if (total > view && view > 0)
{
cardScroll.Visible = true;
cardScroll.Minimum = 0;
cardScroll.Maximum = total;
cardScroll.LargeChange = view;
cardScroll.SmallChange = CardHeight + CardGap;
}
else { cardScroll.Visible = false; scrollOffset = 0; }
}
private void LayoutCards()
{
cardContainer.SuspendLayout();
int y = CardGap - scrollOffset;
foreach (Control c in cardContainer.Controls) { c.Top = y; c.Left = CardGap; y += CardHeight + CardGap; }
cardContainer.ResumeLayout();
}
private void BuildCards()
{
cardContainer.Controls.Clear();
int ok = 0, miss = 0;
for (int i = 0; i < models.Length; i++)
{
ModelInfo m = models[i];
var card = CreateCard(m);
m.Card = card;
cardContainer.Controls.Add(card);
if (m.Btn != null && m.Btn.Enabled) ok++; else miss++;
}
UpdateScroll();
LayoutCards();
if (deckCount != null) deckCount.Text = ok + " ready / " + miss + " missing";
if (cardCountLabel != null) cardCountLabel.Text = cardContainer.Controls.Count + "/" + models.Length + " cards";
}
private Panel CreateCard(ModelInfo m)
{
var card = new Panel { Size = new Size(CardWidth, CardHeight), BackColor = Colors.CardBg };
card.MouseEnter += (st, et) => { if (m.Proc == null) card.BackColor = Colors.CardHover; };
card.MouseLeave += (st, et) => { if (m.Proc == null) card.BackColor = Colors.CardBg; };
bool exists = FindBat(m) != null;
m.StatusLabel = new Label { Text = "*", ForeColor = exists ? Colors.Sub : Color.FromArgb(100, 50, 50), Font = new Font("Segoe UI", 14), Location = new Point(8, 12), Size = new Size(18, 22), TextAlign = ContentAlignment.MiddleCenter };
card.Controls.Add(m.StatusLabel);
card.Controls.Add(new Label { Text = m.DisplayName, ForeColor = Colors.Text, Font = new Font("Segoe UI", 9, FontStyle.Bold), Location = new Point(30, 8), Size = new Size(145, 18), AutoEllipsis = true });
card.Controls.Add(new Label { Text = exists ? ":" + m.Port : "MISSING", ForeColor = exists ? Colors.Sub : Colors.Red, Font = new Font("Consolas", 7), Location = new Point(30, 26), Size = new Size(80, 14) });
m.Btn = new Button { Text = "RUN", FlatStyle = FlatStyle.Flat, BackColor = exists ? Colors.Green : Color.FromArgb(60, 60, 80), ForeColor = Color.White, Font = new Font("Consolas", 8, FontStyle.Bold), Location = new Point(CardWidth - 90, 8), Size = new Size(80, 30), Enabled = exists, Tag = m, Cursor = Cursors.Hand };
m.Btn.FlatAppearance.BorderSize = 1;
m.Btn.FlatAppearance.BorderColor = exists ? Colors.Green : Colors.Dim;
m.Btn.FlatAppearance.MouseOverBackColor = exists ? Colors.GreenDk : Color.FromArgb(70, 70, 90);
m.Btn.FlatAppearance.MouseDownBackColor = exists ? Color.FromArgb(0, 150, 100) : Color.FromArgb(50, 50, 70);
m.Btn.Click += BtnClick;
card.Controls.Add(m.Btn);
return card;
}
private void BtnClick(object sender, EventArgs e)
{
var m = ((Button)sender).Tag as ModelInfo;
if (m == null) return;
if (m.Proc == null || m.Proc.HasExited) StartModel(m); else StopModel(m);
}
private void BuildRight(Control panel)
{
panel.BackColor = Colors.Bg;
panel.Controls.Add(new Label { Text = "LOG", Font = new Font("Consolas", 10, FontStyle.Bold), ForeColor = Color.FromArgb(0, 200, 255), Dock = DockStyle.Top, Height = 24, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(4, 0, 0, 0) });
globalLog = new ListBox { Dock = DockStyle.Fill, BackColor = Color.FromArgb(8, 8, 12), ForeColor = Colors.Text, Font = new Font("Consolas", 9), BorderStyle = BorderStyle.None, IntegralHeight = false, HorizontalScrollbar = true };
panel.Controls.Add(globalLog);
var tb = new Panel { Dock = DockStyle.Bottom, Height = 30, BackColor = Colors.PanelBg, Padding = new Padding(4, 2, 4, 2) };
var clr = new Button { Text = "CLR", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Sub, BackColor = Color.FromArgb(30, 25, 25), Location = new Point(0, 2), Size = new Size(56, 22), Font = new Font("Consolas", 8), Cursor = Cursors.Hand };
clr.Click += (st, et) => globalLog.Items.Clear(); tb.Controls.Add(clr);
var halt = new Button { Text = "HALT", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Red, BackColor = Color.FromArgb(45, 20, 20), Location = new Point(62, 2), Size = new Size(56, 22), Font = new Font("Consolas", 8), Cursor = Cursors.Hand };
halt.Click += (st, et) => StopAll(); tb.Controls.Add(halt);
var rel = new Button { Text = "RELOAD", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Yellow, BackColor = Color.FromArgb(45, 40, 20), Location = new Point(124, 2), Size = new Size(56, 22), Font = new Font("Consolas", 8), Cursor = Cursors.Hand };
rel.Click += (st, et) => { BuildCards(); Log("[OK] Reloaded"); }; tb.Controls.Add(rel);
panel.Controls.Add(tb);
}
private void BuildBottom()
{
var bar = new Panel { Dock = DockStyle.Bottom, Height = 24, BackColor = Colors.PanelBg };
bar.Paint += (st, et) => { using (var p = new Pen(Colors.Border, 1)) et.Graphics.DrawLine(p, 0, 0, bar.Width, 0); };
statusLabel = new Label { Text = "Ready", ForeColor = Colors.Dim, Font = new Font("Consolas", 8), Location = new Point(10, 4), AutoSize = true };
bar.Controls.Add(statusLabel);
var quit = new Button { Text = "QUIT", FlatStyle = FlatStyle.Flat, ForeColor = Colors.Red, BackColor = Color.FromArgb(30, 20, 20), Anchor = AnchorStyles.Right, Location = new Point(bar.Width - 60, 2), Size = new Size(50, 20), Font = new Font("Consolas", 7, FontStyle.Bold), Cursor = Cursors.Hand };
quit.FlatAppearance.BorderSize = 1; quit.FlatAppearance.BorderColor = Colors.Red;
quit.Click += (st, et) => Close(); bar.Controls.Add(quit);
Controls.Add(bar);
}
private void StartModel(ModelInfo m)
{
string bat = FindBat(m);
if (bat == null) { Log("[ERR] BAT not found: " + m.DisplayName, LogLevel.Err); return; }
if (PortBusy(m.Port)) { Log("[WRN] Port " + m.Port + " in use", LogLevel.Warn); return; }
try
{
var psi = new ProcessStartInfo { FileName = bat, WorkingDirectory = Path.GetDirectoryName(bat), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = System.Text.Encoding.UTF8, StandardErrorEncoding = System.Text.Encoding.UTF8 };
var p = new Process { StartInfo = psi, EnableRaisingEvents = true };
m.Proc = p; m.StartTime = DateTime.Now;
p.OutputDataReceived += (st, ev) => SafeRun(() => { if (!string.IsNullOrEmpty(ev.Data)) Log("[" + m.DisplayName + "] " + ev.Data); });
p.ErrorDataReceived += (st, ev) => SafeRun(() => { if (!string.IsNullOrEmpty(ev.Data)) Log("[" + m.DisplayName + "] ! " + ev.Data, LogLevel.Warn); });
p.Exited += (st, et) => SafeRun(() => { Log("[" + m.DisplayName + "] exited"); CardUI(m, false); if (m.Proc != null) m.Proc.Dispose(); m.Proc = null; });
p.Start(); p.BeginOutputReadLine(); p.BeginErrorReadLine();
CardUI(m, true);
Log("[OK] " + m.DisplayName + " started (PID:" + p.Id + " Port:" + m.Port + ")");
}
catch (Exception ex) { Log("[ERR] " + m.DisplayName + " failed: " + ex.Message, LogLevel.Err); m.Proc = null; }
}
private void StopModel(ModelInfo m)
{
if (m.Proc == null || m.Proc.HasExited) return;
try { KillTree(m.Proc.Id); if (!m.Proc.WaitForExit(5000)) m.Proc.Kill(); m.Proc.Dispose(); m.Proc = null; CardUI(m, false); Log("[HALT] " + m.DisplayName + " stopped"); }
catch (Exception ex) { Log("[ERR] Stop failed: " + ex.Message, LogLevel.Err); }
}
private void CardUI(ModelInfo m, bool running)
{
if (m.StatusLabel != null) m.StatusLabel.ForeColor = running ? Colors.Green : Colors.Sub;
if (m.Btn != null) { m.Btn.Text = running ? "HALT" : "RUN"; m.Btn.BackColor = running ? Colors.Red : Colors.Green; }
if (m.Card != null) m.Card.Refresh();
}
private void KillTree(int pid)
{
try { using (var s = new ManagementObjectSearcher("SELECT ProcessId FROM Win32_Process WHERE ParentProcessId=" + pid)) { foreach (var o in s.Get()) KillTree(Convert.ToInt32(o["ProcessId"])); } var p = Process.GetProcessById(pid); if (!p.HasExited) p.Kill(); } catch { }
}
private void StopAll() { foreach (var m in models) StopModel(m); Log("[HALT] All stopped"); }
private bool PortBusy(int port)
{
try { var p = IPGlobalProperties.GetIPGlobalProperties(); return p.GetActiveTcpListeners().Any(e => e.Port == port) || p.GetActiveUdpListeners().Any(e => e.Port == port); }
catch { return false; }
}
private void SafeRun(Action a) { if (!IsDisposed && !Disposing && globalLog != null) { if (globalLog.InvokeRequired) globalLog.BeginInvoke(a); else a(); } }
private enum LogLevel { Info, Warn, Err }
private void Log(string msg, LogLevel level = LogLevel.Info)
{
SafeRun(() =>
{
string ts = DateTime.Now.ToString("HH:mm:ss");
Color c = level == LogLevel.Warn ? Colors.Yellow : level == LogLevel.Err ? Colors.Red : Colors.Sub;
globalLog.Items.Add(new LogItem("[" + ts + "] " + msg, c));
globalLog.TopIndex = globalLog.Items.Count - 1;
while (globalLog.Items.Count > 1000) globalLog.Items.RemoveAt(0);
});
}
private class LogItem { public string T; public Color C; public LogItem(string t, Color c) { T = t; C = c; } public override string ToString() { return T; } }
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
globalLog.DrawMode = DrawMode.OwnerDrawFixed;
globalLog.DrawItem += (st, args) =>
{
if (args.Index < 0) return;
var it = globalLog.Items[args.Index] as LogItem;
if (it == null) return;
args.DrawBackground();
using (var b = new SolidBrush(it.C)) args.Graphics.DrawString(it.T, globalLog.Font, b, new PointF(args.Bounds.X + 2, args.Bounds.Y + 2));
args.DrawFocusRectangle();
};
}
private void UpdateStats()
{
try { CPU(); GPU(); MEM(); DSK(); Uptime(); Title(); Timers(); Bar(); }
catch { }
}
private void CPU() {
float v = cpuCounter != null ? cpuCounter.NextValue() : 0;
cpuVal.Text = v.ToString("F1") + "%";
cpuVal.ForeColor = v > 80 ? Colors.Red : v > 50 ? Colors.Yellow : Colors.Green;
}
private void GPU() {
try {
var psi = new ProcessStartInfo("nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu --format=csv,noheader,nounits") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
using (var p = Process.Start(psi))
if (p != null && p.WaitForExit(2000)) {
var parts = p.StandardOutput.ReadToEnd().Trim().Split(',');
if (parts.Length >= 2) { float u = float.Parse(parts[0].Trim()), t = float.Parse(parts[1].Trim());
gpuVal.Text = u.ToString("F0") + "% " + t.ToString("F0") + "C";
gpuVal.ForeColor = u > 80 ? Colors.Red : u > 50 ? Colors.Yellow : Colors.Green; return; }
}
} catch { }
gpuVal.Text = "N/A";
}
private void MEM() {
float v = 0; using (var s = new ManagementObjectSearcher("SELECT TotalVisibleMemorySize,FreePhysicalMemory FROM Win32_OperatingSystem"))
foreach (var o in s.Get()) { ulong t = Convert.ToUInt64(o["TotalVisibleMemorySize"]), f = Convert.ToUInt64(o["FreePhysicalMemory"]); v = (float)((t - f) * 100.0 / t); }
memVal.Text = v.ToString("F0") + "%"; memVal.ForeColor = v > 80 ? Colors.Red : v > 50 ? Colors.Yellow : Colors.Green;
}
private void DSK() {
float v = 0; foreach (var d in DriveInfo.GetDrives()) if (d.IsReady && d.Name == "C:\\") { v = (float)((d.TotalSize - d.TotalFreeSpace) * 100.0 / d.TotalSize); break; }
diskVal.Text = v.ToString("F0") + "%"; diskVal.ForeColor = v > 80 ? Colors.Red : v > 50 ? Colors.Yellow : Colors.Green;
}
private void Uptime() { var u = uptimeWatch.Elapsed; upTime.Text = string.Format("UPTIME: {0}d {1:D2}h {2:D2}m", u.Days, u.Hours, u.Minutes); }
private void Bar() { int r = 0; foreach (var m in models) if (m.Proc != null && !m.Proc.HasExited) r++; statusLabel.Text = r + "/" + models.Length + " active | " + baseDir; }
private void Timers() {
foreach (var m in models)
if (m.Proc != null && !m.Proc.HasExited && m.Btn != null) {
var el = DateTime.Now - m.StartTime;
m.Btn.Text = string.Format("{0:D2}:{1:D2}:{2:D2}", el.Hours, el.Minutes, el.Seconds);
}
}
private void Title() {
int c = 0, cards = 0; foreach (var m in models) { if (m.Proc != null && !m.Proc.HasExited) c++; if (m.Card != null) cards++; }
Text = c > 0 ? string.Format("llama\u6a21\u578b\u7ba1\u7406\u5668 [{0}/{1}crds] ACTIVE:{2}", cards, models.Length, c) : string.Format("llama\u6a21\u578b\u7ba1\u7406\u5668 [{0}/{1}crds]", cards, models.Length);
}
[STAThread]
static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); }
}
```
---
## 编译方法
```bash
# .NET Framework 4.8 编译器(Windows 自带)
csc /target:winexe /win32icon:app.ico /resource:logo.png /reference:System.Management.dll /reference:System.Drawing.dll /reference:System.Windows.Forms.dll /out:LlamaManager.exe MainForm.cs
```
需要将 `logo.png` 和 `app.ico` 放在与 `MainForm.cs` 同目录下。
---
## 使用方式
1. 将所有模型的 `.bat` 启动脚本放入 `D:\Qwen3XL\`
2. 双击 `LlamaManager.exe`
3. 看到卡片列表后,点击 **RUN** 启动任意模型
4. 状态指示灯从灰色变为绿色,按钮变为 **HALT**
5. 右侧日志区显示模型输出,底部状态栏显示活跃数
6. 点击 **HALT** 停止模型,进程树会被递归杀死
---
## 设计思路
**为什么不用 FlowLayoutPanel?** — FlowLayoutPanel 的 Margin/高度计算在滚动条出现时容易产生偏差,改用手动 Panel + VScrollBar 精确控制每张卡片的位置。
**为什么保留硬编码模型列表?** — 硬编码保持端口号和显示名称的确定性。配合 `FindBat` 的模糊匹配,即使文件名有细微差异也能自动找到。
**C# 5 兼容** — .NET Framework 4.8 自带的编译器只支持到 C# 5,代码中避免使用 `?.`、`_` 丢弃参数等新语法,保证直接用 `csc` 就能编译。
---
## 许可证
MIT License — 自由使用、修改、分享。
---
*欢迎 Star / Fork / PR!*
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)