丹尼拉(Dayfing)
返回文章列表
4,925 字20 分钟

Ollama、llama.cpp 还是 vLLM:如何选择本地 LLM 服务器

如果只是一名开发者在笔记本上使用,就运行 Ollama:几分钟即可装好,按名称下载模型,并在请求到来时按需加载。如果是小团队、Mac、没有 GPU 的服务器或消费级显卡,就选择 llama.cpp 的 llama-server:它运行 GGUF 模型,并让你明确控制并行槽位、API 密钥和 Prometheus 指标。如果要在数据中心级 GPU 上承载生产流量,就运行 vLLM:它的 continuous batching 和 PagedAttention 能在并发增长时保持高吞吐。三者都提供 OpenAI 兼容 API,因此可以先用其中一个,之后只需更换 base URL 和模型名称就能迁移到另一个。

每个服务器的优化方向

Ollama 优化的是拿到第一个回答所需的时间。它以桌面应用或后台服务的形式运行,按名称下载模型,在第一个请求到来时加载模型,并在空闲一段时间后将其卸载;根据 Ollama FAQ,默认空闲时间为五分钟。少量 OLLAMA_* 环境变量取代了精细的调度配置,而这种简单正是它的价值所在。

llama.cpp 优化的是可移植性。它是基于 ggml 库、用 C/C++ 编写的推理引擎,提供面向 Apple Silicon 的 Metal、CUDA、面向 AMD 的 HIP、Vulkan、SYCL 等后端,并为 AVX、AVX2、AVX-512、AMX 和 ARM NEON 优化了 CPU 路径。当模型放不进显存时,它可以把一部分层放在 GPU 上,其余放在系统内存中。它的 HTTP 服务器 llama-server 只需要一个二进制文件、一个 GGUF 文件和若干参数。项目 README 的快速入门现在展示的是统一的 llama serve 命令,而 llama-server 文档 和 Docker 镜像仍然提供本文所用的 llama-server 可执行文件。

vLLM 优化的是加速器上的吞吐量。它的 README 列出了用于管理 KV 缓存的 PagedAttention、continuous batching、chunked prefill、prefix caching、多 LoRA 服务、张量并行与流水线并行,以及一长串量化格式。它支持 NVIDIA、AMD 和 Intel GPU,以及 x86、ARM 和 PowerPC CPU,其他加速器则通过硬件插件接入。

简而言之:Ollama 和 llama.cpp 适合普通硬件和少量同时在线的用户,而当大量请求同时涌入时,vLLM 更复杂的部署才物有所值。选型中的硬件部分,包括权重和 KV 缓存各需要多少内存,见 本地 LLM 硬件指南。

一个 OpenAI 客户端,三个 base URL

三个服务器都实现了 /v1/chat/completions、/v1/completions、/v1/embeddings、/v1/models 和 /v1/responses。Ollama 的 OpenAI 兼容性页面 说明,它对 Responses 的支持是无状态的。llama-server 还提供兼容 Anthropic 的 /v1/messages,vLLM 则在 OpenAI 路由之外列出了 Anthropic Messages、音频转写和 pooling API。

把服务器的选择放在配置里,这样客户端代码永远不用改:

import os
from openai import OpenAI

# Ollama:       http://127.0.0.1:11434/v1
# llama-server: http://127.0.0.1:8080/v1
# vLLM:         http://127.0.0.1:8000/v1
client = OpenAI(
    base_url=os.environ["LLM_BASE_URL"],
    api_key=os.environ.get("LLM_API_KEY", "not-used"),
)

reply = client.chat.completions.create(
    model=os.environ["LLM_MODEL"],
    messages=[{"role": "user", "content": "Explain HTTP 429 in two sentences."}],
    temperature=0.2,
)
print(reply.choices[0].message.content)

OpenAI SDK 要求提供一个密钥字符串,所以当服务器没有设置密钥时,传一个占位值即可。Ollama 在本地会忽略密钥,而 llama-server 和 vLLM 只有在带密钥启动时才会校验它。

三者的差异体现在 model 字段上。Ollama 需要其模型库中的标签,例如 qwen3:8b。vLLM 需要 Hugging Face 仓库名,或者 --served-model-name 设定的值。只加载一个模型的 llama-server 会直接使用已加载的模型,--alias 用来设置 /v1/models 返回的名称,而在 router 模式下,名称决定使用哪个模型。

兼容性止步于 OpenAI 规范的边界。vLLM 通过 extra_body 接收 top_k 等额外参数,并且默认应用模型自带的 generation_config.json;如果不传 --generation-config vllm,默认采样参数可能因此改变。GGUF 转换版本与原始检查点之间的聊天模板和分词器也可能不同,所以切换服务器之前,请在两者上运行同一套评估集,具体做法见 AI 智能体评估指南。

