この記事は クラウドワークス グループ Advent Calendar 2025 シリーズ3の11日目の記事です。
はじめに
Claude Codeくん普通にコードを書かせるだけならよいんだけど、リモートに投げたくないデータを扱うときが稀によくあり困ります。
Claude CodeのバックエンドのAPIエンドポイントは環境変数 ANTHROPIC_BASE_URL で曲げられるので、これをローカルLLMに差し替えればいけるのでは?と軽い気持ちで試してみたら、デフォルト設定では重すぎてパフォーマンスチューニング沼にハマってしまったので、手元のMac Book Proでの設定例やベンチマークの計測方法などの知見をまとめておきます。
GPUやメモリなど計算資源が潤沢にあるクラウド環境とは異なり、ローカル環境のような計算資源が限られたコンシューマ向けのハードウェアで動かすには多少の工夫が必要です。
特にチャットボットと比較して、コーディングエージェントは扱うコンテキスト長も長く、トークンをバカ食いするので、ローカルではプロンプトを処理するPrefillフェーズがボトルネックになりがちで、ワークロードに合わせて希少な計算資源をどうやりくりするのか検討する必要があります。
先に期待値調整ですが、当然ローカルで動かせるレベルのモデルはClaudeくんよりも頭が悪いので、精度は安かろう悪かろうというかんじです。それはそう。
とはいえリモートにデータを投げたくないかつ軽めのタスクであれば、ないよりマシぐらいの選択肢の1つとして道具箱に持っておくとよいのではなかろうか。
環境
手元の環境は以下のとおりです。
- MacBook Pro M4 CPU 14コア/メモリ48GB
- macOS: Sequoia 15.7
- Claude Code: v2.0.60
- llama.cpp: b7270
ローカルLLMの推論サーバは、細かいパラメータ調整が可能なllama.cppを使います。
1点補足しておくと、llama.cppは先日AnthropicのMessage APIのネイティブサポートがマージされたので、llama.cpp b7187以降であれば、claude-code-routerのようなプロキシは不要です。
設定
llama.cppのインストール
llama.cppをインストールします。macOSであれば、Homebrew経由でインストールできます。他のOSの場合は、公式ドキュメントを参照して下さい。
https://github.com/ggml-org/llama.cpp/blob/b7270/docs/install.md
$ brew install llama.cpp
llama.cppにはいくつかのツールが付属していますが、インストールするとllama-serverコマンドが使えるようになります。
$ llama-server --version
Claude Codeの設定
Claude Codeの環境変数に以下を設定します。
export ANTHROPIC_BASE_URL=http://127.0.0.1:8080
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export DISABLE_NON_ESSENTIAL_MODEL_CALLS=1
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1
Claude CodeのバックエンドのAPIをローカルLLMに差し替えるのに、ミニマム必要な設定は ANTHROPIC_BASE_URL です。llama-serverのポート番号はデフォルト8080です。
その他、いくつか設定しておいた方がよさそうなものがあるので補足しておきます。
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC は必須ではない通信を無効化します。具体的には DISABLE_AUTOUPDATER, DISABLE_BUG_COMMAND, DISABLE_ERROR_REPORTING, DISABLE_TELEMETRY をまとめて設定します。
ローカルLLMのユースケースでは、意図せず外部に情報が送信されないように無効化しておくのが無難でしょう。
DISABLE_NON_ESSENTIAL_MODEL_CALLS は必須ではない推論リクエストを抑止する設定です。普段あんまり意識していないと思いますが、通信を曲げて観察していると、メインのセッションのプロンプトとは別に、軽いHaikuを使って雑多な推論リクエストが裏で飛んでいます。ローカルではSonnetとHaikuのようにサイズの異なる複数のモデルを同時に読み込むのはメモリ的に厳しく、計算資源は希少なので、必須ではないものは抑止しておくとよいでしょう。
CLAUDE_CODE_DISABLE_TERMINAL_TITLE は現在のセッション内容をもとにターミナルのタイトルを設定するものです。これも無駄な処理なので無効化しておきます。
(参考) Claude Codeの公式ドキュメント
https://code.claude.com/docs/en/settings
llama-serverの設定
llama.cppでは推論サーバとしてllama-serverコマンドが提供されています。
ドチャクソ大量のオプションが生えているので、詳細は公式ドキュメントを参照して下さい。
(参考) llama-serverの公式ドキュメント
https://github.com/ggml-org/llama.cpp/blob/b7270/tools/server/README.md
ここではClaude Codeのバックエンドとして使う場合に検討すべきオプションを説明します。
以下は設定例ですが、使える計算リソースに合わせて、適宜パラメータを調整して下さい。
トークン処理速度などベンチマークの計測方法については後述します。
$ llama-server -hf ggml-org/gpt-oss-20b-GGUF \
--jinja \
-fa on \
-ngl 99 \
--no-mmap \
-b 2048 \
-ub 2048 \
-np 4 \
-c 262144 \
--kv-unified \
--ctx-checkpoints 32 \
--swa-full \
-cram -1
※上記のコマンドは、初回実行時にモデルファイル(12GB)のダウンロードが発生します。
-hf はHugging Faceのリポジトリからモデルを読み込みます。ここではOpenAIがApache 2.0ライセンスで公開しているGPT-OSS 20Bをllama.cppで読み込めるGGUF形式に変換した ggml-org/gpt-oss-20b-GGUF を使っています。GPT-OSSは寛容なライセンスでユースケースの汎用性も高く、MoE(Mixture of Expert)アーキテクチャかつMXFP4で量子化されているのでトークン処理速度が速く、本稿執筆時点でローカルLLMで動かすにはバランスのよい選択肢です。
モデルの選択は重要な検討事項ですが、モデルの重み以外にコンテキスト長に比例してKVキャッシュなど中間データの容量も必要なので、そのへんも考慮して検討する必要があります。コーディングエージェント用途であれば、トークン処理速度とのバランスで、実際に利用可能なメモリ容量の半分以下に収まるモデルを選ぶとよいかなと思います。
--jinja はモデルファイルに含まれるチャットテンプレートを読み込む設定です。モデルごとに最適化されたものを使ったほうがよいので、有効化しておきます。
-fa はFlash Attentionを有効化します。Flash Attentionはざっくり言うと、計算順序を入れ替えて高速化する手法です。デフォルトがautoでデバイスとモデルが対応していれば自動で有効化されるので、明示するのは必須ではない気もしつつ、一応明示しておきます。
-ngl はGPUに乗せるモデルのレイヤ数です。レイヤ数はモデルのアーキテクチャによりますが、本稿執筆時点で代表的なオープンウェイトモデルはだいたい数十なので、全部乗っける場合は慣例的に99のように指定します。GPUメモリが足りない場合はここで調整可能ですが、処理速度が下がるので、優先度的には後述するKVキャッシュを先にあきらめて、重みはなるべくGPUメモリに載せれる範囲のモデルを使うのがよいかなと思います。
補足として、AppleシリコンのMacの場合は、Unified Memory Architecture (UMA)を採用しており、CPUとGPUでメモリ容量は共有されていますが、割り当てた領域をCPUで扱うのかGPUで扱うのかは区別されています。なので可能な限りGPUメモリとして割り当てた方がトークン処理速度は上がります。一方でCPUで扱うメモリは不足した場合はスワップに逃がすことができるので、メモリが足りない場合はCPUのメモリとして割り当てることでとりあえず動かすことはできますが、処理速度は下がります。
--no-mmap はモデル全体をメモリに読み込みます。デフォルトだと必要なタイミングで読み込む動作になるので、GPUメモリが足りない場合はここでも調整可能です。
-b と -ub は処理するバッチサイズで、bが論理的なバッチサイズで、ubが物理的なバッチサイズです。ここでいうバッチサイズとは並列リクエストの話ではなく、プロンプトのトークンを処理するマイクロバッチサイズです。論理バッチサイズはGPUデバイスが複数ある場合には意味がありますが、Mac BookのようにGPUデバイスが1つしかない場合は、論理バッチサイズと物理バッチサイズは同じでよいでしょう。最適な値はベンチマークを眺めながら適当に調整します。
-np は並列で処理するリクエスト数です。デフォルトは1で、ローカルLLMだと自分しかいないので1でよいと勘違いしがちですが、Claude Codeはサブエージェントが立ち上がったり、バックグラウンドで雑多なリクエストが飛んだりするので、メモリが許せば1より大きな値を指定した方がよいです。ただ並列で処理するリクエスト分KVキャッシュのメモリを食うので、ローカルであまり大きな値にするのも現実的ではありません。
-c はコンテキスト長ですが、いくつか補足が必要です。特に -np に1以外を指定した場合は、各リクエストが使えるコンテキスト長はc/npになります。たとえばコンテキスト128kを2並列に処理する場合は、cには262144(=256k)を指定する必要があります。これは内部実装のデータ構造の都合だと思いますが、ドキュメントからは自明ではない罠なので注意して下さい。
また本稿執筆時点で最新のClaude 4.5のコンテキスト長は200k or 1mですが、GPT-OSSなどオープンウェイトモデルの多くは128kしかありません。一応Qwen3-Coderなど256kあるモデルもありますが、ローカルだとプロンプトを処理するPrefillフェーズがボトルネックになりがちなので、プロンプトが64kを超えてくるとだいぶ体感遅く感じます。従って、ローカルで200k使うのはあまり現実的ではないです。とはいえ、Claude Codeはシステムプロンプトを読み込んだだけで16kぐらいありますし、CLAUDE.mdなどのメモリファイルやMCPのtool定義なども読み込むとそれだけで20〜30kぐらいにはなりがちです。普通に使ってるとすぐに64kぐらいは超えます。以上のような制約を考慮すると、128kモデルを実効32〜64kトークン前後で運用するようにコンテキストを管理するというのが現状の落とし所かなと思ってます。ローカルで使う場合は、なるべく無駄なファイルやMCPを読み込まないようにしたり、こまめにセッションを分けたりするなどの工夫が必要です。
さらに補足として、Claude Codeの現在のコンテキスト長は /context コマンドでも確認できますが、ccusageなどをstatuslineに仕込むと現在のコンテキスト長が常時確認できて便利です。
--kv-unified はリクエスト間でKVキャッシュを共有します。-np に1以外を指定した場合は合わせて指定しておくとよいでしょう。
--ctx-checkpoints は -np あたりのキャッシュ数です。現状llama.cppではプロンプトの意味のある境界ではなく末尾にしかチェックポイントを打てないようなので、会話のターンごとにしかチェックポイントが打てませんが、サブエージェントはコンテキストが別だったりすることを考慮すると、デフォルトの8だとちょっと小さいかなと思います。キャッシュヒット率を上げるために適当に大きくしておくとよいでしょう。
--swa-full はSWA(Sliding Window Attention)のKVキャッシュをウィンドウ内だけではなくフルサイズで保持する設定です。GPT-OSSなど一部のモデルは、ロングコンテキストの計算コストを下げるためにスライディングウィンドウを使ってコンテキスト全体を参照しません。デフォルト設定だとこのウィンドウ内のコンテキストのKVキャッシュしか保持せず、コンテキスト長が長くてもメモリ消費が抑えられますが、結果としてプロンプトをまたいでキャッシュが再利用できません。やたらとキャッシュミスが発生するのでなんだろうと思って調べていたら気づいたんですが、これは気づかないとだいぶ罠っぽいのでデフォルトで有効にしておいて欲しい気がします。
-cram はKVキャッシュのサイズです。デフォルト8GBですが、-1を指定すると無制限になります。ローカルだとPrefillフェーズがボトルネックになりがちで、キャッシュミスが致命的なので、メモリの許す限りキャッシュを保持したいところです。ただ現状は内部実装のデータ構造の都合か、合計キャッシュトークン数がコンテキストサイズcを超えると古いキャッシュが捨てられる挙動をしているように見えるので、無制限に設定しても実用上は問題なさそうです。
その他、GPUメモリが足りない場合に検討すべきオプションをいくつか補足しておきます。
-nkvo はKVキャッシュをGPUメモリに載せない設定です。もちろんGPUメモリに載せた方が速いので、悩ましいところではありますが、優先度的にモデルの重みにGPUメモリを割り当てたかったり、コンテキスト長を長めに取りたかったりで、総合的に判断してあえて載せないというのもありかなというかんじです。
--n-cpu-moe はCPU側で処理するMoEのレイヤ数です。MoE系のモデルは合計のパラメータ数が大きくなりがちなので、GPUメモリに乗り切らない場合はCPU側で処理することができます。当初もしかして遊んでるCPUリソースも活用できて負荷分散になるかと思ったのですが、試した感じトークン処理速度は下がったので、あくまでもモデルがメモリに乗り切らない場合の回避策というかんじです。
-t はCPUのスレッド数です。これも試したところできるだけGPU側で処理した方が速かったのでデフォルト値のままにしていますが、CPU側のリソースも積極的に使うのであれば検討すべきでしょう。Appleシリコンはハイパースレッディングを使っていたり、パフォーマンスコアと効率性コアの2種類が混じってたりするので、ベンチマークの結果を眺めながら適宜調整して下さい。
-ctk と -ctv はKVキャッシュを量子化する設定です。KVキャッシュを量子化するとメモリが節約できますが、精度は低下します。また量子化すると計算コストが下がりそうに思いますが、実際のところ実行時に量子化処理自体の計算コストが増えるので、ベンチマークを取ると処理速度は下がりました。ただメモリが足りない場合には検討の余地はありそうです。
ベンチマークの計測方法
llama.cppにはベンチマーク用のツール llama-bench が付属しています。
以下は実行例です。
$ time llama-bench \
-m ~/Library/Caches/llama.cpp/ggml-org_gpt-oss-20b-GGUF_gpt-oss-20b-mxfp4.gguf \
-fa 1 \
-ngl 99 \
-b 2048 \
-ub 2048 \
-p 2048,4096,8192,16384,32768,65536
ggml_metal_device_init: tensor API disabled for pre-M5 and pre-A19 devices
ggml_metal_library_init: using embedded metal library
ggml_metal_library_init: loaded in 0.012 sec
ggml_metal_device_init: GPU name: Apple M4 Pro
ggml_metal_device_init: GPU family: MTLGPUFamilyApple9 (1009)
ggml_metal_device_init: GPU family: MTLGPUFamilyCommon3 (3003)
ggml_metal_device_init: GPU family: MTLGPUFamilyMetal3 (5001)
ggml_metal_device_init: simdgroup reduction = true
ggml_metal_device_init: simdgroup matrix mul. = true
ggml_metal_device_init: has unified memory = true
ggml_metal_device_init: has bfloat = true
ggml_metal_device_init: has tensor = false
ggml_metal_device_init: use residency sets = true
ggml_metal_device_init: use shared buffers = true
ggml_metal_device_init: recommendedMaxWorkingSetSize = 38654.71 MB
| model | size | params | backend | threads | n_ubatch | fa | test | t/s |
| ------------------------------ | ---------: | ---------: | ---------- | ------: | -------: | -: | --------------: | -------------------: |
| gpt-oss 20B MXFP4 MoE | 11.27 GiB | 20.91 B | Metal,BLAS | 10 | 2048 | 1 | pp2048 | 935.83 ± 1.51 |
| gpt-oss 20B MXFP4 MoE | 11.27 GiB | 20.91 B | Metal,BLAS | 10 | 2048 | 1 | pp4096 | 902.72 ± 1.62 |
| gpt-oss 20B MXFP4 MoE | 11.27 GiB | 20.91 B | Metal,BLAS | 10 | 2048 | 1 | pp8192 | 842.48 ± 1.84 |
| gpt-oss 20B MXFP4 MoE | 11.27 GiB | 20.91 B | Metal,BLAS | 10 | 2048 | 1 | pp16384 | 742.17 ± 2.26 |
| gpt-oss 20B MXFP4 MoE | 11.27 GiB | 20.91 B | Metal,BLAS | 10 | 2048 | 1 | pp32768 | 587.98 ± 2.32 |
| gpt-oss 20B MXFP4 MoE | 11.27 GiB | 20.91 B | Metal,BLAS | 10 | 2048 | 1 | pp65536 | 400.37 ± 8.24 |
| gpt-oss 20B MXFP4 MoE | 11.27 GiB | 20.91 B | Metal,BLAS | 10 | 2048 | 1 | tg128 | 71.79 ± 0.14 |
build: 87a2084c4 (7270)
llama-bench -m -fa 1 -ngl 99 -b 2048 -ub 2048 -p 38.86s user 1.61s system 2% cpu 25:57.81 total
llama-serverで指定可能なオプションとは完全には一致していませんが、いろいろパラメータを調整しながらトークン処理速度を計測することができます。
(参考) llama-benchの公式ドキュメント
https://github.com/ggml-org/llama.cpp/tree/b7270/tools/llama-bench
ここでは、簡単に使い方を説明します。
-m はモデルファイルのパスです。-hf が使えませんが、ダウンロードされたモデルファイルは、macOSの場合は、 ~/Library/Caches/llama.cpp/ 以下に保存されているので、ggufファイルを指定します。
-p はプロンプト長です。カンマ区切りで複数指定すると、それぞれのプロンプト長でベンチマークが実行されます。ここでは2048, 4096, 8192, 16384, 32768, 65536トークンでベンチマークを取っています。
結果の見方ですが、右端のt/s列が1秒あたりのトークン処理数を表しています。
Transformerのアーキテクチャでは、プロンプトを処理するPrefillフェーズと、トークンを生成するDecodeフェーズでパフォーマンス特性が異なりますが、test列でppがPrefillで、tgがDecodeを表しています。
この結果から、プロンプト長が長くなると、Prefillの処理速度が低下することが分かります。例えば手元の環境では、2kのプロンプトを処理するのには2秒ぐらいですが、64kだと単純に32倍の64秒ではなく、164秒かかる計算になります。プロンプトのキャッシュミスがアプリケーションの体感スピードに大きく影響することが分かります。
この例では -p をカンマ区切りで値を変化させて実験していますが、他のパラメータでも同様にカンマ区切りで値を変化させて実験できるので、いろいろパラメータを変えながらベンチマークを取ってみるとよいでしょう。特にコーディングエージェント用途では、Prefillがボトルネックになりがちなので、Prefillの処理速度を重視して各種パラメータを調整するとよいでしょう。
リソース消費を確認するには、macOSの場合は、アクティビティモニタでCPUとGPUの使用率やメモリプレッシャーが見えるので、適宜リソース消費量を確認しながらパラメータをいじるとよいでしょう。
ターミナル上で確認したい場合は、CPUはhtop、GPUはnvtopなどを使うとよいでしょう。
llama-benchはモデルやハードウェアの処理性能を比較するのに便利ですが、コーディングエージェントのユースケースのような、プロンプトをまたいだキャッシュや、並列リクエストを考慮したユーザ体験までは計測できません。そのへんは実際にClaude Codeを動かしながら、llama-serverのログやメトリクスを眺めて地道に動作を確認する必要があります。
おわりに
Claude CodeをローカルLLMで動かすことは技術的には可能なものの、所感としては精度は安かろう悪かろうで、何よりも速さが足りないです。Claudeくんは賢くて速かったことを改めて実感するなど。
とはいえ、精度と速さは技術の進歩で時間が解決してくれそうな気もするので、個人的には楽観しています。まぁその頃にはもっと要求水準が上がっているというオチはありそうですが。
データをどうしてもリモートに投げたくないときとか、寒い冬にGPUを回して暖を取りたいときに試してみて下さい。