値上げが止まらないPCパーツは今のうちにAmazonでチェックして確保してしまうのが吉♪

FreeTokenでGTX1070 8GB!PascalでもQwen3.6 35B動作!ローカルLLM術

※ 記事中にプロモーション広告(アフィリエイトリンク)が含まれます。

管理人の小職さん管理人の小職さん

自宅のProxmoxサーバーの中のVMに、GTX 1070 8GBを入れてローカルAIサーバを作りました。2016年発売の古いGPUですけど、こんな風にまだまだ踊ってくれるんですよ!
ただし、35BモデルをぜんぶGTX 1070だけで処理しているわけではありません。CPU、システムRAM、PCIe、推論エンジンの役割分担がキモなんです。

ロボ小職ロボ小職

自宅のProxmoxサーバーで余っていたGeForce GTX 1070 8GBを、Ubuntu ServerのVMへPCIパススルーしました。そこにPascal世代対応のFreeToken-Pascalをぶち込み、Qwen3.6 35B A3B NVFP4モデルをOpenAI互換APIとして起動しています。

さらにWindows版OpenCodeから、SSHトンネル経由で接続。ファイル調査から編集、コマンド実行、tool calling、WebFetch、WebSearch、自動Compactionまで、一通り動くローカルAI開発環境に仕上がっています。

最初からスムーズに動いたわけではありません。Pascal向けJITカーネル、データ型の不一致、MoEキャッシュ設定、tool-call parser、reasoning parser、コンテキスト超過、日本語の文字化け…。トラブルは数え切れず、全部を一つずつ切り分けて対処しました。

注意:この記事で紹介するユーザー名、IPアドレス、ファイルパスは構成例です。ご自身の環境に合わせて変更してください。また、この記事はGeForce GTX 1070で35B級モデル全体をVRAMへ載せたという内容ではありません。GPU、CPU、システムRAM、PCIe、FreeToken-Pascalを組み合わせたハイブリッド構成です。

※この記事の公開後も検証を続けたため、「実測結果」の直後に後日談を追記しています(コンテキストを65,536トークンへ拡張、CUDA Graphで速度が約2倍になった件)。

GTX 1070でQwen3.6 35Bが動いた

VM300上のFreeToken-Pascalで、Qwen3.6 35B A3B NVFP4モデルをAPI Readyまで起動できました。ここが一番の通過点です。

ロボ小職の豆知識
MoEモデルは、入力ごとに一部のExpertだけを計算します。35Bという規模でも毎回ぜんぶ使うわけではないので、8GBのVRAMでも動かせる余地が生まれるんです。
ニューラルネットワークのモデル構造図
MoEもニューラルネットワークの一種。ぜんぶをVRAMへ載せなくても、一部のExpertだけを動かせるのがいいところです。(画像提供: Wikimedia Commons・CC0・Mikael Häggström)
項目 確認結果
GPU NVIDIA GeForce GTX 1070 8GB
GPU世代 Pascal、sm_61
仮想化基盤 Proxmox VE
AIサーバー Ubuntu Server VM300
推論エンジン FreeToken-Pascal
モデルID qwen3.6-35b-a3b-nvfp4-candidate
コンテキスト 32,768トークン(後日談で65,536へ拡張)
APIポート 8082
クライアント OpenCode 1.18.32
tool calling OpenAI互換の構造化tool_callsを確認
WebFetch 成功
WebSearch 成功
自動Compaction 有効
SSHトンネル ログオン時自動起動、切断時自動再接続
短い直接API 約2.36秒から4.01秒
短いストリーミングAPI 約2.35秒で完了

ただ、OpenCodeでの処理時間は、直接APIの応答時間とは全然違います。システム指示、ツール定義、ファイル内容、Web取得結果、会話履歴まで全部が入力に乗るからですね。初回Prefillやキャッシュミスのときは、結果が返るまで数分待たされることもあります。

管理人の小職さん管理人の小職さん

直接APIは2秒台なのに、OpenCodeだと数分かかることもあるんですね。同じモデルだと思ってました?
モデルは同じでも、入力に含まれるものがまったく違うんですよ。ツール定義、ファイル内容、Web取得結果、会話履歴まで乗ると、Prefillの負荷は跳ね上がります。

ロボ小職ロボ小職

この記事に登場する4つの役割

今回の構成では、役割をこう分けています。ごちゃ混ぜにするとどの数値がどの環境のものか分からなくなるので、最初にすっきり整理しておきますよ。

  • FreeToken-Pascal(現在の推論エンジン):Pascal世代のGTX 1070でFreeTokenを動かすためのforkです。VM300では今はこのエンジンでQwen3.6を常駐させています。
  • llama.cpp(一時的な退避構成):FreeToken-Pascalの問題を切り分けている間に、動作確認とOpenCode接続を進めるために使いました。今は使っていないので、勘違いしないでくださいね。
  • OpenCode(クライアント):Windows側でリポジトリを調査・編集するAIコーディングクライアントです。FreeToken-PascalのAPIへSSHトンネル経由でつながります。
  • SSHトンネル(接続経路):WindowsからVM300のAPIへ安全に届く経路です。APIポートをLANへ出していません。

今回完成したVM300の構成

Windows PC
├─ OpenCode 1.18.32
├─ freetoken-ops
├─ AGENTS.md
├─ WebFetch / WebSearch
└─ 127.0.0.1:8082
        │
        │ SSHトンネル
        ▼
Ubuntu Server VM300
├─ FreeToken-Pascal
├─ freetoken-qwen36.service
├─ Qwen3.6 35B A3B NVFP4
├─ GTX 1070 8GB
├─ CPU / システムRAM
└─ 127.0.0.1:8082

Windows側からVM300の8082番ポートをLANへ直接出さず、SSHトンネルを使っています。OpenCodeから見えた接続先はWindows自身の127.0.0.1:8082。一見どこにも見えない接続ですね。

Pascal世代GPUで挑戦した理由

GTX 1070はPascal世代のGPUです。最近のRTXシリーズと違ってTensor Coreはありません。しかも最近のCUDAカーネルにはVolta以降を前提にした実装が混じっているので、「CUDA対応GPUなら最新AI推論エンジンが動く」とはいかないんです。

