从零搭建一个 AI Infra 实验室⑧:Ollama 与 llama.cpp——本地 LLM 推理为什么还有另一条路线
上一篇,我们终于把:
FP16
BF16
INT8
INT4
AWQ
GPTQ
GGUF
这些经常混在一起的概念拆开了。
并且在 RTX 3060 12GB 上真正研究了:
模型到底怎样才能塞进有限的 GPU 显存?
到最后,我们发现一个很有意思的分叉。
像:
AWQ
GPTQ
这样的模型,依然很容易放到前面已经熟悉的路线中理解:
Hugging Face Model
↓
Transformers
↓
PyTorch
↓
CUDA
↓
GPU
但另外一个名字却越来越频繁地出现:
GGUF
搜索 GGUF 模型时,又几乎一定会遇到:
llama.cpp
而真正想在自己的电脑上简单跑一个模型时,很多教程甚至只有一句:
ollama run qwen2.5:7b
模型就开始回答问题了。
这就出现了一个很值得从 Infra 角度研究的问题:
我们前面费了这么大力气搭好的 PyTorch、Transformers、CUDA 推理链,为什么本地 LLM 又出现了另一条路线?
甚至:
没有 Python
没有 Transformers
没有 PyTorch
模型照样可以跑。
更有意思的是:
GPU 显存装得下
→ 全部放 GPU
GPU 显存装不下
→ 一部分 GPU + 一部分 CPU
甚至没有 GPU
→ 纯 CPU
都可以运行。
到底是谁完成这些工作的?
答案就是这一篇真正要补上的一层:
Model Runtime
一、到目前为止,我们其实少认识了一层
前几篇里,我们一直通过 Python 操作模型。
典型代码大概是:
tokenizer = AutoTokenizer.from_pretrained(...)
model = AutoModelForCausalLM.from_pretrained(...)
model.to("cuda")
outputs = model.generate(...)
于是很容易形成这样一个印象:
Model
↓
PyTorch
↓
CUDA
↓
GPU
似乎:
PyTorch 就是运行 LLM 所必需的东西。
其实不是。
模型本身和运行模型的软件,是两件不同的东西。
一个训练完成的模型,本质上仍然只是:
Architecture
+
Weights
+
Tokenizer
+
Metadata
这些数据并不会自己运行。
就像:
.mp4 文件
不会自己播放,
还需要:
Video Player
一样。
模型同样需要某个软件负责:
读取模型
↓
解析模型结构
↓
准备内存
↓
执行算子
↓
维护 KV Cache
↓
完成 Token Generation
这一层,我们可以先把它统称为:
Model Runtime
或者在很多场景中也会看到:
Inference Runtime
Inference Engine
这些词的边界并不总是完全一样,但目前先不用纠结。

最重要的是理解:
Model 是数据,而 Runtime 才是真正执行这些数据的软件。
二、一个 Model Runtime 到底要做什么?
以前执行:
model.generate()
的时候,很多事情都被:
Transformers
+
PyTorch
+
CUDA
替我们做掉了。
现在把这一层真正展开,就会发现 Runtime 至少要解决几类问题。
Model File
│
▼
Model Runtime
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Model Loader Memory Compute Backend
│ Management │
│ │ ├── CPU
│ │ ├── CUDA
│ │ └── ...
│ │
▼ ▼
Architecture Weights
Tokenizer KV Cache
Metadata Buffers
│ │
└───────┬──────┘
▼
Prefill / Decode
│
▼
Sampling
│
▼
Output Tokens
换句话说,它至少得知道:
这个模型有多少层?
每个 Tensor 是什么?
Weight 在哪里?
Tokenizer 怎么工作?
Context 开多大?
KV Cache 放哪里?
哪些计算放 CPU?
哪些计算放 GPU?
下一个 Token 怎么生成?
前面我们学到的:
Model
Tensor
Context
KV Cache
Prefill
Decode
Quantization
到这里并没有消失。
恰恰相反:
它们现在全部变成了 Runtime 必须真正管理的资源。
这也是为什么从这一篇开始,我们已经不只是在研究“大模型是什么”,而是在研究:
LLM Software Stack
三、所以本地 LLM 实际上出现了两条路线
现在重新画一次整个软件栈。
前几篇走的是:
Hugging Face Model
│
▼
Transformers
│
▼
PyTorch
│
▼
CUDA
│
▼
NVIDIA GPU
这是一条非常重要的路线。
它特别适合:
模型研究
Python 开发
训练 / Fine-tuning
算法实验
快速接入 Hugging Face 生态
但是,本地推理还有另一条非常流行的路线:
GGUF
│
▼
llama.cpp
│
┌────────┴────────┐
▼ ▼
CPU CUDA
│
▼
GPU
甚至:
GGUF
↓
llama.cpp
↓
CPU + GPU Hybrid
llama.cpp 官方的目标本身就是用 C/C++ 在尽可能广泛的硬件上高效运行 LLM,目前支持 CPU、CUDA、HIP、Metal、Vulkan、SYCL 等多种 Backend,并明确支持 CPU+GPU Hybrid Inference。
于是突然会发现:
PyTorch
不是 LLM 的组成部分。
它只是:
众多 Model Runtime / Execution Stack 中的一条路线。

