2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

# 🚀 自宅PCからスマホに! llama.cpp🦙 + Tailscaleで構築する「自前LM Link」とWeb UI改造の記録

2
Last updated at Posted at 2026-04-19

PC Watchの記事で紹介されていた、外出先から自宅のGPUを叩くサービス「LM Link」。
「自前で再現してみたい」と思い立ったのが始まりでした。

本記事では、GPUを搭載したデスクトップPCをホストとし、スマホ(Pixel 8a)やノートPCから安全かつ爆速でLLMを利用できる環境を構築した記録、そして公式Web UIのカスタマイズの試行錯誤をまとめます。

1. なぜ「Ollama」ではなく「llama.cpp」なのか?

現在、手軽にLLMを動かすならOllamaやLM Studioが定番です。しかし、今回はあえて本家本元である llama.cpp (llama-server) を直接採用しました。

選定の理由

  1. 最新アーキテクチャへの追従スピード:
    Ollamaなどのエンジンも内部的には llama.cpp です。しかし、新しいモデルが登場した際、本家の llama.cpp が最初に対応し、他はそれを後から取り込む形になります。最新モデルをいち早く試したい人にはllama.cppがよいと考えています。
  2. 自由度の高さ(Thinkingの制御):
    Ollamaは非常に便利ですが、ThinkingをModelfaileでoffにしてもバックグラウンドで動いているという記事を見て、その自由度の低さが気になりました(参照 :https://qiita.com/ntaka329/items/35f156dbe526121e66f5 )。

2. 構築プロセス:セキュアな「自前LM Link」の完成

「llama.cpp + Tailscale + クライアント端末」という、オープンでセキュアな構成を目指しました。

システム構成

  • ホスト: Windows 11 / 32GB RAM / Ryzen 7 5800H / RTX 4070 Super
  • クライアント: Pixel 8a / Core i5・i7搭載ノートPC
  • ネットワーク: Tailscale (VPN)

2-1. Tailscaleの導入とネットワーク構築

外部から安全にアクセスするため、VPNのTailscaleを使用します。

  1. ホストPCとクライアント(スマホやノートPC)の両方にTailscaleをインストールし、同じアカウントでログインします。
  2. ホストPCのTailscale IPアドレス(100.x.x.x)をメモしておきます。

2-2. llama.cppのインストールとモデルの準備

今回は winget を使って手軽にインストールしました。

# llama.cppのインストール
winget install llama.cpp

モデルはHugging FaceからGGUF形式のものをダウンロードしておきます。今回は Qwen3.5 9BGemma4 e4bを用意しました。

llama-server -hf unsloth/Qwen3.5-9B-GGUF:UD-Q4_K_XL # 例 Qwen3.5-9Bをインストール

2-3. 直面した「壁」:Windowsファイアウォール

サーバーを立ち上げてポート8080を指定しても、スマホから繋がりませんでした。原因はWindowsファイアウォールです。
管理者権限でPowerShellを開き、以下のコマンドで特定のポート(今回は8080)に穴を開けます。

# ポート8080のインバウンド通信を許可するコマンド
New-NetFirewallRule -DisplayName "llama.cpp 8080" -Direction Inbound -LocalPort 8080 -Protocol TCP -Action Allow

2-4. サーバーの起動とアクセス確認

セキュリティを考慮し、0.0.0.0 ではなくTailscaleのIPを指定して起動します。
また、あえて特定のモデルを指定せずに起動することで、UI上から動的にモデルを切り替えられるようにしました。

# 実際の起動コマンド
llama-server --host 100.xxx.xxx.xxx --port 8080 -ngl 99 --path "C:\path\to\your\custom\public\index.html"

スマホ(Pixel 8a)のブラウザから http://100.xxx.xxx.xxx:8080 にアクセスします。

無事にスマートフォンからもllama.cppのweb uiが開けました🎉

実際にスマホからアクセスし、「llama.cppとvLLMの比較をしてください」とプロンプトを入力して推論を回してみました。

結果は 68.72 t/s という爆速生成を確認!スマホからでも全くストレスなく、サクサクとローカルLLMを実行できる環境ができました😊

3. Web UI改造:Unloadボタン実装とThinking制御への挑戦

ここからが本題です。公式GUIには「VRAMからモデルを降ろす(Unload)」ボタンがなく、推論モデルの「Thinking」切替も不十分でした。無い機能は作ってしまおう、ということで改造に挑みました。

成功:アンロードボタンの実装

ビルド済みの public/index.html に直接JavaScriptを注入しました。

index.htmlに実装の要点は以下の通りです。改造元はhttps://github.com/ggml-org/llama.cpp/blob/master/tools/server/public/index.html

        </div><!-- <div style="display: contents">の閉じタグ -->
		<button id="floating-unload-btn" onclick="unloadModel()" style="display: none; position: fixed; bottom: 20px; right: 20px; z-index: 9999; padding: 12px 20px; background-color: #ef4444; color: white; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
        </button>
        
        <script>
            // DOM要素を関数の外で一度だけ取得(キャッシュ)
            const unloadBtn = document.getElementById('floating-unload-btn');
            // 処理中かどうかを判定するフラグ(連打対策)
            let isProcessing = false;

            // モデルのロード状態を定期監視する関数
            async function checkModelStatus() {
                if (isProcessing) return;

                try {
                    const response = await fetch('/models');
                    const data = await response.json();
                    
                    // data.data の中に status が 'loaded' のモデルが存在するかチェック
                    if (data && data.data) {
                        const isLoaded = data.data.some(model => model.status === 'loaded');
                        if (isLoaded) {
                            if (unloadBtn.style.display !== 'block') {
                                unloadBtn.style.display = 'block';
                            }
                        } else {
                            if (unloadBtn.style.display !== 'none') {
                                unloadBtn.style.display = 'none';
                            }
                        }
                    }
                } catch (error) {
                    console.log("[Status Check] モデル状態の取得に失敗しました: " + error);
                }
            }

            // アンロードAPIを叩く関数
            async function unloadModel() {
                if (isProcessing) return;
                
                isProcessing = true;
                unloadBtn.style.opacity = '0.5'; // 視覚的に無効化されていることを示す
                unloadBtn.style.cursor = 'not-allowed';

                try {
                    const response = await fetch('/models/unload', { method: 'POST' });
                    
                    if (response.ok) {
                        // ボタンを即座に隠す
                        unloadBtn.style.display = 'none';
                    } else {
                        console.log("[Unload Action] エラー: サーバーがステータス " + response.status + " を返しました。");
                    }
                } catch (error) {
                    console.log("[Unload Action] 通信エラーが発生しました: " + error);
                } finally {
                    // 処理が終わったらフラグと見た目を元に戻す
                    isProcessing = false;
                    unloadBtn.style.opacity = '1';
                    unloadBtn.style.cursor = 'pointer';
                }
            }

            // 5秒ごとにAPIを叩いて状態を監視 (5000ミリ秒)
            setInterval(checkModelStatus, 5000);
            
            // ページ読み込み時にも一度実行
            checkModelStatus();
        </script>

これにより、ボタン押下で正常にVRAMからモデルを解放できるようになりました。

失敗:Thinking OFFを巡る4つのアプローチ

推論モデル(Qwen3.5等)の思考を強制的にOFFにしようと試みましたが、llama.cpp サーバー側の安全装置(バリデーション)との戦いになりました。

1. Fetch APIフックによるテンプレ書き換え

結果: 失敗。 リクエストを横取りし、Qwen用テンプレートで <think> 終了を偽装して送信しましたが、フォーマット不一致とみなされ無視されました。

2. アシスタントメッセージの事前入力(Prefill)

結果: 失敗。 AIが思考を終えた体で事前入力を試みましたが、llama.cppの最新バリデーションに阻まれ 400 Bad Request となりました。

3. 純正テンプレートの動的取得と改変

結果: 失敗。 /props APIからテンプレを取得し改変しようとしましたが、ルーターモードではモデル名が空で返る仕様のため処理が破綻しました。

4. ロード要求保存と「再Load」

結果: 失敗。 ロード時のJSONを保存し、純粋なChatMLに書き換えて再Loadしましたが、モデル自体の構造やサーバー側の推論モードロックにより、思考プロセスはバイパスできませんでした。

4. 学んだことと次なる展望

今回の挑戦で、llama-server のAPI仕様、特に強力なバリデーション機能やルーターモード時の挙動を少し理解することができました。

「確実な思考OFF」への解

結論として、API経由のテンプレート書き換えには限界があります。
「メタデータを『思考なし』に書き換えた別名のGGUFファイルを物理的に作成し、UIからはモデルの再ロード(切り替え)として処理する」こと、またはAPIの仕様を書き換えることが、確実であるという結論に達しました。

展望

  • Thinking制御: スマホで気軽にローカルLLMをテストできる環境は無事に整ったので、まずはこれであれこれ遊んでみようと思います。気が向いたら、Thinkingを作ってみたいと思います💤
  • モデルインストールの自動化: 欲を言えば、UI上からHugging Faceのモデルを直接選んで落とせたら最高ですよね。これも「いつかやれたらやる」リストに入れておきます。

「無い機能はAPIを叩いて作る」というプロセスは、LLMの挙動を深く知る最高の学習機会になりました。自宅に眠っているGPUがある方、ぜひ「自前LM Link」に挑戦してみてください。


参考リンク
https://pc.watch.impress.co.jp/docs/column/nishikawa/2092683.html
https://qiita.com/ntaka329/items/35f156dbe526121e66f5

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?