まずは、今回PCIパススルーで割り当てたのと同じ世代のカード、顔を見ておきましょう。

GeForce GTX 1070 Founders Edition の外観(デュアルスロットのリファレンスカード)
GeForce GTX 1070 Founders Edition。今回PCIパススルーでVMへ割り当てたのと同じ世代のカードです。(写真: Evan-Amos / Wikimedia Commons、パブリックドメイン)

写真は、8GBのVRAMを積んだPascal世代のリファレンスカードです。今回の構成では、このVRAMに載る範囲をGPUが引き受け、載りきらないExpertの計算はCPUとシステムRAMが受け持ちます。得意なところ同士で分ける、いわば家事分担みたいな構成です。

FreeToken-Pascalは、FreeToken本家をPascal世代へ対応させるforkです。Tesla P100のsm_60と、GTX 10シリーズのsm_61を対象に、pre-Volta環境でもJITカーネルをコンパイルできるよう調整が入っています。

Pascal世代で引っかかるのが、CUDAの__grid_constant__です。FreeToken-Pascalではこの指定を条件付きマクロで保護して、Pascalでは互換性のない指定を外しつつ、Volta以降では本来の処理を残す設計になっています。

新しいGPU向け機能を全部削るのではなく、Pascalでコンパイルできない部分だけを世代別に切り分けている。ここがこのforkの巧さです。

今回使用した環境(VM300)

項目 内容
仮想化基盤 Proxmox VE
Proxmoxホストの物理メモリ 64GB
仮想マシン VM300
ゲストOS Ubuntu Server
VM割り当てメモリ 約27GiB
Swap 8GiB
GPU NVIDIA GeForce GTX 1070 8GB(PCIパススルー)
モデル用ストレージ 約196GB
推論エンジン FreeToken-Pascal
モデル Qwen3.6 35B A3B NVFP4(qwen3.6-35b-a3b-nvfp4-candidate)
コンテキスト 32,768トークン(後日談で65,536へ拡張)
クライアント OpenCode 1.18.32(Windows)

GTX 1070の中身、このGP104ダイです。

GeForce GTX 1070 に搭載された GP104 ダイの顕微鏡写真
GTX 1070の中身、GP104ダイの顕微鏡写真。Pascal世代・16nmのGPUであることが分かります。(写真: Fritzchens Fritz / Wikimedia Commons、CC0)

Pascal世代は16nm世代で、Tensor Coreはありません。そのぶん構成はシンプルですが、最新GPU向けカーネルをそのまま動かせるわけではないので、次に説明する世代別対応が必要になるんです。

構築手順

ここからは、最終構成に必要な操作だけをサクッとまとめます。途中の試行錯誤は、次の「つまずいた問題」の章でばらして解説しますね。

注意:以下には小職の環境で使ったパス、ユーザー名、IPアドレスが含まれます。そのままコピーせず、各自の環境に合わせて変更してください。

Step 1:Ubuntu VMを作成

今回のVM構成はこんな感じです。

(PR)


  • メモリ:約27GiB
  • Swap:8GiB
  • GPU:GTX 1070をPCIパススルー
  • モデル用ディスク:約196GB

Proxmoxホストには64GBの物理メモリがあります。そこから約27GiBをVM300へ割り当てました。モデル用のRAMをしっかり確保しつつ、Proxmoxホストと他のVMに使う分の余裕もちゃんと残しているのがコツです。

VMの作成は、Proxmox VEの管理画面から行います。

Proxmox VE 8.0 の管理画面(クラスターサマリー)のスクリーンショット
Proxmox VEの管理画面。VMの一覧やリソース使用状況をここで確認します。(スクリーンショット: Proxmox Server Solutions GmbH / Wikimedia Commons、パブリックドメイン)

管理画面では、VMの一覧とリソース使用状況がひと目で分かります。メモリ割り当てとディスク容量は後から変えにくいので、先に決めておくと作業がスムーズですよ。

Step 2:必要パッケージを導入

パッケージを更新する前に、まずは今の環境を確認しておきましょう。

apt upgradeを実行する前に、VMのスナップショットを取得してください。カーネルやNVIDIAドライバー、CUDA環境が更新されて、GPUパススルーが動かなくなる可能性があります。更新後は必要に応じて再起動し、nvidia-smiでドライバーバージョンを確認してください。

スナップショットさえあれば、問題が出たときも元の状態へポンッと戻せます。

sudo apt update
sudo apt install -y build-essential cmake ninja-build git curl wget python3 python3-venv

ビルドツールを入れたら、GPUの顔出し確認をしましょう。

nvidia-smi

GTX 1070が表示されれば、GPUパススルーもNVIDIAドライバーもバッチリ動いています。

Step 2.5:GPUパススルー設定

GPUパススルーは本番環境への影響が大きいので、詳しい手順は別記事へ分けています。ここでは要点だけ。

  • GTX 1070をVM300へPCIパススルー済み
  • GPUとAudio Functionを同じVMへ割り当てた
  • VM内でnvidia-smiを確認した
  • 実際のVM設定値:raw=true、pcie=true

詳しくは「Proxmox VE入門」の記事をどうぞ。

パススルーはホストと他のVMにも影響する操作なので、先に注意点を頭へ入れておきましょう。

GPUパススルーの設定は、Proxmoxホストと他のVMにも影響します。作業前にバックアップとスナップショットを取得して、他のVMを止めても問題ない時間帯に実施してください。

冷却対策について:GTX 1070のTDPは150W。長時間のLLM推論では温度が上がりがちです。ケース内のエアフローを整えて、冷却はちゃんと効かせておいてください。高温稼働はVRAMエラーやスロットリングのタネになります。

GTX 1070だけで150Wですが、CPUやストレージ、長時間の連続運転まで考えると、電源ユニットは多少余裕を持たせておきたいですね。

電源に余裕がないと、負荷が上がった瞬間に再起動がかかっちゃったり、GPUがスロットリングしたり。意外とここときしますよ。

