两台闲置 RTX 4090 机器:本地大模型部署与测试教程
原创 · 约 104 分钟阅读 · 阅读 --
Last updated on

两台闲置 RTX 4090 机器:本地大模型部署与测试教程

作者: Alex Xiang


古董级程序员,大厂出来后一直在创业公司,现在还在一线写 AI 相关的后端。更完整的技术记录写在微信公众号「字与码」:踩过的坑、换技术栈时的权衡、和这些年绕过的弯路,会不定期发在那里。若这篇对你有用,欢迎顺手关注。

公司里居然有两台闲置的 RTX 4090 机器。4090 的 24GB 显存虽然跑不了真正的大型闭源模型,但拿来做本地大模型服务、代码助手、内网 API、模型能力评估,已经很够用了。所以这次的目标很朴素:先别让显卡继续吃灰,把两台机器都用起来。

这篇是一个持续更新的本地大模型部署教程。第一台重点测试机器是 Ubuntu 26.04 LTS,单张 RTX 4090 24GB。目标不是跑一次 demo,而是在本地或者内网里长期提供一个能用的模型服务,支持命令行测试、OpenAI-compatible API,以及后续接入 Open WebUI、Continue、Cline 这类客户端。

顺便说一句,这次 Ubuntu 26.04 LTS 给我的印象很好。以前折腾 NVIDIA 显卡,最怕的就是驱动、内核模块、CUDA 工具链互相打架;但这次系统对 RTX 4090 的支持近乎完美,驱动识别、nvidia-smi、Docker GPU 验证都很顺,全程基本没有为显卡驱动犯愁。

还有一个背景值得单独说明:这次从服务器环境检查、代理安装、Ollama 部署、benchmark 脚本生成、API 测试、模型效果评估,到这篇博客的持续修订,基本都是由 Codex 全程操作完成的。我负责给目标、验收结果和补充判断;Codex 负责执行命令、写脚本、整理日志、更新文章。这篇文章也算是一次“用 AI Agent 部署并评估本地 AI 模型”的实战记录。

先说明当前状态:GPU 服务器 A 已经完成基础环境、代理服务和 Ollama 服务安装;GPU 服务器 B 也已经补上代理服务,作为备用内网入口。llama.cpp 源码编译在 Ubuntu 26.04 LTS + CUDA 工具链上遇到编译器崩溃,暂时先用 Ollama 作为可运行路径。现在已经完成 qwen3:30bqwen3-coder:30bdeepseek-r1:32b 三个大模型的初轮 token 速率测试,后面会继续补充真实显存占用、不同上下文长度下的稳定性,以及几个模型的效果对比。

一张正在台式机中运行的 RTX 4090 显卡实拍图
RTX 4090 实拍图。图片来源:Benlisquare / Wikimedia Commons,CC BY-SA 4.0;本文裁剪并压缩为站点配图。

实机记录

这次实际部署的机器配置如下:

项目实测值
服务器代号GPU 服务器 A
系统Ubuntu 26.04 LTS
Kernel7.0.0-15-generic
GPUNVIDIA GeForce RTX 4090
显存24564 MiB
NVIDIA Driver595.58.03
nvidia-smi CUDA Version13.2
nvccCUDA 12.4
内存30GiB RAM + 8GiB swap
磁盘根分区约 1.9T
内网 SSHssh <user>@<GPU-A 内网地址>
公网 SSHssh -p <frp-ssh-port> <user>@<frp-server>,依赖 frpc

因为服务器直接访问 GitHub 和模型仓库不稳定,我先安装了一个 Clash.Meta 兼容代理:mihomo v1.19.24。一开始为了安全只监听本机,确认可用后按局域网使用需求改成:

0.0.0.0:7890      # mixed HTTP/SOCKS proxy,局域网机器可用
127.0.0.1:9090    # external controller,只允许本机访问

当前关键配置是:

bind-address: 0.0.0.0
mixed-port: 7890
allow-lan: true
external-controller: 127.0.0.1:9090

订阅地址只在服务器上用于生成 /etc/mihomo/config.yaml,不写进脚本、博客或仓库。局域网客户端可以把 HTTP/SOCKS 代理设置为:

<GPU-A 内网地址>:7890

验证命令:

systemctl status mihomo
systemctl is-enabled mihomo
curl -x http://<GPU-A 内网地>:7890 -I -L --max-time 30 https://github.com

当前结果是 GitHub 返回 HTTP 200,说明服务器可以通过本机代理访问 GitHub。安装过程中还遇到一个细节:mihomo 首次校验配置时会尝试从 GitHub 下载 geoip.metadb,但当时网络还没走通,所以我先在本地下载 geoip.metadbgeosite.dat,再传到服务器的 /etc/mihomo/

同一套代理服务后来也部署到了另一台内网开发机,也就是 GPU 服务器 B,作为备用代理入口。它的代理端口同样是 7890,systemd 服务同样是 mihomo.service。如果只需要访问 GPU 服务器 A 上的 Ollama,仍然优先使用它自己的内网 API 地址;代理只是解决 GitHub、模型仓库下载和其他外部访问问题。

这台机器的 SSH 入口依赖 frpc。我也把它整理成 systemd 开机自启服务,并把原来的 User=nobody 改成专用系统用户 frpc,避免 systemd 的安全提示。当前 unit 重点如下:

[Unit]
Description=FRP Client Daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStartPre=+/usr/local/sbin/frpc-route-mtu.sh
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
Restart=on-failure
RestartSec=5
User=frpc
Group=frpc

[Install]
WantedBy=multi-user.target

验证结果:

systemctl is-enabled frpc   # enabled
systemctl is-active frpc    # active
journalctl -u frpc -n 20 --no-pager

重启 frpc 时当前 SSH 会短暂断开,这是正常现象,因为 SSH 连接本身就是通过 frp 转发进来的。重连后日志显示 login to server successstart proxy success

公网 frp SSH 入口有时会被服务端直接关闭连接。内网环境下更稳定的方式是直接访问:

ssh <user>@<GPU-A 内网地>

后续所有 Ollama benchmark 都是在内网直连 GPU 服务器 A 后执行的。

这台机器适合跑什么模型

RTX 4090 的 24GB 显存是一个很微妙、也很实用的区间。它不适合把 70B 级模型用高精度完整塞进显存,但很适合跑 30B 到 35B 级别的 4bit 量化模型。个人开发、中文技术写作、代码辅助、文档总结、轻量知识库问答,基本都落在这个区间。

我准备先按下面的顺序测试:

用途候选模型说明
默认通用模型Qwen3.6-35B-A3B 的 GGUF 4bit 量化版作为首选候选,兼顾中文、代码和日常问答
代码模型Qwen3-Coder-30B-A3B-Instruct 的 GGUF 4bit 量化版偏代码生成、仓库理解和 Agentic Coding
推理对照DeepSeek-R1-Distill-Qwen-32B 的 GGUF 4bit 量化版用于复杂推理、数学和分析任务对比