四、为什么会需要 llama.cpp 这种路线?
如果只是服务器 GPU 推理,PyTorch 已经非常强大。
那为什么还会出现 llama.cpp?
因为本地推理的目标和训练环境并不完全一样。
一台普通电脑可能只有:
CPU
+
16GB / 32GB RAM
+
8GB / 12GB VRAM
甚至:
没有 NVIDIA GPU
这时候用户更关心的是:
能不能跑?
安装复杂吗?
内存够不够?
能不能利用 CPU?
能不能只把一部分模型放 GPU?
而不一定需要:
完整 Python Environment
PyTorch
Transformers
Accelerate
bitsandbytes
各种 Python Dependency
于是 llama.cpp 选择了另一种思路:
Model
↓
转换成适合推理的格式
↓
轻量 C/C++ Runtime
↓
针对不同硬件选择 Backend
这也是它和:
GGUF
关系如此紧密的原因。
五、GGUF 不只是“另一个模型后缀”
上一篇我们已经明确:
GGUF
≠
INT4
也就是说:
Q4_K_M
描述的是一种量化类型,
而:
GGUF
是模型文件格式。
这一篇继续往下走。
GGUF 真正重要的地方,是它非常适合:
Inference Runtime
读取。
一个 GGUF 文件内部不只有 Weight。
从格式结构来看,它可以包含:
GGUF Header
│
├── Metadata
│
│ ├── Architecture
│ ├── Context 信息
│ ├── Tokenizer 信息
│ └── ...
│
├── Tensor Information
│ ├── Tensor Name
│ ├── Shape
│ ├── Data Type
│ └── Offset
│
└── Tensor Data
├── Q4
├── Q5
├── Q8
├── F16
└── ...
GGUF 的规范本身就是围绕:
Metadata
+
Tensor Description
+
Tensor Data
组织的,并且设计目标之一就是让模型能够方便地被 Runtime 加载。
这和前面熟悉的 Hugging Face 模型目录很不一样。
以前可能是:
config.json
tokenizer.json
tokenizer_config.json
model.safetensors
generation_config.json
...
Runtime 需要理解这一整套文件。
而 GGUF 更接近:
GGUF
│
┌───────┼─────────┐
▼ ▼ ▼
Metadata Tokenizer Tensor Data
当然,大模型也可能因为文件过大而拆成多个 GGUF shard,并不意味着 GGUF 永远只有一个物理文件。

