上一篇,我们把 GGUF 这个“模型打包盒”从里到外拆开看了一遍:它装了什么,元数据和张量如何组织,量化类型又是怎么记录的。

这一篇不再停留在文件结构上,而是完整走通一条真正能落地的流水线:

从 Hugging Face 下载一个原始模型,转换成高精度 GGUF,再量化成 Q4_K_M 等低比特版本,最后用 CPU、GPU、命令行、HTTP API 和 Python 把它跑起来。

这不是一篇只给几条命令的“复制粘贴教程”。除了告诉你命令怎么写,我们还会解释:

  • 为什么转换和量化通常要分成两步;
  • 为什么中间文件不一定非得是 FP16;
  • 为什么 Q4_K_M 不是“每个权重刚好 4 bit”;
  • 为什么文件只有 5GB,运行时却可能占用更多内存;
  • 为什么低比特模型不一定比高比特模型更快;
  • 为什么模型转换成功了,实际聊天却可能满嘴乱码;
  • 为什么同一个 GGUF,在 CPU、CUDA、Metal 上的最佳参数并不一样;
  • 如何科学比较不同量化档位,而不是只看一次生成速度。

本文以 Qwen2.5-1.5B-Instruct 作为示例。它有约 15.4 亿参数、28 层 Transformer,采用 GQA,包含 12 个 Query 头和 2 个 KV 头,原生上下文长度为 32,768 token。这个体量足够展示完整流程,又不会让第一次操作的读者在下载、转换和量化阶段等待太久。(Hugging Face)

需要提前说明的是,llama.cpp 仍在快速迭代。本文命令和参数按照 2026 年 7 月的主线版本习惯整理。实际操作时,最好记录自己使用的 Git 提交号,并以本地程序的 --help 输出为最终依据。


目录

  • 一、最终要得到什么
  • 二、先看懂整条流水线
  • 三、动手前的模型与硬件检查
  • 四、搭建项目目录与 Python 环境
  • 五、编译 llama.cpp
  • 六、下载 Hugging Face 原始模型
  • 七、把 Hugging Face 模型转换成高精度 GGUF
  • 八、验证转换结果,而不是只看有没有生成文件
  • 九、量化前必须理解的几个核心概念
  • 十、量化成 Q4_K_M 及其他档位
  • 十一、进阶量化:用 Importance Matrix 降低质量损失
  • 十二、如何估算文件大小、内存、显存和 KV Cache
  • 十三、用 llama-cli 在 CPU 和 GPU 上运行
  • 十四、启动 llama-server,提供本地 HTTP API
  • 十五、用 Python 调用本地模型
  • 十六、正确测试不同量化档位的速度
  • 十七、如何评估量化后的质量损失
  • 十八、常见报错、误区与系统化排障
  • 十九、一份可直接执行的完整脚本
  • 二十、量化档位到底该怎么选
  • 二十一、小结

一、最终要得到什么

完成本文后,我们会得到这样一组文件:

models/
├── hf/
│   └── Qwen2.5-1.5B-Instruct/
│       ├── config.json
│       ├── generation_config.json
│       ├── tokenizer.json
│       ├── tokenizer_config.json
│       ├── model.safetensors
│       └── ...
│
└── gguf/
    ├── qwen2.5-1.5b-instruct-high.gguf
    ├── qwen2.5-1.5b-instruct-q8_0.gguf
    ├── qwen2.5-1.5b-instruct-q6_k.gguf
    ├── qwen2.5-1.5b-instruct-q5_k_m.gguf
    ├── qwen2.5-1.5b-instruct-q4_k_m.gguf
    └── qwen2.5-1.5b-instruct-q3_k_m.gguf

其中:

  • hf/Qwen2.5-1.5B-Instruct/ 是从 Hugging Face 下载的原始模型;
  • high.gguf 是高精度 GGUF 中间文件,通常为 F16 或 BF16;
  • 其余文件是不同量化档位;
  • 真正部署时,通常只需要其中一个量化后的 GGUF;
  • 高精度 GGUF 建议先保留,因为以后还可以继续量化出其他版本。

最后,我们会通过三种方式使用它:

llama-cli
    ↓
终端交互式聊天

llama-server
    ↓
HTTP / OpenAI 风格 API

llama-cpp-python
    ↓
直接嵌入 Python 程序

二、先看懂整条流水线

从 Hugging Face 模型到本地推理,大体上分成三步:

Hugging Face 原始模型
通常是 BF16 / FP16 safetensors
多个权重分片 + 配置 + 分词器
                │
                │ ① convert_hf_to_gguf.py
                ▼
高精度 GGUF
F16 / BF16 / AUTO
包含权重、模型元数据、分词器、聊天模板
                │
                │ ② llama-quantize
                ▼
低比特 GGUF
Q8_0 / Q6_K / Q5_K_M / Q4_K_M / Q3_K_M ...
                │
                │ ③ llama-cli / llama-server / Python
                ▼
本地推理
CPU、Metal、CUDA、HIP、Vulkan 或混合运行

三步分别解决不同问题。

2.1 转换解决的是“格式和语义对齐”

Hugging Face 模型通常由多类文件组成:

config.json
model-00001-of-00002.safetensors
model-00002-of-00002.safetensors
model.safetensors.index.json
tokenizer.json
tokenizer_config.json
special_tokens_map.json
generation_config.json
...

llama.cpp 不能直接把这些文件当成一个完整 GGUF 使用。转换脚本需要完成的事情远不只是“改后缀”:

  1. 读取模型架构配置;
  2. 找到所有权重分片;
  3. 把 Hugging Face 张量名映射成 llama.cpp 认识的张量名;
  4. 根据架构进行必要的张量变换;
  5. 写入模型层数、隐藏维度、注意力头数、RoPE 参数等元数据;
  6. 转换并写入 tokenizer;
  7. 写入特殊 token;
  8. 尽可能写入聊天模板;
  9. 按 GGUF 规范重新排列和对齐数据;
  10. 根据 --outtype 决定输出权重精度。

因此,“转换只是换包装”是一个方便入门的比喻,但并不完全准确。它本质上是一次架构感知的数据重编码

2.2 量化解决的是“数值表示压缩”

高精度 GGUF 仍然主要使用 16 bit 浮点权重。

假设一个模型有 15.4 亿参数,若所有权重都用 16 bit 表示,仅理论权重体积就是:

1.54 × 10⁹ × 16 ÷ 8
≈ 3.08 × 10⁹ 字节
≈ 2.87 GiB

把它量化为接近 5 bits/weight 的 Q4_K_M 后,主体权重可能降到大约 1GB 左右。当然,真实文件还包含元数据、分词器、对齐填充以及部分采用不同类型保存的张量,所以不能只用参数量乘比特数得到精确文件大小。

量化不是 ZIP 那样的无损压缩。它会改变权重的数值表示,因此可能带来质量损失。

2.3 推理解决的是“如何执行模型计算”

GGUF 只是模型文件。真正执行矩阵运算、维护 KV Cache、采样 token 的,是 llama.cpp 运行时。

同一个 GGUF 可以:

  • 全部在 CPU 上运行;
  • 全部或部分放到 NVIDIA GPU;
  • 在 Apple Silicon 上通过 Metal 运行;
  • 在 AMD GPU 上通过 HIP 或 Vulkan 运行;
  • 在 Intel GPU 上通过 SYCL 或 Vulkan 运行;
  • 跨多张 GPU 切分;
  • 一部分留在 CPU,一部分放到 GPU。

llama.cpp 当前支持 Metal、CUDA、HIP、Vulkan、SYCL 等多类后端,也支持 CPU 与 GPU 混合推理。(GitHub)


2.4 为什么通常要先转高精度 GGUF,再量化

原稿中有一个核心思路是对的:

转换与量化是两件事,分开做更清晰,也更灵活。

但需要修正一个细节:中间文件不一定非得是 FP16。

当前转换器支持 f32f16bf16q8_0auto 等输出类型。对于原本以 16 bit 精度发布的模型,官方量化文档建议可以使用 --outtype auto,或者直接省略 --outtype,由转换器选择合适的高保真 16 bit 类型。(GitHub)

更准确的说法应该是:

先把 Hugging Face 模型转换成一个高质量 GGUF 源文件,通常是 F16、BF16 或 AUTO;再从这个高精度源文件量化出 Q4、Q5、Q6 等部署版本。

这样做有四个好处。

第一,同一个高精度源可以生成多个档位:

high.gguf
├── Q8_0
├── Q6_K
├── Q5_K_M
├── Q4_K_M
└── Q3_K_M

第二,不同量化版本都来自同一个基准,方便公平比较。

第三,避免从已经量化过的文件再次量化。官方工具虽然提供 --allow-requantize,但明确警告:从已量化权重继续量化,相比从 16 bit 或 32 bit 权重开始,可能严重降低质量。(GitHub)

第四,一旦未来出现新的量化算法,你不用重新下载 Hugging Face 原始模型,只需要保留高精度 GGUF 再量化一次。


三、动手前的模型与硬件检查

很多失败其实不是命令写错了,而是在执行命令之前就选错了源模型、低估了磁盘需求,或者没有确认当前架构是否受支持。

3.1 优先选择原始高精度模型

用于转换的理想来源是:

  • 官方或可信发布者提供的原始模型;
  • 权重为 BF16、FP16 或 FP32;
  • 文件格式通常是 safetensors
  • 包含完整的 config.json 和 tokenizer 文件;
  • 不是已经经过 AWQ、GPTQ、BNB 4bit 等方法量化的版本。

例如本篇使用:

Qwen/Qwen2.5-1.5B-Instruct

而不是:

某个 Qwen2.5-1.5B-AWQ
某个 Qwen2.5-1.5B-GPTQ
某个已经量化过的 GGUF

AWQ、GPTQ 和 GGUF 量化不是可以随意互转的几种压缩包。它们有不同的权重布局、量化元数据和推理内核假设。要制作一个干净的 GGUF 量化版本,最稳妥的路径始终是从原始高精度权重开始。

3.2 检查模型架构是否受支持

不要看到 Hugging Face 上有 safetensors 就默认一定能转换。

llama.cpp 必须同时具备两部分支持:

  1. 转换脚本认识这个 Hugging Face 架构;
  2. C++ 推理端实现了这个模型的计算图。

可以先执行:

python convert_hf_to_gguf.py --print-supported-models

Linux 或 macOS 下,可以进一步搜索:

python convert_hf_to_gguf.py --print-supported-models 2>&1 | grep -i qwen

如果是刚发布的新架构,当前版本可能还没适配。此时首先更新 llama.cpp,而不是立即修改转换脚本:

git pull