模型格式与权重来源

三个服务器读取的文件并不相同,这一点往往比性能更早决定选择。

Ollama 按名称从自己的模型库下载模型,用 ollama run hf.co/{user}/{repo}:{quant} 运行 Hugging Face 上的 GGUF 仓库,并通过 Modelfile 导入本地 GGUF 文件或 Safetensors 目录,其中 FROM 行指向权重。它在导入时不会量化 GGUF 文件,因此需要先用 llama.cpp 的工具完成量化。

llama.cpp 读取 GGUF。可以用 convert_hf_to_gguf.py 转换 Hugging Face 检查点再进行量化,也可以用 -hf user/repo:quant 直接下载现成的 GGUF。README 列出了从 1.5 位到 8 位的整数量化。视觉模型还需要一个 projector 文件,-hf 会自动下载。

vLLM 读取带有 Safetensors 权重的 Hugging Face 模型仓库,也支持 GPTQ、AWQ、FP8、INT8、INT4 和 compressed-tensors 等量化检查点。它的文档称 GGUF 支持仍处于高度实验阶段,而且这部分支持已迁移到外部插件 vllm-gguf-plugin。

# Ollama: a library model, or a GGUF repository on Hugging Face
ollama pull qwen3:8b
ollama run hf.co/bartowski/Llama-3.2-3B-Instruct-GGUF:Q8_0

# llama.cpp: download a GGUF and serve it
llama-server -hf ggml-org/Qwen3.5-0.8B-GGUF --port 8080

# vLLM: serve a Hugging Face repository
vllm serve Qwen/Qwen3-0.6B --host 127.0.0.1 --port 8000

并发、批处理与 KV 缓存内存

并发要消耗内存。每个活跃序列都要为上下文中的每个 token 保存注意力的键和值,因此在权重之外,内存会随“并发序列数 × 序列长度”增长。对于标准 transformer,可以这样估算:

KV bytes per token = 2 × layers × kv_heads × head_dim × bytes_per_value
Qwen3-8B, 16-bit cache: 2 × 36 × 8 × 128 × 2 = 147,456 bytes = 144 KiB
One 32,768-token sequence: 144 KiB × 32,768 = 4.5 GiB

层数、KV 头数和头维度都来自模型的 config.json。四个这样的序列在不计权重的情况下就需要 18 GiB 缓存。采用滑动窗口注意力或混合注意力层的模型需要的更少,因此对它们来说,这个公式给出的是上限。

Ollama

OLLAMA_NUM_PARALLEL 决定每个已加载模型同时处理多少个请求,FAQ 给出的默认值是 1。OLLAMA_MAX_LOADED_MODELS 默认为 GPU 数量的三倍,CPU 推理时为 3。OLLAMA_MAX_QUEUE 默认允许 512 个请求排队,超出后服务器返回 503 错误。FAQ 还提醒,所需内存会随 OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH 增长。默认上下文长度在不同文档页面中的描述并不一致,而且取决于可用显存,所以请显式设置,并在 ollama ps 输出的 CONTEXT 列中确认。

OLLAMA_HOST=127.0.0.1:11434 \
OLLAMA_NUM_PARALLEL=4 \
OLLAMA_CONTEXT_LENGTH=16384 \
OLLAMA_KEEP_ALIVE=30m \
ollama serve

llama-server

llama-server 把工作划分为槽位(slot)。每个槽位承载一段对话,-np 设置槽位数量,默认值 -1 表示自动。continuous batching 默认开启,新请求会在解码步骤之间加入正在运行的批次。当槽位数量为自动时,所有槽位共享一个统一的 KV 缓冲区。如果你手动设置 -np,统一缓冲区默认关闭,-c 会在各槽位之间平分:-c 32768 -np 4 意味着每段对话有 8192 个 token。可以用 GET /slots 检查结果,它会显示每个槽位的 n_ctx。

GPU 卸载由 -ngl 控制,它接受数字、auto(默认)或 all。如果模型放不下,留在 CPU 上的层会受限于系统内存带宽,因此部分卸载的模型可以运行,但生成 token 通常更慢。

llama-server -m /models/qwen3-8b-q4_k_m.gguf \
  --host 127.0.0.1 --port 8080 \
  -c 32768 -np 4 -ngl all \
  --api-key-file /etc/llama/api-keys.txt \
  --metrics

vLLM

