コンテンツにスキップ

学習メモ — ローカルLLM(Ollama)

lectures/local_models/ の写経で得た理解を1枚に圧縮したメモ。混乱はほぼ「用語のすれ違い」と「どこまでがローカル固有か」の取り違えで起きるので、後半の用語集を最初に押さえると速い。

1行で言うと

ローカルLLM = 自分のマシンで立てた「OpenAI 互換の HTTP サーバ」。 クラウド API との違いは宛先(localhost:11434api.openai.com か)と課金の有無だけで、コードの形は同じ。base_url を差し替えればモデルは交換可能な部品になる。

モデルをローカルに「持つ」とは(直感)

最初にいちばん詰まるのがここ。「モデル」と聞くと頭脳やアプリのような“動いている何か”を想像しがちだが、ローカルに持つモデルの実体は、ただのデカいファイル1個

モデルをローカルに持つ = 映画ファイルを1個ダウンロードして HDD に置くのと同じ。 再生する代わりに「質問を投げると答えが返る巨大な計算表」が手に入る。

  • モデル = 学習で決まった数字(重み, weights)の塊llama3.2:3b3b は「30億個の数字」の意味。数字1個を約0.5〜2バイトで保存 → 約2GBのファイルになる(4bit 量子化込み)。
  • このファイルにコードは1行も無い。賢さは「if 文のルール」ではなく、30億個の数字の配置そのものに染み込んでいる(電卓に手順が無く回路の配線で答えが出るのと同じ)。
  • 実物は ~/.ollama/models/blobs/ に置かれる巨大 blob。ollama listllama3.2:3b 2.0GB と登録され、実体は sha256-...(約1.9G)のファイル。

「持つ」と「動かす」は別もの(混乱の元)