更新后重新编译,并再次检查支持列表。

需要注意,转换器显示支持某个“模型家族”,不代表这个家族未来的每个变体都天然兼容。新的 MoE 结构、视觉投影器、多 Token Prediction 头或特殊 tokenizer,都可能需要额外适配。

3.3 检查模型许可证

技术上能转换,不代表法律上能随便分发。

下载前至少确认:

  • 是否允许个人使用;
  • 是否允许商业使用;
  • 是否允许重新分发量化权重;
  • 是否要求保留许可证或模型卡;
  • 是否有特定的可接受使用条款。

自己在本地转换和使用,与把量化文件上传到公开仓库,是两个不同的问题。

3.4 磁盘空间不能只按最终 GGUF 计算

假设最终只想得到一个 1GB 左右的 Q4_K_M,也不能只准备 1GB 磁盘。

转换过程中可能同时存在:

原始 Hugging Face 权重
+ 高精度 GGUF
+ 一个或多个量化 GGUF
+ Hugging Face 下载缓存
+ 临时文件

因此,制作模型需要的磁盘空间通常远高于最终部署文件。

对于大型模型,官方文档特别提醒:转换和量化过程需要同时考虑中间文件的磁盘空间与加载模型所需的系统内存;当前量化流程对大型模型可能需要相当可观的 RAM。(GitHub)

对于第一次练习,1.5B 级模型很合适。不要一上来就用 70B 验证环境。

3.5 先确认硬件目标

在编译前,先决定自己打算使用哪种后端:

硬件 推荐后端
只有 CPU 默认 CPU
NVIDIA GPU CUDA
Apple Silicon Metal
AMD GPU,Linux ROCm 环境 HIP
AMD 或其他支持 Vulkan 的 GPU Vulkan
Intel GPU SYCL 或 Vulkan
多张 NVIDIA/AMD GPU 对应 GPU 后端加多卡切分

编译后端与运行参数必须匹配。没有编译 CUDA,却设置 -ngl all,不会凭空获得 CUDA 加速。


四、搭建项目目录与 Python 环境

下面以 Bash 环境为主,也就是 Linux、macOS、WSL 或 Git Bash。

4.1 拉取 llama.cpp

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

建议马上记录当前提交:

git rev-parse HEAD

也可以保存到文件:

mkdir -p logs
git rev-parse HEAD > logs/llama-cpp-commit.txt

为什么要记录提交号?

因为 llama.cpp 迭代很快。两个人都说“我用的是最新版”,可能实际使用的是不同日期的代码、不同参数默认值和不同量化实现。只有提交号才能让实验真正可复现。

4.2 建立目录

mkdir -p models/hf
mkdir -p models/gguf
mkdir -p benchmarks
mkdir -p logs

最终目录大致如下:

llama.cpp/
├── build/
├── models/
│   ├── hf/
│   └── gguf/
├── benchmarks/
├── logs/
├── convert_hf_to_gguf.py
└── ...

4.3 创建 Python 虚拟环境

Linux 或 macOS:

python3 -m venv .venv
source .venv/bin/activate

Windows PowerShell:

py -m venv .venv
.\.venv\Scripts\Activate.ps1

更新 pip:

python -m pip install --upgrade pip

安装转换脚本依赖:

python -m pip install -r requirements.txt

当前 llama.cpp 官方量化流程同样要求先安装仓库中的 Python 依赖。(GitHub)

如果后面需要使用 Hugging Face 命令行,再安装:

python -m pip install --upgrade huggingface_hub

检查环境:

python --version
hf --help

五、编译 llama.cpp

llama.cpp 当前使用 CMake 构建。建议为不同后端使用不同的构建目录,避免 CPU、CUDA、Vulkan 等配置互相污染。

例如:

build-cpu/
build-cuda/
build-vulkan/

5.1 纯 CPU 编译

cmake -B build-cpu
cmake --build build-cpu --config Release -j

其中:

  • -B build-cpu:把构建文件放到 build-cpu
  • --config Release:构建优化版本;
  • -j:并行编译。

官方最基本的 CPU 构建流程就是:

cmake -B build
cmake --build build --config Release

并可以通过 -j 增加并行构建任务。(GitHub)

编译完成后,Linux 和 macOS 下一般可以看到:

build-cpu/bin/llama-cli
build-cpu/bin/llama-server
build-cpu/bin/llama-quantize
build-cpu/bin/llama-bench
build-cpu/bin/llama-perplexity
build-cpu/bin/llama-imatrix

Windows 使用 Visual Studio 多配置生成器时,可执行文件有时位于:

build-cpu/bin/Release/

因此,如果教程中的路径找不到,先执行:

find build-cpu -type f -name "llama-cli*"

Windows 可以在资源管理器或 PowerShell 中搜索 llama-cli.exe


5.2 NVIDIA CUDA 编译

确保已经正确安装:

  • NVIDIA 驱动;
  • 兼容的 CUDA Toolkit;
  • CMake;
  • C++ 编译器。

然后执行:

cmake -B build-cuda -DGGML_CUDA=ON
cmake --build build-cuda --config Release -j

CUDA 构建开关是:

-DGGML_CUDA=ON

这是当前官方构建文档给出的 CUDA 构建方式。(GitHub)

编译后检查设备:

./build-cuda/bin/llama-cli --list-devices

再检查版本:

./build-cuda/bin/llama-cli --version

如果设备列表里完全没有 CUDA,常见原因包括:

  • CMake 没找到 CUDA Toolkit;
  • 使用的仍然是旧 CPU 构建目录;
  • 驱动与 Toolkit 不兼容;
  • 实际执行的是另一个目录里的 llama-cli
  • Python 环境和 C++ 构建环境混淆。

因此,不要把所有构建都放进同一个 build/ 目录反复覆盖。


5.3 Apple Silicon 与 Metal

在支持 Metal 的 macOS 环境中,Metal 后端默认启用。通常直接执行标准构建即可:

cmake -B build-metal
cmake --build build-metal --config Release -j

运行时再通过 GPU 层参数控制是否使用 Metal。

这里需要纠正一句常见说法:

“Mac 直接用 CPU 编译命令,它会自动用 GPU。”

更准确的描述是:

macOS 上使用标准 CMake 配置时,Metal 后端默认启用;运行时是否进行 GPU offload,仍由具体参数和模型加载策略决定。

Metal 默认启用是 llama.cpp 当前官方构建文档中的行为。(GitHub)

Apple Silicon 使用统一内存。CPU 与 GPU 并不是各自拥有完全独立的一块 RAM 和 VRAM,但 GPU offload 仍然会影响:

  • 内存分配方式;
  • 可用内存上限;
  • 计算位置;
  • 性能;
  • 系统是否发生内存压缩或交换。

所以也不能因为是统一内存,就忽略模型大小和上下文长度。


5.4 AMD GPU

如果使用 ROCm/HIP:

cmake -B build-hip -DGGML_HIP=ON
cmake --build build-hip --config Release -j

如果 HIP 环境难以配置,或者设备更适合 Vulkan,可以尝试:

cmake -B build-vulkan -DGGML_VULKAN=ON
cmake --build build-vulkan --config Release -j

HIP 与 Vulkan 都是 llama.cpp 当前支持的后端。(GitHub)

实际性能取决于:

  • GPU 架构;
  • 驱动;
  • 显存带宽;
  • 对应量化内核是否优化;
  • 模型结构;
  • 上下文长度;
  • 是否发生 CPU/GPU 数据搬运。

不能简单认为“只要打开 GPU 就一定比 CPU 快”。


5.5 Windows 编译注意事项

官方文档建议 Windows 用户安装 Visual Studio 2022,并选择 C++ 桌面开发、CMake 工具等组件,然后在 Developer PowerShell 或 Developer Command Prompt 中构建。(GitHub)

CPU 构建:

cmake -B build-cpu
cmake --build build-cpu --config Release -j

CUDA 构建:

cmake -B build-cuda -DGGML_CUDA=ON
cmake --build build-cuda --config Release -j

运行路径可能是:

.\build-cuda\bin\Release\llama-cli.exe

而不是:

.\build-cuda\bin\llama-cli.exe

六、下载 Hugging Face 原始模型

6.1 使用当前的 hf CLI

安装:

python -m pip install --upgrade huggingface_hub

下载完整模型仓库:

hf download Qwen/Qwen2.5-1.5B-Instruct \
    --local-dir ./models/hf/Qwen2.5-1.5B-Instruct

hf download 是当前 Hugging Face CLI 的下载命令,--local-dir 会把仓库文件保存到指定目录,同时在目标目录下维护下载元数据,以便后续更新时避免重复下载未变化的文件。(Hugging Face)

下载完成后检查:

ls -lah ./models/hf/Qwen2.5-1.5B-Instruct

至少应看到:

config.json
tokenizer.json
tokenizer_config.json
*.safetensors

某些模型使用多个权重分片:

model-00001-of-00004.safetensors
model-00002-of-00004.safetensors
model-00003-of-00004.safetensors
model-00004-of-00004.safetensors
model.safetensors.index.json

这是正常情况。

6.2 私有或受限模型

如果模型需要登录:

hf auth login

也可以设置环境变量:

export HF_TOKEN="你的令牌"

不要把令牌硬编码进公开脚本,也不要把包含令牌的终端历史或配置文件提交到 Git。

6.3 固定模型版本

为了让结果可复现,可以下载指定 revision:

hf download Qwen/Qwen2.5-1.5B-Instruct \
    --revision <模型提交号> \
    --local-dir ./models/hf/Qwen2.5-1.5B-Instruct

“同一个模型名”并不保证永远对应完全相同的文件。发布者可能更新 tokenizer、配置或权重。因此,严谨实验应同时记录:

llama.cpp commit
模型仓库 revision
转换参数
量化参数
编译后端
硬件信息

6.4 先做 dry run

想先查看要下载哪些文件和总大小,可以执行:

hf download Qwen/Qwen2.5-1.5B-Instruct --dry-run

这在下载大型模型前很有用。


七、把 Hugging Face 模型转换成高精度 GGUF

7.1 先确认转换器支持当前模型

python convert_hf_to_gguf.py --print-supported-models

Qwen2 系列在当前 llama.cpp 中属于受支持架构,但仍建议实际执行一次检查,因为你的本地仓库可能不是最新版本。

7.2 推荐命令:使用 AUTO 高精度输出

python convert_hf_to_gguf.py \
    ./models/hf/Qwen2.5-1.5B-Instruct \
    --outfile ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    --outtype auto

参数说明:

./models/hf/Qwen2.5-1.5B-Instruct

原始 Hugging Face 模型目录。

--outfile

输出 GGUF 路径。

--outtype auto