只要 KV 缓存还有空闲块,vLLM 的 continuous batching 就会在每一次解码迭代中接纳新序列;PagedAttention 则随着序列增长,以固定大小的块分配缓存,而不是预先按最大长度预留。启动时,vLLM 会占用 --gpu-memory-utilization 指定比例的显存,并把扣除权重和激活工作区之后剩下的部分变成 KV 缓存块。负载下缓存块耗尽时,它会抢占一部分请求,等空间释放后再重新计算,这会体现在 vllm:num_preemptions 指标和尾延迟上。

主要的调节手段包括:用 --max-model-len 限定可接受的最长上下文,用 --max-num-seqs 限定每次迭代的最大序列数,以及 --max-num-batched-tokens、--gpu-memory-utilization,还有用于把一个模型拆分到多块 GPU 上的 --tensor-parallel-size。把 --max-model-len 降到应用真正需要的长度,通常是容纳更多并发请求最便宜的办法。

export VLLM_API_KEY="$(openssl rand -hex 32)"
vllm serve Qwen/Qwen3-8B \
  --host 127.0.0.1 --port 8000 \
  --max-model-len 32768 \
  --max-num-seqs 64 \
  --gpu-memory-utilization 0.90

结构化输出与工具调用

三个服务器都能通过 OpenAI 的 response_format 字段把输出约束为某个 JSON 模式,因此下面这个请求可以在它们之间通用:

schema = {
    "type": "object",
    "properties": {
        "severity": {"type": "string", "enum": ["low", "medium", "high"]},
        "summary": {"type": "string"},
    },
    "required": ["severity", "summary"],
    "additionalProperties": False,
}

reply = client.chat.completions.create(
    model=os.environ["LLM_MODEL"],
    messages=[{"role": "user", "content": "Triage this log line: disk /var is 97% full"}],
    response_format={
        "type": "json_schema",
        "json_schema": {"name": "triage", "schema": schema},
    },
)

Ollama 的原生 API 还可以在 format 字段中接受 "json" 或一个模式,其文档建议在提示词中重复一遍模式。llama-server 会把 JSON 模式转换为自己的 GBNF 语法格式,也接受原始语法来约束其他输出形式。vLLM 支持 response_format,也支持在 extra_body 中传入 structured_outputs 对象,其键包括 json、regex、choice、grammar 和 structural_tag。正如 vLLM structured outputs 页面 所说,旧的 guided_* 参数已在 v0.12.0 中移除。约束解码只保证语法正确,不保证内容真实;被 max_tokens 截断的回答依然是无效 JSON,所以务必在自己的代码中按模式校验每个结果。

三者都支持工具调用,但配置方式不同:

  • Ollama 在 /api/chat 和 /v1/chat/completions 上都接受 tools,包括并行调用,前提是模型模板支持工具。
  • llama-server 通过默认启用的 Jinja 聊天模板支持 OpenAI 风格的工具。llama.cpp 的 function calling 说明 列出了针对多个模型系列的原生处理器,以及一个消耗更多 token 的通用后备处理器。除非请求设置 "parallel_tool_calls": true,否则并行调用保持关闭。
  • vLLM 需要 --enable-auto-tool-choice,以及与模型系列匹配的 --tool-call-parser,例如 Qwen2.5 用 hermes,Llama 3.1 和 3.2 用 llama3_json。vLLM tool calling 页面 解释说,命名函数和 tool_choice="required" 会使用 structured outputs,而 auto 模式不保证参数一定能被正确解析。
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --host 127.0.0.1 --port 8000 \
  --enable-auto-tool-choice --tool-call-parser hermes

请把工具参数当作不可信输入。参数由模型选择,而检索到的文档中的文本可能左右这个选择,详见 提示词注入与 MCP 安全指南。

在 Docker 中运行并访问 GPU

在带 NVIDIA GPU 的 Linux 上,先安装 NVIDIA Container Toolkit,--gpus 才能生效。下面的命令沿用各项目文档中的镜像和参数,只做了一处修改:所有端口都只发布在 127.0.0.1 上。Docker 端口发布文档 指出,未指定主机地址的端口会发布到主机的所有地址上;Docker 关于防火墙的说明还补充道,已发布端口的流量会在 ufw 规则看到之前就被转发。

# Ollama with NVIDIA GPUs
docker run -d --name ollama --gpus=all \
  -v ollama:/root/.ollama \
  -p 127.0.0.1:11434:11434 \
  ollama/ollama

# llama-server, CUDA build
docker run -d --name llama --gpus all \
  -v /srv/models:/models:ro \
  -p 127.0.0.1:8080:8080 \
  ghcr.io/ggml-org/llama.cpp:server-cuda \
  -m /models/qwen3-8b-q4_k_m.gguf --host 0.0.0.0 --port 8080 -ngl all