PCIe帯域幅について:GTX 1070が刺さるPCIeスロットの世代と幅は、性能に直結します。たとえばGen3 x16なら片方向16GB/s、Gen2 x16なら8GB/s。ExpertデータがCPU側RAMとGPUの間を頻繁に行き来する構成なので、ここがボトルネックになることは十分あり得ます。可能ならGen3 x16スロットへ接続してください。

Step 3:モデル用ストレージ準備

sudo mkdir -p /srv/ai/freetoken-data/models
sudo mkdir -p /srv/ai/freetoken-data/hf-cache
sudo mkdir -p /srv/ai/inventory
sudo chown -R <Ubuntuユーザー名>:<グループ名> /srv/ai/freetoken-data
sudo chown -R <Ubuntuユーザー名>:<グループ名> /srv/ai/inventory

<Ubuntuユーザー名>と<グループ名>は、各自の環境に合わせて書き換えてください。

Step 4:FreeToken-Pascal導入

Pascal世代向けのforkを取得して、Python仮想環境へインストールします。

sudo mkdir -p /opt/ai/src
sudo chown -R "$USER":"$USER" /opt/ai/src
cd /opt/ai/src
git clone https://github.com/CryptoJones/FreeToken-Pascal.git freetoken-pascal
cd freetoken-pascal
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .

FreeToken-Pascalは、Tesla P100のsm_60とGTX 10シリーズのsm_61を対象に、pre-Volta環境でもFreeTokenのJITカーネルをコンパイルできるようにしたforkです。

動作を確認できたコミットは、記録しておきましょう。

git rev-parse HEAD
git status --short

回帰防止のため、動作確認済みのコミットを固定しておくに越したことはありません。

git checkout <動作確認済みコミット>

最初につまずいた問題

FreeToken-Pascalのビルドが成功しても、そこでOpenCodeで即実用、にはなりませんでした。

データ型の不一致

生成試験で、hc.pyのLinear処理にて、BFloat16入力とHalf重みのデータ型不一致エラーが出ました。

この段階ではPascal向けJITカーネルのビルドは確認できても、文章生成まで成功したとは言えない状態でした。

MoEキャッシュサイズ0の扱い

--moe-cache-size 0を明示的に指定しても、内部でmoe_cache_auto=Trueへ変換されてしまう問題も発見です。

そこで、明示的な0を正しく尊重するようにargs.pyを局所修正しました。大規模な書き換えはせず、問題が発生している条件だけをつつくように修正して、既存動作への影響は最小限。修正後は回帰テストを実施して、63件がPASSです。

一時的にllama.cppへ退避

FreeToken-Pascalの問題を切り分けている間は、llama.cppを使ってQwen3.6の動作確認とOpenCode接続を進めていました。そのため、当時の記事にはllama.cppを常用エンジンとして書いた版が残っています。

あの頃に測ったGGUFのUD-Q4_K_M、モデルサイズ約22.1GB、VRAM使用量約3.2GB、生成速度約20〜22トークン/秒、GPU温度約41度といった数値は、あくまで退避構成当時の記録です。今のFreeToken-Pascalの性能として使えないですからね。

API Readyまでの起動設定

いよいよ、FreeToken-Pascal上で次のモデルを起動します。

qwen3.6-35b-a3b-nvfp4-candidate

現在の主要設定はこんな感じです。

  • 最大シーケンス長:32,768
  • 最大出力トークン:1,024
  • Prefill分割長:2,048
  • キャッシュ方式:radix
  • tool-call parser:qwen3_coder
  • reasoning parser:qwen3
  • MoE CPU threads:10
  • MoE cache size:0
  • APIポート:8082

FreeToken-Pascalはsystemdのユーザーサービスとして運用しています。

systemctl --user status freetoken-qwen36.service

起動時は、次のログを目印にします。

Prefill warmup complete
Scheduler is idle, waiting for new reqs...
API server is ready to serve on 0.0.0.0:8082

モデル起動からAPI Readyまでは、実測で約6分30秒前後かかりました。モデルロードとPrefill warmup込みの起動時間です。ちょっと待ち時間ですよね。

API Readyが終われば、次のOpenAI互換エンドポイントが使えます。

http://127.0.0.1:8082/v1/models
http://127.0.0.1:8082/v1/chat/completions

tool callingの正常化

OpenAI互換APIから普通の文章が返るだけでは、OpenCodeのコーディングエージェントとしては物足りません。ここは用途を分けて確認しました。

tool-call parserの変更

OpenCodeがツールを動かすには、モデルの応答がOpenAI互換の構造化されたtool_callsになっている必要があります。

初期状態は、tool callがXML風の文字列のまま通常本文に混ざっていて、OpenCodeはツール呼び出しとして認識してくれませんでした。そこでtool-call parserをqwen3_coderへ変更。効きました。

直接API試験では、こう確認しています。

  • finish_reason = tool_calls
  • tool_calls_present = True
  • tool name:list_directory
  • arguments:{"path":"."}

これで、OpenCodeからbash、edit、WebFetch、WebSearchなどのツールを呼べるようになりました。

用語メモ
tool_callsは、モデルが「このツールをこの引数で呼び出す」と構造化して返す形式です。本文に文字列として書かれているだけでは、OpenCode側はツールを実行できません。だからparserの設定が効くんです。

reasoning parserの設定

初期状態では、内部推論を示す</think>などの文字列が、OpenCodeの通常本文へ紛れ込むことがありました。今はreasoning parserへqwen3を設定しています。

確定している組み合わせはこれです。

tool-call parser: qwen3_coder
reasoning parser: qwen3

これはThinking機能を全面オフにした、という意味ではありません。FreeToken側でreasoningと通常本文を分け、OpenCodeの表示へ不要な内部推論を混ぜないための構成です。

Radixキャッシュの効果

OpenCodeは、ファイル読取りやツール実行のたびにモデルを何回も呼び出します。そのたびにシステム指示、ツール定義、AGENTS.md、会話履歴などを再処理するのは、古いGPUには大変な負担です。

初期設定のキャッシュ方式はnaiveでした。これをradixへ切り替えです。

