在自己電腦上跑模型:Ollama 和量化
把開源模型放到自己電腦上執行。先估算一個模型要佔多少記憶體,再手寫量化,看權重壓到 8 位、4 位、2 位時變小多少、變差多少,最後用 Ollama 跑起來。
- 約 40 分鐘
- 難度:進階
- 實測:2026-09-15 torch 2.14,Apple M4 CPU;Ollama 用法以其官方文件為準
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
第一部分一直在呼叫 DeepSeek 的 API。這很方便,但有些場合你會希望模型跑在自己的機器上:資料不能出公司、沒有網路、呼叫量大到 API 太貴,或者只是想試試。
這一課講在自己電腦上執行開源模型。關鍵的問題是:我的電腦裝得下多大的模型?答案取決於兩件事,模型有多少參數,以及每個參數用幾個位元組存。
python memory_estimate.py
python quantize.py
估算記憶體
模型的權重是一大堆數字。每個數字用 32 位小數(FP32)存要 4 個位元組,用 16 位(BF16 或 FP16)要 2 個位元組。所以權重佔的記憶體大約是:
参数量 × 每个参数的字节数
以第 3 課用的 Qwen2.5-0.5B-Instruct 為例(它有 4.94 億個參數):
== 1. Qwen2.5-0.5B-Instruct 的权重,在不同精度下占多少
FP32 1.84 GB
BF16/FP16 0.92 GB
INT8 0.46 GB
4 比特 0.23 GB
它下載下來的檔案 model.safetensors 是 988 MB,就是 BF16 的大小(988 MB 約等於 0.92 GB,差別是 1000 和 1024 的換算)。
執行時,除了權重,還有第 09 模組第 6 課講的 KV 快取。它的大小可以從模型的配置算出來:每個詞元、每一層,存一份 K 和一份 V:
def kv_cache_gb(n_layers, n_kv_heads, head_dim, n_tokens, bytes_per_value=2):
# 每个词元、每一层,要存一个 K 和一个 V,各是 n_kv_heads × head_dim 个数
return 2 * n_layers * n_kv_heads * head_dim * n_tokens * bytes_per_value / 1024**3
== 2. 它的 KV 缓存(每层 2 组 K/V,每组 64 维,BF16)
每个词元 12 KB
1000 个词元:0.01 GB
32000 个词元:0.37 GB
如果不用分组查询注意力(14 个头各存一份 K/V),32000 个词元要 2.56 GB,是现在的 7 倍
最後一行是分組查詢注意力的作用:Qwen2.5-0.5B 有 14 個注意力頭,但只有 2 組 K、V,快取省了 7 倍。上下文長的時候,這個差距非常關鍵。
實際估算時,一個簡單的規則是:參數量 × 每個參數的位元組數,再留兩成餘量給快取和其他開銷:
== 3. 粗略估算:参数量 × 每个参数的字节数,再留两成余量给缓存和其他开销
0.5 B 参数:FP32 2.2 GB BF16/FP16 1.1 GB INT8 0.6 GB 4 比特 0.3 GB
7 B 参数:FP32 31.3 GB BF16/FP16 15.6 GB INT8 7.8 GB 4 比特 3.9 GB
14 B 参数:FP32 62.6 GB BF16/FP16 31.3 GB INT8 15.6 GB 4 比特 7.8 GB
32 B 参数:FP32 143.1 GB BF16/FP16 71.5 GB INT8 35.8 GB 4 比特 17.9 GB
70 B 参数:FP32 312.9 GB BF16/FP16 156.5 GB INT8 78.2 GB 4 比特 39.1 GB
這張表可以直接拿來用。一臺 16 GB 記憶體的筆記本,BF16 只能勉強跑 7B 的模型;換成 4 位元,14B 的模型也能跑。長上下文要另外多算 KV 快取。
從表裡也能看出為什麼大家都在談"量化":同一個模型,4 位元只要 BF16 的四分之一。
手寫量化
量化就是用更少的位元存每個參數。最簡單的做法叫對稱量化:一組數里找出絕對值最大的,把範圍 [-最大值, 最大值] 平均分成若干格,每個數就近落到一個格子上,只存格子的編號(一個小整數)和這組數的縮放係數。
def quantize(w, bits, group=None):
"""对称量化:每组数用一个缩放系数,把 [-最大绝对值, 最大绝对值] 映射到整数 [-qmax, qmax]。
返回"量化后再还原"的权重,以及实际要存的整数和缩放系数。"""
qmax = 2 ** (bits - 1) - 1 # 8 比特是 127,4 比特是 7
shape = w.shape
w = w.reshape(-1, group) if group else w.reshape(shape[0], -1) # 按组,或者按行
scale = w.abs().amax(dim=1, keepdim=True) / qmax
q = torch.round(w / scale).clamp(-qmax, qmax) # 这就是要存下来的整数
return (q * scale).reshape(shape), q, scale
看一個例子,8 個數量化成 4 位元(-7 到 7 的整數):
== 1. 一个例子:把 8 个小数量化成 4 比特整数
原来: [0.077, -0.0147, -0.1089, 0.0284, -0.0542, -0.0699, 0.0202, 0.0419]
整数: [5, -1, -7, 2, -3, -4, 1, 3](缩放系数 0.01556)
还原后: [0.0778, -0.0156, -0.1089, 0.0311, -0.0467, -0.0623, 0.0156, 0.0467]
最大的 -0.1089 對應 -7,縮放係數就是 0.1089 / 7 = 0.01556。其他數除以它再四捨五入。還原時乘回去,大部分數的誤差在 0.003 左右,-0.0542 變成了 -0.0467,誤差大一些。
每個數從 32 位變成了 4 位,只多存了一個縮放係數。
量化我們的小 GPT
把第 09 模組訓練的小 GPT 的所有矩陣都量化,看看模型變小了多少、變差了多少(LayerNorm 和偏置很小,保持原樣):
== 2. 整个模型量化之后
大小 验证损失
32 位小数(原模型) 6.16 MB 4.476 白露起春光,知君得舞衣。雪时疑是静,山意势悠扬。
8 比特,每行一个系数 1.58 MB 4.476 白露起春光,知君得舞衣。雪时疑是静,山意势悠扬。
4 比特,每行一个系数 0.81 MB 4.542 云泉四百古,一树两三五。雪路之山在,山闾势悠扬。
4 比特,每 32 个数一个系数 0.89 MB 4.512 白露起春光,知君得舞衣。雪时疑报晓,山下势悠扬。
2 比特,每 32 个数一个系数 0.51 MB 6.933 百日起春光津倚郭对舞儿年,一之州在子。闾里里里里
每一行用同樣的隨機種子寫一首詩,方便比較。
8 位元:大小變成原來的四分之一,驗證損失是 4.476,和原模型一模一樣,連寫出的詩都一字不差。8 位元量化幾乎是"免費"的。
4 位元,每行一個係數:大小再減半,損失從 4.476 升到 4.542,詩也變了。一行有幾百個數,只要其中有一個特別大的,縮放係數就被它撐大,其他數都只能擠在少數幾個格子裡,誤差就大了。
4 位元,每 32 個數一個係數:多存一些縮放係數(大小從 0.81 MB 變成 0.89 MB),損失降回 4.512,詩也基本回到了原來的樣子,只改了兩處。分組越小,每組的縮放係數越貼合這組數,誤差越小。實際使用的 4 位元量化,基本都是分組的。
2 位元:只有 -1、0、1 三個值可用,模型徹底壞了,損失 6.93,寫出來的東西格式全亂,最後還在重複"裡裡裡裡"。
結論和實際使用的經驗一致:8 位元幾乎無損;4 位元有一點損失,但換來了四分之一的大小,是在自己電腦上跑模型最常用的選擇;再往下,效果會迅速變差。
真實的量化方法比這裡複雜:會處理特別大的"異常值",會根據一些資料來決定怎麼量化誤差最小,還有專門為 4 位元設計的資料格式(第 2 課提到的 QLoRA 就用了一種叫 NF4 的格式)。但基本原理就是這一節的這幾行程式碼。
用 Ollama 執行模型
自己寫程式碼量化、執行大模型很繁瑣。Ollama 是一個把這些都打包好的工具:一條命令下載一個已經量化好的模型並執行。
安裝(截至 2026 年 9 月,以官方說明為準):
# macOS 和 Linux
curl -fsSL https://ollama.com/install.sh | sh
# Windows(PowerShell)
irm https://ollama.com/install.ps1 | iex
macOS 和 Windows 也可以從 ollama.com 下載安裝包。
執行第 3 課用過的 Qwen2.5-0.5B:
ollama run qwen2.5:0.5b
第一次執行會自動下載模型,然後進入對話。截至 2026 年 9 月,Ollama 模型庫裡的 qwen2.5:0.5b 預設是 Q4_K_M 量化的版本,大小 398 MB,比原來 BF16 的 988 MB 小了一半多。Q4_K_M 是 llama.cpp 定義的一種 4 位元分組量化格式,和上面手寫的"4 位元,分組"是一個思路,只是更精細。
同一個模型通常有好幾個量化版本可選,比如 qwen2.5:0.5b-instruct-q8_0(8 位元)、qwen2.5:0.5b-instruct-fp16(16 位,不量化)。按上面的記憶體估算選擇合適的版本。
用 API 呼叫本地模型
Ollama 執行後,會在本機的 11434 埠提供一個和 OpenAI 相容的介面。這意味著第一部分寫的所有程式碼,改三個環境變數就能換成本地模型:
export LLM_BASE_URL=http://localhost:11434/v1
export LLM_API_KEY=ollama # 本地服务不检查密钥,但 openai 库要求有一个值
export LLM_MODEL=qwen2.5:0.5b
第 00 模組第 2 課把模型的地址、金鑰、名字都放在環境變數裡,就是為了這一刻。
不過要有合理的預期:0.5B 的模型在第 3 課已經見識過,它會把北京的地理說錯。本地能流暢執行的小模型,在複雜任務上和 DeepSeek 這類大模型差距很大。第 06 模組的評估集這時又派上了用場:換成本地模型後跑一遍,看看哪些任務它還能勝任。
用上自己微調的模型
Ollama 也能執行自己的模型。它可以匯入 Hugging Face 格式(safetensors)的模型和 GGUF 格式的模型,方法是寫一個叫 Modelfile 的配置檔案,用 FROM 指向模型檔案,再用 ollama create 建立。
第 3 課微調的 RepoBot,可以先把 LoRA 合併回原模型(merge_and_unload)再儲存,然後按官方文件匯入。官方文件特別說明:匯入 GGUF 模型時 Ollama 不會幫你量化,需要先用 llama.cpp 的工具量化好。具體步驟請以 Ollama 的 匯入文件 為準,這部分變化比較快。
練習
- 查一下你電腦的記憶體(或顯示卡的視訊記憶體),按本課的估算表,你最大能跑多少參數的模型?4 位元和 8 位元各是多少?
- 在
quantize.py裡試試 3 位元、分組 32,以及 4 位元、分組 8 和 128,把大小和驗證損失列成一張表。 - 安裝 Ollama,執行一個小模型,把第 03 模組的 RepoBot v1 改成呼叫它(只改環境變數),看看它能不能正常工作。
自測
1. 怎樣估算一個 7B 參數的模型在 BF16 下要佔多少記憶體?
參數量乘以每個參數的位元組數:70 億 × 2 位元組 = 140 億位元組,約 13 GB。再加上 KV 快取和其他開銷,留兩成餘量,大約 16 GB。
2. 4 位元量化時,為什麼"每 32 個數一個縮放係數"比"每行一個縮放係數"效果好?
縮放係數由這組數里絕對值最大的那個決定。一行有幾百個數,只要有一個特別大,整行的縮放係數就被撐大,其他數只能擠在很少的幾個整數上,誤差很大。分成小組後,每組的縮放係數更貼合這組數,誤差小得多,代價只是多存一些縮放係數。
3. 為什麼第一部分的程式碼換成 Ollama 的本地模型時,幾乎不用改?
Ollama 提供了和 OpenAI 相容的介面,而第一部分的程式碼用的是 openai 庫,並把介面地址、金鑰和模型名都放在環境變數裡。只要把這三個變數改成 Ollama 的地址和模型名,程式碼就能直接呼叫本地模型。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…