# vLLM with NVIDIA GPUs
docker run -d --name vllm --runtime nvidia --gpus all --ipc=host \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --env "HF_TOKEN=$HF_TOKEN" \
  -p 127.0.0.1:8000:8000 \
  vllm/vllm-openai:latest \
  --model Qwen/Qwen3-8B

在容器内部,服务器必须监听 0.0.0.0,否则发布的端口无法到达它;真正让它保持私有的是主机一侧的 127.0.0.1。vLLM 文档中的命令加上了 --ipc=host,因为 PyTorch 通过共享内存在进程之间交换数据,张量并行推理时尤其如此。对于 AMD GPU,请使用带 --device /dev/kfd --device /dev/dri 的 ollama/ollama:rocm、llama.cpp 的 server-rocm 或 server-vulkan 镜像,或者 vllm/vllm-openai-rocm。在生产环境中,请固定镜像标签或 digest,而不是使用 latest。

安全:本地监听,在代理处认证

像对待数据库端口一样对待推理端口。任何能访问到它的人都可以消耗你的 GPU 时间、读取模型返回的全部内容,在某些配置下还能调用管理类路由。

先看监听地址。Ollama 默认监听 127.0.0.1:11434,llama-server 监听 127.0.0.1:8080。vLLM 则不同:如果没有设置 --host,它的启动器会监听所有网络接口,所以在容器之外务必传入 --host 127.0.0.1。

然后检查每个服务器的密钥实际保护了什么:

  • Ollama 的本地 API 没有 API 密钥设置。任何能访问该端口的人都能使用它,因此只能通过带认证的代理来共享。
  • llama-server 会在其 API 上校验 --api-key 或 --api-key-file,而 /health 按设计保持公开。它的 README 提到,CORS 默认会回显任意 Origin,并建议在局域网中使用 --cors-origins。
  • vLLM 的 --api-key 只覆盖少数几个路径前缀,其中包括 /v1。vLLM 安全指南 列出了不受保护的路由,例如能调用同样推理功能的 /invocations,以及 /pause 或 /abort_requests,并建议使用只放行白名单端点的反向代理。

一个小型 Nginx 前端即可同时满足两点需求:它终止 TLS、校验 bearer 令牌、只转发 /v1/,其他请求一律返回 404。

map $http_authorization $llm_client {
    default "";
    "Bearer REPLACE_WITH_A_LONG_RANDOM_TOKEN" "team";
}

server {
    listen 443 ssl;
    server_name llm.example.internal;
    ssl_certificate     /etc/nginx/tls/llm.crt;
    ssl_certificate_key /etc/nginx/tls/llm.key;
    client_max_body_size 10m;

    location /v1/ {
        if ($llm_client = "") { return 401; }
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_read_timeout 600s;
    }

    location / {
        return 404;
    }
}

proxy_buffering off 让流式 token 一生成就能送达客户端。如果上游服务器有自己的密钥,就给它配置同一个令牌,这样绕过代理的请求仍会失败。对于 Ollama,把 proxy_pass 指向 11434 端口,并按照 FAQ 示例添加 proxy_set_header Host localhost:11434。多个用户共享一块 GPU 时,加上 limit_req。在开发者的笔记本上要记住,OLLAMA_ORIGINS=* 会允许你访问的任何网页通过浏览器调用本地服务器。

监控与健康检查

先从就绪状态入手。llama-server 的 /health 在模型加载期间返回 503,就绪后返回 200;vLLM 同样提供 /health。它们适合用于容器健康检查和负载均衡探针,但不能证明生成质量没有问题。

vLLM 默认在 /metrics 上提供 Prometheus 指标。vLLM 指标页面 列出了 vllm:num_requests_running、vllm:num_requests_waiting、vllm:kv_cache_usage_perc,首 token 时间、token 间延迟和端到端延迟的直方图,以及一个抢占计数器。llama-server 只有在使用 --metrics 启动时才提供 /metrics,其中包括 llamacpp:requests_processing、llamacpp:requests_deferred、llamacpp:prompt_tokens_seconds 和 llamacpp:predicted_tokens_seconds,而 GET /slots 显示每个槽位的状态。

scrape_configs:
  - job_name: vllm
    static_configs:
      - targets: ["127.0.0.1:8000"]
  - job_name: llama-server
    static_configs:
      - targets: ["127.0.0.1:8080"]

Ollama 的文档化 API 中没有 Prometheus 端点。可以用 GET /api/ps 查看已加载的模型、它们的显存占用、上下文长度和卸载时间,并读取每个响应中的计时字段。时长单位是纳秒,所以生成速度为每秒 eval_count / eval_duration × 10^9 个 token:

