AI 工程基础体系 · 第 28/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
本地模型与推理引擎:Ollama、llama.cpp、vLLM、量化和部署取舍
一、先区分“模型”和“推理引擎”
模型是参数、网络结构、分词器、配置和对话模板等静态或半静态工件。以自回归语言模型为例,给定输入 token 序列 ,模型估计下一个 token 的条件概率:
生成过程从已有上下文开始,每次选择或采样一个 token,再把它追加到上下文中:
重复这个过程,直到生成停止 token、达到最大输出长度或被服务端截断。
推理引擎负责把这个数学过程变成可执行的系统。它至少要完成:
- 加载模型权重和 tokenizer;
- 把文本转换为 token;
- 按模型要求构造 chat template;
- 执行矩阵乘法、归一化、位置编码等算子;
- 管理 KV Cache;
- 执行贪心、temperature、top-p 等解码策略;
- 在 CPU、GPU 或多 GPU 上调度请求;
- 输出 token、错误、指标和取消结果。
因此,“同一个模型换一个引擎”并不一定得到完全相同的结果。差异可能来自:
- tokenizer 或 chat template 不同;
temperature=0时的浮点计算和 tie-breaking 不同;- 量化方式不同;
- RoPE、FlashAttention、采样器实现不同;
- 停止 token 或最大上下文长度不同;
- 引擎对模型结构的支持不完整。
Hugging Face 的模型仓库通常包含权重、config.json、tokenizer 文件和生成配置,但这些文件并不自动意味着任何引擎都能加载。模型架构、权重格式和 tokenizer 必须同时兼容。
二、一次生成请求内部发生了什么
1. Prefill 与 Decode
推理通常分为两个阶段。
Prefill 阶段一次性处理已有输入。例如用户输入 2,000 个 token,模型会依次计算这些 token 的隐藏状态,并建立 KV Cache。这个阶段的计算量与输入长度相关,通常适合 GPU 并行,因此经常影响首 token 延迟。
Decode 阶段每次只生成一个新 token。新 token 会读取已有 KV Cache,而不重新计算所有历史 token。这个阶段受内存带宽、KV Cache 访问和调度影响明显,生成速度通常以 tokens/s 表示。
可以用以下流程表示:
sequenceDiagram
participant C as 客户端
participant S as 推理服务
participant T as Tokenizer
participant M as 模型执行器
participant K as KV Cache
participant D as Decoder
C->>S: chat 请求
S->>T: 文本 + chat template
T-->>S: input_ids
S->>M: Prefill(input_ids)
M->>K: 写入历史 K/V
M-->>S: 首个 logits
S->>D: 采样下一个 token
loop 直到停止
S->>M: Decode(新 token, K)
M->>K: 追加 K/V
M-->>S: logits
S->>D: 采样 token
D-->>C: 流式输出 token
end
这里的关键状态不是“请求是否收到”,而是:
- 输入 token 是否完成;
- Prefill 是否完成;
- KV Cache 是否分配成功;
- 请求是否进入 decode 调度;
- 是否被客户端取消;
- 是否达到停止条件;
- 是否因 OOM、超时或模型错误终止。
一个服务即使模型加载成功,也可能在长上下文或并发请求开始后失败,因为权重显存和 KV Cache 显存是两类不同资源。
2. KV Cache 的容量推导
对典型 Transformer,KV Cache 保存每层注意力中的 Key 和 Value。单个请求的缓存大小可近似写为:
其中:
- :Transformer 层数;
- :分别保存 K 和 V;
- :当前上下文 token 数;
- :KV head 数;
- :每个 head 的维度;
- :每个元素的字节数,例如 FP16 为 2。
假设模型有:
- 32 层;
- 8 个 KV heads;
- 每个 head 维度为 128;
- 上下文长度为 4096;
- KV 使用 FP16。
则单个请求约占:
约为 512 MiB。8 个同时保留 4096 token 上下文的请求,仅 KV Cache 就可能需要约 4 GiB,尚未计算模型权重、激活、CUDA workspace 和运行时预留。
这解释了两个常见现象:
- 把模型从 FP16 量化到 4 bit 后仍然可能在高并发下 OOM;
- 限制最大上下文长度,往往比继续压缩权重更直接地降低显存峰值。
使用 GQA 或 MQA 的模型时, 小于 query head 数,因此 KV Cache 会比普通多头注意力小。但不能根据模型参数量直接推断 KV Cache 大小,必须查看配置中的层数、KV head 数和 head dimension。
三、Ollama:以本地开发和单机服务为中心的封装
1. 它解决什么问题
Ollama 提供模型拉取、模型文件管理、运行命令和本地 HTTP 服务。它的价值不主要在于发明新的 Transformer 推理算法,而在于把以下工作封装起来:
- 下载和缓存模型;
- 选择适合本机的运行方式;
- 通过 Modelfile 定义模型参数和模板;
- 启动本地模型服务;
- 提供相对简单的 CLI 和 API。
Ollama 底层使用的具体执行后端和支持范围会随版本变化,不能把 Ollama API 等同于某一个固定版本的 llama.cpp。工程上应把它看成“模型管理与服务封装层”,而不是直接假设其内部实现永远不变。
2. 可执行的最小调用
安装 Ollama 后,先运行一个模型:
ollama run llama3.2
随后可以通过本地 API 调用:
curl http://localhost:11434/api/chat \
-H 'Content-Type: application/json' \
-d '{
"model": "llama3.2",
"messages": [
{"role": "user", "content": "用一句话解释 KV Cache"}
],
"stream": false
}'
预期返回 JSON,其中通常包含模型名称、消息内容、是否结束以及耗时统计字段。字段名称和具体统计项应以所安装版本的官方 API 为准;不要在生产代码中无条件依赖未声明的附加字段。
stream: false 适合脚本和测试。流式输出则会让服务分多次返回结果,客户端必须按协议逐段读取,不能把整个响应简单当作一个 JSON 文档处理。
一个常见的 Modelfile 形态如下:
FROM llama3.2
PARAMETER temperature 0.2
PARAMETER num_ctx 4096
SYSTEM 你是一个严谨的中文技术助手。
创建并运行:
ollama create tech-assistant -f Modelfile
ollama run tech-assistant
这里的 num_ctx 会影响最大上下文和 KV Cache 预算,但不代表每个请求都会实际使用这么多 token。设置过大可能增加可用内存压力,设置过小则可能截断输入或无法完成长任务。
3. Ollama 的边界
Ollama 适合:
- 本地开发和功能验证;
- 单机个人工具;
- 低并发内网服务;
- 希望减少模型转换和启动配置工作的场景。
它不自动解决:
- 多租户认证和授权;
- 细粒度配额;
- 高并发排队;
- 请求级优先级;
- 生产级指标、追踪和审计;
- 多副本流量切换;
- 模型版本审批。
本地监听地址也不等于安全边界。若把服务暴露到局域网或公网,必须在反向代理或 API 网关增加认证、TLS、请求大小限制、速率限制和审计。不能因为接口路径简单、没有云账单,就认为它没有安全风险。
四、llama.cpp:轻量、可移植和 GGUF 生态
1. llama.cpp 的定位
llama.cpp 是以 C/C++ 为核心的本地推理项目,重点是:
- CPU 推理;
- Apple Silicon 等统一内存设备;
- CUDA、Metal、HIP、Vulkan 等不同后端;
- 量化模型;
- 低依赖部署;
- 通过命令行或 HTTP server 提供推理。
它最常见的模型交换格式是 GGUF。GGUF 不只是“把权重压缩成一个文件”,还可以携带 tokenizer、模型配置和其他元数据,使推理程序更容易正确恢复模型信息。
但 GGUF 是格式,不等于某一种量化算法。Q4_K_M、Q5_K_M 等名称表示特定实现中的量化类型和混合策略,不能把它们简单解释为“所有参数严格使用 4 bit”或与任意 GPTQ/AWQ 文件互换。
2. 编译和启动示例
具体二进制名称会随仓库版本变化。一个常见的构建过程是:
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
cmake -B build
cmake --build build --config Release -j
如果使用 CUDA,通常需要按该版本文档启用 CUDA 构建选项;不同操作系统和 CUDA 版本的 CMake 参数可能不同,不能直接把某个机器上的构建命令当作普适规范。
加载一个 GGUF 文件时,常见命令形态为:
./build/bin/llama-cli \
-m /models/model.Q4_K_M.gguf \
-p "解释 Prefill 和 Decode 的区别。" \
-n 128 \
-c 4096
参数含义是:
-m:模型文件;-p:提示词;-n:最多生成的 token 数;-c:上下文容量。
HTTP 服务通常使用仓库提供的 server 程序,命令名和参数在版本间可能变化。启动前应执行:
./build/bin/llama-server --help
再依据当前版本的帮助文本配置端口、模型路径、上下文长度和 GPU offload 层数。验证服务时,可以先用健康检查或最小请求,再逐步增加上下文和并发;不要一开始就用长上下文压测,否则难以区分模型不兼容、显存不足和服务参数错误。
3. GPU offload 的取舍
在 CPU 主机或显存不足时,llama.cpp 可以把部分层放到 GPU,其他层留在 CPU。这样:
- GPU 显存不足问题可能缓解;
- 但 CPU-GPU 之间的数据传输会增加;
- 每 token 延迟可能明显变大;
- 性能取决于 PCIe、统一内存架构、后端实现和模型结构。
“能启动”不等于“适合服务”。必须同时测量:
- 首 token 延迟;
- decode tokens/s;
- 并发下的 p50/p95 延迟;
- 内存峰值;
- 长上下文时的退化;
- CPU 占用和温度。
llama.cpp 适合把一个模型可靠地放进边缘设备、开发机或资源受限环境,但在大量并发 GPU 服务上,通常需要更专门的批处理和显存调度能力。
五、vLLM:面向 GPU 服务吞吐的运行时
1. vLLM 解决的核心问题
vLLM 主要面向 GPU 上的模型服务。它的关键机制包括:
- PagedAttention 或类似的分页式 KV Cache 管理;
- 连续批次(continuous batching);
- 对多个请求进行动态调度;
- 对 Hugging Face 模型格式提供较直接的加载路径;
- OpenAI 风格的 HTTP API;
- 在适用版本和硬件上支持张量并行、量化和分布式部署。
这里的“连续批次”不是把一批请求静态收齐后一起执行,而是允许请求在不同时间进入和退出批次。一个请求完成后,调度器可以把新请求插入正在运行的批次,从而减少 GPU 空转。
静态批处理的简化过程是:
等待 N 个请求 -> 一起执行 -> 等最慢请求结束 -> 才接收下一批
连续批次更接近:
请求 A/B 正在 Decode
请求 C 到达并完成 Prefill
A 结束后释放块
请求 D 使用释放的块进入调度
这通常提高 GPU 利用率,但调度器更复杂,公平性、优先级、长请求对短请求的影响也需要额外治理。
2. 启动和调用
一个常见启动形式如下:
pip install vllm
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 \
--port 8000
实际可用参数取决于 vLLM 版本、GPU、CUDA、模型架构和安装方式。启动前应查看:
vllm serve --help
例如,某些版本支持通过参数限制 GPU 显存使用比例或指定张量并行规模,但不能假设所有参数在所有版本均存在。对于需要量化的模型,也必须确认该量化格式被当前版本和当前 GPU 后端支持。
vLLM 通常提供 OpenAI 风格接口。调用示例:
curl http://localhost:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer local-token' \
-d '{
"model": "Qwen/Qwen2.5-7B-Instruct",
"messages": [
{"role": "user", "content": "解释连续批次为什么能提高吞吐"}
],
"temperature": 0.2,
"max_tokens": 128,
"stream": false
}'
这里的 Authorization 是否真正被验证,取决于前置网关或服务配置。接口长得像 OpenAI API,不代表它自动具备 OpenAI 的认证、计费、内容策略和全部参数语义。客户端应依据实际部署的 API 文档处理错误码、模型名称、流式协议和可用字段。OpenAI API 的请求结构可参考其官方文档,但兼容实现仍需单独验证。
3. vLLM 的边界
vLLM 适合:
- NVIDIA GPU 上的高吞吐在线服务;
- 多请求并发;
- 希望利用连续批次和集中式调度;
- 需要与 OpenAI 风格客户端集成的团队。
它并不意味着:
- 任意模型都能无修改加载;
- 任意 GGUF 文件都能直接使用;
- 启动后自动获得生产级网关;
- 量化后一定比 FP16 更快;
- 多 GPU 一定线性扩展。
模型结构、权重格式、量化 kernel、GPU 架构和 CUDA 版本共同决定可用性。若启动时报“不支持架构”或找不到 kernel,应先确认模型类型和版本矩阵,而不是盲目增加显存参数。
六、量化:压缩权重,不是免费获得更多算力
1. 量化的数学含义
量化把高精度数值映射到较少的离散表示。最简单的对称线性量化可以写为:
其中:
- 是原始 FP16 或 FP32 权重;
- 是缩放因子;
- 是低 bit 整数;
- 是反量化后的近似值。
量化误差为:
误差并不是均匀影响所有层。某些层、通道或权重离群值对模型质量更敏感,因此实际方法常按 group 或 channel 使用不同 scale,甚至对不同层采用不同 bit 数。
常见概念包括:
- 权重量化:主要压缩模型权重;
- 激活量化:压缩运行中产生的激活;
- KV Cache 量化:减少长上下文和高并发下的缓存占用;
- 训练后量化(PTQ):训练完成后转换;
- 量化感知训练(QAT):训练阶段模拟量化误差。
“4 bit 模型”通常首先指权重存储精度,不表示输入、激活、KV Cache 和所有算子都以 4 bit 执行。
2. 一个容量算例
假设模型有 7 billion 个参数:
- FP16 权重理论大小约为
- 4 bit 权重理论大小约为
但实际文件还包括 scale、zero point、分组元数据、对齐空间,以及运行时 workspace。因此不能简单用“参数量 × bit 数”作为最终显存预算。
如果 4 bit 只压缩权重,而 KV Cache 仍为 FP16,前面算出的单请求 4096 token 约 512 MiB 仍然存在。8 个请求就是约 4 GiB。于是可能出现:
模型文件只有 4 GB
启动单请求成功
并发到 8 个长上下文请求时 OOM
这不是量化失效,而是容量由权重主导转变为 KV Cache 主导。
3. 量化的速度反例
量化可能减少显存读写,但不保证更快:
- GPU 没有适合该格式的高效 kernel;
- 推理时需要频繁反量化;
- 算子仍在 FP16 中执行;
- CPU 解码受限于其他瓶颈;
- batch 太小,权重带宽优势无法体现。
因此量化选择必须同时验证质量和性能。一个合理的评测集合应至少包含:
- 业务任务准确率或人工可接受率;
- 长上下文检索和引用正确率;
- 首 token 延迟;
- 单请求生成速度;
- 并发吞吐;
- 显存峰值;
- 错误率和服务重启次数。
不能只看模型文件大小,也不能只看单次回答样例。
七、三种方案的运行时取舍
可以把三者放在不同抽象层理解:
| 方案 | 主要抽象 | 常见硬件 | 优势 | 主要代价 |
|---|---|---|---|---|
| Ollama | 模型管理与易用服务封装 | 开发机、单机 CPU/GPU | 上手快、模型管理简单 | 调度、治理和高级性能控制有限 |
| llama.cpp | 轻量本地推理运行时 | CPU、Apple Silicon、边缘设备、混合 CPU/GPU | 可移植、低依赖、GGUF 生态 | 高并发 GPU 调度能力通常不如专用服务引擎 |
| vLLM | GPU 高吞吐服务运行时 | NVIDIA GPU 集群 | 连续批次、KV 管理、服务吞吐 | 环境复杂,版本和模型兼容性要求更高 |
实际选择应从流量和约束反推:
- 个人开发工具首先关心安装和模型切换,Ollama 往往足够;
- 设备端离线推理首先关心二进制体积、内存和 CPU 性能,llama.cpp 更自然;
- 在线多用户服务首先关心排队、连续批次、KV Cache 和 GPU 利用率,vLLM 更适合;
- 复杂场景可以分层:开发环境使用 Ollama,边缘节点使用 llama.cpp,中心 GPU 集群使用 vLLM。
不能把“本地模型”理解为一种部署形态。它可能是:
- 开发者笔记本上的单进程;
- 内网中的共享模型服务;
- 工厂边缘节点;
- 没有公网访问的 GPU 集群;
- 通过网关提供给多个业务系统的生产平台。
部署形态决定权限、数据流和故障边界。
八、从请求到生产系统的完整数据流
一个有治理要求的生产系统通常不是客户端直接访问模型,而是:
flowchart LR
U[业务客户端] --> G[API 网关]
G --> A[认证与授权]
A --> R[路由与配额]
R --> Q[队列/限流]
Q --> M[模型服务]
M --> K[KV Cache]
R --> V[模型版本注册表]
M --> S[对象存储/日志]
M --> O[指标与追踪]
G --> O
关键路径如下:
- 客户端提交请求;
- 网关验证身份、租户和权限;
- 路由根据模型版本、任务类型和 GPU 容量选择后端;
- 队列执行限流、超时和优先级调度;
- 模型服务完成 tokenizer、Prefill、Decode 和流式返回;
- 指标系统记录延迟、token 数、排队时间、OOM 和取消;
- 审计系统按数据分类策略记录必要元数据,而不是无条件保存全部敏感提示词。
模型权限和数据权限不是一回事。一个用户可能有权调用某模型,但无权把某类机密数据发送给该模型;一个服务账号可能可以读取模型仓库,但不能下载训练数据。生产系统应分别控制:
- 谁能发布模型;
- 谁能调用模型;
- 谁能访问模型权重;
- 谁能访问提示词和输出日志;
- 谁能修改系统提示词和采样参数;
- 谁能回滚版本。
九、并发、队列和错误路径
1. 并发不是简单的线程数
模型服务的有效并发受以下资源共同限制:
其中:
- :算力和显存允许的并发;
- :KV Cache 容量允许的并发;
- :系统可接受的排队长度;
- :租户和网关限流。
如果无限增加并发,请求不会无限变快。通常会先出现排队增长,然后出现 p95 延迟恶化,最后因 KV Cache 或 workspace 分配失败而 OOM。
2. 常见失败表现
| 表现 | 可能原因 | 验证方法 |
|---|---|---|
| 模型启动即失败 | 权重格式或架构不支持 | 查看启动日志、模型 config、引擎版本 |
| 单请求正常,并发后 OOM | KV Cache 或 workspace 不足 | 降低上下文和并发,观察显存峰值 |
| 输出乱码或角色错乱 | tokenizer/chat template 不匹配 | 对比官方模板和实际 input token |
| 首 token 很慢 | 输入过长、Prefill 排队或 CPU offload | 分离排队、Prefill、Decode 指标 |
| 生成速度逐步下降 | 长上下文 KV 访问、显存带宽或热降频 | 固定输出长度,比较不同上下文 |
| 流式请求断开后 GPU 仍繁忙 | 取消没有传播到推理引擎 | 检查服务端取消日志和活跃请求数 |
| HTTP 兼容但参数无效 | 兼容层只实现了部分字段 | 用最小请求和错误请求验证字段语义 |
客户端必须处理至少四类错误:
- 连接错误:服务不可达或进程崩溃;
- 超时:排队、Prefill 或生成过慢;
- 资源错误:OOM、并发达到上限;
- 输入错误:模型名、消息格式、上下文过长或参数非法。
对于流式响应,客户端断开时,网关应尝试向后端传播取消;否则模型服务可能继续生成,造成“用户已经退出、GPU 仍在消耗”的隐性成本。
十、模型发布、验证和回滚
模型发布不应只是把一个新文件复制到服务器。至少要绑定以下版本:
- 模型权重版本;
- tokenizer 版本;
- chat template;
- 推理引擎版本;
- 量化方法和参数;
- 启动配置;
- 安全策略;
- 评测结果。
可以为每个候选版本建立固定验证流程:
# 1. 计算文件哈希
sha256sum /models/model.Q4_K_M.gguf
# 2. 启动候选实例
# 使用当前引擎版本和固定配置
# 3. 发送固定回归集
# 包括短输入、长输入、中文、结构化输出、拒答和工具调用样例
# 4. 记录:
# 首 token 延迟、生成速度、显存峰值、输出质量、错误率
验证时应固定随机种子、temperature、top-p、最大输出长度和停止条件;对于非零 temperature 的生成,仍不能要求逐 token 完全一致,而应比较任务级指标和可接受输出范围。
发布可以采用:
- 新版本先启动为独立实例;
- 运行健康检查和回归集;
- 小比例流量灰度;
- 比较延迟、错误率、质量反馈和 GPU 成本;
- 达标后扩大流量;
- 保留上一版本实例用于快速回滚。
回滚的本质是恢复“模型版本 + tokenizer + 模板 + 引擎配置”的一致组合,而不是只替换权重文件。若新旧版本使用不同上下文配置或 API 行为,单独回退权重可能仍然无法恢复原服务。
十一、成本模型:不要只计算 GPU 租赁费
生成式 AI 服务的单位成本可以粗略分解为:
其中:
- :GPU/CPU 运行时间;
- :模型权重、缓存和日志;
- :模型下载、跨节点传输和 API 流量;
- :部署、监控、升级和人工维护;
- :错误输出、重试、数据泄露和停机损失。
本地模型可能降低外部 API 调用费,但增加:
- 模型下载和升级成本;
- GPU 空闲成本;
- 安全补丁和驱动维护;
- 质量评测与回归成本;
- 高峰期容量预留成本。
批处理任务通常可以牺牲延迟来提高 GPU 利用率;交互式服务则要控制排队和首 token 延迟。把离线批任务和在线对话放在同一队列中,可能导致长批任务阻塞交互请求,因此应至少进行队列隔离或优先级调度。
十二、常见误解与正确判断
误解一:量化 bit 数越低越好
低 bit 通常节省权重空间,但可能损失任务质量、增加实现复杂度,甚至因为缺少高效 kernel 而变慢。应以业务评测和目标硬件实测为准。
误解二:模型能加载就代表兼容
加载成功只说明权重和部分算子能运行。chat template 错误、停止 token 错误、工具调用格式错误,仍可能让线上行为失效。
误解三:OpenAI 兼容 API 就是完全兼容
兼容通常指请求路径和部分字段相似。模型名、流式事件格式、错误码、工具调用、JSON Schema、采样参数和认证行为都可能不同。生产客户端要针对实际后端做契约测试。
误解四:显存只需要容纳模型文件
显存还要容纳 KV Cache、激活、临时 workspace、CUDA 上下文、通信缓冲区和其他进程。容量规划必须按“权重 + 最大并发 KV + 运行时余量”估算。
误解五:本地部署天然更安全
本地部署减少了部分外部传输,但新的风险包括模型服务未认证、日志保存敏感数据、模型文件供应链被替换、内网横向访问和错误配置暴露端口。安全性取决于完整数据流和权限控制,而不是“模型在本地”这一事实。
结语:按约束选择引擎,而不是按名称选择引擎
Ollama、llama.cpp 和 vLLM 不是简单的同类替代品:
- Ollama 优先降低本地使用和模型管理门槛;
- llama.cpp 优先解决轻量、可移植和资源受限设备上的推理;
- vLLM 优先解决 GPU 在线服务中的批处理、KV Cache 管理和吞吐问题。
量化解决的是权重表示和部分内存带宽问题,不能自动解决 KV Cache、并发调度、模型质量或生产治理。真正的部署决策应同时回答:
- 模型格式和架构是否兼容;
- tokenizer 和 chat template 是否一致;
- 权重、KV Cache 和 workspace 是否有容量预算;
- 目标是低延迟、低成本还是高吞吐;
- 请求取消、超时、排队和 OOM 如何处理;
- 模型、数据、权限、日志和版本如何审计;
- 出现质量或稳定性问题时能否快速回滚。
当这些问题都能用可验证的配置、指标和恢复流程回答时,本地模型才真正成为生产系统的一部分,而不只是运行在开发机上的一个命令。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:AI 模型服务:批处理、连续批次、KV Cache、量化和 GPU 容量
- 下一篇:LLM 与 Agent 评测:数据集、规则、Judge、轨迹、回归和统计
- 延伸:AI 生产系统架构:网关、模型、RAG、Agent、队列、存储和发布
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论