試験 所要時間 new-token cached-token
同一要求1回目 41秒 310 0
同一要求2回目 13秒 54 256

同一prefixの試験では、約68%の短縮を確認しました。68%は無視できませんよ。

用語メモ
prefixは、プロンプトの先頭から共通している部分のこと。先頭が一致していれば、その部分の計算結果をもらい読みできます。

ただし、Radixを有効にしても全部が速くなるわけではありません。OpenCodeがツール結果や会話構造を組み替えるとprefixがずれて、cached-token=0になることもあります。

管理人の小職さん管理人の小職さん

1回目が41秒で、2回目が13秒ですか。同じ内容をもう一度聞いただけで、これだけ変わるんですね。びっくりしました。
同じprefixを使い回せるかどうかが勝負です。会話の前半が変わればキャッシュは効かず、また41秒側へ戻っちゃいますよ。

ロボ小職ロボ小職

コンテキストとCompaction

OpenCodeは、あなたが入力した文章だけをモデルへ送るわけではありません。次の要素も全部入り込みます。

  • システム指示
  • エージェント定義
  • ツール定義
  • AGENTS.md
  • 会話履歴
  • ファイル内容
  • コマンド結果
  • Web取得結果

長いセッションでは、こんなコンテキスト超過が飛び出しました。

prompt is too long: 38879 tokens > 32768 maximum

現在のOpenCode設定はこれです。

  • input limit:32,000
  • context limit:32,768
  • output limit:512
  • request max_tokens:512
  • provider timeout:30分
  • chunk timeout:30分
  • automatic compaction:有効
  • compaction keep:4,000
  • compaction buffer:12,000(後日談で8,000へ調整)

自動Compactionは、長いセッションを続けるなら必需品です。ただしローカル35BモデルではCompaction自体も重くて、約6分9秒かかった例もあります。軽い動作だとは思わないでくださいね。

bufferを12,000へ増やして早めに圧縮を開始し、keepを4,000へ減らして圧縮後のコンテキストを軽くしました。それでも、無関係な作業を一つのセッションへ詰め込まず、おおむね1作業1セッションで使う方が安定します。

この記事の公開後、コンテキストを65,536トークンへ拡張したときに、bufferは8,000へ調整しました。keepは4,000のままです。広いコンテキストを使いながら、圧縮後の体積は抑えめ。この組み合わせにすることで、公開時点より1セッションを長く続けられるようになりました。詳しい経緯は後日談で書っています。

Web取得機能の追加

OpenCodeから最新の公式情報や既知URLを確認できるよう、常用エージェントfreetoken-opsへWeb機能を統合しました。

  • 既知URLの取得:webfetch
  • Web検索:websearch
  • Web権限:ask
  • Web検索有効化:OPENCODE_ENABLE_EXA=true

Webツールを使うときは、モデルが通常本文で「実行してよいですか」と二重に聞いてくる方式ではなく、Webツールを直接呼び出します。承認はOpenCodeの権限確認画面で行うので、流れがスムーズです。

次の動作を確認しました。

  • WordPress公式トップページのWebFetch
  • WordPress公式開発者向け資料のWebSearch
  • 公式サイト優先
  • 検索結果を最大3件に制限
  • Web調査時にプロジェクトファイルを変更しない

Web調査の入力がふくらみすぎないよう、次のルールも追加しています。

  • WebFetchは1ターン1 URL
  • WebSearchとWebFetchを同一ターンで連続実行しない
  • WebSearch結果は原則3件以内
  • 同じURLを同一セッションで再取得しない
  • ページ内リンクを自動取得しない
  • Web取得後は再取得せず、取得済み結果を整理する

AGENTS.mdで操作範囲を制御

参考 OpenCodeとFreeTokenの構築手順(Manjaro環境)

ローカルLLMだからといって、プロジェクト全体を無制限に調査・変更させる構成にはしていません。プロジェクトルートへAGENTS.mdを置いて、OpenCodeの操作規則をGitで管理しているんです。

主なルールはこんな感じです。

  • 作業対象は現在のプロジェクト内に限定
  • Read .による全体読取りを禁止
  • 無制限の全体走査を禁止
  • 一度に読むファイルは最大3件
  • 一度に変更するファイルは最大3件
  • ディレクトリ一覧は最大20件
  • 既存の未コミット変更を上書きしない
  • git reset、git clean、強制checkoutを禁止
  • ファイル削除は明示的な許可が必要
  • パッケージ追加・削除は許可制
  • 巨大なルール文書は全文を読まず、関連箇所だけ確認
  • Web結果を大量にコンテキストへ追加しない

関連するコミットはこちらです。

ee69fc299 Add OpenCode project operating rules
9ab941df4 Unify approved web access into OpenCode ops agent
4a837daf9 Add bounded web access performance rules

この記事でいうバイブコーディングは、AIへ無制限の操作権を渡すことではありません。対象、権限、変更量、テスト、バックアップ、ロールバックをはっきり決めたうえで、AIと対話しながら開発を進める方式です。ここが一番大事なところです。

SSHトンネルの自動起動

WindowsからVM300へつなぐため、専用のSSH鍵を作りました。

$HOME\.ssh\id_ed25519_vm300_tunnel

記事中の$HOMEは、今使っているWindowsユーザーのホームディレクトリです。フルパスで書くと、例えばC:\Users\<Windowsユーザー名>\.ssh\id_ed25519_vm300_tunnelの形になります。<Windowsユーザー名>は各自のユーザー名へ置き換えてください。

SSHの設定例はこれです。

Host vm300
    HostName <VM300_IP>
    User <Ubuntuユーザー名>
    IdentityFile ~/.ssh/id_ed25519_vm300_tunnel
    IdentitiesOnly yes
    BatchMode yes
    StrictHostKeyChecking yes
    ConnectTimeout 10
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes

BatchModeでパスワード入力なしの接続を試験して、終了コード0を確認しています。

トンネル監督スクリプトは、次へ保存しました。

$HOME\Scripts\Start-FreeTokenTunnel.ps1

監督スクリプトには、こんな機能を入れてあります。

  • 127.0.0.1:8082のSSH転送
  • 切断時の自動再接続
  • Mutexによる多重起動防止
  • /v1/modelsのヘルスチェック
  • ログ保存