但从 Runtime 的角度,它明显更强调:
把推理所需的信息组织成一种容易部署、容易读取的格式。
六、这其实有点像“模型的可执行镜像”
严格来说,GGUF 并不是 executable。
它自己依然不会运行。
但从 Infra 思维来看,我觉得可以用一个很有帮助的类比:
Container Image
│
▼
Container Runtime
对应:
GGUF Model
│
▼
llama.cpp Runtime
当然两者技术上完全不是一回事。
这个类比只是帮助理解:
GGUF
负责:
把 Runtime 所需要的模型数据组织好
而:
llama.cpp
负责:
把这些数据真正执行起来
所以我们终于可以把上一章留下的问题回答一半:
量化解决“模型有多大”,Runtime 解决“这个模型怎么跑”。
七、CPU/GPU Offload 才是 llama.cpp 最值得 Infra 学习的地方
这一次真正值得实验的,其实不是:
ollama run ...
本身。
而是:
CPU / GPU Offload
因为它把前面学过的:
System RAM
GPU VRAM
PCIe
Model Layers
全部连接起来了。
假设现在有一个:
7B Q4_K_M
模型。
如果模型和运行时数据都能够放进 12GB VRAM,可以:
Model Layers
│
▼
┌────────────────┐
│ GPU │
│ │
│ Layer 0 │
│ Layer 1 │
│ Layer 2 │
│ ... │
│ Layer N │
│ KV Cache │
└────────────────┘
但假设模型太大:
Model + KV Cache
>
GPU VRAM
怎么办?
一种方案当然是:
OOM
但 llama.cpp 还可以做另一件事:
Model
│
┌──────┴──────┐
▼ ▼
CPU / RAM GPU / VRAM
│ │
Layer 0 Layer X
Layer 1 Layer X+1
Layer 2 ...
...
这就是:
GPU Offload
或者更准确地说:
CPU + GPU Hybrid Inference
llama.cpp 当前的 --n-gpu-layers 参数可以控制最多有多少模型层放入 VRAM。
于是:
GPU Layers = 0
可以接近纯 CPU 路线,
而:
GPU Layers ↑
意味着越来越多模型 Weight 放进 GPU。
如果显存足够:
GPU Layers = all
就可以尽量让整个模型运行在 GPU 上。
八、Offload 并不是“显存不够就借点内存”这么简单
这里很容易产生一个误解。
好像:
12GB VRAM
+
16GB RAM
=
28GB 显存
当然不是。
CPU RAM 和 GPU VRAM 是两个不同的 Memory Domain。
在我们的机器上,中间还有:
PCIe
所以:
CPU Weight
│
▼
CPU Compute
GPU Weight
│
▼
GPU Compute
并不是没有代价地混在一起。
如果推理过程中需要跨设备协调:
CPU
│
PCIe
│
GPU
那么:
Latency
Bandwidth
Synchronization
都可能成为新的成本。
所以:
CPU Offload 的意义首先是“让模型能够运行”,其次才是“跑得快不快”。
这和上一篇强调的:
Can Run
≠
Runs Fast
其实是一回事。
只是这一篇把它真正落实到了 Runtime。
九、Context 到这里又多了一层含义
Context 在前面已经讲过很多次,所以这一篇不再解释:
Context 是什么
为什么产生 KV Cache
为什么 Context 越长显存越高
这里真正要补充的是:
Model 支持多长 Context,和 Runtime 实际给这次进程分配多长 Context,并不是一回事。
可以分成三层理解:
Model Capability
↓
Runtime Context Size
↓
Actual Request Tokens
例如模型文件里可能声明:
Model Max Context
= 很大
但启动 Runtime 时完全可以只设置:
4096
于是 Runtime 只需要为当前配置准备相应规模的资源。
在 llama.cpp 当前 CLI 中:
-c
--ctx-size
控制的就是 Runtime Context Size。
例如:
-c 4096
和:
-c 16384
即使:
Model
Quantization
GPU
Prompt
全部相同,
Runtime 的 Memory Layout 也可能不同。
因为:
Context Size ↑
↓
KV Cache Capacity ↑
↓
Memory Requirement ↑
这就是为什么:
“模型只有 4.7GB,为什么 6GB 显卡还是跑不起来?”
这个问题不能只看 Model File Size。
十、Ollama 为什么又出现了?
理解 llama.cpp 以后,再看 Ollama 就容易很多了。
llama.cpp 已经能够:
加载 GGUF
执行推理
CPU/GPU Offload
启动 Server
提供 API
那为什么还需要 Ollama?
因为普通用户其实还要解决另外一堆问题:
模型去哪里下载?
模型叫什么?
文件放哪里?
怎么升级?
Chat Template 怎么配置?
模型什么时候加载?
什么时候释放?
API 怎么调用?
多个模型怎么管理?
如果全部自己处理,还是比较麻烦。
所以可以先粗略把 Ollama 放在更上面一层:
Application
│
▼
Ollama
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
Model Manager Local API Runtime Config
│ │ │
└─────────────┼─────────────┘
▼
Model Runtime
│
┌───────┴───────┐
▼ ▼
CPU GPU
这里要注意:
不要把 Ollama 永久理解成“给 llama.cpp 套了一层 Shell”。
Ollama 自己的 Runtime/Runner 实现会继续演进。
从 Infra 的层级上,更适合把它理解成:
Model Runtime
+
Model Management
+
Lifecycle
+
Prompt Template
+
Local API
+
CLI
组成的一套本地模型运行环境。
Ollama 当前默认还会提供:
http://localhost:11434/api
作为本地 API。
于是:
本地模型
开始第一次真正有点像:
Local Model Service
了。
十一、实验环境
实验继续使用前几篇同一台机器:
Ubuntu 24.04
Intel Core i5-10400F
16GB RAM
NVIDIA GeForce RTX 3060
12GB VRAM
这一次依然选择:
Qwen2.5-7B-Instruct
原因不是它最新。
而是为了控制变量。
上一篇我们已经围绕:
Qwen2.5-7B-Instruct
研究了:
BF16
INT8
INT4
AWQ
GPTQ
这一篇继续用它,就不需要再引入:
Model Capability
这个新变量。
只改变:
Runtime
实验选择:
Qwen2.5-7B-Instruct
Q4_K_M
当前 Ollama 中的 qwen2.5:7b-instruct 为约 7.62B 参数、Q4_K_M,模型大小约 4.7GB,非常适合 RTX 3060 12GB 做这一组实验。
十二、实验一:编译一个支持 CUDA 的 llama.cpp
首先安装基础编译环境:
sudo apt update
sudo apt install -y \
build-essential \
cmake \
git
下载:
cd ~/ai-infra-lab
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
因为这台机器已经有 CUDA Toolkit,所以直接构建 CUDA Backend:
cmake -B build -DGGML_CUDA=ON
cmake --build build \
--config Release \
-j
当前 llama.cpp 官方 CUDA Build 依然使用:
-DGGML_CUDA=ON
启用 NVIDIA GPU Backend。



