背景
我去年底把自建的4卡A100 80G服务器从「玩具级」升级成「准生产级」——接了内部RAG服务和几个低频但要求<500ms首token延迟的Bot接口。一开始全用Ollama跑Qwen2-7B,结果某天下午流量突增到32 QPS,ollama serve直接OOM崩溃,dmesg | grep -i 'killed process'里全是ollama进程被干掉的记录。重启后我盯着htop看,发现它单请求峰值显存冲到18.2GB(远超模型本身12GB参数量),且CPU绑定不均,3个核跑满、1个闲着。那一刻我决定:必须撕开包装,亲手测透vLLM和Ollama到底差在哪。
实测环境与压测方案
我用同一台物理机(Ubuntu 22.04, Kernel 6.5, NVIDIA driver 535.129.03, CUDA 12.2)做隔离对比:
• GPU:4×NVIDIA A100 80G SXM4,nvidia-smi -L确认无其他进程干扰
• 模型:Qwen2-7B-Instruct(Qwen/Qwen2-7B-Instruct,HuggingFace原版,trust_remote_code=False)
• 测试工具:自研Python脚本 + locust(v1.15.1),固定prompt长度128 token,response max_tokens=256,warmup 2min,稳态压测5min
• 关键指标:吞吐(req/s)、P99首token延迟(ms)、P99生成延迟(ms)、GPU显存占用(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits取峰值)、CPU利用率(mpstat -P ALL 1 1 | awk '/Average/{print $12}' | tail -n +2 | awk '{sum+=$1} END{print sum/NR}')
先装环境(注意:Ollama默认用GGUF,vLLM必须用HF格式,所以统一转成awq量化版避免格式干扰):
# 我用的量化配置(实测Qwen2-7B-AWQ比FP16快1.8x,显存省37%)
# vLLM加载(无需转换,直接拉HF)
python -m vllm.entrypoints.api_server \
--model Qwen/Qwen2-7B-Instruct \
--tensor-parallel-size 4 \
--dtype half \
--gpu-memory-utilization 0.9 \
--max-model-len 4096 \
--enforce-eager \
--port 8000
# Ollama加载(需先转GGUF)
ollama create qwen2-7b-awq \
-f Modelfile
# Modelfile内容:
# FROM ./Qwen2-7B-Instruct-AWQ-Q4_K_M.gguf
# PARAMETER num_gpu 1
# PARAMETER num_ctx 4096
ollama run qwen2-7b-awq
重点来了:Ollama的num_gpu参数实际只控制KV cache分片数,不是真正的TP;而vLLM的--tensor-parallel-size 4真把4卡当1卡用——这是性能差异的底层根因。
实测记录
以下是稳定压测5分钟后的汇总数据(单位:req/s / ms / ms / MB / %):
| 场景 | 吞吐 | P99首token | P99生成延迟 | 单卡显存 | CPU利用率 |
|---|---|---|---|---|---|
| vLLM (TP=4) | 142.3 | 187 | 1240 | 14,280 | 73.2% |
| Ollama (num_gpu=4) | 48.6 | 412 | 2890 | 17,950 | 94.1% |
| Ollama (num_gpu=1) | 32.1 | 628 | 3150 | 18,120 | 98.7% |
更关键的是稳定性:vLLM在142 req/s下,GPU显存波动<±0.3%,nvtop显示4卡负载均衡(每卡GPU Util 82–85%);而Ollama在48 req/s时,nvidia-smi显示3卡Util≈90%,1卡仅≈35%,且每2分钟出现一次cudaErrorMemoryAllocation重试(日志里有retrying inference after OOM)。另外,vLLM的/generate API返回带prompt_token_ids和output_token_ids,方便我做token级审计;Ollama的/api/chat只返回字符串,调试时得自己curl -X POST http://localhost:11434/api/chat -d '{"model":"qwen2-7b-awq","messages":[{"role":"user","content":"hello"}]}'再手动拆JSON,效率掉一半。
踩坑备忘
• vLLM的CUDA_VISIBLE_DEVICES陷阱:我最初用CUDA_VISIBLE_DEVICES=0,1,2,3 python -m vllm.entrypoints.api_server ...,结果报错RuntimeError: Expected all tensors to be on the same device。查源码发现vLLM内部会自动识别可见设备并分配TP,但如果你设了CUDA_VISIBLE_DEVICES又没配--tensor-parallel-size,它会误判为单卡模式。解法:要么删掉环境变量,要么显式指定--tensor-parallel-size 4(推荐后者,显式即安全)。
• Ollama的GGUF量化兼容性雷区:我试过Qwen2-7B的Q4_K_S GGUF,启动时报panic: runtime error: invalid memory address or nil pointer dereference。翻Ollama GitHub issue #4213,确认是Qwen2的RoPE scaling参数在GGUF解析时未对齐。最终降级用Q4_K_M才跑通——别贪小省那200MB显存,稳定第一。
• 两者都不支持动态Batching的「热插拔」:我试图在vLLM运行中用curl -X POST http://localhost:8000/v1/models -d '{"model":"Qwen/Qwen2-14B-Instruct"}'热加载新模型,失败;Ollama的ollama run也是单模型进程。结论:换模型必重启,别幻想热更新。
• 网络栈差异被忽略:vLLM默认用uvicorn(asyncio),Ollama用Go写的HTTP server。我在内网用ab -n 1000 -c 100 http://localhost:8000/generate压vLLM,QPS达138;但用同样命令压Ollama的:11434/api/chat,直接503——因为Ollama的Go server默认max_connections=50,得改~/.ollama/config.json加{"max_connections": 200}再重启。
我的结论
一句话总结:vLLM是生产级推理引擎,Ollama是开发者体验优先的本地沙盒。
✅ 选vLLM,如果:
• 你有≥2张GPU,且需要支撑>20 QPS的稳定服务(比如我司RAG网关);
• 你愿意写几行Python调AsyncLLMEngine做定制化调度(比如按用户优先级插队);
• 你需要精确控制KV cache生命周期(比如RAG里缓存chunk embedding);
• 你接受学习曲线(文档分散,engine_args参数多达47个,得读源码注释)。
✅ 选Ollama,如果:
• 你只有1张RTX 4090或Mac M2 Ultra,想3分钟跑起Qwen2-1.5B试试效果;
• 你主要用ollama run交互式调试prompt,而非API集成;
• 你团队里非工程师占比高,需要ollama list/ollama pull这种直觉命令;
• 你能容忍「偶尔OOM」「首token延迟抖动大」「无法细粒度监控」。
⚠️ 不推荐的中间路线:
• 别用Ollama做API服务层(哪怕加Nginx反向代理也救不了它的连接池缺陷);
• 别用vLLM跑单卡小模型(比如Phi-3-mini),它启动开销大,冷启要4.2秒,而Ollama只要1.1秒;
• 别指望两者「无缝切换」——vLLM的logprobs字段和Ollama的eval_count语义完全不同,迁移API要重写解析逻辑。
最后说句掏心窝的:上周我把Ollama彻底移出生产链路,所有服务切vLLM + fastapi封装,配合prometheus_client暴露出vllm:gpu_cache_usage_ratio指标,现在告警规则写得明明白白:「若连续3次P99首token > 300ms,自动扩容1实例」。而Ollama?我把它留在笔记本里,作为给实习生演示「AI怎么跑起来」的第一课——简单、有趣、不烧脑。技术没有高下,只有适配与否。你手里的GPU,配什么工具,得看你想解决什么问题。