让转换器选择高保真的 16 bit 浮点类型。

当前官方量化文档明确说明:对于通常以 16 bit 形式发布的模型,--outtype auto 或省略 --outtype 都是合理选择。(GitHub)

7.3 明确要求 F16

如果实验要求所有中间源都统一为 F16,可以写:

python convert_hf_to_gguf.py \
    ./models/hf/Qwen2.5-1.5B-Instruct \
    --outfile ./models/gguf/qwen2.5-1.5b-instruct-f16.gguf \
    --outtype f16

7.4 明确要求 BF16

python convert_hf_to_gguf.py \
    ./models/hf/Qwen2.5-1.5B-Instruct \
    --outfile ./models/gguf/qwen2.5-1.5b-instruct-bf16.gguf \
    --outtype bf16

Qwen2.5-1.5B-Instruct 的官方配置将原始 torch_dtype 标为 bfloat16。(Hugging Face)

这并不意味着你绝对不能输出 F16,而是说明:

  • BF16 更接近原始发布精度;
  • F16 的尾数精度更高,但指数范围更窄;
  • 对一般推理量化源而言,两者通常都可以;
  • 使用 auto 可以减少不必要的主观选择。

7.5 远程转换

当前转换器还支持实验性的 --remote 模式,可以直接远程读取 safetensors,而不先完整下载全部权重:

python convert_hf_to_gguf.py \
    Qwen/Qwen2.5-1.5B-Instruct \
    --remote \
    --outfile ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    --outtype auto

远程模式仍会下载配置和 tokenizer;访问受限仓库时需要设置 HF_TOKEN。该功能在转换器帮助中被标记为实验性。(GitHub)

对于学习和可复现实验,我仍然更推荐先下载到本地。原因是:

  • 更容易检查源文件;
  • 失败后不依赖网络重新读取;
  • 可以固定模型 revision;
  • 便于生成校验和;
  • 便于反复量化;
  • 更容易区分网络错误与转换错误。

7.6 大模型转换选项

面对特别大的模型,可以查看:

python convert_hf_to_gguf.py --help

其中值得关注的参数包括:

--use-temp-file
--split-max-size
--split-max-tensors
--dry-run

--use-temp-file 可以改变输出过程中的内存与磁盘使用方式。

--split-max-size--split-max-tensors 可以输出分片 GGUF。例如:

model-00001-of-00004.gguf
model-00002-of-00004.gguf
model-00003-of-00004.gguf
model-00004-of-00004.gguf

分片不等于转换失败,也不意味着模型被拆成四个独立模型。运行时一般指定第一片,程序再根据分片元数据加载其他部分。

7.7 多模态模型不是只有一个 GGUF

本文示例是纯文本模型。

如果转换视觉或音频模型,通常还需要额外生成 mmproj,也就是多模态编码器或投影器对应的 GGUF。

官方量化文档建议多模态组件通常保持 BF16、Q8 等较高质量,因为它们相对语言模型主体较小,继续激进压缩节省的空间有限,却可能直接损害输入特征质量。(GitHub)

因此,多模态流程更像:

语言模型主体
    → high GGUF
    → Q4_K_M GGUF

视觉/音频组件
    → mmproj BF16 或 Q8 GGUF

运行时需要同时传入模型文件和 mmproj


八、验证转换结果,而不是只看有没有生成文件

转换命令退出且目录中出现了 .gguf,只能说明“写出了一个文件”,不能完全证明它正确。

至少应做五层检查。


8.1 检查退出状态和日志

转换日志中不应该出现:

Traceback
KeyError
unsupported architecture
unknown tensor
missing tokenizer
cannot find safetensors

如果使用 Shell,可以在命令后检查:

echo $?

输出 0 通常表示命令正常结束。


8.2 检查文件大小

ls -lh ./models/gguf/qwen2.5-1.5b-instruct-high.gguf

如果一个 1.5B 高精度模型最后只有几十 MB,基本可以判定不正常。

但也不要死记“1.5B 必须刚好 3GB”。真实大小受以下因素影响:

  • 是否 F16 或 BF16;
  • 是否有 tied embeddings;
  • 是否有部分张量使用其他类型;
  • tokenizer 大小;
  • 对齐和元数据;
  • 模型架构;
  • 是否分片。

8.3 查看 GGUF 元数据

安装 GGUF Python 包:

python -m pip install gguf

查看元数据:

python -m gguf.scripts.gguf_dump \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf

如果当前包没有暴露这个模块,可以直接使用 llama.cpp 仓库内脚本:

python gguf-py/gguf/scripts/gguf_dump.py \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf

重点检查:

general.architecture
general.name
general.file_type
模型层数
attention head 数
KV head 数
context length
tokenizer 类型
tokenizer.chat_template
tensor 数量
tensor 类型分布

对于当前示例,架构应该对应 Qwen2 系列,层数应与原模型的 28 层匹配,注意力头和 KV 头也应与配置一致。(Hugging Face)

8.4 用 Python 做快速结构检查

from pathlib import Path

from gguf import GGUFReader


model_path = Path(
    "./models/gguf/qwen2.5-1.5b-instruct-high.gguf"
)

if not model_path.is_file():
    raise FileNotFoundError(model_path)

reader = GGUFReader(str(model_path))

print("文件:", model_path)
print("文件大小:", f"{model_path.stat().st_size / 1024**3:.3f} GiB")
print("元数据字段数:", len(reader.fields))
print("张量数:", len(reader.tensors))

for key in (
    "general.architecture",
    "general.name",
    "general.file_type",
    "tokenizer.chat_template",
):
    field = reader.fields.get(key)
    print(f"{key}: {field}")

print("\n前 10 个张量:")
for tensor in reader.tensors[:10]:
    print(
        f"name={tensor.name}, "
        f"shape={tensor.shape}, "
        f"type={tensor.tensor_type}"
    )

这里不建议把某个 Python 对象的打印格式当成稳定 API。不同版本的 gguf 包可能调整字段对象表现形式。自动化程序最好只依赖经过版本锁定和实际测试的访问方式。

8.5 实际加载一次

真正可靠的检查是让 llama.cpp 解析并加载它:

./build-cpu/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    -ngl 0 \
    -p "你好,请用一句话介绍你自己。" \
    -n 32

即使最终部署量化版本,也建议先对高精度文件做一次加载测试。

关注日志中的:

model architecture
model size
context size
number of layers
file type
CPU/GPU buffer
chat template

如果高精度版本能正确加载,而量化版本不能,问题大概率出在量化阶段。

如果高精度版本本身就不能加载,应先解决转换问题,不要继续量化。

8.6 生成校验和

Linux:

sha256sum ./models/gguf/qwen2.5-1.5b-instruct-high.gguf

macOS:

shasum -a 256 ./models/gguf/qwen2.5-1.5b-instruct-high.gguf

Windows PowerShell:

Get-FileHash `
  .\models\gguf\qwen2.5-1.5b-instruct-high.gguf `
  -Algorithm SHA256

把结果记录到日志:

sha256sum ./models/gguf/*.gguf > logs/gguf-sha256.txt

以后复制到另一台机器时,就能确认文件是否损坏。


九、量化前必须理解的几个核心概念

9.1 量化主要压缩的是权重

很多人看到“4 bit 模型”,会误以为模型运行时所有东西都变成 4 bit。

实际上,需要区分:

模型权重
激活值
KV Cache
中间计算缓冲区
输出 logits
采样状态

Q4_K_M 主要描述权重张量的量化方案

KV Cache 默认可能仍然使用 F16。激活值和部分计算也可能使用浮点类型。某些敏感张量还可能以高于主体权重的精度保存。

因此:

Q4_K_M 不是“整个推理过程都使用 4 bit”。

9.2 Q4_K_M 中每一段是什么意思

可以先用一个不完全但实用的方式理解:

Q4_K_M
│  │ │
│  │ └── M:某种混合量化配方
│  └──── K:K-quant 家族
└─────── Q4:以约 4 bit 量级为核心的量化档位

这里最容易误解的是 MS

它们不应简单理解成:

M = 所有张量 4.5 bit
S = 所有张量 4 bit

更准确地说,它们是混合量化策略。不同类型、不同敏感度的张量可能使用不同量化表示,最终形成一个整体档位。

这也是为什么同样叫 Q4,真实平均 bits/weight 并不等于精确的 4.000。

9.3 名字里的数字不等于真实平均 bits/weight

官方量化文档给出的 Llama 3.1 8B 示例中:

档位 示例有效 bits/weight
Q2_K 3.1593
Q3_K_S 3.6429
Q3_K_M 3.9960
Q4_K_S 4.6672
Q4_K_M 4.8944
Q5_K_S 5.5704
Q5_K_M 5.7036
Q6_K 6.5633
Q8_0 8.5008
F16 16.0005

这些数字来自特定模型和量化实现,只适合用来理解数量级,不能当成所有架构都完全相同的常数。(GitHub)

因此,Q4_K_M 更准确的描述是:

一个以 4 bit 级别量化为核心、最终平均权重成本常落在接近 5 bits/weight 的混合量化方案。

9.4 Q4_K_M 为什么常被当作甜点档

它受欢迎,不是因为它在任何模型、任何设备上都绝对最优,而是因为它经常取得不错的综合平衡:

  • 文件明显小于 F16、Q8;
  • 通常比 Q3 更稳定;
  • 大多数后端有成熟实现;
  • 内存压力适中;
  • 对一般聊天、摘要、轻量代码任务,质量往往仍然可用;
  • 在有限内存下,可以把更大的基础模型装进设备。

但这不等于:

Q4_K_M 永远比 Q5_K_M 更值得选
Q4_K_M 在所有后端都最快
Q4_K_M 对所有模型质量损失都一样

最终仍需结合模型、硬件和任务实测。

9.5 越低比特不一定越快

原稿中的规律是:

档位越低,推理越快、体积越小、质量越低。

其中“体积通常越小”和“质量风险通常越高”大体成立,但“越低一定越快”不成立。

速度取决于:

  • 内存带宽;
  • 量化数据解码成本;
  • CPU 指令集;
  • GPU 内核是否优化;
  • 张量尺寸;
  • batch size;
  • prompt processing 还是 token generation;
  • 模型是否完全放入 GPU;
  • 是否发生 CPU/GPU 搬运;
  • 后端对某种量化格式的实现质量。

官方量化表里的速度本身就不是严格按 bits/weight 单调变化。例如某些更低比特格式的 prompt processing 或 text generation,并不一定比稍高比特格式更快。(GitHub)

所以正确结论是:

低比特通常能减少文件体积和权重内存流量,但真实推理速度必须在目标硬件、目标后端上测量。


十、量化成 Q4_K_M 及其他档位

10.1 最基础的 Q4_K_M 命令

如果使用 CPU 构建目录:

./build-cpu/bin/llama-quantize \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    Q4_K_M

如果使用 CUDA 构建目录,量化本身仍可调用对应目录中的工具:

./build-cuda/bin/llama-quantize \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    Q4_K_M

三个核心参数分别是:

输入高精度 GGUF
输出量化 GGUF
量化类型

官方当前的标准流程同样是先生成高质量 GGUF,再调用 llama-quantize 输出 Q4_K_M 等量化版本。(GitHub)

10.2 一次准备多个常用档位

./build-cpu/bin/llama-quantize \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q8_0.gguf \
    Q8_0

./build-cpu/bin/llama-quantize \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q6_k.gguf \
    Q6_K

./build-cpu/bin/llama-quantize \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q5_k_m.gguf \
    Q5_K_M

./build-cpu/bin/llama-quantize \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    Q4_K_M

./build-cpu/bin/llama-quantize \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q3_k_m.gguf \
    Q3_K_M

注意:每一个版本都从 high.gguf 开始。

不要这样做:

F16 → Q6_K → Q5_K_M → Q4_K_M → Q3_K_M

应该这样做:

             ┌→ Q8_0
             ├→ Q6_K
高精度 GGUF ─┼→ Q5_K_M
             ├→ Q4_K_M
             └→ Q3_K_M

10.3 用 Bash 循环批量量化

#!/usr/bin/env bash

set -euo pipefail

SRC="./models/gguf/qwen2.5-1.5b-instruct-high.gguf"
OUT_DIR="./models/gguf"
QUANTIZER="./build-cpu/bin/llama-quantize"

for QUANT in Q8_0 Q6_K Q5_K_M Q4_K_M Q3_K_M; do
    QUANT_LOWER="$(printf '%s' "$QUANT" | tr '[:upper:]' '[:lower:]')"
    OUTPUT="${OUT_DIR}/qwen2.5-1.5b-instruct-${QUANT_LOWER}.gguf"

    echo "========================================"
    echo "Quantizing to ${QUANT}"
    echo "Output: ${OUTPUT}"
    echo "========================================"

    "$QUANTIZER" "$SRC" "$OUTPUT" "$QUANT"
done

set -euo pipefail 可以让脚本在命令失败、变量未定义或管道出错时尽快停止,避免前一步失败后继续生成一堆误导性的结果。

10.4 查看量化文件大小

from pathlib import Path


model_dir = Path("./models/gguf")

files = sorted(
    model_dir.glob("qwen2.5-1.5b-instruct-*.gguf"),
    key=lambda path: path.stat().st_size,
    reverse=True,
)

print(f"{'文件':55s} {'MiB':>12s} {'GiB':>10s}")
print("-" * 82)

for path in files:
    size_bytes = path.stat().st_size
    size_mib = size_bytes / 1024**2
    size_gib = size_bytes / 1024**3

    print(f"{path.name:55s} {size_mib:12.1f} {size_gib:10.3f}")

不要把教程里的“典型输出”当成你的文件必须达到的精确数字。

实际大小会受以下因素影响:

  • 模型参数量;
  • tied embeddings;
  • 词表大小;
  • 某些张量是否保留高精度;
  • 量化器版本;
  • 架构专用处理;
  • 是否使用 Importance Matrix;
  • 是否分片;
  • GGUF 元数据。

10.5 不要急着删除高精度源

至少在以下检查完成前,不要删除:

qwen2.5-1.5b-instruct-high.gguf

你应先确认:

  • 所有目标量化版本都成功生成;
  • 文件校验和已经记录;
  • 至少一个量化版本能加载;
  • 聊天模板正常;
  • 速度和质量测试完成;
  • 没有打算继续生成其他档位。

高精度 GGUF 占空间,但它是你后续所有量化实验的干净基准。


十一、进阶量化:用 Importance Matrix 降低质量损失

第一次走流程时,可以直接使用默认 Q4_K_M,不必增加复杂度。

当你开始追求更低比特,或者希望在固定体积下尽量减少质量损失,就需要理解 Importance Matrix,也常简称为 imatrix

11.1 Importance Matrix 在做什么

不同权重对模型输出的影响并不相同。

最朴素的量化方法可能只根据权重自身的数值分布决定如何映射。但一个权重数值很大,不一定意味着它在实际推理中最重要;某些数值不大的权重,可能在特定激活分布下对输出产生明显影响。

Importance Matrix 会让模型在一组校准文本上运行,收集激活相关统计,再把这些信息用于量化优化。

它不是重新训练模型,也不修改模型能力边界。它做的是:

在必须舍弃一部分数值精度时,尽量把有限的表示能力留给更重要的位置。

官方工具提供 llama-imatrix 生成 Importance Matrix,并支持在 llama-quantize 中通过 --imatrix 使用它。(GitHub)

11.2 准备校准数据

例如准备:

calibration-data.txt

内容应尽量代表你的真实任务:

  • 中文对话;
  • 技术文档;
  • 代码;
  • 数学;
  • 英文;
  • 长文本;
  • 结构化输出。

如果模型主要用于中文客服,却只用英文百科生成 Importance Matrix,得到的统计未必最适合目标任务。

校准数据不必包含真实答案,但文本分布应具有代表性。

同时注意:

  • 不要使用未经处理的敏感用户数据;
  • 不要把测试集答案混进后续正式评测;
  • 不要只放几句高度重复的提示词;
  • 不要让校准集完全偏向一个非常窄的主题。

11.3 生成 Importance Matrix

./build-cpu/bin/llama-imatrix \
    -m ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    -f ./calibration-data.txt \
    -o ./models/gguf/qwen2.5-1.5b-imatrix.gguf \
    --no-ppl

参数含义:

-m

高精度 GGUF。

-f

校准文本。

-o

输出 Importance Matrix。

--no-ppl

不额外计算困惑度,专注收集统计。

如果已配置 GPU,可以根据本地 --help 加入 GPU offload 参数,加快处理。

11.4 使用 Importance Matrix 量化

./build-cpu/bin/llama-quantize \
    --imatrix ./models/gguf/qwen2.5-1.5b-imatrix.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-high.gguf \
    ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m-imatrix.gguf \
    Q4_K_M

然后与默认 Q4_K_M 比较:

qwen2.5-1.5b-instruct-q4_k_m.gguf
qwen2.5-1.5b-instruct-q4_k_m-imatrix.gguf

不要预设 imatrix 版本一定在所有测试上全面胜出。仍然要用固定任务集做比较。

11.5 哪些情况下更值得做

Importance Matrix 通常更适合:

  • Q3 或更激进的量化;
  • 模型本身对量化敏感;
  • 有明确垂直任务;
  • 需要批量发布高质量 GGUF;
  • 愿意投入时间做严格评测;
  • 希望在固定文件大小下进一步优化质量。

对于第一次本地运行一个 1.5B Q4_K_M,它不是必需步骤。


十二、如何估算文件大小、内存、显存和 KV Cache

“文件多大就占多少内存”是本地模型中最常见的误解之一。

运行时内存大致由以下部分组成:

模型权重
+ KV Cache
+ 计算缓冲区
+ 输入输出张量
+ tokenizer 与元数据
+ 后端运行时
+ GPU 驱动分配
+ 并发槽位
+ 操作系统与其他程序

12.1 权重体积的粗略估算

可使用:

权重字节数
≈ 参数量 × 有效 bits/weight ÷ 8

以 15.4 亿参数为例。

F16:

1.54 × 10⁹ × 16 ÷ 8
≈ 3.08 GB
≈ 2.87 GiB

假设 Q4_K_M 的有效权重成本接近官方示例中的 4.8944 bits/weight:

1.54 × 10⁹ × 4.8944 ÷ 8
≈ 0.94 GB
≈ 0.88 GiB

再加上:

  • tokenizer;
  • 元数据;
  • 对齐;
  • 混合量化;
  • 高精度张量;
  • 文件结构开销;

最终文件落在 1GB 左右并不奇怪。

这里用到的参数量来自 Qwen 官方模型卡,而 4.8944 bits/weight 来自 llama.cpp 针对另一架构给出的示例,所以只能作为估算,不能当成该 Qwen 文件的精确预测。(Hugging Face)


12.2 KV Cache 为什么会随着上下文增长

自回归生成时,模型需要保存之前 token 的 Key 和 Value,避免每生成一个新 token 都从头计算整个历史。

一个简化的 KV Cache 估算公式是:

KV 字节数
≈ 2
× 层数
× KV 头数
× 每个头的维度
× 上下文 token 数
× 每个元素字节数

最前面的 2 表示 Key 和 Value 两份缓存。

对于 Qwen2.5-1.5B:

层数 = 28
Query 头数 = 12
KV 头数 = 2
hidden size = 1536
head dimension = 1536 ÷ 12 = 128

若 KV Cache 使用 F16,每个元素约 2 字节。

4096 token 时,理论数量级约为:

2 × 28 × 2 × 128 × 4096 × 2
= 117,440,512 字节
≈ 112 MiB

32768 token 时:

≈ 896 MiB

这只是简化估算。实际分配还受:

  • 对齐;
  • batch;
  • 并发槽位;
  • 滑动窗口;
  • 后端布局;
  • 缓存类型;
  • 模型特殊结构;

等因素影响。

Qwen2.5-1.5B 的 KV 头数只有 2,GQA 显著降低了 KV Cache 成本。模型层数、隐藏维度和 KV 头数可在其官方配置中确认。(Hugging Face)

12.3 为什么不要一上来就把上下文设到最大

模型支持 32768 token,不等于每次启动都必须分配 32768。

如果实际只是短对话,先使用:

-c 4096

通常更合理。

上下文越大,可能带来:

  • 更大的 KV Cache;
  • 更长的 prompt processing 时间;
  • 更高的 GPU 内存压力;
  • 更低的并发能力;
  • 更高的首 token 延迟。

“模型最大上下文”是能力上限,不是日常推荐默认值。

当前 llama.cpp 的 -c--ctx-size 在设为 0 时可从模型元数据加载上下文配置;为了控制资源和保证实验一致,本文示例会显式设置 4096。(GitHub)

12.4 KV Cache 也可以量化

当前运行参数允许分别设置 K Cache 和 V Cache 类型,例如:

-ctk q8_0
-ctv q8_0

默认通常是 F16,并支持 q8_0、q4_0 等类型。(GitHub)

示例:

./build-cuda/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -c 16384 \
    -ctk q8_0 \
    -ctv q8_0 \
    -ngl all \
    -cnv

KV Cache 量化适合:

  • 长上下文;
  • 显存紧张;
  • 多并发;
  • 模型权重能放下,但 KV Cache 放不下。

代价可能包括:

  • 一定质量变化;
  • 后端兼容性差异;
  • 速度变化;
  • 某些组合不适合特定模型。

所以不要把权重量化和 KV Cache 量化混成同一个概念。

12.5 mmap 与实际内存占用

llama.cpp 默认通常会对模型使用内存映射。

这意味着:

  • 文件可以映射进虚拟地址空间;
  • 操作系统按需加载页面;
  • 进程的虚拟内存、常驻内存和模型文件大小可能不相等;
  • 多进程场景中,部分只读页面可能共享;
  • 磁盘缓存也会影响你在系统监控工具中看到的数字。

当前服务器参数中,--mmap 默认启用,也可使用 --no-mmap 关闭。(GitHub)

因此,观察内存时不要只盯一个指标。至少区分:

文件大小
进程虚拟内存
进程常驻内存
系统可用内存
GPU 已用显存
交换空间

十三、用 llama-cli 在 CPU 和 GPU 上运行

模型转换并量化后,终于进入推理阶段。

13.1 最简单的 CPU 单次生成

./build-cpu/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -ngl 0 \
    -p "请用通俗的语言解释什么是模型量化。" \
    -n 256

核心参数:

-m

模型路径。

-ngl 0

不把 Transformer 层放到 GPU。

-p

输入 prompt。

-n 256

最多生成 256 token。

不过,对于 Instruct 或 Chat 模型,直接传原始 -p 并不总是最佳方式,因为模型在训练时通常使用特定聊天模板。


13.2 推荐使用对话模式

./build-cpu/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -ngl 0 \
    -c 4096 \
    -cnv

当前 llama.cpp 对包含内置聊天模板的 GGUF,可以自动进入对话模式;如果没有自动识别,也可以手动使用 -cnv,必要时再通过 --chat-template 指定模板。(GitHub)

进入后:

> 你好,请用三句话解释 GGUF。

模型会按照它的聊天协议组织输入。

13.3 聊天模板为什么这么重要

一个聊天模型看到的真实文本,往往不是:

你好,请介绍你自己。

而是类似:

<特殊开始标记>
system
你是一个有帮助的助手
<特殊结束标记>
<特殊开始标记>
user
你好,请介绍你自己
<特殊结束标记>
<特殊开始标记>
assistant

不同模型的特殊 token 和排列方式不同。

如果模板错了,可能出现:

  • 重复用户问题;
  • 输出角色标签;
  • 直接结束;
  • 回答风格异常;
  • 乱码;
  • 无法停止;
  • 把 system prompt 当普通文本;
  • 多轮上下文混乱。

所以“模型能加载”不等于“聊天格式正确”。

如果输出明显异常,第一时间检查:

tokenizer.chat_template

而不是马上认定量化损坏了模型。

13.4 CPU 线程数

可以显式设置:

./build-cpu/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -ngl 0 \
    -c 4096 \
    -t 8 \
    -cnv

-t 8 表示使用 8 个线程。

线程越多也不一定越快。达到物理核心数附近后,继续增加线程可能因为:

  • 内存带宽饱和;
  • 调度开销;
  • 缓存竞争;
  • NUMA;
  • 超线程收益有限;

而不再提升 text generation。

最佳线程数应该通过 llama-bench 测量,而不是默认等于逻辑处理器总数。


13.5 NVIDIA、Metal 或其他 GPU 全量 offload

使用 CUDA 构建:

./build-cuda/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -c 4096 \
    -ngl all \
    -cnv

Apple Silicon:

./build-metal/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -c 4096 \
    -ngl all \
    -cnv

当前公共参数中,-ngl 可以接受:

具体层数
auto
all

当前默认值为 auto,并且运行时还提供自动适配设备内存的 --fit 行为。(GitHub)

这比过去常见的:

-ngl 99

更清晰。

老版本不认识 all 时,可以:

./build-cuda/bin/llama-cli --help

然后按照本地版本使用一个足够大的整数。

13.6 部分层放 GPU

显存不够时:

./build-cuda/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -c 4096 \
    -ngl 16 \
    -cnv

这表示最多把 16 层放到 GPU,其余留在 CPU。

它的价值是:

显存装不下整个模型
≠
完全不能使用 GPU

但部分 offload 的性能不一定按层数线性提升。

可能出现:

  • 前几层 offload 提升明显;
  • 达到某个层数后收益减小;
  • CPU 与 GPU 数据传输成为瓶颈;
  • KV Cache 位置影响性能;
  • 某个模型在全部 offload 后才有明显跃升。

仍然需要实测。

13.7 如何确认 GPU 真的在工作

不要只看命令里写了 -ngl all

启动日志应出现类似:

CUDA
Metal
Vulkan
HIP
offloaded ... layers
model buffer
compute buffer

NVIDIA 可以同时观察:

nvidia-smi

但需要注意:

  • 显存占用不等于 GPU 算力利用率;
  • Windows 任务管理器默认图表未必显示 CUDA Compute;
  • 小模型可能生成很快,监控采样看不到峰值;
  • prompt processing 和逐 token 生成的 GPU 利用模式不同。

最可靠的证据是:

  1. llama.cpp 加载日志;
  2. 后端设备列表;
  3. GPU 内存变化;
  4. -ngl 0 的基准速度对比。

13.8 设置可复现采样参数

为了比较量化版本,可以固定:

./build-cpu/bin/llama-cli \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -ngl 0 \
    -c 4096 \
    --seed 42 \
    --temp 0 \
    -p "解释什么是大语言模型。" \
    -n 256

temperature=0 更接近贪心解码,适合减少随机性。

但即使固定种子,也不应期待不同量化、不同后端、不同 batch 策略产生逐 token 完全相同的文本。浮点误差可能在后续采样中被放大。

13.9 不做转换时的快速运行路径

llama.cpp 当前可以直接通过 -hf 下载并运行兼容 GGUF:

./build-cpu/bin/llama-cli \
    -hf Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M

官方 Qwen GGUF 仓库提供 Q2_K、Q3_K_M、Q4_K_M、Q5_K_M、Q6_K、Q8_0 等版本。(Hugging Face)

这条路径非常适合:

  • 快速确认 llama.cpp 环境能否运行;
  • 对比自己转换的文件;
  • 不关心制作过程,只想使用现成 GGUF。

但它绕过了本文的核心学习目标:亲手完成转换和量化。


十四、启动 llama-server,提供本地 HTTP API

命令行交互适合测试。真正做应用时,更常见的是启动一个常驻服务。

14.1 启动本地服务

CPU:

./build-cpu/bin/llama-server \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    --alias qwen2.5-1.5b-local \
    --host 127.0.0.1 \
    --port 8080 \
    -c 4096 \
    -ngl 0

CUDA:

./build-cuda/bin/llama-server \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    --alias qwen2.5-1.5b-local \
    --host 127.0.0.1 \
    --port 8080 \
    -c 4096 \
    -ngl all

当前服务器默认监听 127.0.0.1:8080--alias 可以设置 API 中使用的模型名。(GitHub)

启动后,可以在浏览器打开:

http://127.0.0.1:8080

llama-server 提供基础 Web UI,并提供常用的 OpenAI 风格聊天接口:

/v1/chat/completions

官方 README 同样将 8080 作为默认端口示例。(GitHub)

原稿使用 11434 并没有技术错误,因为端口可以自定义。但 11434 经常与 Ollama 联系在一起,为了减少混淆,本文使用 llama-server 默认的 8080。

14.2 检查健康状态

curl http://127.0.0.1:8080/health

模型加载完成后应返回类似:

{
  "status": "ok"
}

服务器的 /health 端点是公开端点,即使设置了 API Key,也不会对该健康检查执行相同认证。(GitHub)

14.3 调用聊天接口

curl http://127.0.0.1:8080/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
      "model": "qwen2.5-1.5b-local",
      "messages": [
        {
          "role": "system",
          "content": "你是一个擅长解释技术概念的中文助手。"
        },
        {
          "role": "user",
          "content": "请用三个要点解释 GGUF。"
        }
      ],
      "temperature": 0.2,
      "max_tokens": 256
    }'

流式返回:

curl http://127.0.0.1:8080/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
      "model": "qwen2.5-1.5b-local",
      "messages": [
        {
          "role": "user",
          "content": "解释模型量化。"
        }
      ],
      "stream": true,
      "max_tokens": 256
    }'

14.4 不要轻易监听 0.0.0.0

下面这条命令会让服务监听所有网络接口:

--host 0.0.0.0

这可能使同一局域网甚至外部网络中的设备访问你的模型服务。

如果确实需要远程访问,至少设置 API Key:

./build-cuda/bin/llama-server \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    --alias qwen2.5-1.5b-local \
    --host 0.0.0.0 \
    --port 8080 \
    --api-key "请替换为足够长的随机字符串" \
    -c 4096 \
    -ngl all

调用时加入:

-H "Authorization: Bearer 请替换为足够长的随机字符串"

llama-server 当前支持 --api-key 和 API Key 文件。(GitHub)

但 API Key 不是完整的公网安全方案。真正暴露到公网时还应考虑:

  • TLS;
  • 防火墙;
  • 反向代理;
  • 请求限流;
  • 身份认证;
  • 日志脱敏;
  • 输入长度限制;
  • 文件上传策略;
  • 服务隔离。

最安全的入门设置仍是:

--host 127.0.0.1

14.5 并发与上下文预算

例如:

./build-cuda/bin/llama-server \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    --alias qwen2.5-1.5b-local \
    -c 16384 \
    -np 4 \
    -ngl all

可以理解为给多个并发槽位准备总上下文预算。

并发增加会消耗更多:

  • KV Cache;
  • 计算缓冲区;
  • 显存;
  • 调度资源。

单请求速度快,不代表四并发仍然能保持同样的 token/s。服务部署必须分别评估:

单请求延迟
首 token 延迟
总吞吐量
并发吞吐量
尾延迟
最大稳定上下文

十五、用 Python 调用本地模型

有两种主流方式:

Python → HTTP → llama-server

Python → llama-cpp-python → GGUF

前者适合服务化和多语言客户端,后者适合单进程嵌入。


15.1 通过 llama-server 调用

安装客户端:

python -m pip install openai

Python:

from openai import OpenAI


client = OpenAI(
    base_url="http://127.0.0.1:8080/v1",
    api_key="local-only",
)

response = client.chat.completions.create(
    model="qwen2.5-1.5b-local",
    messages=[
        {
            "role": "system",
            "content": "你是一个严谨、简洁的中文技术助手。",
        },
        {
            "role": "user",
            "content": "用三句话说明 GGUF 与 llama.cpp 的关系。",
        },
    ],
    temperature=0.2,
    max_tokens=256,
)

content = response.choices[0].message.content
print(content)

这里的 model 应与启动服务器时设置的 --alias 对应:

qwen2.5-1.5b-local

不要依赖“随便填一个字符串也能运行”的偶然行为。明确设置 alias 更利于:

  • 日志分析;
  • 多模型路由;
  • 客户端配置;
  • 后续迁移;
  • 避免模型名不一致。

llama-server 提供 OpenAI 风格的兼容接口,但“兼容”不代表与任何云端实现的每个扩展参数、每种工具调用行为都完全相同。迁移时仍应测试使用到的具体功能。

15.2 流式调用

from openai import OpenAI


client = OpenAI(
    base_url="http://127.0.0.1:8080/v1",
    api_key="local-only",
)

stream = client.chat.completions.create(
    model="qwen2.5-1.5b-local",
    messages=[
        {
            "role": "user",
            "content": "从零解释模型量化,控制在 300 字以内。",
        }
    ],
    temperature=0.3,
    max_tokens=300,
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)

print()

15.3 使用 llama-cpp-python 直接加载

安装最基础版本:

python -m pip install llama-cpp-python

但这里有一个非常重要的坑:

pip install llama-cpp-python 成功,不代表一定启用了 GPU。

很多情况下,普通安装会在本机编译一个 CPU 版本。

CUDA 安装

Linux 或 macOS 风格的 Shell:

CMAKE_ARGS="-DGGML_CUDA=on" \
python -m pip install \
    --upgrade \
    --force-reinstall \
    --no-cache-dir \
    llama-cpp-python

Windows PowerShell:

$env:CMAKE_ARGS="-DGGML_CUDA=on"

python -m pip install `
    --upgrade `
    --force-reinstall `
    --no-cache-dir `
    llama-cpp-python

Metal 安装

CMAKE_ARGS="-DGGML_METAL=on" \
python -m pip install \
    --upgrade \
    --force-reinstall \
    --no-cache-dir \
    llama-cpp-python

HIP 安装

CMAKE_ARGS="-DGGML_HIP=on" \
python -m pip install \
    --upgrade \
    --force-reinstall \
    --no-cache-dir \
    llama-cpp-python

Vulkan 安装

CMAKE_ARGS="-DGGML_VULKAN=on" \
python -m pip install \
    --upgrade \
    --force-reinstall \
    --no-cache-dir \
    llama-cpp-python

这些后端开关来自 llama-cpp-python 当前官方安装说明。项目也提供部分预编译 wheel,但可用的 Python、CUDA、macOS 和硬件版本范围会变化,安装时应以官方 README 为准。(GitHub)

15.4 直接加载 GGUF

from pathlib import Path

from llama_cpp import Llama


model_path = Path(
    "./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf"
)

if not model_path.is_file():
    raise FileNotFoundError(f"找不到模型文件: {model_path}")

llm = Llama(
    model_path=str(model_path),
    n_ctx=4096,
    n_gpu_layers=-1,  # GPU 全量 offload;纯 CPU 改成 0
    seed=42,
    verbose=True,
)

response = llm.create_chat_completion(
    messages=[
        {
            "role": "system",
            "content": "你是一个擅长解释本地大模型的中文助手。",
        },
        {
            "role": "user",
            "content": "什么是模型量化?请简短回答。",
        },
    ],
    temperature=0.2,
    max_tokens=200,
)

print(response["choices"][0]["message"]["content"])

在 llama-cpp-python 中:

n_gpu_layers=0

表示不进行 GPU 层 offload。

n_gpu_layers=-1

通常表示尽可能把所有层放到 GPU。官方示例也使用 -1 表示 GPU 加速。(GitHub)

15.5 第一次运行保留 verbose=True

不要一开始就:

verbose=False

第一次运行时,日志可以告诉你:

  • 加载了哪个后端;
  • offload 了多少层;
  • 使用什么 chat format;
  • 上下文大小;
  • KV Cache 大小;
  • 模型张量类型;
  • 是否真的使用 GPU。

确认一切正常后,再关闭详细日志。

15.6 llama-cpp-python 如何选择聊天模板

当前高层 Chat API 按以下优先级决定格式:

  1. 显式传入的 chat_handler
  2. 显式传入的 chat_format
  3. GGUF 元数据中的 tokenizer.chat_template
  4. 回退到默认格式。

设置 verbose=True 可以看到最终选择的聊天格式。(GitHub)

这说明一个正确转换的现代 GGUF,不只是“装了权重”,还应尽量保留 tokenizer 和聊天模板。

15.7 不要无缘无故手动指定 chat_format

如果 GGUF 已经正确包含 Qwen 聊天模板,通常不需要再写:

chat_format="chatml"

手动覆盖可能反而使格式与模型训练时不一致。

只有在以下情况下才考虑显式指定:

  • GGUF 缺少聊天模板;
  • 日志显示识别错误;
  • 使用的是旧 GGUF;
  • 模型卡明确要求特定格式;
  • 经过固定测试确认手动格式正确。

十六、正确测试不同量化档位的速度

原稿中的 Python 基准代码有一个常见问题:

start = time.time()
llm = Llama(...)
out = llm(...)

如果把模型加载也放进计时,就测到的是:

文件读取
+ 内存映射
+ 模型初始化
+ 后端初始化
+ 第一次运行预热
+ prompt processing
+ token generation

这个结果不能代表纯推理速度。

此外,不同模型可能提前输出 EOS,实际生成 token 数不同;一次运行也可能受到系统负载、温度、缓存和后台进程干扰。

llama.cpp 已经提供了专门的 llama-bench,应优先使用它。


16.1 区分 prompt processing 与 text generation

大模型推理包含两个性能阶段。

Prompt Processing

把已有输入一次性送进模型:

用户输入 1000 token
→ 模型处理这 1000 token
→ 建立 KV Cache

一般用:

pp

表示。

Text Generation

模型逐 token 生成新内容:

生成第 1 个 token
生成第 2 个 token
生成第 3 个 token
...

一般用:

tg

表示。

两者的硬件瓶颈可能完全不同。

一个模型可能:

  • prompt processing 很快;
  • 单 token 生成一般;

也可能相反。

llama-bench 会分别测量 pp、tg 或两者组合,并给出多次重复的平均 token/s 和标准差;其测量不包含 tokenizer 和采样时间。(GitHub)

16.2 CPU 基准

./build-cpu/bin/llama-bench \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q3_k_m.gguf \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q5_k_m.gguf \
    -p 512 \
    -n 128 \
    -r 5 \
    -ngl 0 \
    -o md

含义:

-p 512

测试处理 512 个 prompt token。

-n 128

测试生成 128 个 token。

-r 5

每组重复 5 次。

-ngl 0

纯 CPU。

-o md

Markdown 格式输出。

保存结果:

./build-cpu/bin/llama-bench \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q3_k_m.gguf \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q5_k_m.gguf \
    -p 512 \
    -n 128 \
    -r 5 \
    -ngl 0 \
    -o md \
    > benchmarks/cpu-quant-comparison.md

16.3 GPU 基准

./build-cuda/bin/llama-bench \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q3_k_m.gguf \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q5_k_m.gguf \
    -p 512 \
    -n 128 \
    -r 5 \
    -ngl -1 \
    -o md

llama-bench 的传统整数参数中,-ngl -1 常用于全部 offload。具体仍以当前工具的 --help 为准。

16.4 测试不同线程数

./build-cpu/bin/llama-bench \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -p 512 \
    -n 128 \
    -t 1,2,4,8,12,16 \
    -ngl 0 \
    -r 5

你可能看到:

  • pp 在更多线程下继续提升;
  • tg 到某个线程数后停止提升;
  • 线程过多反而下降。

不要根据 CPU 的“逻辑核心数”直接决定最佳 -t

16.5 测试不同 GPU offload 层数

./build-cuda/bin/llama-bench \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -p 512 \
    -n 128 \
    -ngl 0,4,8,12,16,20,24,28,-1 \
    -r 5

这能回答一个很实际的问题:

我的显存不足以全部放入模型时,offload 多少层最划算?

16.6 一份可信基准至少要记录什么

建议同时记录:

模型 SHA256
GGUF 量化类型
llama.cpp commit
编译后端
编译参数
CPU 型号
内存规格
GPU 型号
显存
驱动版本
操作系统
线程数
上下文设置
GPU offload 层数
KV Cache 类型
Flash Attention 设置
测试 pp/tg 长度
重复次数
平均值
标准差

如果只说:

Q4 跑了 20 tokens/s

这个数据几乎无法复现,也无法与别人公平比较。


十七、如何评估量化后的质量损失

速度容易测,质量更难测。

看一两个聊天回答,很容易受到随机性、措辞偏好和主观印象影响。

比较量化质量,建议分三层。


17.1 第一层:固定提示词人工检查

准备一个固定任务集:

01_basic_knowledge
02_chinese_summary
03_instruction_following
04_json_output
05_code_generation
06_math_reasoning
07_long_context
08_information_extraction
09_refusal_boundary
10_multiturn_chat

例如:

{"id":"zh_summary_01","prompt":"请把下面内容压缩成三个要点……"}
{"id":"json_01","prompt":"只输出合法 JSON,字段为 name、age……"}
{"id":"math_01","prompt":"逐步计算……"}
{"id":"code_01","prompt":"写一个带类型注解的 Python 函数……"}

保持:

  • 相同 prompt;
  • 相同 system message;
  • 相同聊天模板;
  • 相同上下文;
  • 相同采样参数;
  • 相同最大输出长度;
  • 相同 stop 条件;
  • 尽量固定 seed。

然后同时比较:

高精度
Q8_0
Q6_K
Q5_K_M
Q4_K_M
Q3_K_M

关注的不是“文字是否完全一样”,而是:

  • 事实正确性;
  • 指令遵循;
  • 结构完整性;
  • 格式合法性;
  • 推理步骤;
  • 中文流畅度;
  • 是否漏答;
  • 是否重复;
  • 是否提前结束;
  • 是否出现异常 token。

17.2 第二层:任务指标

针对具体应用设置可计算指标。

信息抽取

Precision
Recall
F1
字段完整率
JSON 解析成功率

分类

Accuracy
Macro-F1
混淆矩阵

代码

单元测试通过率
语法通过率
静态检查结果
执行超时率

结构化输出

JSON Schema 通过率
缺失字段率
额外字段率
数据类型错误率

摘要

可以结合:

关键信息覆盖
事实一致性
压缩率
人工评分

模型回答更“好看”不等于任务指标更高。

17.3 第三层:Perplexity

llama.cpp 提供:

./build-cpu/bin/llama-perplexity \
    -m ./models/gguf/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -f ./evaluation-corpus.txt

困惑度衡量模型预测下一个 token 的能力,通常越低越好。

它特别适合比较:

同一个基础模型
同一个 tokenizer
不同量化版本

不适合直接拿来比较:

不同 tokenizer 的模型
不同架构
不同微调模型
不同数据处理方式

官方文档也明确提醒,Perplexity 尤其适合判断同一模型从 FP16 到量化版本的损失;不同模型之间的值通常不能直接横向比较,而且微调模型即使人工质量更好,也可能有更高的困惑度。(GitHub)

17.4 KL Divergence

更严格的比较可以记录高精度模型 logits,再计算量化模型与高精度模型输出分布之间的 KL Divergence。

直观上:

KL 越接近 0
→ 两个输出概率分布越相似

但完整保存 logits 可能占用大量磁盘。官方文档举例说明,在标准评测语料上记录全量 logits,文件可能达到数十 GiB。(GitHub)

这更适合:

  • 量化发布者;
  • 研究人员;
  • 大规模模型评测;
  • 需要严格排序多个量化方案的场景。

普通用户先做好任务集评测和 Perplexity,通常已经足够。

17.5 为什么不能只比较一次回答

即使两个量化版本整体质量非常接近,也可能在某个 token 上出现微小概率差异。

一旦采样选中了不同 token,后续上下文就发生分叉,最终答案可能看起来完全不同。

因此:

答案不同
≠
某一个一定坏了

更合理的方式是:

  • 多题;
  • 多次运行;
  • 固定采样;
  • 看整体任务指标;
  • 对关键失败案例人工复核。

十八、常见报错、误区与系统化排障


18.1 误区:转换就是把文件合并成一个 GGUF

转换还包含:

  • 张量名映射;
  • 架构处理;
  • tokenizer 转换;
  • 元数据写入;
  • 数据类型转换;
  • 聊天模板写入;
  • 张量对齐。

因此,不能用通用文件合并工具替代转换器。


18.2 误区:中间文件必须是 FP16

更准确的说法是:

中间文件应尽量是高质量 GGUF,通常为 F16、BF16 或 AUTO 选择的 16 bit 类型。

对于原始 BF16 模型,强行说“必须先转成 FP16”没有必要。


18.3 误区:可以从 Q5 再量化成 Q4

技术上某些情况下可通过 --allow-requantize 执行,但官方明确警告这可能比从 16/32 bit 源开始产生严重得多的质量损失。(GitHub)

正确做法:

每个档位都从高精度源开始。

18.4 报错:unsupported architecture

排查顺序:

1. 查看 config.json 中的 model_type 和 architectures
2. 更新 llama.cpp
3. 重新安装 requirements
4. 重新执行 --print-supported-models
5. 搜索该架构是否已有推理实现
6. 确认下载的不是自定义魔改架构

不要只修改 config.json,把一个未知架构伪装成已知架构。名称相似不代表计算图相同。


18.5 报错:找不到 safetensors 或分片

检查:

ls -lah ./models/hf/Qwen2.5-1.5B-Instruct

确认:

  • 所有分片都下载完成;
  • model.safetensors.index.json 存在;
  • 没有 .incomplete 临时文件;
  • 下载过程没有中断;
  • 目录没有指错一层。

例如正确目录:

models/hf/Qwen2.5-1.5B-Instruct/config.json

错误目录可能是:

models/hf/Qwen2.5-1.5B-Instruct/Qwen2.5-1.5B-Instruct/config.json

18.6 报错:No space left on device

先查看:

df -h

不要只检查模型输出目录,还要检查:

  • Hugging Face 缓存所在分区;
  • /tmp
  • Python 临时目录;
  • Docker 虚拟磁盘;
  • WSL 虚拟磁盘;
  • 系统盘。

删除文件前确认自己是否还要保留:

原始 HF 权重
高精度 GGUF
旧量化版本
下载缓存

18.7 报错:转换或量化时系统被杀死

Linux 出现单独一行:

Killed

常见原因是 OOM Killer。

检查:

dmesg | tail

或:

journalctl -k | tail

解决方向:

  • 换更小模型验证流程;
  • 增加 RAM;
  • 增加交换空间;
  • 关闭其他占内存程序;
  • 使用临时文件选项;
  • 使用分片;
  • 不要同时量化多个大型模型。

18.8 误区:GGUF 文件 5GB,5GB 内存就一定能跑

运行时还需要:

  • KV Cache;
  • 计算缓冲区;
  • 后端分配;
  • 操作系统;
  • 并发请求;
  • 输入输出;
  • GPU 驱动内存。

而且系统发生交换后,“能启动”也不等于“可用”。

更合理的要求是:

模型加载后仍有足够余量容纳目标上下文和系统运行,而不是刚好把可用内存填满。


18.9 报错:GPU 显存不足

先缩小上下文:

-c 4096

再减少 GPU 层:

-ngl 20
-ngl 16
-ngl 12

或者尝试:

-ngl auto

还可以:

  • 使用更低量化;
  • 量化 KV Cache;
  • 降低 batch;
  • 降低并发槽位;
  • 关闭其他占显存程序;
  • 在多 GPU 间切分。

当前运行时还提供 --fit,可以自动调整未显式设置的参数以适配设备内存,并预留默认目标余量。(GitHub)

但生产基准中应记录最终实际参数,不要只写“用了 auto”。


18.10 误区:-ngl 越大一定越好

GPU 层数受显存和后端行为约束。

如果模型已经全部 offload,再把数字从 99 改成 999 不会产生更多层。

如果显存不足,过高设置可能导致:

  • OOM;
  • 加载失败;
  • 自动回退或调整;
  • 上下文被压缩;
  • 运行时余量不足;
  • 长对话中途失败。

当前版本可以更清晰地使用:

-ngl auto
-ngl all
-ngl 具体数字

而不是所有场景都照抄 -ngl 99。(GitHub)


18.11 模型能加载,但回答质量异常

优先检查:

1. 是否用了 Instruct/Chat 模型
2. 聊天模板是否正确
3. tokenizer 是否完整
4. system/user/assistant 角色是否正确
5. stop token 是否正确
6. 是否使用了过激量化
7. 上下文是否被截断
8. 模型本身是否太小

尤其要区分:

模型能力不足

和:

量化导致能力下降

一个 1.5B 模型本身就不擅长复杂推理。Q8 也不会把它变成 70B 模型。


18.12 输出特殊标记或角色名

例如模型输出:

<|im_start|>
assistant

常见原因:

  • 聊天模板未应用;
  • 模板与 tokenizer 不匹配;
  • 使用原始 completion 模式调用 chat 模型;
  • stop token 缺失;
  • 手动指定了错误的 chat_format

先查看 GGUF 中是否包含:

tokenizer.chat_template

再观察 verbose 日志实际用了什么格式。


18.13 一运行就结束,没有输出

可能原因:

  • prompt 已包含错误的结束 token;
  • 模板重复添加 EOS;
  • 模型认为回答已经完成;
  • -n 设置为 0;
  • 上下文没有剩余空间;
  • stop 字符串过于宽泛;
  • tokenizer 转换错误。

可以先用非常简单的输入和默认采样进行测试。


18.14 Python 中设置 n_gpu_layers=-1,仍然只用 CPU

首先确认安装的 llama-cpp-python 是否真的包含 GPU 后端。

重新安装时使用对应 CMake 开关:

CMAKE_ARGS="-DGGML_CUDA=on" \
python -m pip install \
    --force-reinstall \
    --no-cache-dir \
    llama-cpp-python

然后:

verbose=True

查看初始化日志。

不要根据:

代码里写了 n_gpu_layers=-1

就认定 GPU 已启用。


18.15 llama-cli 用 GPU,Python 却不用 GPU

这是因为它们是两个不同构建。

build-cuda/bin/llama-cli

可能启用了 CUDA。

但:

pip install llama-cpp-python

可能安装的是 CPU 版 Python 扩展。

C++ 命令行工具能用 GPU,不代表 Python 包自动继承那个构建。


18.16 低比特模型反而更慢

这不一定是错误。

可能原因:

  • 对应量化内核未充分优化;
  • 反量化开销更高;
  • CPU 不支持某些指令;
  • GPU 对另一种格式更友好;
  • 模型太小,启动或调度开销占比高;
  • prompt processing 和 tg 表现不同;
  • 内存带宽并不是当前瓶颈;
  • 测试混入了模型加载时间。

使用 llama-bench 分别测 pp 和 tg,而不是凭一次聊天体验判断。


18.17 Q3 输出差很多,是不是量化器坏了

不一定。

越激进的量化对以下模型更敏感:

  • 参数量很小的模型;
  • 数学和代码模型;
  • 多语言能力较弱的模型;
  • 特殊 MoE;
  • 已经进行过其他压缩的模型;
  • 对少数关键张量特别敏感的模型。

解决方向:

  • 回到 Q4_K_M;
  • 使用 Q5_K_M;
  • 使用 Importance Matrix;
  • 确认源文件是高精度;
  • 检查是否错误 requantize;
  • 用任务集验证,而不是只看一个回答。

18.18 误区:GGUF 只适合 CPU,GPU 服务端不该用

GGUF 与 llama.cpp 不只支持 CPU。

当前项目支持 CUDA、Metal、HIP、Vulkan、SYCL 等多种后端,以及 CPU/GPU 混合推理。(GitHub)

真正应该比较的是:

具体模型
具体量化
具体硬件
具体并发
具体上下文
具体推理引擎

AWQ、GPTQ、GGUF 各有适合的生态和内核。不能脱离部署场景,只按格式名称宣布谁一定更快。


18.19 误区:API 兼容就等于所有参数完全相同

llama-server 提供 OpenAI 风格接口,这让现有客户端迁移更容易。

但以下能力仍可能存在实现差异:

  • 工具调用;
  • JSON Schema;
  • 多模态消息;
  • token 统计;
  • logprobs;
  • stop 行为;
  • 流式事件;
  • 错误格式;
  • 并发与缓存;
  • 模型名校验。

正式应用应为自己实际使用的接口写集成测试。


十九、一份可直接执行的完整脚本

下面是一份面向 Linux、macOS 或 WSL 的基础脚本。

默认构建 CPU 版本。使用 CUDA 时,把 CMake 配置部分改成 -DGGML_CUDA=ON

保存为:

build_qwen_gguf.sh

内容:

#!/usr/bin/env bash

set -euo pipefail

MODEL_ID="Qwen/Qwen2.5-1.5B-Instruct"
MODEL_NAME="qwen2.5-1.5b-instruct"

BUILD_DIR="./build-cpu"
HF_DIR="./models/hf/Qwen2.5-1.5B-Instruct"
GGUF_DIR="./models/gguf"
LOG_DIR="./logs"

HIGH_GGUF="${GGUF_DIR}/${MODEL_NAME}-high.gguf"

mkdir -p "$HF_DIR"
mkdir -p "$GGUF_DIR"
mkdir -p "$LOG_DIR"

echo "========================================"
echo "1. 记录 llama.cpp 提交"
echo "========================================"

git rev-parse HEAD | tee "${LOG_DIR}/llama-cpp-commit.txt"

echo "========================================"
echo "2. 创建 Python 虚拟环境"
echo "========================================"

if [[ ! -d ".venv" ]]; then
    python3 -m venv .venv
fi

# shellcheck disable=SC1091
source .venv/bin/activate

python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install --upgrade huggingface_hub gguf

echo "========================================"
echo "3. 编译 CPU 版本"
echo "========================================"

cmake -B "$BUILD_DIR"
cmake --build "$BUILD_DIR" --config Release -j

LLAMA_CLI="${BUILD_DIR}/bin/llama-cli"
QUANTIZER="${BUILD_DIR}/bin/llama-quantize"

if [[ ! -x "$LLAMA_CLI" ]]; then
    echo "找不到 llama-cli: $LLAMA_CLI" >&2
    echo "Windows 多配置构建可能位于 bin/Release。" >&2
    exit 1
fi

if [[ ! -x "$QUANTIZER" ]]; then
    echo "找不到 llama-quantize: $QUANTIZER" >&2
    exit 1
fi

echo "========================================"
echo "4. 下载 Hugging Face 模型"
echo "========================================"

hf download "$MODEL_ID" \
    --local-dir "$HF_DIR"

echo "========================================"
echo "5. 检查模型支持列表"
echo "========================================"

python convert_hf_to_gguf.py \
    --print-supported-models \
    > "${LOG_DIR}/supported-models.txt" \
    2>&1

echo "========================================"
echo "6. 转换为高精度 GGUF"
echo "========================================"

python convert_hf_to_gguf.py \
    "$HF_DIR" \
    --outfile "$HIGH_GGUF" \
    --outtype auto

if [[ ! -f "$HIGH_GGUF" ]]; then
    echo "高精度 GGUF 未生成: $HIGH_GGUF" >&2
    exit 1
fi

echo "========================================"
echo "7. 查看高精度 GGUF"
echo "========================================"

ls -lh "$HIGH_GGUF"

python -m gguf.scripts.gguf_dump \
    "$HIGH_GGUF" \
    > "${LOG_DIR}/${MODEL_NAME}-high-dump.txt"

echo "========================================"
echo "8. 量化多个档位"
echo "========================================"

for QUANT in Q8_0 Q6_K Q5_K_M Q4_K_M Q3_K_M; do
    QUANT_LOWER="$(
        printf '%s' "$QUANT" |
        tr '[:upper:]' '[:lower:]'
    )"

    OUTPUT="${GGUF_DIR}/${MODEL_NAME}-${QUANT_LOWER}.gguf"

    echo "----------------------------------------"
    echo "Quant:  $QUANT"
    echo "Output: $OUTPUT"
    echo "----------------------------------------"

    "$QUANTIZER" \
        "$HIGH_GGUF" \
        "$OUTPUT" \
        "$QUANT"
done

echo "========================================"
echo "9. 记录文件大小"
echo "========================================"

ls -lh "${GGUF_DIR}/${MODEL_NAME}"*.gguf |
    tee "${LOG_DIR}/gguf-sizes.txt"

echo "========================================"
echo "10. 生成 SHA256"
echo "========================================"

if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "${GGUF_DIR}/${MODEL_NAME}"*.gguf |
        tee "${LOG_DIR}/gguf-sha256.txt"
elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "${GGUF_DIR}/${MODEL_NAME}"*.gguf |
        tee "${LOG_DIR}/gguf-sha256.txt"
else
    echo "未找到 sha256sum 或 shasum,跳过校验和。"
fi

echo "========================================"
echo "11. Q4_K_M 加载测试"
echo "========================================"

"$LLAMA_CLI" \
    -m "${GGUF_DIR}/${MODEL_NAME}-q4_k_m.gguf" \
    -ngl 0 \
    -c 4096 \
    --seed 42 \
    --temp 0 \
    -p "请用一句话解释 GGUF。" \
    -n 64

echo
echo "========================================"
echo "全部完成"
echo "========================================"
echo "推荐入门模型:"
echo "${GGUF_DIR}/${MODEL_NAME}-q4_k_m.gguf"

增加执行权限:

chmod +x build_qwen_gguf.sh

执行:

./build_qwen_gguf.sh

CUDA 版本把:

BUILD_DIR="./build-cpu"

改为:

BUILD_DIR="./build-cuda"

并把:

cmake -B "$BUILD_DIR"

改为:

cmake -B "$BUILD_DIR" -DGGML_CUDA=ON

最后加载测试可以把:

-ngl 0

改为:

-ngl all

这份脚本的目标不是覆盖所有架构,而是提供一条具有错误检查、日志记录和校验和的基础流水线。


二十、量化档位到底该怎么选

没有一个档位能脱离场景成为永远正确的答案。

可以先按目标分类。

20.1 质量优先

优先考虑:

Q8_0
Q6_K
Q5_K_M

适合:

  • 内存充足;
  • 数学、代码、复杂推理;
  • 对格式稳定性要求高;
  • 希望尽量接近高精度;
  • 模型较小,没必要过度压缩;
  • 需要建立质量基线。

其中 Q8_0 常用于接近高精度的对照,但它仍然不是 F16 的完全等价物。

20.2 综合平衡

优先考虑:

Q4_K_M

适合:

  • 第一次下载或量化;
  • 本地聊天;
  • 一般摘要与问答;
  • 想兼顾文件大小和质量;
  • 不确定该选什么;
  • 希望多个后端都有成熟支持。

它是合理默认值,不是绝对真理。

20.3 内存比较紧张

考虑:

Q4_K_S
Q3_K_M

适合:

  • 更大模型刚好放不下;
  • 设备内存有限;
  • 能接受一定质量下降;
  • 任务相对简单;
  • 已经做过实测。

在一个更大模型的 Q3 和一个更小模型的 Q5 之间,谁更好没有统一答案。

有时:

更大模型 + 更激进量化

仍胜过:

更小模型 + 高精度量化

但在数学、代码、结构化输出等敏感任务上,也可能相反。

20.4 极限压缩

例如:

Q2_K
更低比特 IQ 系列

适合:

  • 没有更高档位能放入设备;
  • 明确知道质量代价;
  • 使用 Importance Matrix;
  • 有完整任务评测;
  • 主要目标是“能运行”。

不应仅因为它最小,就把它当成默认下载项。

20.5 一个更科学的选择流程

不要先问:

哪个量化最好?

而是按下面顺序决策。

第一步:确定可用资源

测量:

系统可用 RAM
可用 VRAM 或统一内存
目标上下文
并发数
操作系统需要的余量

第二步:筛掉放不下的版本

使用:

权重文件
+ KV Cache
+ 计算缓冲区
+ 系统余量

而不是只看 GGUF 文件大小。

第三步:在能放下的档位中选择质量最高者

例如都能运行:

Q4_K_M
Q5_K_M
Q6_K

先测 Q6、Q5 是否满足速度要求,而不是条件反射选择 Q4。

第四步:用 llama-bench 测性能

分别测:

pp
tg
目标上下文深度
CPU/GPU offload
线程数

第五步:用任务集测质量

特别关注:

你的真实任务
你的主要语言
你的输出格式
你的长上下文长度
你的错误成本

第六步:选择满足要求的最小版本

最终目标不是:

文件越小越好

而是:

在质量、速度、内存和部署成本都满足要求的前提下,选择最小且最稳定的版本。

这才是真正工程化的量化选择。


二十一、小结

这一篇,我们完成了从 Hugging Face 到本地 GGUF 推理的完整闭环。

整条流程可以压缩成:

下载原始模型
    ↓
确认架构与文件完整性
    ↓
转换成高精度 GGUF
    ↓
检查元数据与实际加载
    ↓
从高精度源量化多个档位
    ↓
用 llama-cli 本地运行
    ↓
用 llama-server 提供 API
    ↓
用 Python 集成
    ↓
分别评估速度、内存和质量

最重要的结论有十个。

第一,转换不是简单改后缀。

它需要理解模型架构、映射张量、转换 tokenizer、写入元数据和聊天模板。

第二,中间文件不必机械限定为 FP16。

更准确的做法是生成高质量 GGUF,通常使用 F16、BF16 或 --outtype auto

第三,所有量化版本都应从高精度源生成。

不要把 Q5 再量化成 Q4,更不要把来路不明的低比特权重当作干净量化源。

第四,Q4_K_M 不等于平均每个权重刚好 4 bit。

它是混合量化方案,真实有效 bits/weight 会高于名字中的整数。

第五,Q4_K_M 是合理默认值,不是宇宙最优解。

质量优先可以尝试 Q5_K_M、Q6_K;内存紧张可以尝试 Q4_K_S、Q3_K_M。

第六,低比特不保证更快。

真实速度由硬件、后端、内核、模型结构和测试阶段共同决定,必须使用 llama-bench 实测。

第七,GGUF 文件大小不等于运行内存。

还要计算 KV Cache、计算缓冲区、并发槽位、后端分配和系统余量。

第八,聊天模板与权重同样重要。

模型能加载却不会正常聊天,很多时候不是量化坏了,而是模板、tokenizer 或角色格式出了问题。

第九,GPU 加速必须同时满足编译和运行两个条件。

C++ 工具启用 CUDA,不代表 llama-cpp-python 自动启用 CUDA;写了 -ngl all,也不代表日志里真的完成了 GPU offload。

第十,量化选择必须以任务为中心。

真正科学的顺序是:

先看能否放下
再看速度是否达标
再看质量是否达标
最后选择满足要求的最小版本

走到这里,你已经不只是“下载了一个别人做好的 GGUF”,而是掌握了完整的本地模型制作链:

Hugging Face
→ 高精度 GGUF
→ 低比特量化
→ CPU/GPU 推理
→ HTTP 服务
→ Python 应用
→ 性能与质量评估

这套能力真正重要的地方,不是省下了几 GB 硬盘,而是你开始拥有对本地模型部署过程的控制权:模型从哪里来、采用什么精度、占用多少资源、如何运行、如何验证、质量损失能否接受,都不再是一个黑盒。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