ログは次へ溜まります。

$HOME\Logs\freetoken-tunnel.log

タスクスケジューラ方式も試しましたが、最後に選んだのはWindowsのStartupフォルダー方式です。StartupフォルダーのStart-FreeTokenTunnel.cmdから、監督スクリプトを非表示で起動しています。

@start "" /min powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "%USERPROFILE%\Scripts\Start-FreeTokenTunnel.ps1"

同じ監督スクリプトを二重に起動すると、ログにこんな風に残って、後発プロセスはお仕舞いになります。

Another tunnel supervisor is already running.

日本語本文とファイル名の文字化け対策

Windows PowerShellで日本語ファイルを直接読む場合は、UTF-8指定で問題なく表示できました。ところがOpenCodeのbashツール経由では、PowerShellの標準出力に入っている日本語が文字化けしてしまったんです。

これはファイルが壊れているのではなく、PowerShellからOpenCodeへ標準出力を渡す経路の文字コード問題です。日本語本文はPythonでUTF-8として読み、ASCIIだけで表せるUnicodeエスケープへ変換してモデルへ渡しています。

python -c "import json,pathlib; print(json.dumps(pathlib.Path('TARGET_FILE').read_text(encoding='utf-8')[:4000], ensure_ascii=True))"

ファイル名やディレクトリ名も、Pythonのpathlibで取得してjson.dumps(..., ensure_ascii=True)で出力します。モデルは\u958b\u767aのようなUnicodeエスケープをちゃんと日本語へ戻して読んでくれます。

文字化けした表示だけを根拠に、ファイル名の改名や文字コード変換、再保存はしないでください。かえって事態を悪くしますよ。

実測結果

短い直接API

試験 所要時間 結果
1回目 約4.01秒 OK
2回目 約2.36秒 OK

入力は17トークン、出力は2トークンでした。

ストリーミングAPI

項目 値
HTTPステータス 200
最初のデータ受信 約0.027秒
ストリーム完了 約2.346秒

この試験で、FreeTokenの基本API、SSHトンネル、ストリーミング終了処理はすべて正常と判断できます。

OpenCodeのWebFetch

WordPress公式サイトをWebFetchした試験では、最終回答まで約8分56秒かかりました。ログを見ると、WebFetch結果を受け取ったあとに次のPrefillが走っています。

2048 + 2048 + 584 = 4680 tokens
cached-token = 0

FreeTokenの短い直接APIは数秒で返るのに、OpenCodeではツール定義やWeb取得結果を含む大きな入力を処理し直すので、数分かかることがあるんです。

参考 ローカルLLM推論環境比較2026|LM Studio・Ollama・FreeTokenを実測比較

後日談:コンテキストを64Kへ拡張してわかったこと

この記事を公開したあとも、VM300のチューニングは続いていました。狙いは一つ、コンテキストを32K(32,768トークン)から64K(65,536トークン)へ広げることです。長いセッションで「prompt is too long」にイライラさせられたくないので、一度に一気ふかしではなく、32K→48K→64Kと段階的に試しました。

管理人の小職さん管理人の小職さん

この記事を書いたあとも、まだ触っていたんですね。32Kだと何が物足りなかったんですか?
長いセッションの後半で、コンテキスト超過に到達するタイミングが早すぎたんです。そこで広げつつ、速度が落ちないかもセットで確認。結果、一番効いたのがCUDA Graphでして、これが約2倍になりましたよ。

ロボ小職ロボ小職

32K→48K→64Kの段階拡張と実測

構成 実効速度 VRAM使用量
32K(この記事公開時点) 4.252 token/s 5,623 MiB
48K 4.207 token/s 5,943 MiB
48K + CUDA Graph 1 8.453 token/s 6,103 MiB
64K + CUDA Graph 1 平均7.739 token/s 6,423 MiB

速度はDecode(本文を1トークンずつ生成していく処理)の実効値です。単純にコンテキストを広げるだけだと、32Kの4.252 token/sから48Kの4.207 token/sへ、むしろわずかに落ちました。VRAMは5,623 MiBから6,423 MiBへ増えて、8GB(8,192 MiB)の範囲内ではありますが、余裕はひかえめです。

CUDA Graphが一番効いた

NVIDIA CUDAのロゴ
後日談で効果が一番大きかった、NVIDIAの技術「CUDA」のロゴです。(画像提供: Wikimedia Commons・Apache License 2.0・Nvidia)

いちばん効果が大きかったのがCUDA Graph 1です。CUDA Graphは、GPUで行う処理の手順を一度記録しておき、以降はその記録を再生して実行するNVIDIAの仕組み。毎回処理の呼び出しを組み直す手間が減るので、繰り返しの多い推論に向いています。48K構成でCUDA Graph 1を有効にしたら、4.207 token/sが8.453 token/sへ、約100.9%の改善。32Kから64Kへ広げたあとの平均7.739 token/sは、CUDA Graphなしの時代(4.252 token/s)のおよそ1.8倍です。コンテキストを広げたのに速くなるって、こういうことなんです。

Prefill・Offload・MoEキャッシュの結果

併せて3つ試しました。結論から言うと、効いたのはCUDA Graphだけでした。

  • Prefillの2,048対4,096:改善は約0.40%で、ほぼ誤差の範囲でした。Prefillは入力文をまとめて読み込む処理で、この構成ではボトルネックになりにくいと判断できます。
  • Offload:不採用にしました。VRAMに載せきれない部分をCPUとシステムRAMへ移して処理する方式ですが、有効にして15分待っても実Readyにならず、結局CPU側へ戻しました。スタートアップが安定しない状態で常用する意味はないためです。
  • MoEキャッシュの自動設定:実効512で有効にしましたが、速度の変化はありませんでした。
