モジュール 10 · 第 4 回

自分のパソコンでモデルを動かす:Ollama と量子化

オープンソースモデルを自分のパソコンで動かします。まずモデルがどれだけのメモリを占めるかを見積もり、次に量子化を手書きして、重みを 8 ビット、4 ビット、2 ビットに圧縮するとどれだけ小さく、どれだけ悪くなるかを見て、最後に Ollama で動かします。

  • 約 40 分
  • 難易度:中級
  • 検証:2026-09-15 torch 2.14、Apple M4 CPU。Ollama の使い方は公式ドキュメントに従う

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

第 1 部ではずっと 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 個のアテンションヘッドがありますが、K と V は 2 組しかなく、キャッシュが 7 分の 1 で済んでいます。コンテキストが長いとき、この差はとても重要になります。

実際に見積もるときの簡単なルールは、パラメータ数 × パラメータあたりのバイト数に、2 割の余裕を持たせることです。余裕はキャッシュやその他のオーバーヘッドの分です。

== 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 の 4 分の 1 で済むのです。

量子化を手書きする

量子化とは、各パラメータをより少ないビットで保存することです。最も簡単なやり方は対称量子化と呼ばれます。数のグループの中で絶対値が最も大きいものを見つけ、範囲 [-最大値, 最大値] をいくつかの目盛りに均等に分け、各数を最も近い目盛りに落とし、目盛りの番号(小さな整数)とそのグループのスケーリング係数だけを保存します。

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 分の 1 になり、検証の損失は 4.476 で、元のモデルとまったく同じ、書かれた詩まで一字違いません。8 ビットの量子化はほぼ「無料」です。

4 ビット、行ごとに係数一つ:大きさはさらに半分になり、損失は 4.476 から 4.542 に上がり、詩も変わりました。1 行には数百個の数があり、その中に特に大きいものが一つでもあれば、スケーリング係数がそれに引き伸ばされ、ほかの数はわずかな目盛りに押し込められるしかなく、誤差が大きくなるのです。

4 ビット、32 個ごとに係数一つ:スケーリング係数を少し多く保存する(大きさは 0.81 MB から 0.89 MB になる)ことで、損失は 4.512 まで戻り、詩もほぼ元の姿に戻って、変わったのは 2 か所だけでした。グループが小さいほど、各グループのスケーリング係数がそのグループの数によく合い、誤差が小さくなります。実際に使われる 4 ビットの量子化は、ほぼすべてグループ化されています。

2 ビット:使える値は -1、0、1 の三つだけで、モデルは完全に壊れ、損失は 6.93、書かれたものは形式がすっかり乱れ、最後には「里里里里」と繰り返しています。

結論は実際に使ったときの経験と一致します。8 ビットはほぼ無損失。4 ビットは少し損失があるものの、4 分の 1 の大きさと引き換えにでき、自分のパソコンでモデルを動かすときに最もよく使われる選択です。それより下では、効果が急速に悪くなります。

本物の量子化の方法はここより複雑です。特に大きな「外れ値」を処理し、いくらかのデータをもとに誤差が最小になる量子化の仕方を決め、4 ビットのために特別に設計されたデータ形式もあります(第 2 課で触れた QLoRA は NF4 という形式を使っています)。しかし基本的な原理は、この節の数行のコードです。

Ollama でモデルを動かす

自分でコードを書いて LLM を量子化し、動かすのはとても面倒です。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 互換の API を提供します。つまり第 1 部で書いたすべてのコードは、環境変数を三つ変えるだけでローカルモデルに切り替えられるのです。

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 のような LLM と大きな差があります。ここでまたモジュール 06 の評価セットが役に立ちます。ローカルモデルに替えたら一通り実行し、どのタスクならまだこなせるかを見るのです。

自分でファインチューニングしたモデルを使う

Ollama は自分のモデルも動かせます。Hugging Face 形式(safetensors)のモデルや GGUF 形式のモデルを取り込めて、やり方は Modelfile という設定ファイルを書き、FROM でモデルのファイルを指し、ollama create で作成するというものです。

第 3 課でファインチューニングした RepoBot なら、まず LoRA を元のモデルにマージし直して(merge_and_unload)保存し、それから公式ドキュメントに従って取り込めます。公式ドキュメントには、GGUF のモデルを取り込むとき Ollama は量子化してくれないので、先に llama.cpp のツールで量子化しておく必要があると特に書かれています。具体的な手順は Ollama の取り込みのドキュメントに従ってください。この部分は変化が速いのです。

練習問題

  1. 自分のパソコンのメモリ(あるいは GPU のメモリ)を調べ、この課の見積もりの表に従うと、最大でどれくらいのパラメータのモデルを動かせますか。4 ビットと 8 ビットでそれぞれいくつですか。
  2. quantize.py で 3 ビット・グループ 32、それに 4 ビット・グループ 8 と 128 を試し、大きさと検証の損失を表にしてください。
  3. Ollama をインストールして小さなモデルを動かし、モジュール 03 の RepoBot v1 をそれを呼ぶように変えて(環境変数だけを変える)、正常に動くか見てください。

確認テスト

1. 7B パラメータのモデルが BF16 でどれだけのメモリを占めるか、どう見積もりますか?

パラメータ数に、パラメータあたりのバイト数を掛けます。70 億 × 2 バイト = 140 億バイトで、約 13 GB。さらに KV キャッシュとその他のオーバーヘッドの分として 2 割の余裕を持たせ、約 16 GB です。

2. 4 ビットの量子化で、「32 個ごとにスケーリング係数一つ」のほうが「行ごとにスケーリング係数一つ」より効果がよいのはなぜですか?

スケーリング係数は、そのグループの中で絶対値が最も大きいものによって決まります。1 行には数百個の数があり、特に大きいものが一つでもあれば行全体のスケーリング係数が引き伸ばされ、ほかの数はごくわずかな整数に押し込められるしかなく、誤差が大きくなります。小さなグループに分ければ、各グループのスケーリング係数がそのグループの数によく合い、誤差はずっと小さくなります。代償は、スケーリング係数を少し多く保存することだけです。

3. 第 1 部のコードを Ollama のローカルモデルに切り替えるとき、ほとんど変更が要らないのはなぜですか?

Ollama は OpenAI 互換の API を提供していて、第 1 部のコードは openai ライブラリを使い、API のアドレス、キー、モデル名を環境変数に入れているからです。この三つの変数を Ollama のアドレスとモデル名に変えるだけで、コードはそのままローカルモデルを呼び出せます。