持つ(ollama pull 動かす(ollama serve + 質問)
何をする ディスクに2GBファイルを置く そのファイルをメモリに展開し、毎回30億回の掛け算
例えると 映画を「DL済み」で寝かせてる 映画を「再生中」
食う資源 ディスク容量 メモリ(理想は GPU)+電気
電源切ると 残る 消える(ファイルは残る)

ローカルの登場人物は4人(1台のPC内で同居)

ローカルが分かりにくいのは、クラウドでは OpenAI が持っていた役割を、自分のPC内の別々のものが分担するから。

役者 正体 クラウドでは誰が持っていた
① コード 自分が書く Python(ex01〜06) (同じ)
② ランタイム/サーバ ollama serve(:11434 で待ち受ける常駐プロセス) OpenAI の API サーバ
③ 重みファイル ~/.ollama/models/ の約2GB blob(数字の塊) OpenAI が秘匿する重み
④ GPU/CPU+メモリ 自分の Mac のチップ データセンタの GPU 群

「重み(③)を、サーバ(②)が、自分のチップ(④)に載せて計算する」。これがローカル推論の全部。クラウドは②③④を全部 OpenAI が代行していただけ。

1回の質問が流れる様子(時系列)

sequenceDiagram
    participant Code as ① Python (ex01)
    participant Srv as ② ollama serve (:11434)
    participant Wt as ③ 重みファイル (~/.ollama)
    participant HW as ④ GPU/CPU

    Code->>Srv: POST /api/chat {model, messages}
    Srv->>Wt: 重みをメモリにロード(初回のみ)
    Wt-->>HW: 30億個の数字を展開
    loop トークンの数だけ繰り返す
        Srv->>HW: 次の1トークンを予測(行列計算)
        HW-->>Srv: 「こ」「ん」「に」… を1個ずつ
    end
    Srv-->>Code: 返答 JSON {message.content}

ポイントは loop の部分: LLM は「次の1トークン予測」を何百回もループしているだけstream=True(ex03)にすると、この1語ずつをリアルタイムで受け取れる=タイプライター表示の正体。

Ollama と LM Studio の関係(別倉庫・窓口だけ共通)

複数のローカルランタイムを併用すると必ず混乱する点。Ollama と LM Studio は別アプリ・別倉庫で、モデルを共有しない。

Ollama LM Studio
モデル入手 ollama pull アプリ内検索 → Download
保存場所 ~/.ollama/models/(独自 blob 形式) ~/.lmstudio/models/(GGUF ファイル)
待ち受けポート :11434 :1234
倉庫の共有 しない(完全に別) しない
  • ollama pull したモデルは LM Studio には現れない(逆も同様)。倉庫もファイル形式も別。
  • LM Studio 内でも「ダウンロード ≠ ロード」。DL は倉庫に置くだけ、別途 "Load"(メモリ展開)が要る(または初回リクエストで JIT ロード)。
  • 共通なのは「OpenAI 互換の窓口」だけ。だからコード側は OLLAMA_HOST:11434(Ollama)↔ :1234(LM Studio)に変えるだけで乗り換えられる(_client.py の設計)。アプリ・倉庫は別物でも、窓口の形が揃っているから差し替え1行で済む。

倉庫を共有して二重ダウンロードを避ける(検証済み手順)

倉庫は別でも、Ollama の重み blob は中身が GGUF(LM Studio が読む形式)なので、再ダウンロードせず共有できる。要点は方向の非対称:

  • Ollama → LM Studio = ハードリンクで共有(ディスク増分ゼロ)。
  • LM Studio → Ollama = ollama create(Modelfile の FROM x.gguf)でインポート(=コピー)。

Ollama を主にして LM Studio へ配るのが無駄が無い。現行の正解は LM Studio 公式 CLI の lms import:

# Ollama の重み blob を LM Studio へハードリンク取り込み(実体は1個を共有)
lms import <blobパスを .gguf 名にしたリンク> --hard-link --user-repo ollama/<名前> -y
# → ~/.lmstudio/models/ollama/<名前>/<...>.gguf にハードリンク生成
# 検証: stat -f '%i' で両者の inode が一致すれば実体共有成功(links=2 になる)

注意点:

  • gollama は使えない: v2.0.1 で LM Studio 連携を削除済み(「LM Studio の仕様変更が頻繁で symlink 維持が割に合わない」)。フォルダに symlink を手置きする方法も同じ理由で不安定。公式の lms import が確実。
  • ハードリンクの安心: ollama rm してももう片方が実体を保持(参照カウントが減るだけ)。同一ボリューム(~ 配下)必須なので Mac 内なら常に可。
  • gollama 自体は Ollama モデル管理(一覧 -l、VRAM 見積もり -vram)には有用なので残してよい。

実行ログ(実測 — 自分のマシンの実力値)

ex01(生 HTTP で /api/chat を叩く)を llama3.2:3b で実行した実出力:

--- 生レスポンス(要所だけ)---
{ "model": "llama3.2:3b", "done": true, "done_reason": "stop" }

--- 本文(message.content)---
1. 人性化されたアクセス:ローカルLLMは、日常の会話を理解し、適応できる。
2. 文脈のある答え:地元の知識を活用し、より正確な回答が得られる。
3. Dataの制約緩和:ローカルデータセットを利用して、制限られたデータで精度高いモデルを作成できる。

生成速度: 20.0 tok/s(94 tokens)

観察できたこと:

  • 20.0 tok/s がこのマシンの実力値eval_count(94) / eval_duration(ナノ秒) から算出。クラウドと違い「自分のチップの速さ」がそのまま出る。
  • 小型モデルの粗さが実際に出る: 「Dataの制約緩和」「制限られた」など日英混じり・不自然な日本語。3B は軽い代わりに品質はクラウドのフロンティアモデルに劣る、というトレードオフが体感できる(改善するなら qwen2.5:7b 等の大きめモデルへ)。
  • done_reason: "stop" = モデルが自然に生成を終えた(長さ上限で切れたわけではない)。

ex02(ドロップイン)— 動いた仕組みと、出た幻覚

base_url 差し替えだけで OpenAI SDK がローカルモデルを呼べた(choices[0].message.content / usage の形はクラウドと完全同型、[tokens] prompt=57 completion=65、課金ゼロ)。一方で「RAG とは何か3文で」への実応答が完全な幻覚だった:

RAG は、ロシアのウクライナ侵攻を受けて…国際的な報道 blackout を指します。

正解は Retrieval-Augmented Generation。3B は RAG の知識を持たず、知らないと言えずに「RAG→ロシア」で自信満々に捏造した(temperature=0.7 の揺らぎも逸れを後押し)。「base_url 差し替えで動く」と「3B は事実問いに弱い」を同時に体感できた。

ex03(streaming)— TTFT 実測

stream=True で戻り値がチャンクのイテレータに変わり、delta.content(差分)を継ぎ足して逐次表示。実測 TTFT 0.01s / 合計 7.72s / 約135 chunk / 17.5 chunk/s。TTFT がほぼ0なのはネット往復が無く、モデルが既にメモリ常駐(warm)だから(cold の初回はモデルロード時間が乗る)。

ex04(temperature / seed)— 全ノブが効いた

temperature=0.0 ×3:  コーヒー・ハウス ×3            ← 決定的(毎回最確トークン)
temperature=1.2 ×3:  はじめや / ミルクサロン / ルドルフ ← 多様(分布が平ら)
seed=42, temp=0 ×2:  コーヒー・ハウス ×2 → ✅一致     ← 再現

temperature=「確率分布を尖らせるか平らにするか」、seed=再現性。評価/抽出は温度0、ブレストは高温、の使い分けが実出力で確認できた。

ex05(構造化出力)— 小型でも JSON 成立

response_format={"type":"json_object"} + 温度0 で {"shop":"海風亭","score":4,"good":["刺身","米"]} を取得、scoreint でパースできた。ただし100%保証ではないので json.loads を try で囲む(境界の検証)。これが langchain/dspy の構造化層の最下層。

ex06(統合・ルーター)— ローカル優先+クラウド縮退

キー無しで実行。通常は [local] こんにちは。 とローカルで応答し、わざとローカルを失敗させた分岐は OPENAI_API_KEY 未設定のため [local-failed] フォールバック先なし安全に縮退した。ローカル/クラウドが同型(base_url だけ違う)なので、ルーターは try: ローカル except: クラウド の数行。振り分け基準=コスト/レイテンシ/プライバシー/品質/オフライン。クラウド経路も試すなら op run --env-file=.env.op -- uv run python ex06_local_vs_cloud.py

num_ctx 実証 — 机からはみ出すと冒頭が黙って消える(重要)

冒頭に合言葉『紫の月曜日』を仕込み、約2361トークンの埋め草で押し出してから合言葉を尋ねた:

num_ctx 応答 覚えてた?
128(極小) 「グレート」です。 ❌ 忘れた(幻覚)
4096(十分) 紫の月曜日です。 ✅ 覚えてた

サーバログが物理的な切り捨てを示す:

num_ctx=128  → n_ctx_slot=256,  n_keep=4, task.n_tokens=127    ← 2361→127 に切り捨て
num_ctx=4096 → n_ctx_slot=4096, n_keep=4, task.n_tokens=2361   ← 全部載った
  • num_ctx を超えると、先頭(n_keep=4 の制御トークン以外)から黙って切り捨てられる。エラーは出ず 200 OK(=静かなバグ)。だから「一番古い指示」が真っ先に死ぬ。
  • 重要 gotcha: /v1(OpenAI互換)は extra_body.options.num_ctx を無視する。最初 OpenAI() 経由で同じ実験をしたら両方とも合言葉を覚えていて、ログは n_ctx_slot=32768(既定のまま)だった。num_ctx を本当に効かせるにはネイティブ /api/chat(または ollama Python SDK)で options.num_ctx を渡す必要がある。→ ex04 の extra_body={"options":{"num_ctx":2048}} も /v1 経由なので実際には効いていない点に注意。

全体像(このフォルダのデータの流れ)

flowchart LR
    subgraph yourmachine["自分のマシン"]
        code["Python コード<br/>(OpenAI SDK)"]
        subgraph ollama["ollama serve (:11434)"]
            api["/v1/chat/completions<br/>(OpenAI 互換)"]
            native["/api/chat<br/>(Ollama ネイティブ)"]
            model[("llama3.2:3b<br/>等のモデル重み")]
        end
    end
    cloud["api.openai.com/v1<br/>(gpt-4o-mini)"]

    code -- "ex01: 生 HTTP" --> native
    code -- "ex02-06: base_url 差し替え" --> api
    api --> model
    native --> model
    code -. "ex06: フォールバック" .-> cloud

ポイントは ex01 だけ /api/chat(Ollama 独自の形)を生 HTTP で叩き、ex02 以降は /v1(OpenAI 互換)に寄せて OpenAI SDK で叩くこと。後者にすると、クラウドへ向けるのも base_url を消すだけになる(ex06 のフォールバックがそれ)。

各 ex で「何が分かれば OK か」

ex 一言 「分かった」の判定
01 生 HTTP 「LLM API は localhost にも立つただの HTTP サーバ」と言える / tok/s を計算できる
02 ドロップイン base_url を変えるだけで既存 OpenAI コードがローカルで動く理由を説明できる
03 streaming stream=True で戻り値がイテレータになり delta.content を継ぎ足すと説明できる / TTFT の意味
04 制御 temperature と seed が何を操るか、評価時はなぜ温度0+seed固定かを言える
05 構造化 JSON を出させても境界で検証が要る理由(100%保証されない)を言える
06 統合 ローカル/クラウドが同型だから try/except で縮退できる、振り分け基準を挙げられる

つまずきやすい点(実際に踏む所)

  • ollama serve を起動し忘れる → 接続エラー。各例は check_server() で起動手順を出して止まる。GUI 版アプリ起動中なら既に :11434 で動いていることが多い
  • モデル未 pull → サーバには繋がるが生成で失敗。ollama pull llama3.2:3b を先に
  • api_key を空にすると OpenAI SDK が怒る → ローカルでは検証されないので "ollama" 等の適当な非空文字列を渡す(_client.local_client() がやっている)
  • 小型モデルの JSON 逸脱response_format を付けても壊れた JSON が来うる。json.loads を try で囲む(ex05)。プロンプトに例を入れる/温度0/モデルを大きくするで改善
  • seed 固定でも完全一致しないことがある → 量子化・並列実装・バックエンド差で揺れる。「クラウドより決定的に寄せやすい」程度に捉える
  • extra_body={"options": {...}}num_ctx(文脈長)など Ollama 固有パラメータは OpenAI SDK の正式引数に無いので extra_body から渡す(ex04)。ただし /v1(OpenAI互換)はこの options.num_ctx を無視する(実測。上の「num_ctx 実証」参照)。本当に効かせたいならネイティブ /api/chatollama Python SDK を使う

用語集

混同しやすい語を対比で並べる。「それはローカル固有の話か、LLM 一般の話か」を毎回切り分けるのがコツ。

ランタイムと API の階層

具体例(このフォルダ)
ランタイム / サーバ モデル重みを読み込み、HTTP で推論を提供するプロセス ollama serve(:11434)。他に LM Studio(:1234)、llama.cpp の llama-server(:8080)、vLLM
モデル 重みファイル本体。ランタイムが読み込む差し替え可能な部品 llama3.2:3bqwen2.5:7bollama pull で取得
ネイティブ API そのランタイム独自の形のエンドポイント Ollama の /api/chat/api/generate/api/tags(ex01)
OpenAI 互換 API api.openai.com と同じ形(/v1/chat/completions 等)を出すエンドポイント Ollama の /v1(ex02 以降)。これがあるから OpenAI SDK が流用できる

判定: 「OpenAI SDK でそのまま叩けるか?」→ Yes なら /v1(互換)経由、No ならネイティブ /api/*

呼び出しパラメータ(LLM 一般 = クラウドでも同じ)

どこで 効く向き
temperature サンプリング分布の平坦さ ex04 0=決定的・最確、高い=多様/突飛
seed 乱数の種 ex04 固定すると(理想的に)再現する
stream 逐次返すか一括か ex03 True=トークンが流れる、False=出来てから一括
response_format 出力形式の制約 ex05 {"type":"json_object"} で JSON のみ
num_ctx 読み込む最大トークン長=「作業机の広さ」(入力+出力の合計。Ollama 固有) ex04 大=長い履歴可・メモリ食う。超過分は先頭から黙って切り捨て。/v1 経由では無視(ネイティブ /api/chat で効く)

計測語

出どころ
TTFT (Time To First Token) 最初の1トークンが返るまでの待ち ex03 で実測
tok/s (tokens per second) 生成スループット = 自分のマシンの実力 ex01(eval_count/eval_duration)、ex03
量子化 (quantization) 重みを低ビット化してメモリ削減・高速化(精度は少し落ちる) Ollama の既定タグは大抵 4bit 量子化済み

モデルのサイズと速度(B・MoE・メモリ帯域)

ローカルで「載るか・速いか」を左右する語。容量(GB)・遅延(ns)・帯域(GB/s)を別軸で切り分けるのがコツ。

効く向き
B (Billion) パラメータ数の単位。10B=100億、70B=700億。学習で得た重みの数 = 規模・賢さの目安 大=賢い/知識多いが、メモリも計算も食う
AxB(例 30B-A3B MoE(Mixture of Experts)の表記。A=Active。総30B のうち1トークンで実際に発火するのは3B だけ 総数=知識量・メモリ容量、Active=計算量・速度。両者を分離するのが MoE
メモリ帯域 (memory bandwidth) CPU/GPU↔メモリで1秒に運べるデータ量(GB/s)。道路の太さ=運搬能力 大量データを繰り返し読む処理で律速になる

判定のキモ:

  • メモリ性能は3軸: 容量(どれだけ入るか/GB=倉庫の広さ)・遅延(1回の往復/ns=1個届く速さ)・帯域(単位時間の総量/GB/s=道路の太さ)。別物。大量運搬では遅延より帯域が効く。
  • LLM 推論は「メモリ帯域律速」: トークンを1個出すたびに全重みを読み直すので、生成速度は GPU の計算力(TFLOPS)でなくメモリ帯域(GB/s)でほぼ決まる。「演算器は速いのに遅い」=帯域が詰まっている状態(compute-bound でなく memory-bound)。HBM や Apple Silicon のユニファイドメモリ帯域が売りになる理由。
  • MoE の 30B-A3B が手元で美味しい理由: メモリ容量は30B ぶん必要(どの専門家が呼ばれるか不明なので全員載せる)が、1トークンの計算・読込は3B ぶん。「30B の賢さを 3B 並みの速さで」。容量は食うが速度は軽い、の典型。量子化(重みを低ビット化)と組み合わせると容量側も削れる。

ざっくり帯域の桁: ふつうの DDR5 PC メモリ≈数十 GB/s、GPU の GDDR≈数百 GB/s〜1 TB/s、データセンタ GPU の HBM≈数 TB/s。

混同しやすいペア

  • ローカル固有 vs LLM 一般: ollama serve / pull / num_ctx / 量子化 = ローカル固有。temperature / seed / stream / messages / 構造化出力 = LLM 一般(クラウドでも同じ)。新しい挙動に出会ったらまずこの仕分け
  • /api/chat(ネイティブ)vs /v1/chat/completions(互換): 同じモデルに届くが、リクエスト/レスポンスの JSON の形が違う。ex02 以降は互換側に寄せて SDK 一本化
  • 「無料」vs「タダではない」: API 課金はゼロだが、ダウンロード容量・ディスク・VRAM/メモリ・電気代・速度を払っている。クラウドはその逆

困りごと → 見る/使うもの(クイック早見表)

困りごと 見る/使う
繋がらない ollama serve 起動 → curl localhost:11434/api/tags で疎通確認
model not found ollama pull <model>。入っているものは ollama list
遅い 小型モデル(llama3.2:1b)に変える / GPU を使う / 量子化版を選ぶ
既存 OpenAI コードをローカル化 OpenAI(base_url=".../v1", api_key="ollama") に差し替えるだけ(ex02)
JSON が崩れる response_format + 温度0 + プロンプトに例 + json.loads を try(ex05)
ローカルが頼りない 難しいタスクだけクラウドへフォールバック(ex06)
別ランタイムを使いたい OLLAMA_HOST を LM Studio(:1234) 等に向ける(OpenAI 互換なら動く)

次にやること

  • eval_basics/ で温度0+seed固定の決定性を評価に活かす
  • dspy_basics/ langchain_v1/ で ex05 の構造化出力がフレームワークの中でどう抽象化されるか見る
  • 本番サービングが要るなら vLLM(バッチ処理・高スループット)を別途調べる

作成: 2026-06-12 / 最終更新: 2026-06-27