完成以后检查:
./build/bin/llama-cli --version
再看 Runtime 能不能发现 GPU:
./build/bin/llama-cli --list-devices
应该能够看到:
CUDA
NVIDIA GeForce RTX 3060

到这里其实已经发生了一件很有意思的事情:
Python
Transformers
PyTorch
一个都没启动。
但:
C/C++
↓
llama.cpp
↓
CUDA
↓
RTX 3060
这条新路线已经打通。
十三、实验二:运行 Qwen2.5-7B GGUF
1. 使用 Hugging Face CLI 下载模型
和上篇类似下载模型:
tmux new -s hf-download
cd ~/ai-infra-lab
source .venv/bin/activate
pip install -U huggingface_hub
export HF_HUB_DOWNLOAD_TIMEOUT=120
export HF_HUB_DISABLE_XET=1

建立单独的模型目录:
mkdir -p ~/models/qwen2.5-7b-instruct-q4_k_m
然后只下载 Q4_K_M:
hf download \
Qwen/Qwen2.5-7B-Instruct-GGUF \
--include "qwen2.5-7b-instruct-q4_k_m-*.gguf" \
--local-dir ~/models/qwen2.5-7b-instruct-q4_k_m
这里之所以使用通配符,是因为 Qwen 官方的这个 Q4_K_M 模型并不是单一 GGUF 文件,而是被拆成了两个 shard:
qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf
qwen2.5-7b-instruct-q4_k_m-00002-of-00002.gguf
下载完成后检查:
ls -lh ~/models/qwen2.5-7b-instruct-q4_k_m/
应该能看到两个文件。
也可以检查总大小:
du -sh ~/models/qwen2.5-7b-instruct-q4_k_m/

到这里:
Hugging Face
↓
GGUF Download
↓
Local Disk
这一阶段就已经结束了。
2. llama.cpp 只负责加载本地模型
接下来进入:
cd ~/llama.cpp
指向本地模型的第一个 shard:
./build/bin/llama-cli \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
-c 4096 \
-ngl all \
-p "请从 AI Infra 的角度解释什么是 Model Runtime。" \
-n 128
虽然这里只指定:
00001-of-00002.gguf
但 llama.cpp 支持 Sharded GGUF,会从第一个 shard 的 Metadata 中发现模型还有其他分片,并自动加载同一目录里的后续 shard。
所以不需要分别运行两个文件,也不用先把它们手工合并。