ロボ小職の豆知識
用語をここでまとめておきます。Prefillは入力文をまとめて読み込む処理、Decodeは1トークンずつ生成する処理。OffloadはVRAMに載せきれない部分をCPU/RAMへ逃がす技術。NVFP4はNVIDIA GPU向けの4bit浮動小数点量子化形式で、モデルIDの「nvfp4」がこれに当たります。Active parametersはMoEモデルが1回の生成ごとに実際に計算に使うパラメータ数で、Qwen3.6 35B A3Bの「A3B」はActive 3Bという意味です。

Ready判定は/v1/modelsではなくChat Completions基準

もう一つ変わったのが判定方法です。以前は/v1/modelsが応答したタイミングでReadyと見なしていましたが、これは実際の生成準備が終わるより早いらしいんです。実際に1往復させるChat Completionsを基準にしたら、32K構成で約330秒、40K構成で約390秒かかることが判明(Ready判定の試験は32K構成と40K構成で実施しています)。「APIが応答した=すぐ使える」ではないので、スクリプトで起動を監視するなら生成テストまで待つのが確実です。

Windows側のOpenCodeも64Kへ同期

VM300側だけでなく、WindowsとVM105のOpenCode設定も64Kへ揃えました。

context:        65536
input:          63488
output:         2048
body.max_tokens: 2048

compaction: auto = true
             keep = 4000
           buffer = 8000(変更前 12000)

コンテキスト上限を広げたら、入力側の上限(63,488)と出力側の上限(2,048)も合わせて調整しないとダメです。片方だけ変えても、結局はその上限で止まりますよ。bufferは変更前の12,000から8,000へ、keepは4,000据え置きです。

なお、opencode --auto を使う場合は注意してください。自動実行モードはコンテキストが大きい状態でも作業を続けやすい反面、Compaction(会話履歴の自動圧縮)が走るタイミングも長くなります。重要な操作を伴う作業では、途中の様子を確認しながら進める方が安全です。

管理人の小職さん管理人の小職さん

64KにするとVRAMはどれくらいになるんですか? GTX 1070、8GBしかないんですよね。
6,423 MiBでした。8GB(8,192 MiB)の範囲には収まっていますが、残りは1,700 MiBほど。CPU側へ逃がすOffloadも検討しましたが、先ほど書いた通り安定して起動しないので不採用です。ここまで来たら、むしろ設定ファイルを必ずバックアップして、戻せる状態にしておきます。

ロボ小職ロボ小職

バックアップとロールバック

設定を変える前に、設定ファイル本体とOpenCode側の設定をコピーして日付を付けて残しています。元に戻したいときは、このコピーを戻すだけで32K時代へ復元できます。VMのスナップショットを取ってから1項目ずつ変更する、この流儀は記事本編で行ったときと同じです。変更項目が多い日ほど、バックアップの日付は細かくしています。

以上が公開後の追加チューニングです。古くてもPascal世代のGPUで、コンテキストを広げた状態でも無理なく動かせました(詳しい値は各表のとおり)。次の「確認できたことと未確認のこと」では、この後日談も含めて、確定したこととまだ分からないことを整理しています。

確認できたことと未確認のこと

今回の検証で確定したことと、断定できないこと、はっきり分けておきます。

区分 内容
確定 FreeToken-Pascalのビルド成功、Pascal向けJITカーネルのコンパイル確認
確定 Qwen3.6 35B A3B NVFP4のAPI Ready到達、OpenAI互換エンドポイントの応答
確定 OpenAI互換の構造化tool_calls、WebFetch、WebSearch、自動Compactionの動作
確定 Radixキャッシュによる同一prefixの短縮、ローカル修正後の回帰テスト63件PASS
確定 SSHトンネルのログオン時自動起動と、切断時の自動再接続
未確認 NVFP4モデルの入手元と取得手順の詳細(環境により異なるため、本記事では確定情報として示していません)
未確認 各Expertの配置、Active Expertの演算場所、CPUとGPU間の実転送量、レイヤーごとの配置
未確認 他環境での生成速度。CPU、メモリ帯域、PCIe、量子化形式、モデル構造に依存します

速い処理と遅い処理

今回の環境で、わりと速い処理です。

  • 短い直接API
  • 短いストリーミング
  • 同じprefixを再利用できる処理
  • 対象ファイルが明確な小規模調査
  • 短いGit状態確認

時間のかかりやすい処理です。

  • 初回Prefill
  • キャッシュが一致しない大きな入力
  • WebFetch結果の再投入
  • 長い会話のCompaction
  • プロジェクト全体の無制限走査
  • 大量ファイルの一括読取り

「GTX 1070で35B級モデルが動くこと」と「すべてのAIコーディング処理が数秒で終わること」は、まったく別ものです。

GTX 1070で35Bモデルを使う際の制約

  • 35B全体を8GB VRAMへ載せているわけではない
  • CPU性能とメモリ帯域の影響を受ける
  • PCIeリンク速度やスロット配置の影響を受ける
  • 量子化形式とモデル構造に依存する
  • 同じGTX 1070でも同じ性能になるとは限らない
  • 長いコンテキストはPrefill負荷が大きい
  • Compactionにも推論時間が必要
  • 電力、ハードウェア、ストレージ、保守コストは必要

FreeTokenはソフトウェア利用料をゼロで済ませる構成は作れますが、ローカル推論全体が完全無料という意味ではありません。電気代も保守の手間もありますからね。

処理速度を左右するのはGPUだけではありません。VRAMに載りきらないExpertの計算はCPU側が担当するので、CPUのコア数とメモリ帯域も体感速度に効いてきます。

とはいえ、CPUを強化してもVRAMとPCIeの制約は残ります。まずは今の環境で、どこがボトルネックなのかをのぞいてみるのがおすすめです。

向いている用途と向いていない用途

向いている用途

  • 指定ファイルの調査
  • 小規模なコード修正
  • Git差分の確認
  • 対象を限定したテスト
  • 公式仕様のWeb検索
  • 既知URLの確認
  • WordPress、SEO、運用を横断した相談
  • ローカル環境へコードや情報を残したい作業

向いていない用途

  • プロジェクト全体の無制限走査
  • 大量URLの一括取得
  • 大規模Webクロール
  • 複数の無関係な作業を一つのセッションで継続
  • 最新GPUと同等の応答速度を要求する用途
  • 時間制限の厳しいリアルタイム処理