curl -s http://127.0.0.1:11434/api/generate \
  -d '{"model": "qwen3:8b", "prompt": "Say ready.", "stream": false}' |
  jq '{tokens: .eval_count, tokens_per_second: (.eval_count / .eval_duration * 1e9)}'

针对“用户正在等待”的信号设置告警:waiting 或 deferred 队列长期不为零、KV 缓存使用率接近 1、抢占计数持续上升,以及 p95 首 token 时间不断变长。服务器指标描述的是容量,它不会告诉你是哪个提示词、哪次工具调用或哪一步检索拖慢了请求,因此需要把它们与请求追踪关联起来,做法见 AI 智能体可观测性指南。

并排对比

Ollama llama.cpp llama-server vLLM
优化目标 快速安装与模型管理 在 CPU、Apple Silicon 和消费级 GPU 上的可移植性 数据中心 GPU 上的吞吐量
模型格式 模型库模型、GGUF、Safetensors 导入 GGUF Hugging Face Safetensors、GPTQ、AWQ、FP8 等;GGUF 为实验性
默认监听地址 127.0.0.1:11434 127.0.0.1:8080 所有接口,端口 8000
内置 API 密钥 无 --api-key、--api-key-file --api-key 或 VLLM_API_KEY,仅限部分路径前缀
并发 每个模型 OLLAMA_NUM_PARALLEL,默认 1 槽位(-np)加 continuous batching continuous batching 加 PagedAttention
结构化输出 format、response_format response_format、JSON 模式、GBNF response_format、structured_outputs
工具调用 原生路由和 OpenAI 路由上的 tools Jinja 模板,原生或通用处理器 --enable-auto-tool-choice 加解析器
指标 /api/ps 和响应计时 --metrics 开启的 /metrics、/slots 默认提供 /metrics
最适合 单个开发者、原型 小团队、普通或混合硬件 大量并发用户、生产 SLO

面向开发、团队和生产的选型路径

按顺序回答下面的问题,遇到第一个明确答案就停下。

  1. 是否只有一个人在笔记本或工作站上试用模型?用 Ollama。当你需要指标、认证或按请求控制时,再考虑下一步。
  2. 你用的是 Mac、没有 GPU 的服务器,还是模型勉强放得进甚至放不进显存的消费级显卡?用 llama-server。GGUF 量化和部分卸载能让它在 vLLM 根本加载不了模型的地方运行。
  3. 是否是一台机器上有少量并发用户的小团队?用 llama-server,把 -np 设为峰值并发,并配置 API 密钥、--metrics 和代理。如果大家都只用一两个模型,并由代理负责认证,Ollama 也可以胜任。
  4. 是否预期有持续的并发流量、明确的延迟目标、多块 GPU,或需要 FP8、AWQ 这类 GPU 量化格式?用 vLLM,绑定到 localhost 并放在网关之后,固定镜像版本,并从第一天起接入 Prometheus 采集。

很多团队最终会同时用到其中两个:开发机上用 Ollama 或 llama-server,预发布和生产环境用 vLLM,各处使用同一套 OpenAI 客户端代码。前提是评估集必须在生产所用的模型文件上运行,因为同一个模型的 GGUF 量化版本和 FP8 检查点在实践中表现得就像两个不同的模型。生产级 AI 智能体架构指南 介绍了应当放在这些服务器前面的网关、超时、重试和降级方案。

部署检查清单

  • 按选型路径确定服务器,并记录将要提供服务的确切模型文件、量化方式和聊天模板。
  • 按峰值并发和最大上下文计算 KV 缓存内存,然后相应设置 OLLAMA_CONTEXT_LENGTH,或 -c 与 -np,或 --max-model-len 与 --max-num-seqs。
  • 绑定到 127.0.0.1,为 vLLM 显式传入 --host 127.0.0.1,并以 127.0.0.1:port:port 的形式发布 Docker 端口。
  • 把 TLS、认证、端点白名单和限流放进反向代理。绝不暴露未经认证的推理端口。
  • 在 llama-server 或 vLLM 上设置密钥作为第二层防护,并确保它不会留在 shell 历史和镜像层中。
  • 通过私有网络采集 llama-server 和 vLLM 的 /metrics,并轮询 Ollama 的 /api/ps。
  • 用你自己的提示词在预期并发下做压测,记录首 token 时间、每秒 token 数、队列长度和内存占用。
  • 在代码中按模式校验结构化输出,并把工具参数当作不可信输入。
  • 固定版本和镜像 digest,每当服务器、模型文件、量化方式或镜像发生变化时,重新运行评估集。

更多文章