现在整个实验路径就清晰多了:
Hugging Face
│
│ hf download
▼
Local GGUF
│
│ llama-cli -m
▼
llama.cpp
│
├── Model Loader
├── Memory Management
├── CPU / GPU Placement
└── Inference
│
▼
Output Tokens
这其实也更符合我们这一篇要建立的 Infra 思维:
模型获取(Model Distribution)和模型运行(Model Runtime)本来就是两个不同阶段。

十四、实验三:固定一个模型,只改变 GPU Offload
接下来才是最有意思的实验。
我们不换:
Model
不换:
Quantization
也不换:
Context
只改变:
GPU Offload
1. CPU-only
可以先明确禁用 GPU:
./build/bin/llama-cli \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
--device none \
-c 4096 \
-p "请用三句话解释什么是 Kubernetes。" \
-n 128
这时候观察:
nvidia-smi
GPU 不应该承担模型推理的主要工作。


而:
CPU Utilization
System RAM
会明显增加。
现在的数据路径大概是:
GGUF
↓
System RAM
↓
CPU
↓
Token
2. 部分 GPU Offload
例如先放 10 层:
./build/bin/llama-cli \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
-ngl 10 \
-c 4096 \
-p "请用三句话解释什么是 Kubernetes。" \
-n 128
然后:
nvidia-smi
应该可以看到部分 VRAM 被使用。


继续增加:
-ngl 20
例如:
./build/bin/llama-cli \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
-ngl 20 \
-c 4096 \
-p "请用三句话解释什么是 Kubernetes。" \
-n 128
此时:
GPU Layers ↑
↓
VRAM ↑
同时一般会看到:
Decode Performance ↑
但这个增长不一定严格线性。
因为整个系统仍然受到:
CPU
Memory Bandwidth
PCIe
GPU
Runtime
共同影响。


3. 尽量全部 Offload 到 GPU
最后:
./build/bin/llama-cli \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
-ngl all \
-c 4096 \
-p "请用三句话解释什么是 Kubernetes。" \
-n 128
对于:
Qwen2.5-7B
Q4_K_M
+
RTX 3060 12GB
这是非常适合观察完整 GPU Offload 的组合。


这时数据路径进一步变成:
GGUF
↓
Runtime
↓
Model Weights
↓
GPU VRAM
↓
CUDA Kernels
↓
Prefill / Decode
我们前面几篇所有知识,到这里终于重新接上了。

十五、不要靠“感觉快不快”,直接用 llama-bench
手动聊天适合确认模型能跑。
真正比较:
CPU
Partial GPU
Full GPU
最好还是使用 benchmark。
llama.cpp 自带:
llama-bench
它可以分别测试:
Prompt Processing
Text Generation
并直接给出:
tokens/s
官方文档也明确支持比较不同 n-gpu-layers 设置。
例如:
./build/bin/llama-bench \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
-p 512 \
-n 128 \
-ngl 0,10,20,999
这里:
-p 512
测试 Prompt Processing,
而:
-n 128
测试 Text Generation。
llama-bench 会重复运行并给出平均:
tokens/s

于是可以整理成:
| GPU Offload | Prompt Processing | Text Generation | GPU VRAM | 观察 |
|---|---|---|---|---|
| 0 Layer | 757.22 ± 5.74 | 6.49 ± 0.00 | 17 MiB | CPU 为主 |
| 10 Layers | 983.58 ± 6.50 | 10.03 ± 0.00 | 2055 MiB | CPU + GPU |
| 20 Layers | 1410.17 ± 7.05 | 18.03 ± 0.01 | 3441 MiB | GPU 占比继续增加 |
| Full | 2346.89 ± 17.29 | 67.31 ± 1.05 | 4681 MiB | GPU 为主 |
这里我不预设数字。
因为实际结果会受到:
llama.cpp Version
CUDA Version
Driver
CPU Frequency
GPU Frequency
Context
Quantization
Runtime 参数
影响。
这一篇真正值得观察的是:
GPU Offload ↑
↓
VRAM Consumption ↑
↓
CPU Work ↓
↓
Inference Performance 发生变化
这条关系。

