第25章 本地运行 DeepSeek-R1 动态 1.58-bit 量化指南
动态 1.58-bit 2025 年 1 月 27 日 • 作者 Daniel & Michael2025 年 1 月 27 日• 作者 Daniel & Michael DeepSeek-R1 近期反响巨大,它在完全开源的情况下,其推理能力比肩 OpenAI 的 O1 模型。我们探索了如何让更多本地用户能够运行该模型,并成功将 DeepSeek R1 671B 参数模型量化至 131GB,体积较原始 720GB 减少 80%,同时保持极佳的功能性。
通过研究 DeepSeek R1 的架构,我们得以选择性地将某些层量化至较高比特(如 4bit),而将大多数 MoE 层(如 GPT-4 中使用的)量化至 1.5bit。若天真地将所有层统一量化,模型会彻底失效,导致无限循环和胡言乱语的输出。我们的动态量化解决了这一问题——在此查看我们的动态量化对 Phi-4 和 Llama Vision 等模型的影响及基准测试。
模型运行指南见:R1 运行指南。
1.58bit 量化版本适合在 160GB VRAM 中运行以实现快速推理(如 2 张 H100 80GB GPU),吞吐量约为每秒 140 tokens,单用户推理速度约为每秒 14 tokens。运行 1.58bit R1 并非必须使用 VRAM(GPU),仅 20GB RAM(CPU)即可,但速度会非常慢。为了获得最佳性能,我们建议 VRAM + RAM 总和至少为 80GB 以上。
2025 年 2 月 6 日更新:现在你可以使用 GRPO + Unsloth 训练像 R1 这样的专属推理模型。
我们上传了大小从 131GB 到 212GB 不等的动态量化版本至:huggingface.co/unsloth/DeepSeek-R1-GGUF
附言:如果你喜欢我们的作品,欢迎 ⭐ Star 我们:github.com/unslothai/unsloth 或关注我们的账号 @UnslothAi 💖🦥 1. 动态量化版本 我们提供了 4 个动态量化版本。前三个版本使用重要性矩阵(imatrix via llama.cpp)来校准量化过程,从而支持更低的位宽表示。最后一个 212GB 的版本是通用的 2-bit 量化,未进行校准。 | MoE Bits | 磁盘大小 | 类型 | 质量 | 链接 | | :--- | :--- | :--- | :--- | :--- | | Down_proj 1.58-bit | 131GB | IQ1_S | 一般 | 链接 | | 2.06/1.56bit 1.73-bit | 158GB | IQ1_M | 良好 | 链接 | | 2.06bit 2.22-bit | 183GB | IQ2_XXS | 更好 | 链接 | | 2.5/2.06bit 2.51-bit | 212GB | Q2_K_XL | 最佳 | 链接 | 你可以查看我们完整的 R1 GGUF 合集,其中包括 4-bit、蒸馏版本等更多内容:huggingface.co/collections/unsloth/deepseek-r1 📊 2. 基准测试与消融实验 为了测试所有量化模型,我们没有依赖通用的基准测试,而是要求 DeepSeek r1 在三次尝试(pass@3)内创建一个 Flappy Bird 游戏,并根据 10 项标准(如使用随机颜色、随机形状、是否能在 Python 解释器中运行等)进行评分。我们使用了种子 3407、3408 和 3409,以及建议的温度 0.6。
左侧是 chat.deepseek.com 生成的示例,右侧是 1.58-bit 版本的结果。 DeepSeek 原版 1.58-bit 版本 令人惊讶的是,我们的动态 1.58-bit 版本在将模型体积缩小 80% 后,仍能产生有效的输出!
然而,如果你不使用我们的动态 1.58-bit 版本,而是简单地对所有层进行量化,你会得到无限重复的输出,例如在种子 3407 中:“Colours with dark Colours with dark Colours with dark Colours with dark Colours with dark” 或在种子 3408 中:“Set up the Pygame's Pygame display with a Pygame's Pygame's Pygame's Pygame's Pygame's Pygame's Pygame's Pygame's Pygame's”。
同样,如果你不使用动态版本,而是将所有层量化到 1.75-bit(149GB),无限重复的问题虽然消失了,但结果完全错误。所有输出都生成了全黑的屏幕。即使将所有层量化到 2.06-bit(175GB),结果也比 1.58-bit(131GB)的动态量化版本更糟。你最好使用性能更优的 2.22-bit(183GB)版本。
1.58-bit 动态量化偶尔会在每 8000 个 token 中产生 1 个错误 token,这需要被注释掉。使用 min_p = 0.1 或 0.05 可以缓解 1.58-bit 量化生成单个错误 token 的问题。
作为总分(满分10分)和 Pass@3 的汇总:131GB 的 1.58bit 版本在我们的 Flappy Bird 基准测试中得分为 69.2%,而 183GB 的 2bit 版本达到 91.7%。
相比之下,非动态量化表现糟糕得多。把所有层都量化到 1.58bit 后得分为 0%,即使提高到 175GB 也只有 61.7%,比我们的动态量化还要低(而且体积还更大)!
| 模型大小 | 动态量化 | 模型大小 | 基础量化 |
| 131GB | 6.92 | 133GB | 0 |
| 158GB | 9.08 | 149GB | 1.67 |
| 183GB | 9.17 | 175GB | 6.17 |
通过结合以下 4 个思路:我们的 4-bit 动态量化方法、1.58-bit LLMs 论文、Llama.cpp 的 1.5-bit 量化,以及 Super Weights 论文,我们得出了以下洞见: - 前 3 个稠密层只占全部权重的 0.5%,我们保留为 4bit 或 6bit。 - MoE 层使用共享专家,占权重的 1.5%,我们使用 6bit。 - 所有 MLA 注意力模块占权重不到 5%,可以保留为 4bit 或 6bit。注意力输出(占 3%)虽然可以量化,但最好保留较高精度。 - down_proj 对量化最敏感,尤其是前几层。我们通过 Super Weights 论文、自己的动态量化方法和 llama.cpp 的 GGUF 量化方法交叉验证了这一发现。因此,前 3 到 6 个 MoE 的 down_proj 矩阵应保留较高精度。例如在 Super Weights 论文中,几乎所有不应被量化的权重都位于 down_proj 中:
为什么最关键、“超级”权重集中在 down_proj 中,原因在于 SwiGLU 机制:
f(XWgate) * (XWup) 然后 乘以 Wdown
也就是说,up 和 gate 投影本质上相乘后会生成更大的数值,而 down_proj 需要将这些数值缩小。因此,对 down_proj 进行量化可能并非明智之举,尤其是在 Transformer 的早期层。 我们应保留 embedding 和 lm_head 分别为 4bit 和 6bit。MoE router 以及所有 layer norms 保持 32bit。 这样,约 88% 的权重都集中在 MoE 权重上!将它们量化到 1.58bit,可以大幅缩减模型体积。 我们提供了动态量化代码,作为 llama.cpp 的分支发布在:github.com/unslothai/llama.cpp 我们借助了 Bartowski 的重要性矩阵来处理低比特量化。 💬 关于 Chat Template 的问题 所有蒸馏版本以及 671B R1 主模型使用相同的 chat template: <|begin▁of▁sentence|> <|User|>What is 1+1? <|Assistant|>It's 2. <|end▁of▁sentence|> <|User|>Explain more! <|Assistant|> BOS 会被强制添加,且每个交互之间由 EOS 分隔。为避免在推理时出现重复的 BOS token,调用 tokenizer.encode(...) 时应设置 add_special_tokens = False,因为 chat template 已自动添加了一个 BOS token。
针对 llama.cpp 和 GGUF 推理,由于系统会自动处理,建议手动跳过 `<|begin▁of▁sentence|>`。特殊 token(如 `<|User|>` 和 `<|Assistant|>`)被分配了专门的 ID;在 Qwen 和 Llama 的蒸馏版本中,由于部分 token 被重新映射(例如 Qwen 没有 BOS token),因此使用了特定的 token 进行替代。 各模型的 Token ID 映射如下:| Token | R1 | Qwen 蒸馏版 | Llama 蒸馏版 |
|---|---|---|---|
| <think> | 128798 | 151648 | 128013 |
| </think> | 128799 | 151649 | 128014 |
| <|begin_of_sentence|> | 0 | 151646 | 128000 |
| <|end_of_sentence|> | 1 | 151643 | 128001 |
| <|User|> | 128803 | 151644 | 128011 |
| <|Assistant|> | 128804 | 151645 | 128012 |
| Padding | 2 | 151654 | 128004 |
| Token | Qwen 2.5 32B Base | Llama 3.3 70B Instruct | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <think> |
如果想直接使用 llama.cpp,可以按照这里的构建说明操作——别忘了开启 GPU 支持!我通常使用以下命令:apt-get update apt-get install build-essential cmake curl libcurl4-openssl-dev -y git clone https://github.com/ggerganov/llama.cpp cmake llama.cpp -B llama.cpp/build \ -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=ON -DLLAMA_CURL=ON cmake --build llama.cpp/build --config Release -j --clean-first --target llama-quantize llama-cli llama-gguf-split cp llama.cpp/build/bin/llama-* llama.cpp 然后通过 huggingface.co/unsloth/DeepSeek-R1-GGUF 下载模型,可以用 Hugging Face 来完成。要下载 1.58bit 版本,运行下面这段代码即可。如果想加快下载速度,取消注释前几行来启用 hf_transfer。# pip install huggingface_hub hf_transfer # import os # Optional for faster downloading # os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" from huggingface_hub import snapshot_download snapshot_download( repo_id = "unsloth/DeepSeek-R1-GGUF", local_dir = "DeepSeek-R1-GGUF", allow_patterns = ["*UD-IQ1_S*"], ) 该命令将把 3 个 GGUF 文件下载到 DeepSeek-R1-GGUF/DeepSeek-R1-UD-IQ1_S 目录。接下来,使用以下公式计算可卸载(offload)到 GPU 的层数。如果没有 GPU,则将卸载层数设为 0: $$ n_{offload} = \frac{\operatorname{VRAM}(GB)}{\operatorname{Filesize}(GB)} \times n_{layers} - 4 $$ DeepSeek R1 共有 61 层。例如,使用 24GB 或 80GB 显存的 GPU,根据下表向下取整得到的卸载层数,若发生显存溢出需减少 1 层:
运行模型时,我们将 K 缓存量化为 4bit。V 缓存的量化需要为 llama.cpp 编译 flash attention 内核。我们使用机器的所有线程,并采用 DeepSeek 推荐的 0.6 温度值。上下文长度(context size)决定模型生成的 token 数量。 进入主目录,应能看到 llama.cpp 文件夹和 DeepSeek-R1-GGUF 文件夹。 --threads:CPU 核心数量 例如,在配备 24GB 显存的 RTX 4090 GPU 上,执行以下命令: ./llama.cpp/llama-cli \ 🍎 在 Mac / Apple 设备上运行 针对 Apple Metal 设备,需留意 --n-gpu-layers 的设置。若发现机器内存溢出,请适当减少该数值。对于 128GB 统一内存的机器,通常可以卸载约 59 层。 ./llama.cpp/llama-cli \ 🦙 在 Ollama / Open WebUI 中运行 Open WebUI 已制作官方教程,指导如何运行 R1:docs.openwebui.com/tutorials/integrations/deepseekr1-dynamic/ 如果希望在 Ollama 中对 GGUF 进行推理,首先需要将 3 个拆分文件合并为 1 个,代码如下。随后需在本地运行该模型。 ./llama.cpp/llama-gguf-split --merge \ 💡 Prompt 与结果 使用的完整 Prompt 如下: 用 Python 创建一个 Flappy Bird(鸟类横移)游戏。需包含以下内容:必须使用 pygame。背景色应随机选择浅色,以浅蓝色开始。多次按下空格键会加速鸟的飞行。鸟的形状随机选定为正方形、圆形或三角形,颜色随机选定为深色。在底部放置随机选择深棕色或黄色的土地。在右上角显示分数,通过管道且不碰撞时分数增加。生成间距随机且间距足够大的管道,颜色随机选为深绿色、浅棕色或深灰色。失败时显示最高分,文字需显示在屏幕内。按 q 或 Esc 退出游戏,再次按空格重新开始。最终游戏代码需包裹在 Python 的 Markdown 代码块中。在生成最终代码块前,请检查并修正代码错误。 完整表格与结果见:docs.unsloth.ai/basics/deepseek-r1-dynamic-1.58-bit 💕 感谢! 一如既往,欢迎加入我们的 Reddit 页面和 Discord 服务器寻求帮助,或单纯表达支持!你也可以在 Twitter 上关注我们,或订阅我们的 newsletter。感谢阅读!Daniel & Michael Han 🦥 2025年1月27日 立即微调你自己的模型!了解更多 加入我们的 Discord 评论 (0) |