OllamaでローカルLLM

外部に出すのがはばかられる情報を扱う場合や、費用をかけたくない場合、再現性が必要な場合、rate limitを気にせず大量ループ処理したい場合に、ローカルLLMは魅力的である。ここでは Ollama(オラーマ)というツールを使ってコマンドラインまたはPythonからLLMを使う方法を説明する。Ollamaは llama.cpp から派生したたいへん使いやすいツールである。

ここではMacを使う。OllamaサイトよりバイナリのZIPファイルをダウンロードし、展開してOllama.appを /Applications(あるいは適当な場所)に移動し、起動すると /Applications/Ollama.app/Contents/Resources/ollama へのシンボリックリンクが /usr/local/bin にできる。また、バックグラウンドのプロセスが起動し、メニューバーにOllamaアイコンが常駐する。利用はターミナルからollamaコマンドを使って行うか、後述するようにPythonなどを使ってバックグラウンドのプロセスのAPIを呼び出す。

まずは適当なモデルをダウンロードして動かす。ここではGoogleの Gemma 3(ジェンマ・スリー)を使ってみる。

OllamaのGemma 3のページを参考にして、デフォルトの4ビット量子化(Q4_K_M)された4サイズのGemma 3のうち適当なものを選ぶ。メモリ8GのMacなら4B、メモリ16GのMacなら12B、それ以上なら27Bを使えばよかろう(Bはbillionつまり10億パラメータ)。例えば12Bモデルなら

ollama run gemma3:12b

と打ち込むと、初回はモデルのダウンロードが始まり、それが終われば入力を受け付ける状態になるので、何か質問すれば、答えが返る。

モデルは ~/.ollama に入る。ドライブの容量が足りなければ、あとで外部ドライブにモデルを移動してシンボリックリンクを張ってもよい。

私はメモリ128GのMac Studioを買ったので、4ビットでなくても8ビットでも、おそらく16ビットでも大丈夫そうだが、8ビットあれば十分だ。ドロップダウンメニューの最後の「View all」から探すと、8ビット量子化の gemma3:27b-it-q8_0 が見つかる。これを実行してみよう。

ollama run gemma3:27b-it-q8_0

モデルの詳細は /show info で調べられる:

>>> /show info     
  Model
    architecture        gemma3    
    parameters          27.4B     
    context length      131072    
    embedding length    5376      
    quantization        Q8_0      

  Capabilities
    completion    
    vision        

  Parameters
    top_p          0.95               
    stop           "<end_of_turn>"    
    temperature    1                  
    top_k          64                 

  License
    Gemma Terms of Use                  
    Last modified: February 21, 2024    

8ビット27Bなら27Gバイト+αのメモリを使う。+αの量はコンテキスト長(num_ctx)にも依存する。Gemma 3は128Kまで対応する(1Bモデルは32Kまで)。メモリ・CPUの使用率はMac標準のアクティビティモニタで表示できるが、GPUの使用率も含めてグラフィカルに表示できる mactop は便利である。

ちなみに、MacのメモリでGPUから使えるのはデフォルトでは3/4(64G以上のMac)または2/3(64G未満)に設定されているようだ(macOS でローカル LLM を使うときの VRAM 最適化設定参照)。例えば128GのMacなら sudo sysctl iogpu.wired_limit_mb=122880 と打ち込むと120Gまで使えるようになる。このあたりの事情は mlx.core.set_wired_limit の説明にも書かれている。

Ollamaは上記のようにコマンドラインで使うのが簡単だが、Pythonで使うこともできる。あらかじめ ollama-python というパッケージ pip install ollama でインストールしておく。

簡単なプログラム例(温度0でランダムな文字列を出力させ、比較する。モデル名は置き換えてください):

from ollama import chat

def ai(prompt):
    response = chat(
        model="hf.co/unsloth/gemma-3-27b-it-GGUF:Q8_0",
        messages=[{ 'role': 'user', 'content': prompt }],
        options={ "temperature": 0, "num_ctx": 512 }
    )
    return response['message']['content']

ans1 = ai("Please output as many random tokens as possible.")
ans2 = ai("Please output as many random tokens as possible.")
print(ans1 == ans2)

まったく同じである(→ ランダムな文字列をLLMに出力させる)。

簡単なプログラム例(ファイルの各行に収めた文章を分類する):

from ollama import chat

def ai(prompt):
    response = chat(
        model="hf.co/unsloth/gemma-3-27b-it-GGUF:Q8_0",
        messages=[{ 'role': 'user', 'content': prompt }],
        options={ "temperature": 0, "num_ctx": 2048 }
    )
    return response['message']['content']

with open("list.txt") as f:
    lines = f.readlines()

for line in lines:
    prompt = f"次の文章を○○○と×××に分類して、○○○なら「A」、×××なら「B」と答えてください。\n\n{line}"
    print(ai(prompt).strip())

このように、多数の文章を分類するような場合は、一つのプロンプトにまとめず、ループにする。

より高度なプログラム例(ストリーミング使用、会話を続ける):

from ollama import chat

class Ollama:
    def __init__(self,
                 model="hf.co/unsloth/gemma-3-27b-it-GGUF:Q8_0",
                 messages=None, temperature=1, num_ctx=131072):
        self.model = model
        self.messages = messages or []
        self.temperature = temperature
        self.num_ctx = num_ctx

    def chat(self, prompt, num_ctx=None, temperature=None):
        if len(self.messages) > 0 and self.messages[-1]["role"] == "user":
            self.messages.pop()
        self.messages.append({"role": "user", "content": prompt.strip()})
        temperature = self.temperature if temperature is None else temperature
        num_ctx = self.num_ctx if num_ctx is None else num_ctx
        stream = chat(
            model=self.model,
            messages=self.messages,
            stream=True,
            options={
                "temperature": temperature,
                "num_ctx": num_ctx
            }
        )
        ans = ""
        for chunk in stream:
            content = chunk['message']['content']
            print(content, end="")
            ans += content
        self.messages.append({"role": "assistant", "content": ans})

    def get_messages(self):
        return self.messages

ai = Ollama(temperature=1)

ai.chat('''
最初のプロンプト...
''')

ai.chat('''
次のプロンプト...
''')

コンテクスト長のデフォルトは環境変数でも設定できる。環境変数を変えたなら、Ollamaプロセスをいったん終了して、シェルから再起動(Macなら open -a Ollama)する。

export OLLAMA_CONTEXT_LENGTH=131072

Emacsから使う方法は こちらこちら に書いた。