十六、实验四:Context 从 4K 加到 16K,会发生什么?
现在保持:
Model
=
Qwen2.5-7B Q4_K_M
保持:
Full GPU Offload
只改变:
Context Size
先运行:
./build/bin/llama-cli \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
-ngl all \
-c 4096 \
-p "什么是 AI Infrastructure?" \
-n 128
记录:
VRAM


然后改成:
./build/bin/llama-cli \
-m ~/models/qwen2.5-7b-instruct-q4_k_m/qwen2.5-7b-instruct-q4_k_m-00001-of-00002.gguf \
-ngl all \
-c 16384 \
-p "什么是 AI Infrastructure?" \
-n 128
再次观察:
nvidia-smi
即使 Prompt 本身没变,
也可能看到 Runtime 的 Memory Requirement 发生变化。


因为:
-c
并不是在告诉模型:
“这一次 Prompt 有多少 Token。”
而是在告诉 Runtime:
这次 Context Window 按多大规模准备。
于是前面一直学习的 Context,在这里第一次变成了一个非常具体的:
Runtime Resource Configuration
这也是 Infra 视角和单纯模型使用视角很不一样的地方。

十七、实验五:安装 Ollama,看看它替我们藏掉了多少东西
现在再走另一条路线。
Ubuntu 上安装:
curl -fsSL https://ollama.com/install.sh | sh
这是目前 Ollama 官方 Linux 安装方式。
检查:
ollama -v

然后:
ollama run qwen2.5:7b-instruct
输入:
请从 AI Infra 的角度解释什么是 Model Runtime。
模型就直接开始回答了。

但现在我们已经知道:
“一条命令跑起来”不代表底层没有复杂度。
只是:
Model Download
Model Format
Tokenizer
Template
Runtime
GPU Detection
Memory Allocation
Context
Sampling
Streaming
API
大量东西都被 Ollama 封装掉了。
这就是:
Abstraction
的意义。
十八、别只聊天,看看 Ollama 实际加载到了哪里
执行:
ollama ps

这里有两个特别值得关注的字段:
PROCESSOR
CONTEXT
例如可能看到:
NAME PROCESSOR CONTEXT
qwen2.5:7b-instruct 100% GPU 4096
也可能在更大的模型上看到:
CPU/GPU
混合。
Ollama 官方文档目前明确说明:
100% GPU
表示模型完整加载到 GPU,
而类似:
48%/52% CPU/GPU
则表示模型被分布到了 CPU 和 GPU。
突然会发现:
llama.cpp 里我们手工研究的 CPU/GPU Offload,在 Ollama 里仍然存在,只不过被封装成了更高层的自动管理。
这就是学习底层以后再看工具的好处。
如果一开始只会:
ollama run
看到:
PROCESSOR
可能只觉得它是一个状态字段。
现在却知道它背后实际上对应:
Weight Placement
+
RAM / VRAM
+
Compute Backend
十九、RTX 3060 上,Ollama 为什么往往不会默认把 Context 开到最大?
这一点尤其值得注意。
模型自己的 Context Capability 可能很大。
但是我们的 GPU 只有:
12GB VRAM
Ollama 当前会根据 VRAM 自动选择默认 Context;官方文档目前给出的策略是:
< 24 GiB VRAM
→ 4K Context
24–48 GiB
→ 32K
>= 48 GiB
→ 256K
而且官方特别提醒:
增大 Context 会增加模型运行所需 Memory。
所以 RTX 3060 12GB 上:
Model 可以支持更长 Context
和:
Runtime 默认只分配 4K
完全可以同时成立。
这非常重要。
以后再看到:
Model supports 128K context
不能直接理解成:
我运行它时一定正在使用 128K
必须问:
Runtime 当前配置是多少?
这就是:
Model Capability
≠
Runtime Configuration
二十、实验六:从 Ollama API 直接拿到 Prefill 和 Decode 数据
前面第⑥篇,我们已经建立:
Prefill
Decode
TTFT
TPOT
这些概念。
当时还要自己写 Python 测量。
现在 Runtime 已经开始直接暴露这类指标。
例如:
curl -s http://localhost:11434/api/generate \
-d '{
"model": "qwen2.5:7b-instruct",
"prompt": "请从 Infra 角度解释为什么 LLM 推理需要 KV Cache。",
"stream": false,
"options": {
"num_ctx": 4096,
"num_predict": 128,
"temperature": 0
}
}' | jq
Ollama 当前返回的信息里包括:
total_duration
load_duration
prompt_eval_count
prompt_eval_duration
eval_count
eval_duration
这些字段是官方 API 明确定义的 Runtime Metrics。


