上一篇,我们终于把:

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 OffloadPrompt ProcessingText GenerationGPU VRAM观察
0 Layer757.22 ± 5.746.49 ± 0.0017 MiBCPU 为主
10 Layers983.58 ± 6.5010.03 ± 0.002055 MiBCPU + GPU
20 Layers1410.17 ± 7.0518.03 ± 0.013441 MiBGPU 占比继续增加
Full2346.89 ± 17.2967.31 ± 1.054681 MiBGPU 为主

这里我不预设数字。

因为实际结果会受到:

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

的开始。

更多推荐