用途

本地用 llama.cpp 启动一个带识图能力的模型服务,配套两个文件放在同一目录:start.bat(双击入口)+ start_server.ps1(实际逻辑)。

1 使用方法

  1. 把 start.bat、start_server.ps1、llama-server.exe、models 文件夹放在同一目录下
  2. models 文件夹里放好主模型(文本)和视觉模型(mmproj- 开头的文件)
  3. 双击 start.bat 即可,不用手动改 PowerShell 执行策略
  4. 启动成功后访问 http://127.0.0.1:8080

2 脚本原理

  • 自动读取 nvidia-smi 显存信息(总量/已用/空闲)
  • 自动扫描 models 目录:文件名以 mmproj 开头的当视觉模型,其余当主模型
  • 按剩余显存自动挑选能装下的最大主模型(多个量化版本时优先选质量最好的)
  • 根据是否完全装得下显存,自动决定 -ngl(GPU 卸载层数):装得下就全部塞 GPU(99),装不下就按比例部分卸载,保证至少能跑起来

3 踩过的坑(重要)

  • .ps1 文件必须保存为 UTF-8 with BOM,否则 Windows PowerShell 5.1 会用系统默认代码页(GBK)解析文件,中文注释会把字符串/引号解析错位,导致一堆莫名其妙的语法报错(缺少表达式、字符串缺少终止符等)
  • .bat 文件反过来必须用纯英文或 ANSI(GBK) 编码,UTF-8 编码的 .bat 会导致中文行被拆散成乱码命令
  • 单 GPU 时 nvidia-smi 只输出一行,PowerShell 会把这行当作纯字符串而不是数组,直接用 $gpuLines[0] 取到的是第一个字符而不是整行,必须用 @() 强制转数组:$gpuLines = @($gpuLines)
  • $args 是 PowerShell 内建自动变量,脚本里存启动参数不要用这个名字,改用 $serverArgs

4 start.bat

@echo off
title Smart Launcher v3
 
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0start_server.ps1"
 
echo.
echo ====================================
echo  Done. Press any key to close.
echo ====================================
pause >nul

5 start_server.ps1

# ==============================
# Auto GPU Safe Mode
# Smart Launcher v5 (auto scan + auto select text/vision model)
# ==============================
 
cd $PSScriptRoot
 
Write-Host "===================================="
Write-Host " Smart Launcher v5"
Write-Host "===================================="
 
if (-not (Test-Path "models")) {
    Write-Host "未找到 models 目录,请确认脚本放置位置正确" -ForegroundColor Red
    exit 1
}
 
# 1. 获取显存信息(只取第一块 GPU,避免多卡时 -split 出错)
try {
    $gpuLines = nvidia-smi --query-gpu=memory.total,memory.used --format=csv,noheader,nounits
    if (-not $gpuLines) {
        throw "nvidia-smi 未返回数据"
    }
    # 强制转换为数组:只有一块 GPU 时 nvidia-smi 只输出一行,
    # PowerShell 会把它当作纯字符串而不是数组,此时 [0] 取到的是第一个字符而非整行,
    # 用 @() 包一层强制变成数组,保证单卡/多卡场景下取的都是第一行完整内容
    $gpuLines = @($gpuLines)
    $gpuLine = $gpuLines[0]
    $total, $used = $gpuLine -split ",\s*"
    $total = [int]$total
    $used  = [int]$used
    $free  = $total - $used
} catch {
    Write-Host "无法获取 GPU 信息,请确认已安装 NVIDIA 驱动且 nvidia-smi 可用" -ForegroundColor Red
    Write-Host "错误详情: $_" -ForegroundColor Red
    exit 1
}
 
Write-Host "GPU Total: $total MB"
Write-Host "GPU Used : $used MB"
Write-Host "GPU Free : $free MB"
 
# 2. 扫描 models 目录,自动区分「主模型(文本)」与「视觉模型(mmproj)」
$allGguf = Get-ChildItem models\*.gguf -ErrorAction SilentlyContinue
if (-not $allGguf) {
    Write-Host "models 目录中没有找到任何 .gguf 文件" -ForegroundColor Red
    exit 1
}
 