于是可以直接计算:
Prefill Throughput
=
prompt_eval_count
/
(prompt_eval_duration / 1e9)
以及:
Decode Throughput
=
eval_count
/
(eval_duration / 1e9)
也就是:
tokens/s
例如可以写一个非常简单的脚本:
curl -s http://localhost:11434/api/generate \
-d '{
"model": "qwen2.5:7b-instruct",
"prompt": "请从 Infra 角度解释为什么 LLM 推理需要 KV Cache。",
"stream": false,
"options": {
"num_ctx": 4096,
"num_predict": 128,
"temperature": 0
}
}' |
jq '{
prompt_tokens: .prompt_eval_count,
prompt_seconds: (.prompt_eval_duration / 1000000000),
prompt_tps:
(.prompt_eval_count /
(.prompt_eval_duration / 1000000000)),
output_tokens: .eval_count,
decode_seconds:
(.eval_duration / 1000000000),
decode_tps:
(.eval_count /
(.eval_duration / 1000000000))
}'

现在回头看,会发现很有意思。
第⑥篇我们研究的是:
Prefill 是什么?
Decode 是什么?
这一篇已经变成:
Runtime
直接把它们变成了 Metrics
这就是从:
LLM 原理
逐渐进入:
AI Infra
的过程。
二十一、现在把两条本地推理路线画到一张图上
到这里,可以画出这一篇最重要的一张架构图。
LLM Model
│
┌─────────────┴─────────────┐
│ │
▼ ▼
Hugging Face Route GGUF Route
│ │
▼ ▼
Transformers GGUF
│ │
▼ ▼
PyTorch llama.cpp
│ │
▼ ┌──────┴──────┐
CUDA │ │
│ ▼ ▼
▼ CPU CUDA
GPU │
▼
GPU
GGUF / Model
│
▼
Ollama
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Model Mgmt Runtime API
│
┌──────┴──────┐
▼ ▼
CPU GPU
这张图真正想表达的不是:
谁更好
而是:
LLM 并没有唯一一种运行方式。
训练、研究和 Python AI 生态很自然地走:
Transformers
+
PyTorch
而本地轻量推理又发展出了:
GGUF
+
llama.cpp
这样的路线。
Ollama 再把:
Model Management
Runtime
API
Lifecycle
进一步封装起来。
二十二、几个实验里特别容易踩的坑
1. 编译成功,不等于启用了 CUDA
如果只是:
cmake -B build
构建出来的是 CPU Runtime。
RTX 3060 实验需要:
cmake -B build -DGGML_CUDA=ON
然后一定检查:
./build/bin/llama-cli --list-devices
不要仅仅因为:
机器有 NVIDIA GPU
就默认程序一定在使用 GPU。
2. 模型只有 4.7GB,不代表显存只需要 4.7GB
GGUF 文件主要解决 Model Weight Storage。
真正运行还需要:
Model Weights
+
KV Cache
+
Compute Buffers
+
Runtime Overhead
所以:
Model File Size
≠
Runtime VRAM
上一篇的量化知识,到这里再次发挥作用。
3. Context 开太大,可能突然把显存吃光
例如:
Q4_K_M
模型本身明明轻松能放进 12GB,
但从:
-c 4096
一路加到:
-c 32768
甚至更高,
Runtime 的 Memory Requirement 会明显增加。
所以排查 OOM 时不能只问:
模型多大?
还要问:
Context 多大?
4. llama.cpp 现在会自动帮你做很多事情
当前版本的 llama.cpp 已经越来越智能,--n-gpu-layers 甚至可以使用:
auto
并配合显存自动 Fit。
但这对于学习实验反而可能造成困扰。
因为我们的目标就是观察:
CPU
vs
Partial Offload
vs
Full GPU
所以实验阶段最好显式指定:
--device none
-ngl 10
-ngl 20
-ngl all
这样控制变量更清楚。
5. 第一次运行和第二次运行不要直接比较
第一次可能包含:
Download
+
Disk Cache
+
Model Load
+
CUDA Initialization
第二次则可能只是:
Model Load
+
Inference
甚至 Ollama 模型还可能已经:
Keep Alive
留在内存中。
所以真正 benchmark 时一定区分:
Cold Start
和:
Warm Run
否则:
load_duration
会严重干扰结果。
6. Ollama 和 llama.cpp 不要只拿一个 tokens/s 就直接比较
即使模型名字都叫:
Qwen2.5-7B
也要确认:
Quantization
Context
Prompt
Output Tokens
Sampling
GPU Offload
是否一致。
否则:
18 tokens/s
vs
25 tokens/s
几乎没有比较意义。
Benchmark 最重要的永远是:
控制变量。
二十三、这一篇真正建立的几个认知
走到这里,我们终于补上了前七篇一直缺失的一层。
第一:
Model 和 Model Runtime 是两回事。
模型只是:
Architecture
Weights
Tokenizer
Metadata
真正负责把模型跑起来的是 Runtime。
第二:
PyTorch 不是运行 LLM 的唯一方式。
前面的:
Transformers
↓
PyTorch
↓
CUDA
是一条极其重要的路线。
但本地 LLM 还有:
GGUF
↓
llama.cpp
↓
CPU / CUDA
这条路线。
第三:
GGUF 不只是一个量化模型的文件后缀。
它解决的是:
如何把模型结构、Metadata、Tokenizer 信息和 Tensor Data
组织成适合 Runtime 读取的模型格式
而:
Q4_K_M
才是在描述模型 Weight 使用什么量化表示。
第四:
CPU/GPU Offload 本质上是 Runtime 的 Device Placement 问题。
当 VRAM 不够时:
CPU RAM
+
GPU VRAM
可以一起承担模型。
但代价是:
CPU Compute
PCIe
Synchronization
Memory Bandwidth
所以:
能跑
和:
跑得快
仍然是两件事。
第五:
Context 不只是模型参数,它还是 Runtime Resource。
真正运行时必须同时考虑:
Model Max Context
Runtime Context
Actual Request Context
三层概念。
最终:
Context
↓
KV Cache
↓
Memory
↓
Performance
才真正落到了 Infra 上。
第六:
Ollama 的价值不是“让模型神奇地一条命令运行”,而是把大量 Runtime 复杂度封装了。
它帮我们管理:
Model
Runtime
Template
Context
GPU
Lifecycle
API
所以用户最终只看到:
ollama run qwen2.5:7b
但底下的 Infra 一点都没有消失。
二十四、为什么下一篇终于该出现 vLLM 了?
做到这里,我们的 RTX 3060 已经真正拥有了一套:
Local LLM Runtime
无论:
llama.cpp
还是:
Ollama
都已经能够解决一个核心问题:
怎样在一台机器上方便、高效地运行一个本地模型?
但如果把问题稍微改一下:
1 个用户
→ 100 个用户
事情马上就不一样了。
假设现在同时来了:
Request A
Request B
Request C
Request D
...
每一个请求都有自己的:
Prompt
Context
Prefill
KV Cache
Decode
GPU 应该先算谁?
如果:
A 正在 Decode
是不是整个 GPU 都只能等它一个 Token 一个 Token 地生成?
多个请求能不能一起:
Batch
?
有的请求刚进入:
Prefill
有的已经进入:
Decode
怎么调度?
更麻烦的是:
几十个 Request
↓
几十份 KV Cache
↓
GPU VRAM
很快又会遇到资源碎片和显存利用率的问题。
于是问题已经从这一篇的:
How to run one model?
升级成:
How to serve many requests efficiently?
这时候:
Model Runtime
就开始继续进化成:
Inference Serving Engine
而 AI Infra 领域一个非常重要的名字终于该正式出现:
vLLM

下一篇,我们将开始真正测:
TTFT
TPOT
Throughput
并第一次面对:
Batching
Continuous Batching
KV Cache Management
GPU Utilization
Concurrent Requests
到那时候,我们研究的问题就不再是:
我的 RTX 3060 能不能跑一个 LLM?
而会变成:
既然 GPU 这么贵,怎样才能让它一次服务更多请求,并且尽可能不要闲着?
这才是真正进入:
LLM Serving
的开始。
更多推荐


所有评论(0)