本文先以 Qwen3.6-35B-A3B GGUF 为主线写部署步骤。模型文件建议优先试 Q4_K_M 或同级 4bit 量化。如果实际显存吃紧,就降到 IQ4_NLQ3_K_M 一类更小的量化;如果显存还有余量,再考虑更高量化质量。

如果不只看 Qwen,还有几类硬件要求接近、值得一起测的模型:

模型4090 24GB 适配度推荐量化适合场景我的判断
DeepSeek-R1-Distill-Qwen-32BQ4_K_M 起步推理、数学、复杂分析值得重点测,但它不是完整 DeepSeek R1,而是 32B 蒸馏版
Gemma 3 27BQ4_K_M 或官方量化多语言、长上下文、轻量多模态Google 系本地模型首选;注意它是 Gemma,不是 Gemini
Mistral-Small-3.2-24B-Instruct很高Q4_K_M / Q5_K_M英文、通用指令、低延迟24B 对 24GB 显存更宽松,适合作为稳定对照
QwQ-32B 或其他 Qwen 32B 推理模型Q4_K_M中文推理、代码、长链路分析和 DeepSeek 32B 蒸馏版在同一硬件档位
GLM-4.5-Air低到中更低量化 + CPU/RAM 混合Agent、推理、代码能折腾,但不建议作为第一选择;总参数和文件体积太大

这里有两个名字容易混淆。

DeepSeek:完整的 DeepSeek-R1 是 671B 级 MoE 模型,单张 RTX 4090 不是它的舒适运行环境。4090 24GB 真正应该测的是 DeepSeek-R1-Distill-Qwen-32B 这类蒸馏模型。

Gemini / Gemma:Gemini 是 Google 的闭源商用模型系列,主要通过 API 使用;本地可下载权重部署的是 Gemma。对这台机器来说,Gemma 3 27B 才是同一硬件档位的候选。

参考入口:

0. 系统前提

下文假设你已经能 SSH 登录这台机器,并且用户有 sudo 权限。

lsb_release -a
uname -a

期望看到 Ubuntu 26.04 LTS。如果机器是刚装好的系统,先更新软件源和基础包:

sudo apt update
sudo apt upgrade -y
sudo apt install -y \
  build-essential \
  cmake \
  git \
  curl \
  wget \
  unzip \
  pkg-config \
  python3 \
  python3-venv \
  python3-pip \
  htop \
  nvtop

nvtop 不是必须,但调模型时很好用。它能直观看到 GPU 使用率、显存占用和进程。

1. 安装并验证 NVIDIA 驱动

Ubuntu 官方推荐用 ubuntu-drivers 选择驱动。先安装工具并查看推荐项:

sudo apt install -y ubuntu-drivers-common
ubuntu-drivers devices

输出里通常会有一项 recommended。不要硬背某个驱动版本号,直接用系统推荐版本更稳:

sudo ubuntu-drivers install
sudo reboot

这一步在 Ubuntu 26.04 LTS 上比预期顺利很多。系统推荐驱动安装后,重启就能直接识别 RTX 4090,没有遇到内核模块加载失败、驱动版本不匹配、nvidia-smi 找不到设备这类常见问题。后面真正花时间的地方反而是模型下载和 llama.cpp 编译链,而不是 NVIDIA 驱动。

重启后检查:

nvidia-smi

至少要确认三件事:

  • 能看到 NVIDIA GeForce RTX 4090
  • 能看到驱动版本和 CUDA Version。
  • 空闲状态下显存占用不异常。

如果 nvidia-smi 报错,先别继续下载模型。优先排查:

dkms status
lsmod | grep nvidia
journalctl -k | grep -i nvidia | tail -n 80

如果机器启用了 Secure Boot,NVIDIA 内核模块可能加载失败。最省事的做法通常是进 BIOS 关闭 Secure Boot,或者按 Ubuntu 提示给内核模块签名。

2. 准备 Docker GPU 环境

不是所有部署都必须用 Docker,但我建议先配好。以后测试 vLLM、SGLang、Open WebUI 时都会用得上。

先安装 Docker:

sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

把当前用户加入 docker 组:

sudo usermod -aG docker "$USER"
newgrp docker

然后安装 NVIDIA Container Toolkit:

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt update
sudo apt install -y nvidia-container-toolkit

sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

验证 Docker 能看到 GPU:

docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi

这里容器镜像用 Ubuntu 24.04 base 没关系,关键是能通过宿主机驱动访问 GPU。只要容器里也能看到 RTX 4090,后面跑容器化推理服务就有基础了。

3. 准备模型目录

建议把模型放在单独的数据盘或固定目录,不要散落在项目目录里。

sudo mkdir -p /data/models/gguf
sudo chown -R "$USER:$USER" /data/models

安装 Hugging Face 下载工具:

python3 -m venv ~/.venvs/hf
source ~/.venvs/hf/bin/activate
pip install -U pip huggingface_hub

如果访问 Hugging Face 较慢,可以临时设置镜像端点:

export HF_ENDPOINT=https://hf-mirror.com

下载 Qwen3.6 的 GGUF 文件时,先到模型仓库确认实际文件名。下面命令写法保留变量,方便替换:

cd /data/models/gguf

MODEL_REPO="unsloth/Qwen3.6-35B-A3B-GGUF"
MODEL_FILE="Qwen3.6-35B-A3B-Q4_K_M.gguf"

huggingface-cli download "$MODEL_REPO" \
  "$MODEL_FILE" \
  --local-dir /data/models/gguf \
  --local-dir-use-symlinks False

如果仓库里的推荐文件名不是 Q4_K_M,就把 MODEL_FILE 改成实际文件名。下载后确认大小:

ls -lh /data/models/gguf
sha256sum /data/models/gguf/*.gguf | tee /data/models/gguf/SHA256SUMS.txt

24GB 显存下,Q4_K_M 级别文件通常会比较贴近上限。后面启动时上下文不要一开始拉满,先用 8K 验证。

4. 编译 llama.cpp CUDA 版本

llama.cpp 是跑 GGUF 最直接的一条路。先安装 CUDA 编译工具。Ubuntu 仓库里的包如果能满足当前版本,直接用:

sudo apt install -y nvidia-cuda-toolkit
nvcc --version

如果系统仓库的 CUDA 包太旧,或者 nvcc 不存在,再换 NVIDIA 官方 CUDA repository。本文先按 Ubuntu 26.04 LTS 的基础路径写。

拉代码并编译:

mkdir -p ~/src
cd ~/src

git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp

cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j"$(nproc)"

编译完成后检查二进制:

./build/bin/llama-server --help | head
./build/bin/llama-cli --help | head

如果编译阶段找不到 CUDA,通常是 nvcc 或 CUDA 头文件路径问题。先确认:

which nvcc
nvcc --version
echo "$CUDA_HOME"

有些环境需要显式指定 CUDA 编译器,例如:

cmake -B build \
  -DGGML_CUDA=ON \
  -DCMAKE_CUDA_COMPILER=/usr/bin/nvcc \
  -DCMAKE_BUILD_TYPE=Release

本次实机编译结果

这台 Ubuntu 26.04 LTS 机器上,git clone 和 CMake 配置都能成功,CMake 也识别到了 CUDA backend,架构为 89-real。但实际编译没有顺利完成。

第一次使用系统默认 GCC 15 编译:

cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j"$(nproc)"

失败现象是 GCC 内部编译器错误和 CUDA 编译阶段段错误,典型日志包括:

internal compiler error: Segmentation fault
Segmentation fault (core dumped)

第二次降到 GCC/G++ 13,并把并发降到 2:

rm -rf build-gcc13
CC=/usr/bin/gcc-13 CXX=/usr/bin/g++-13 \
  cmake -B build-gcc13 -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build-gcc13 --config Release -j2

结果仍然失败,错误集中在 ggml-cpu/ops.cpp 和部分 CUDA template instance 编译阶段。这个现象更像是 Ubuntu 26.04、GCC、新版 CUDA 工具链和当前 llama.cpp 源码组合下的编译链问题,而不是模型或 GPU 本身的问题。

后续可选处理路径:

  • 使用 llama.cpp 官方或社区预编译二进制。
  • 换到更保守的容器环境,例如 Ubuntu 24.04 + CUDA 12.x + GCC 12/13。
  • 固定 llama.cpp 到一个已验证 commit,再逐步升级。
  • 先用 Ollama 跑通服务和模型测试,等需要精细控制参数时再回到 llama.cpp

5. 先用命令行跑通一次

不要一上来就起服务。先用 llama-cli 跑一个短 prompt,确认模型、CUDA、显存三件事都正常。

cd ~/src/llama.cpp

MODEL=/data/models/gguf/Qwen3.6-35B-A3B-Q4_K_M.gguf

./build/bin/llama-cli \
  -m "$MODEL" \
  -ngl 99 \
  -c 8192 \
  -p "请用三句话解释一下,为什么 24GB 显存适合跑 30B 级 4bit 量化模型。"

同时另开一个终端观察:

watch -n 1 nvidia-smi

这里的几个参数含义:

参数作用
-m指定 GGUF 模型文件
-ngl 99尽量把模型层 offload 到 GPU
-c 8192上下文长度先设为 8K,避免一开始就被 KV cache 打爆
-p测试 prompt

如果报显存不足,先不要怀疑驱动。优先把上下文降下来:

./build/bin/llama-cli \
  -m "$MODEL" \
  -ngl 99 \
  -c 4096 \
  -p "你好,请做一个简单自我介绍。"

如果还是 OOM,再换更小的量化文件。

6. 启动 OpenAI-compatible API

命令行跑通后,再启动 llama-server

cd ~/src/llama.cpp

MODEL=/data/models/gguf/Qwen3.6-35B-A3B-Q4_K_M.gguf

./build/bin/llama-server \
  -m "$MODEL" \
  -ngl 99 \
  -c 8192 \
  --host 0.0.0.0 \
  --port 8000

本机测试:

curl http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "local-qwen",
    "messages": [
      {
        "role": "user",
        "content": "用三点说明本地部署大模型相比云端 API 的优缺点。"
      }
    ],
    "temperature": 0.7,
    "stream": false
  }'

流式输出也要测:

curl http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "local-qwen",
    "messages": [
      {
        "role": "user",
        "content": "写一段 200 字左右的中文技术博客开头,主题是本地大模型部署。"
      }
    ],
    "temperature": 0.7,
    "stream": true
  }'

如果要给局域网其他机器访问,确认防火墙:

sudo ufw status
sudo ufw allow 8000/tcp

如果只是本机用,就不要开放端口,把 --host 改成 127.0.0.1 更安全。

7. 用 systemd 托管服务

长期使用不要手工开终端挂着。新建一个专用目录放启动脚本:

mkdir -p ~/bin
vim ~/bin/start-local-llm.sh

写入:

#!/usr/bin/env bash
set -euo pipefail

cd "$HOME/src/llama.cpp"

MODEL="/data/models/gguf/Qwen3.6-35B-A3B-Q4_K_M.gguf"

exec "$HOME/src/llama.cpp/build/bin/llama-server" \
  -m "$MODEL" \
  -ngl 99 \
  -c 8192 \
  --host 0.0.0.0 \
  --port 8000

加执行权限:

chmod +x ~/bin/start-local-llm.sh

创建 systemd user service:

mkdir -p ~/.config/systemd/user
vim ~/.config/systemd/user/local-llm.service

写入:

[Unit]
Description=Local LLM server via llama.cpp
After=network-online.target

[Service]
Type=simple
ExecStart=%h/bin/start-local-llm.sh
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

启用并启动:

systemctl --user daemon-reload
systemctl --user enable --now local-llm.service
systemctl --user status local-llm.service

如果希望用户退出 SSH 后服务仍然运行:

sudo loginctl enable-linger "$USER"

查看日志:

journalctl --user -u local-llm.service -f

8. 可选:用 Ollama 快速体验

如果只是想先聊天,不想编译 llama.cpp,Ollama 更省事:

curl -fsSL https://ollama.com/install.sh | sh

这次实机安装时,官方安装脚本下载 ollama-linux-amd64.tgz 返回了 404,所以改用 GitHub Release 的 .tar.zst 包手动安装。因为服务器直连 GitHub 不稳定,安装包先在本地下载,再传到服务器:

curl -L --fail -C - \
  -o ollama-linux-amd64.tar.zst \
  https://github.com/ollama/ollama/releases/download/v0.23.2/ollama-linux-amd64.tar.zst

scp ollama-linux-amd64.tar.zst server:/tmp/

服务器上解压到 /usr/local

sudo tar --zstd -xf /tmp/ollama-linux-amd64.tar.zst -C /usr/local
sudo mkdir -p /data/models/ollama

创建专用用户:

sudo useradd --system \
  --create-home \
  --home-dir /usr/share/ollama \
  --shell /usr/sbin/nologin \
  --user-group ollama

sudo usermod -a -G render,video ollama
sudo chown -R ollama:ollama /data/models/ollama /usr/share/ollama

/etc/systemd/system/ollama.service

[Unit]
Description=Ollama Service
After=network-online.target mihomo.service
Wants=network-online.target

[Service]
Type=simple
User=ollama
Group=ollama
ExecStart=/usr/local/bin/ollama serve
Restart=always
RestartSec=3
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_MODELS=/data/models/ollama"
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=127.0.0.1,localhost"

[Install]
WantedBy=multi-user.target

启动:

sudo systemctl daemon-reload
sudo systemctl enable --now ollama

确认服务:

systemctl status ollama
ollama --version
curl http://127.0.0.1:11434/api/version

一开始我只让 Ollama 监听 127.0.0.1:11434。后来为了让局域网里的其他机器可以直接通过 API 调用,把 OLLAMA_HOST 改成:

Environment="OLLAMA_HOST=0.0.0.0:11434"

重启后监听状态:

sudo systemctl daemon-reload
sudo systemctl restart ollama
ss -ltnp | grep :11434

当前实测版本是 0.23.2,服务监听 *:11434,模型目录为 /data/models/ollama。局域网客户端可以访问:

curl http://<GPU-A 内网地>:11434/api/version
curl http://<GPU-A 内网地>:11434/api/tags

如果 Ollama 官方库里已有目标模型,可以直接:

ollama pull qwen3.6:35b
ollama run qwen3.6:35b

第一次端到端测试可以先拉一个小模型,确认下载、加载、推理、GPU 调用链路都正常:

ollama pull qwen3:0.6b
ollama run qwen3:0.6b "用一句中文说明你已经可以在本机运行。"
ollama ps
nvidia-smi

这台服务器走代理下载模型时速度不稳定,qwen3:0.6b 的 522MB 权重实际下载花了比较久。这个阶段先验证服务可用,正式性能测试再换 30B 级模型。

下载过程中一度速度降到 0.3MB/s 左右,所以我把任务转成后台继续跑,日志放在用户目录:

nohup /usr/local/bin/ollama pull qwen3:0.6b \
  > "$HOME/ollama-pull-qwen3-0.6b.log" 2>&1 < /dev/null &

tail -f "$HOME/ollama-pull-qwen3-0.6b.log"

为了从本地机器验证局域网 API,我在博客项目里放了一个测试脚本:

python3 scripts/test_ollama_lan_api.py \
  --base-url http://<GPU-A 内网地>:11434 \
  --model qwen3:0.6b

脚本会先测试 /api/version/api/tags。如果模型还没下载完成,会输出 model_not_ready;模型就绪后会继续调用 /api/chat 做一次非流式生成测试。

本次从本地机器调用局域网 API 的结果:

version: 0.23.2
models: qwen3:0.6b
elapsed_seconds: 22.82
response:
我已经通过局域网API被访问。

服务器侧同时能看到模型跑在 GPU 上:

NAME          ID              SIZE      PROCESSOR    CONTEXT    UNTIL
qwen3:0.6b    7df6b6e09427    4.5 GB    100% GPU     32768      4 minutes from now

小模型验证通过后,继续在服务器后台顺序拉取几个 4090 24GB 适合测试的模型。因为单个模型就有 15GB 到 20GB,当前网络又不稳定,所以不要并发下载。服务器上创建了 ~/pull-ollama-models.sh

MODELS=(
  "qwen3:30b"
  "qwen3-coder:30b"
  "deepseek-r1:32b"
  "gemma3:27b"
  "mistral-small3.2:24b"
)

启动方式:

nohup ~/pull-ollama-models.sh \
  > ~/ollama-pull-logs/nohup.log 2>&1 < /dev/null &

查看队列进度:

tail -f ~/ollama-pull-logs/pull-queue.log
tail -f ~/ollama-pull-logs/qwen3_30b.log
ollama list
pgrep -af "pull-ollama-models.sh|ollama pull"

当前已经完成下载并能被 /api/tags 列出的模型如下:

模型参数量文件大小说明
deepseek-r1:32b32.8B19GBDeepSeek-R1-Distill-Qwen-32B,dense 32B 档位
qwen3:30b30.5B18GBQwen3 MoE,适合通用中文、写作和问答
qwen3-coder:30b30.5B18GBQwen3 Coder MoE,偏代码任务
qwen3:0.6b0.6B522MB小模型,只用于验证下载、服务和 API 链路

模型下载完成后,可以继续用局域网 API 测试脚本逐个跑一遍:

python3 scripts/test_ollama_lan_api.py \
  --base-url http://<GPU-A 内网地>:11434 \
  --model qwen3:30b

为了更稳定地记录 token 速率,我又写了一个只依赖 Python 标准库的 benchmark 脚本,已经同步到 GPU 服务器 A:

/home/<user>/benchmark_ollama_token_rate.py

脚本会调用 Ollama 的 /api/tags 自动列出本机模型,默认筛选 10B+ 的大模型,然后用 /api/generate 的非流式接口跑固定 prompt。输出 token/s 使用 Ollama 响应中的 eval_count / eval_duration 计算,prompt token/s 使用 prompt_eval_count / prompt_eval_duration 计算。

这次测试命令是:

/home/<user>/benchmark_ollama_token_rate.py \
  --host http://127.0.0.1:11434 \
  --min-params-b 10 \
  --runs 3 \
  --warmup \
  --num-predict 256 \
  --timeout 1800 \
  --output /home/<user>/ollama-token-rate.csv \
  --jsonl /home/<user>/ollama-token-rate.jsonl

这里有两个细节要注意:

  • qwen3-coder:30b 在这个短 prompt 下提前结束,实际生成了 212 个 token,不是完整 256 个 token。
  • qwen3:30bqwen3-coder:30b 是 MoE 模型,实际激活参数远小于总参数,所以吞吐明显高于 dense 的 deepseek-r1:32b。这不是简单的“Qwen 比 DeepSeek 快 20 倍”,而是模型结构不同。

如果官方库还没有,或者你想用自己下载的 GGUF,可以写一个 Modelfile

mkdir -p ~/ollama-models/qwen36
cd ~/ollama-models/qwen36
cp /data/models/gguf/Qwen3.6-35B-A3B-Q4_K_M.gguf .

cat > Modelfile <<'EOF'
FROM ./Qwen3.6-35B-A3B-Q4_K_M.gguf

PARAMETER num_ctx 8192
PARAMETER temperature 0.7
EOF

ollama create qwen36-35b-q4 -f Modelfile
ollama run qwen36-35b-q4

Ollama 的 API 默认在 11434

curl http://127.0.0.1:11434/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen36-35b-q4",
    "messages": [
      {"role": "user", "content": "写一个 Python 脚本,读取目录下所有 Markdown 文件并统计字数。"}
    ],
    "stream": false
  }'

我个人会把 Ollama 当作快速体验路径,把 llama.cpp server 当作更可控的服务路径。

9. 固定测试问题集

只问“你好”没有意义。每次换模型、换量化、换上下文长度,都应该用同一组问题做对比。

中文写作

请把下面这段说明改写成适合技术博客读者阅读的中文,不要改变技术含义:

我们在 Ubuntu 26.04 LTS 上使用 RTX 4090 部署本地大模型。模型采用 4bit GGUF 量化,推理服务通过 llama.cpp 暴露 OpenAI-compatible API。

观察它是否自然、是否废话多、是否偷换含义。

代码理解

请阅读下面这段 Python 代码,指出潜在问题,并给出最小修改建议:

import requests

def fetch_all(urls):
    result = []
    for url in urls:
        r = requests.get(url)
        result.append(r.json())
    return result

好的回答应该能提到超时、异常处理、状态码、串行请求性能等问题。

运维排障

有一个本地 LLM API 服务,平均响应很快,但偶发 60 秒超时。机器是单张 RTX 4090,Ubuntu 26.04 LTS。请列出排查步骤。

我会看它是否能想到显存碎片、上下文长度、并发排队、CPU 采样瓶颈、磁盘换页、反向代理超时、客户端重试等问题。

代码生成

写一个 Bash 脚本,每 5 秒记录一次 nvidia-smi 的显存占用和 GPU 利用率,输出到 CSV 文件。

这个问题能快速看出模型写脚本的可靠性。

2026-05-12:qwen3-coder:30b 实际问答评估

首轮 token 速率测试后,我选了 qwen3-coder:30b 做实际问答。原因很简单:它在 4090 24GB 上速度和 qwen3:30b 接近,短输出能到 200 tok/s 左右;同时它的输出更干净,没有把思考过程混进最终回答里。qwen3:30b 的吞吐同样很好,但当前通过 Ollama /api/chat 调用时,即使提示“不要输出思考过程”,仍然容易把内部推理文字输出到正文里,所以暂时不作为默认推荐模型。

这次测试不是登录到 GPU 服务器 A 上执行,而是在本地机器直接访问局域网 Ollama API:

curl --noproxy '*' http://<GPU-A 内网地>:11434/api/version

如果本机环境里配置了 ALL_PROXYHTTP_PROXYHTTPS_PROXY,访问内网地址时要绕过代理,否则请求可能会被本机代理拦截:

ALL_PROXY= HTTP_PROXY= HTTPS_PROXY= python3 run_local_eval.py

测试问题沿用上面的四类任务,接口使用 http://<GPU-A 内网地址>:11434/api/chat,模型为 qwen3-coder:30b。实测结果如下:

任务耗时生成速度评价
中文技术改写2.79s200.72 tok/s改写自然,保留了 Ubuntu、RTX 4090、4bit GGUF、API 服务等关键信息,没有明显偷换含义。
Python 代码审查3.38s205.01 tok/s能抓到缺少超时、异常处理、状态码检查、JSON 解析异常,并给出可直接落地的最小修改。
LLM 服务排障3.73s203.58 tok/s排查项覆盖日志、GPU、进程、内存、API 响应时间、系统限制等,适合作为初始 checklist;但对上下文长度、并发排队、反向代理超时的提示还不够具体。
Bash 脚本生成5.90s203.36 tok/s脚本结构完整,包含参数检查、CSV 表头、循环采样和信号处理;awk 解析 nvidia-smi CSV 的写法后续还要在真实机器上再验证一次。

下面按“Prompt / 返回结果 / 观察”的格式记录每一次调用。这样比只看结论更直观,也方便后续换模型、换量化、换推理框架时逐项对比。

调用 1:中文技术改写

Prompt

请把下面这段说明改写成适合技术博客读者阅读的中文,不要改变技术含义,控制在 180 字以内。不要输出思考过程。

我们在 Ubuntu 26.04 LTS 上使用 RTX 4090 部署本地大模型。模型采用 4bit GGUF 量化,推理服务通过 llama.cpp 暴露 OpenAI-compatible API。

返回结果

在 Ubuntu 26.04 LTS 环境下,我们利用 RTX 4090 显卡部署本地大语言模型。模型采用 4bit GGUF 量化格式,通过 llama.cpp 框架提供 OpenAI 兼容的 API 接口进行推理服务。

观察

结果直接可用,没有输出思考过程;保留了系统版本、显卡、量化格式和 API 方式,没有自行改写技术事实。

调用 2:Python 代码审查

Prompt

请阅读下面这段 Python 代码,指出潜在问题,并给出最小修改建议。回答要具体,不要泛泛而谈,不要输出思考过程。

import requests

def fetch_all(urls):
    result = []
    for url in urls:
        r = requests.get(url)
        result.append(r.json())
    return result

返回结果

潜在问题:

  1. 缺少异常处理,requests.get 可能抛出 ConnectionErrorTimeout 等异常。
  2. 没有设置超时时间,可能导致请求长时间阻塞。
  3. 没有检查 HTTP 响应状态码,可能返回错误状态码但程序继续执行。
  4. 没有处理 JSON 解析异常。

最小修改建议:

import requests

def fetch_all(urls):
    result = []
    for url in urls:
        try:
            r = requests.get(url, timeout=10)
            r.raise_for_status()  # 检查状态码
            result.append(r.json())
        except (requests.RequestException, ValueError):  # 捕获请求和JSON解析异常
            continue  # 或者返回None/空值,根据业务需求
    return result

观察

这个回答抓住了最关键的生产问题:超时、状态码、请求异常和 JSON 解析异常。修改建议足够小,但 continue 会丢弃失败项,真实业务里还要决定是否记录错误、返回占位值或让异常继续抛出。

调用 3:LLM 服务排障

Prompt

有一个本地 LLM API 服务,平均响应很快,但偶发 60 秒超时。机器是单张 RTX 4090,Ubuntu 26.04 LTS。请按优先级列出排查步骤,最多 10 条,每条说明要检查什么、用什么命令、如何判断。不要输出思考过程。

返回结果

  1. 检查系统日志 - 命令:dmesg | grep -i "timeout\|error",判断:查看是否有硬件或驱动相关错误信息。
  2. 监控 GPU 使用率 - 命令:nvidia-smi -l 1,判断:确认 GPU 是否长时间满载导致响应延迟。
  3. 检查网络连接 - 命令:ping -c 10 localhost,判断:确认本地网络无延迟或丢包问题。
  4. 查看进程状态 - 命令:ps aux | grep your_api_process,判断:确认服务进程是否正常运行。
  5. 检查内存使用 - 命令:free -h,判断:确认内存是否不足导致交换或进程被杀。
  6. 分析 API 日志 - 命令:tail -f /path/to/api/log,判断:查看超时发生时的详细错误信息。
  7. 测试 API 响应时间 - 命令:ab -n 10 -c 1 http://localhost:port/endpoint,判断:确认是否为特定请求导致超时。
  8. 检查系统资源限制 - 命令:ulimit -a,判断:确认文件描述符或进程数限制是否过低。
  9. 验证 CUDA 驱动 - 命令:nvidia-smi,判断:确认驱动版本兼容且无异常。
  10. 检查防火墙设置 - 命令:sudo ufw status,判断:确认无防火墙规则阻断请求。

观察

适合作为第一版排查 checklist,但专业性还不够。它没有优先强调 LLM 服务常见的并发排队、上下文过长、KV cache、反向代理 read timeout、客户端重试、模型冷加载这些更贴近实际的问题。

调用 4:Bash 脚本生成

Prompt

写一个完整可运行的 Bash 脚本,每 5 秒记录一次 nvidia-smi 的显存占用、GPU 利用率、功耗和温度,输出到 CSV 文件。要求脚本可以用 Ctrl+C 停止,CSV 有表头,优先使用 nvidia-smi --query-gpu。只输出脚本和少量必要说明,不要输出思考过程。

返回结果

#!/bin/bash

# 检查 nvidia-smi 是否可用
if ! command -v nvidia-smi &> /dev/null; then
    echo "错误: 未找到 nvidia-smi 命令"
    exit 1
fi

# 检查参数
if [ $# -ne 1 ]; then
    echo "用法: $0 <输出文件名>"
    exit 1
fi

OUTPUT_FILE="$1"

# 创建 CSV 文件并写入表头
echo "Timestamp,Memory_Used_MiB,Memory_Total_MiB,Utilization_GPU_Percent,Utilization_Memory_Percent,Power_Draw_Watts,Temperature_GPU_Celsius" > "$OUTPUT_FILE"

# 信号处理函数
cleanup() {
    echo -e "\n停止监控..."
    exit 0
}

# 设置信号处理器
trap cleanup SIGINT SIGTERM

echo "开始监控 GPU 状态,按 Ctrl+C 停止..."
echo "数据已写入: $OUTPUT_FILE"

# 主循环
while true; do
    # 获取 GPU 信息
    nvidia-smi --query-gpu=timestamp,memory.used,memory.total,utilization.gpu,utilization.memory,power.draw,temperature.gpu --format=csv,noheader,nounits | \
    awk -v timestamp="$(date '+%Y-%m-%d %H:%M:%S')" '{
        printf "%s,%s,%s,%s,%s,%s,%s\n", timestamp, $2, $3, $4, $5, $6, $7
    }' >> "$OUTPUT_FILE"

    sleep 5
done

观察

脚本结构完整,有参数检查、表头、循环采样和信号处理。需要注意的是,它同时查询了 timestamp 又用 date 生成时间戳,awk 字段对应关系在真实机器上还要验证;如果用于多 GPU 机器,也需要额外增加 GPU index。

综合看,qwen3-coder:30b 适合作为这台 4090 机器上的默认本地代码 / 技术助手模型。它的优势是响应快、输出干净、代码类任务可用性高;短板是复杂排障回答仍然偏 checklist,需要人根据现场日志继续收敛问题。它可以覆盖日常代码审查、脚本生成、技术文档改写和轻量运维问答,但还不能完全替代更强的云端模型做深层架构推理或长上下文仓库分析。

2026-05-12:qwen3:30b 同题评估

随后我用同一组问题测试了 qwen3:30b。从速度看,它和 qwen3-coder:30b 基本在同一档,生成速度也在 200 tok/s 左右;但实际可用性差很多。即使 system prompt 写了“只输出最终答案,不要输出思考过程”,请求体也加了 "think": false,它仍然把分析过程输出到了正文里,并且四个问题都因为先输出思考过程而触发 done_reason=length 截断。后来我又在用户问题末尾追加 /no_think 做对照,结果仍然泄漏思考过程并被截断。

任务耗时生成速度done_reason评价
中文技术改写9.12s202.47 tok/slength没有给出最终改写,主要输出了分析过程,还错误判断 Ubuntu 26.04 LTS 是笔误。
Python 代码审查6.36s202.08 tok/slength能分析到异常、状态码、JSON 和串行性能,但正文混入大量推理,最终代码被截断。
LLM 服务排障5.70s201.57 tok/slength方向上覆盖资源、日志、I/O、并发等,但没有输出完整的最终 checklist。
Bash 脚本生成7.45s199.46 tok/slength主要在解释如何写脚本,最终没有产出完整可运行脚本。

下面同样按每次调用记录 Prompt 和返回结果。这里的“截断”不是我手工删减,而是 Ollama 返回的 done_reason=length,也就是模型在本次 num_predict 限制内没有来得及输出完整最终答案。因为返回内容很长,而且主要是思考过程,所以这里保留摘要和关键片段,不全文粘贴。

调用 1:中文技术改写

Prompt

请把下面这段说明改写成适合技术博客读者阅读的中文,不要改变技术含义:

我们在 Ubuntu 26.04 LTS 上使用 RTX 4090 部署本地大模型。模型采用 4bit GGUF 量化,推理服务通过 llama.cpp 暴露 OpenAI-compatible API。

返回结果摘要

模型没有直接给出改写后的正文,而是输出了思考过程,并且把 Ubuntu 26.04 LTS 判断成笔误,试图改成 Ubuntu 24.04 LTS。这在本次测试里属于明显问题:评测目标是“不改变技术含义”,它却准备自行修正输入事实。

关键片段

首先,用户要求我作为中文技术编辑,只输出最终答案,不要输出思考过程。所以,我需要直接提供改写后的文本。

原句是:我们在 Ubuntu 26.04 LTS 上使用 RTX 4090 部署本地大模型。模型采用 4bit GGUF 量化,推理服务通过 llama.cpp 暴露 OpenAI-compatible API。

我需要改写成适合技术博客读者阅读的中文,不能改变技术含义。

关键点:

  • Ubuntu 26.04 LTS:Ubuntu 26.04 LTS 是一个错误。Ubuntu 的版本通常是 22.04、24.04 等。26.04 可能是个笔误。标准版本是 24.04 LTS(Noble Numbat),但用户写了 26.04。我应该纠正它,因为技术博客需要准确。但用户说“不要改变技术含义”,所以如果 26.04 是错误的,我应该修正它。在技术上下文中,Ubuntu 26.04 不存在,应该是 24.04。我应该假设是笔误,并改写为正确的版本。作为编辑,我应该确保准确性。

回顾:Ubuntu 24.04 LTS 是当前的 LTS 版本(2024 年发布),26.04 可能是打字错误。在改写时,我应该使用正确的版本。

观察

这次调用没有产出可用答案。主要问题不是文笔,而是没有遵守“只输出最终答案”,并且擅自质疑输入里的系统版本。

调用 2:Python 代码审查

Prompt

请阅读下面这段 Python 代码,指出潜在问题,并给出最小修改建议:

import requests

def fetch_all(urls):
    result = []
    for url in urls:
        r = requests.get(url)
        result.append(r.json())
    return result

返回结果摘要

模型能识别出 HTTP 错误、JSON 解析异常、串行请求效率等问题,也想到了 raise_for_status()。但它把大量推理过程输出到正文里,最后停在“修改代码”的中途,导致最终答案不完整。

关键片段

首先,用户要求我作为资深 Python 工程师,只输出最终答案,不要输出思考过程。回答要简洁。

代码是:

import requests

def fetch_all(urls):
    result = []
    for url in urls:
        r = requests.get(url)
        result.append(r.json())
    return result

我需要指出潜在问题,并给出最小修改建议。

潜在问题:

  1. 错误处理缺失:代码中没有处理请求可能失败的情况。例如,如果 requests.get(url) 返回非 200 状态码,r.json() 可能会抛出异常(如 requests.exceptions.JSONDecodeErrorrequests.exceptions.RequestException)。这会导致整个函数崩溃,而不是处理错误。
  2. 效率问题:使用循环顺序发送请求,而不是并行处理。对于大量 URL,这会很慢。但用户要求最小修改,所以可能不需要引入异步或并发。
  3. 其他问题:代码假设所有响应都是 JSON 格式,但实际中可能不是。例如,如果响应是 HTML 或纯文本,r.json() 会失败。

最小修改建议:

  • 添加错误处理,例如在循环中捕获异常,并可能记录错误或跳过失败的 URL。
  • 但用户要求最小修改,所以应该只做必要的改动。
  • 最小修改:添加一个 try-except 块来捕获异常,并将错误信息或默认值添加到结果中,而不是让程序崩溃。

例如:

import requests

def fetch_all(urls):
    result = []
    for url in urls:
        try:
            r = requests.get(url)
            r.raise_for_status()  # 检查 HTTP 错误
            result.append(r.json())
        except Exception as e:
            result.append(None)  # 或者记录错误
    return result

r.raise_for_status() 会抛出 HTTPError 如果状态码不是 200,所以需要捕获它。

更简洁的修改:只捕获异常,不添加额外检查。

最小修改建议:添加 try-except。

另一个潜在问题:requests.get 可能超时,但代码中没有设置超时。不过,这不是主要问题,因为默认超时是 None(无限等待),但通常建议设置超时。

最终输出在这里被截断,停在后续修改代码的开头。

观察

知识点覆盖还可以,但交付质量差。对于代码助手来说,“能想到”不够,最终必须给出完整、可复制的修改建议。

调用 3:LLM 服务排障

Prompt

有一个本地 LLM API 服务,平均响应很快,但偶发 60 秒超时。机器是单张 RTX 4090,Ubuntu 26.04 LTS。请列出排查步骤。最多 10 条,每条包含要执行的命令或要看的指标,以及如何判断。

返回结果摘要

模型先解释了问题背景,再列出系统资源、GPU、日志、I/O、并发等方向。方向并不离谱,但回答没有收敛成完整的 10 条排查步骤,输出在第 7 条附近被截断。

关键片段

首先,用户描述了一个本地 LLM API 服务,平均响应很快,但偶发 60 秒超时。机器是单张 RTX 4090,Ubuntu 26.04 LTS。我需要列出排查步骤,最多 10 条。每条必须包含要执行的命令或要看的指标,以及如何判断。

作为运维工程师,我应该专注于常见的性能问题:CPU、内存、GPU、I/O、网络、服务配置等。超时通常表示服务在处理请求时卡住了或资源不足。

关键点:

  • 偶发超时:不是持续问题,所以可能是资源争用、临时瓶颈或特定请求导致。
  • 机器:RTX 4090 GPU,Ubuntu 26.04 LTS(注意:Ubuntu 26.04 可能是个笔误,因为当前最新是 24.04,但我会假设是 22.04 或 24.04,不影响排查)。
  • LLM API 服务:可能基于像 vLLM、Text Generation WebUI 或其他框架。

排查步骤应逻辑顺序:从系统级到服务级。

列出可能的排查步骤:

  1. 检查系统资源使用情况:CPU、内存、GPU 内存。偶发超时可能由于资源不足。命令:tophtop 查看 CPU 和内存;nvidia-smi 查看 GPU 使用率和内存。判断:如果超时发生时,GPU 内存使用率接近 100%,或 CPU 使用率高,可能资源瓶颈。
  2. 检查 GPU 驱动和 CUDA 状态:GPU 问题可能导致服务挂起。命令:nvidia-smi -l 1 实时监控,或 nvidia-smi 查看驱动版本和错误。判断:如果有错误消息,如 GPU hang,或驱动版本不兼容。
  3. 检查服务日志:查看 API 服务的日志。命令:journalctl -u your-service-name 或查看服务日志文件。判断:日志中是否有 timeoutGPU out of memory 等错误。
  4. 检查 I/O 性能:如果服务涉及磁盘 I/O,例如加载模型,I/O 慢可能导致超时。命令:iostat -x 1。判断:如果超时发生时磁盘 I/O 使用率高,例如 %util > 90%,可能是 I/O 瓶颈。
  5. 检查网络延迟:但服务是本地的,所以网络可能不是问题,除非是内部网络。命令:ping localhostnetstat 查看连接。
  6. 检查模型加载和缓存:LLM 服务可能在处理请求时动态加载模型,导致偶发延迟。命令:查看服务配置,如 vLLM 的 --model 参数或日志。
  7. 检查并发请求:高并发可能导致资源争用。输出在这里被截断。

观察

这类排障题最需要的是优先级和可执行命令。qwen3:30b 的内容方向有参考价值,但因为先输出大量分析,最终 checklist 不完整。

调用 4:Bash 脚本生成

Prompt

写一个可以直接运行的 Bash 脚本,每 5 秒记录一次 nvidia-smi 的显存占用和 GPU 利用率,输出到 CSV 文件。要求使用 nvidia-smi --query-gpu,支持 Ctrl+C 正常退出。

返回结果摘要

模型花了大量篇幅解释脚本应该怎么写,包括 nvidia-smi --query-gpu 字段、CSV 表头、trap 信号处理、单位清洗等。它给出了一些代码片段,但没有在本次输出内产出完整可运行脚本。

关键片段

嗯,用户需要写一个 Bash 脚本,每 5 秒记录一次 nvidia-smi 的显存占用和 GPU 利用率,输出到 CSV 文件,还要支持 Ctrl+C 正常退出。首先,我得确定 nvidia-smi 的正确查询参数。记得 nvidia-smi --query-gpu 可以指定字段,比如 memory.usedutilization.gpu

然后,CSV 文件需要有标题行,所以脚本开始的时候应该先写标题,比如 timestamp,used_memory,utilization。接下来,用循环每 5 秒执行一次命令。不过要注意,用户要求支持 Ctrl+C 退出,所以得处理信号,比如用 trap 捕获 SIGINT

不过 Bash 脚本里,trap 可以设置在退出时清理,但这里可能不需要清理,主要是确保在 Ctrl+C 时脚本退出而不留下不完整的数据。不过用户可能希望数据写到文件,所以需要确保在退出时文件是完整的。

然后,如何获取当前时间戳?可以用 date +%s 或者更详细的格式,比如 date +"%Y-%m-%d %H:%M:%S",这样 CSV 里的时间更易读。

接下来,nvidia-smi 的命令需要正确:

nvidia-smi --query-gpu=memory.used,utilization.gpu --format=csv,noheader

输出可能是 1024 MiB, 50 %,需要去掉单位。可以用 awk

nvidia-smi --query-gpu=memory.used,utilization.gpu --format=csv,noheader | \
awk -F, '{gsub(/[^0-9]/, "", $1); gsub(/[^0-9]/, "", $2); print $1 "," $2}'

循环大致是:

while true; do
  timestamp=$(date +"%Y-%m-%d %H:%M:%S")
  data=$(nvidia-smi --query-gpu=memory.used,utilization.gpu --format=csv,noheader | awk -F, '{gsub(/[^0-9]/, "", $1); gsub(/[^0-9]/, "", $2); print $1 "," $2}')
  echo "$timestamp,$data" >> gpu_data.csv
  sleep 5
done

后续输出继续解释多 GPU、表头和信号处理,但没有在本次 token 限制内产出完整脚本。

观察

这次调用不能作为脚本生成结果使用。它展示了模型知道该怎么做,但没有完成用户真正需要的产物。

我还用相同问题在末尾追加 /no_think 做了一轮对照。结果没有本质改善:四个问题仍然输出思考过程,done_reason 仍然是 length。因此在当前 Ollama 调用方式下,qwen3:30b 更适合做吞吐测试,不适合作为默认交互模型;如果后续能找到稳定关闭 thinking 输出的配置,再重新评估它的实际能力。

10. 记录性能数据

建议建一个简单表格,每次测试都填进去:

日期模型量化上下文框架显存峰值输出速度结论
2026-05-12deepseek-r1:32bQ4_K_MOllama 默认Ollama 0.23.2待补10.49 tok/sdense 32B 档位,推理模型,吞吐明显低
2026-05-12qwen3:30bQ4_K_MOllama 默认Ollama 0.23.2待补203.90 tok/sMoE 模型,短输出吞吐很高
2026-05-12qwen3-coder:30bQ4_K_MOllama 默认Ollama 0.23.2待补205.08 tok/sMoE 代码模型,本轮 prompt 下提前结束

这张表先记录输出 token/s,显存峰值后面会用 nvidia-smi 采样补齐。当前测试结束后,nvidia-smi 显示 GPU 上还保留了 Ollama 加载的模型缓存,显存占用约 21555 MiB / 24564 MiB。这个数字不能当作单次推理峰值,只能说明 30B/32B 级 4bit 模型已经接近 4090 24GB 的实用上限。

本轮 benchmark 的 CSV 摘要如下:

model,params_b,parameter_size,size_gib,runs,successful_runs,median_output_tokens_per_second,median_prompt_tokens_per_second,median_wall_seconds,median_load_seconds,generated_tokens,prompt_tokens
deepseek-r1:32b,32.800,32.8B,18.488,3,3,10.490,274.658,24.743,0.065,256,28
qwen3:30b,30.500,30.5B,17.282,3,3,203.895,4902.763,1.368,0.062,256,35
qwen3-coder:30b,30.500,30.5B,17.282,3,3,205.075,5938.249,1.135,0.060,212,33

显存可以用:

nvidia-smi --query-gpu=timestamp,name,memory.used,memory.total,utilization.gpu,power.draw,temperature.gpu \
  --format=csv \
  -l 5 \
  | tee gpu-llm-test.csv

如果要记录接口耗时,可以先用简单脚本:

cat > test_chat_latency.py <<'PY'
import json
import time
import urllib.request

url = "http://127.0.0.1:8000/v1/chat/completions"
payload = {
    "model": "local-qwen",
    "messages": [
        {"role": "user", "content": "请用 300 字说明本地大模型部署的关键注意事项。"}
    ],
    "temperature": 0.7,
    "stream": False,
}

data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})

start = time.perf_counter()
with urllib.request.urlopen(req, timeout=300) as resp:
    body = resp.read().decode("utf-8")
elapsed = time.perf_counter() - start

print(f"elapsed={elapsed:.2f}s")
print(body[:1000])
PY

python3 test_chat_latency.py

后续我会把这部分改成更完整的批量测试脚本。

现在这个脚本已经有了一个 Ollama 版本,核心思路是直接读取 Ollama 返回的纳秒级 duration 字段,而不是只用客户端墙钟时间粗略估算。墙钟时间仍然保留,用来发现网络、排队和客户端侧开销。

11. 常见问题

nvidia-smi 正常,但 llama.cpp 没用 GPU

先看启动日志里有没有 CUDA backend。再确认编译时确实带了:

cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j"$(nproc)"

如果用的是系统里另一个旧版本 llama-server,也可能出现“你以为自己跑了 CUDA 版,实际跑的是 CPU 版”的情况。用绝对路径启动最稳。

模型能启动,但一问就 OOM

优先降上下文:

-c 4096

再考虑换更小量化。模型权重只是显存的一部分,KV cache 会随着上下文长度和并发继续吃显存。24GB 显存不要一上来追 64K、128K。

下载模型很慢

先用 huggingface-cli download,不要浏览器手工下。国内网络可以临时使用:

export HF_ENDPOINT=https://hf-mirror.com

如果公司或内网有代理,也可以设置:

export HTTPS_PROXY=http://127.0.0.1:7890
export HTTP_PROXY=http://127.0.0.1:7890

服务给别人访问时不安全

llama-server 直接暴露在公网并不合适。至少要放在内网,或者前面加 Nginx、鉴权和 HTTPS。最简单的内网反代示例:

server {
    listen 80;
    server_name local-llm.internal;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_read_timeout 300s;
    }
}

如果只是自己用,SSH 端口转发更干净:

ssh -L 8000:127.0.0.1:8000 user@your-server

然后本机访问 http://127.0.0.1:8000

12. 后续要补的实测结果

接下来我会按下面顺序补数据:

  • Ubuntu 26.04 LTS 实机上的驱动版本、CUDA 版本。
  • 服务器访问 GitHub 不稳定时的 mihomo 代理安装记录。
  • Ollama 0.23.2 systemd 服务安装记录。
  • llama.cpp 在当前 Ubuntu 26.04 编译链上的失败日志和绕行方案。
  • Ollama 大模型 token 速率 benchmark 脚本与首轮测试结果。
  • Qwen3.6-35B-A3B Q4_K_M 在 8K / 16K 上下文下的显存占用。
  • llama.cpp server 的 tokens/s、首 token 延迟。
  • Ollama 长输出、长上下文和多轮对话体验对比。
  • Qwen3-Coder-30B-A3B-Instruct 代码任务测试结果。
  • 是否需要切到 SGLang 或 vLLM 做服务化。

先把教程写到这里。下一步会补显存采样、长上下文压力测试、代码任务质量对比,以及最终推荐参数。