Qwen3.6を選んだ理由

最初はQwen3-Coder-30B-A3Bを使いました。明確なコード修正やファイル検索には使えますが、しばらく使うと、小職の用途には少し融通が利かないと感じてきたんです。

  • 明確なコード変更:比較的得意
  • 曖昧な相談:やや苦手
  • 日本語の細かい意図:伝わりにくいことがある
  • 複数案の比較:一つの方針へ固執することがある
  • WordPress、SEO、多言語、運用の横断:やや不得意

小職が求めていたのは、コードを書くだけのモデルではありません。たとえば、こんな相談です。

「WordPressサイトを速くしたい。ただし、SEO、多言語ページ、既存デザインは壊したくない。プラグインを増やすだけでなく、サーバー、テーマ、画像、キャッシュまで含めて考えたい。」

Qwen3.6へ切り替えると、この依頼をぐっと整理して返してくれました。バイブコーディングって、コード生成能力だけでなく、ちゃんと話が通じることが大事なんです。その点、今回の用途ではQwen3.6の方がずっと扱いやすいです。

Proxmoxへ64GB積んだ意味

自宅のProxmoxサーバーへ64GBのメモリを積んだときは、複数のVMを余裕を持って動かすのが主な目的でした。ところがローカルLLMを始めたら、このメモリが急に価値ある資源に変わります。

DDR4 SDRAMメモリモジュールの写真
ローカルLLMで最初に効いてくるのはメモリ。VRAMに載りきらないExpertをCPU側で受け持つので、RAMが増えるほど扱えるモデルに余裕が出ます。(画像提供: Wikimedia Commons・CC BY-SA 4.0・ElooKoN)
  • Proxmoxホスト:64GB RAM
  • Qwen3.6用のUbuntu Server VM300:約27GiB RAM
  • GPU:GTX 1070 8GB

メモリは、ローカルLLMでいちばん先に効いてくるパーツです。VRAMに載りきらないExpertをCPU側へ逃がす構成では、システムRAMの容量がそのまま「扱えるモデルの余裕」につながります。

増設する場合は、マザーボードの対応規格とスロット数、そしてホスト側に残す分まで含めて確認してくださいね。

VMへ割り当てるメモリを増やしすぎると、Proxmoxホストと他のVMが使う分が足りなくなります。全部をLLMへ渡してホストがSwap地獄になったら本末転倒です。余裕を残した割り当てを維持してください。

GPUを最新製品へ買い替えるのではなく、既存のProxmoxサーバー、余裕のあるRAM、古いGTX 1070を組み合わせて、新しい用途を作ることができました。

ローカルAIサーバについてのよくある質問

読者さんからよく届く質問を、ここにまとめておきますね。

8GB VRAMで35Bモデルが動く理由は?

管理人の小職さん管理人の小職さん

GTX 1070のVRAMは8GBしかない。それなのに35Bのモデルが動くなんて、最初は半信半疑でした。どういう仕組みなんですか?
35BのぜんぶをVRAMへ載せているわけではありません。MoEモデルは毎回すべてのパラメータを使わず、選ばれたExpertだけを計算します。そこに量子化とCPUオフロードを組み合わせて、GPUへ収まらない部分はシステムRAM側で引き受けているんです。

ロボ小職ロボ小職

実際の生成速度はどのくらい?

管理人の小職さん管理人の小職さん

OpenCodeから使うとき、待ち時間はどのくらいですか?
短い直接APIなら約2.36秒から4.01秒で返ります。一方OpenCodeは、ツール定義、ファイル内容、Web取得結果まで全部入力に含むので、初回Prefillやキャッシュミスだと数分待たされることがあります。WebFetchの試験では約8分56秒かかった例も…あります。

ロボ小職ロボ小職

llama.cppの数値は今も使える?

管理人の小職さん管理人の小職さん

以前の記事にあった20〜22トークン毎秒って、今の環境でも同じ数字ですか?
あれはllama.cppへ一時退避していたときの値です。今は常用エンジンがFreeToken-Pascalなので、そのまま流用はできません。当時の記録として読んでくださいね。

ロボ小職ロボ小職

64Kへ拡張したら速度は落ちませんか?

管理人の小職さん管理人の小職さん

コンテキストを64Kに広げたら、速度はどっちに転びますか?遅くなるなら怖くて手を出せないんですよね。
落ちません。むしろ速くなっていますよ。32K時代は4.252 token/sでしたが、64K + CUDA Graph 1の構成では平均7.739 token/s。コンテキストを広げても速度が落ちるとは限らないので、拡張そのものを避ける理由にはなりません。

ロボ小職ロボ小職

CUDA Graphとは何ですか?

管理人の小職さん管理人の小職さん

ところでCUDA Graphって、よく聞くけど一体どんな技術なんですか?
GPUで行う処理の手順を一度記録しておき、以降はその記録を再生して実行するNVIDIAの仕組みです。毎回呼び出しを組み直す手間が減るので、推論みたいな同じ処理の繰り返しに効きます。今回の試験では約100.9%の改善で、公開後のチューニングで一番効いたのはこの子でした。

ロボ小職ロボ小職

VRAMの使用量はどれくらい増えますか?

管理人の小職さん管理人の小職さん

64KにするとVRAMはどれくらい食うんですか? GTX 1070、8GBしかないんですよね。
32K構成の5,623 MiBから、64K + CUDA Graph 1で6,423 MiBへ増えました。8GB(8,192 MiB)の範囲内ですが、残りは1,700 MiBほどです。VMへ割り当てるメモリを増やしすぎるとホスト側と競合するので、上限まで埋める運用は避けた方が無難です。

ロボ小職ロボ小職

コンテキストを広げるとモデルの挙動は変わりますか?

管理人の小職さん管理人の小職さん

広げると、回答の傾向とか変わったりしますか?
モデルファイル自体は変わりません。変わるのは「一度に読み込める履歴の上限」だけです。読み込める履歴が増える分、長いセッションで要約を挟むタイミングが遅くなります。生成結果の傾向が変わるというより、作業の続き方が変わるイメージですね。