$mmprojFiles = $allGguf | Where-Object { $_.Name -match "^mmproj" }
$textModels  = $allGguf | Where-Object { $_.Name -notmatch "^mmproj" }
 
if (-not $textModels) {
    Write-Host "models 目录中没有找到主模型文件(排除 mmproj 后为空)" -ForegroundColor Red
    exit 1
}
 
if (-not $mmprojFiles) {
    Write-Host "models 目录中没有找到 mmproj 视觉模型文件,无法启用识图" -ForegroundColor Red
    exit 1
}
 
$mmproj = $mmprojFiles | Sort-Object Name | Select-Object -First 1
$mmprojSizeMB = [math]::Round($mmproj.Length / 1MB)
 
Write-Host "扫描到 $($textModels.Count) 个主模型候选,$($mmprojFiles.Count) 个视觉模型候选(重复项已去重,取第一个)"
Write-Host "Vision Model: $($mmproj.Name) (约 $mmprojSizeMB MB)"
 
# 3. 根据剩余显存自动选择合适大小的主模型
$ctx = 4096
$contextOverheadMB = 800
$safetyMarginMB = 500
$reserveMB = $mmprojSizeMB + $contextOverheadMB + $safetyMarginMB
$budgetMB = $free - $reserveMB
 
Write-Host "预留显存(视觉模型 + 上下文 + 安全余量): $reserveMB MB"
Write-Host "可用于主模型的显存预算: $budgetMB MB"
 
$sortedModels = $textModels | Sort-Object Length -Descending
$chosenModel = $null
$fullyFits = $false
 
foreach ($m in $sortedModels) {
    $sizeMB = [math]::Round($m.Length / 1MB)
    if ($sizeMB -le $budgetMB) {
        $chosenModel = $m
        $fullyFits = $true
        break
    }
}
 
if (-not $chosenModel) {
    $chosenModel = $sortedModels | Sort-Object Length | Select-Object -First 1
    $fullyFits = $false
    Write-Host "警告: 显存不足以完整装下任何模型,将选择最小的模型并降低 GPU 卸载层数(推理速度会变慢)" -ForegroundColor Yellow
}
 
$modelSizeMB = [math]::Round($chosenModel.Length / 1MB)
Write-Host "Selected Model: $($chosenModel.Name) (约 $modelSizeMB MB)"
 
# 4. 决定 -ngl(GPU 卸载层数)
if ($fullyFits) {
    $ngl = 99
} else {
    $ratio = $budgetMB / $modelSizeMB
    if ($ratio -lt 0) { $ratio = 0 }
    $ngl = [math]::Max(1, [math]::Floor(40 * $ratio))
    Write-Host "部分卸载模式,估算 NGL: $ngl(如仍报显存不足,可手动调低此值,或删除更大的模型只保留小量化版本)" -ForegroundColor Yellow
}
 
Write-Host "Selected NGL: $ngl"
Write-Host "CTX: $ctx"
 
# 5. 组装启动参数
$modelPath  = $chosenModel.FullName
$mmprojPath = $mmproj.FullName
 
$serverArgs = @(
    "-m", $modelPath,
    "--mmproj", $mmprojPath,
    "-ngl", "$ngl",
    "-c", "$ctx",
    "--host", "127.0.0.1",
    "--port", "8080",
    "--cont-batching"
)
 
# 6. 检查可执行文件是否存在
$exePath = ".\llama-server.exe"
if (-not (Test-Path $exePath)) {
    Write-Host "未找到 $exePath,请确认脚本与可执行文件在同一目录" -ForegroundColor Red
    exit 1
}
 
# 7. 启动服务
Write-Host "Starting server..."
Write-Host "Command: $exePath $($serverArgs -join ' ')"
Start-Process -FilePath $exePath -ArgumentList $serverArgs -NoNewWindow -Wait