ロボ小職ロボ小職

Offloadはなぜ使わなくなったのですか?

管理人の小職さん管理人の小職さん

VRAMが足りないなら、Offloadを使えばいいのでは?
有効にして15分待っても実Readyにならず、結局CPU側へ戻したからです。VRAMに載せきれない部分をCPUとRAMで処理する方式自体は魅力的ですが、常用するエンジンが安定して起動しないのは避けたいですね。今はCUDA Graphと段階的な拡張で速度を確保しています。

ロボ小職ロボ小職

Readyになったかどうかはどう確認すればいいですか?

管理人の小職さん管理人の小職さん

起動完了の合図、何を見ればいいんですか?
/v1/modelsではなく、実際にChat Completionsで1往復させることを基準にしてください。今回の環境では、32K構成で約330秒、40K構成で約390秒でした。起動スクリプトから監視するなら、生成テストが返るまで待つのが確実です。

ロボ小職ロボ小職

Windows側のOpenCodeも設定を変える必要がありますか?

管理人の小職さん管理人の小職さん

VM300だけ64Kにすれば、いいんじゃないんですか?
いいえ、接続する側が32Kのままだと結局は上限で止まります。WindowsとVM105では context 65,536 / input 63,488 / output 2,048 / body.max_tokens 2,048 へ揃えました。片方ずつではなく、両側同時に合わせるのがコツです。

ロボ小職ロボ小職

compaction bufferはなぜ8,000にしたのですか?

管理人の小職さん管理人の小職さん

bufferって、なぜ8,000なんですか?前は12,000だったような気がしますが。
64Kへの拡張に合わせて12,000から8,000へ調整したためです。keepは4,000の据え置きで、圧縮後の体積を抑えめにしつつ、広いコンテキストを活かす組み合わせにしています。数値を変えるときは、バックアップを取ってから1項目ずつ試すのが安全ですよ。

ロボ小職ロボ小職

opencode –auto の運用で注意することはありますか?

管理人の小職さん管理人の小職さん

opencode –auto、便利そうですが放っておいて大丈夫ですか?
自動実行モードはコンテキストが大きい状態でも作業を進めやすい反面、Compactionが走るタイミングも長くなります。ファイル変更やコマンド実行を伴う作業では、途中の様子を確認しながら進める方が安全です。この記事の本編で書いた「1作業1セッション」の習慣は、64Kになっても変わりませんよ。

ロボ小職ロボ小職

設定を戻したいときはどうすればいいですか?

管理人の小職さん管理人の小職さん

うまくいかなかったとき、元に戻せますか?
変更前の設定ファイルのコピーを戻すだけです。VM側はスナップショット、OpenCode側は設定ファイルを日付付きで残してあるので、32K時代へそのまま復元できます。数値の変更は、全部戻せる状態にしてから行っています。

ロボ小職ロボ小職

MoEキャッシュの自動設定は効きましたか?

管理人の小職さん管理人の小職さん

MoEキャッシュの自動設定、結局どうだったんですか?
実効512で有効にしましたが、速度の変化はありませんでした。効いたのはCUDA Graphだけなので、時間があるならまずCUDA Graphから試す価値がありますよ。

ロボ小職ロボ小職

このやり方は他のGPUでもできますか?

管理人の小職さん管理人の小職さん

GTX 1070じゃないとダメってことではないんですか?
手順自体はGPUを選ばないものがほとんどです。ただし、この記事の前提は「VRAMに載せきらない部分をCPUとRAMが受け持つ構成」なので、VRAMが広いGPUなら話が別です。GPU選びの目安は古いGPUでローカルLLMを作る際の選び方にまとめています。Pascal世代に絞った構成の話はRTX 3060 + 32GB RAMでの記録も参考にしてください。

ロボ小職ロボ小職

まとめ

2016年世代のGeForce GTX 1070 8GBでも、FreeToken-Pascal、CPU、システムRAM、PCIe、適切な量子化モデルを組み合わせれば、Qwen3.6 35B A3B NVFP4をOpenAI互換APIとして動かせました。

さらに、Windows版OpenCodeからSSHトンネル経由でつなぎ、構造化tool calling、WebFetch、WebSearch、自動Compaction、Radixキャッシュ、AGENTS.mdによる安全制御まで、ちゃんと確認済みです。

最初から簡単に動いたわけではありません。Pascal向けJIT、データ型不一致、MoEキャッシュ、tool-call parser、reasoning parser、コンテキスト超過、日本語文字化け、SSH自動復旧。トラブルは次から次へとでしたが、一つずつ確認しては小さく直し、問題があれば戻せる状態を作ってきました。

管理人の小職さん管理人の小職さん

最新GPUと比べると速度は控えめですが、手元のGTX 1070でQwen3.6 35Bを動かせるところまで来ました。古いGPUも、まだまだ活躍できますね。
ポイントは、GPUだけへ無理に載せようとしないことですね。CPU、RAM、PCIe、キャッシュ、コンテキスト運用まで含めて設計するのが大事です。

ロボ小職ロボ小職

最新GPUのような圧倒的な速度ではありません。それでも、自宅にある古いGPUへ新しい役割を与え、ローカルLLMと相談しながら実際のリポジトリを扱えるところまで構築できました。RAMを増やせば何でも速くなるわけではありませんが、MoE、量子化、CPUオフロード、システムRAMをうまく組み合わせれば、古いハードウェアにも新しい使い道はあります。

トラブルの切り分けまで含めて、まさに「思い立ったら弄る!」を実践したローカルAI環境になりました。

後日談も含めた要点は次のとおりです。コンテキストは32,768から65,536トークンへ段階的に拡張し、速度はCUDA Graph 1の有効化で4.252 token/sから平均7.739 token/sへ改善。VRAMは6,423 MiBで、8GBの範囲内に収めています。Offloadは安定起動の確認が取れなかったので不採用、Ready判定はChat Completions基準へ変更し、Windows側OpenCodeも64Kへ同期済みです。古いGPUでも、設定を広げる前にバックアップを取り、1項目ずつ試して戻せる状態にしておくこと。それが一番の近道でした。

参考資料

(PR)