tk3.biz
ブログ一覧に戻る Back to Blog

ローカルLLMをターミナルから日常的に使えるようにした ローカルLLMをターミナルから日常的に使えるようにした

AIローカルLLMCLIPowerShellllama.cppQwen

はじめに

前回、ローカルLLMを7倍速くしました。 生成 20 t/s 前後。数字としては満足でした。

そしてほとんど使いませんでした。

理由は単純で、使うまでが面倒だったからです。

pwsh -File C:\Projects\local-llm\scripts\serve.ps1
# …40秒待つ…
# ブラウザで localhost:8080 を開く

ターミナルで作業している最中に、これをやる気にはなりません。 速いかどうか以前に、動線が長すぎました。

今回はここを潰します。目標は「PC を起動して llm と打ったら答えが返る」。

完成形

llm "PowerShellでファイル一覧を取得するには?"
llm    # 引数なしで対話モード

サーバの起動は気にしません。止まっていれば勝手に立ち上がります。

面倒さをどこに隠すか

「使う前にサーバを起動する」という手順が要るなら、 選択肢は 2 つです。

  1. 常駐させる — 起動時に立ち上げっぱなしにする
  2. 遅延起動 — 必要になった瞬間に立ち上げる

最初は 1 を考えました。タスクスケジューラに登録すればすぐです。 ただ、このモデルは常時 24 GB を抱えます。 96 GB 積んでいるので余裕はありますが、 週に数回しか使わない日もあるのに 24 GB を寝かせ続けるのは気が進みませんでした。

選んだのは 2 です。クライアント側でこうしました。

llm "質問"
  ↓
サーバは動いている?
  ├─ Yes → そのまま聞く
  └─ No  → 裏で起動 →(約40秒)→ 聞く

初回だけ 40 秒待ちますが、一度立ち上がれば次からは待ちなし。 手順が消えるのではなく、手順が見えなくなるのが狙いです。

使わない時間は 24 GB を返してほしい

遅延起動にしても、一度使うと立ち上がりっぱなしです。 朝に一度聞いて、そのまま夜まで 24 GB を抱えている。

「12 時間使わなかったら落とす」仕組みを自分で書こうとして、 その前に --help を眺めたら、ありました。

--sleep-idle-seconds SECONDS
        number of seconds of idleness after which the server will sleep

説明が一行なので、メモリが本当に解放されるのかは分かりません。 60 秒に設定して測りました。

アイドル時間を超えるとモデルがメモリから抜ける様子。直後22.7GB、45秒後も22.7GB、アイドル上限を超えた85秒後に0.1GB、125秒後も0.1GB。プロセスは生きたままでhealthはOKを返し続ける

リクエスト直後 : 22.7 GB
45 秒後        : 22.7 GB
85 秒後        : 0.1 GB   ← 22.6 GB 解放
125 秒後       : 0.1 GB
health         : OK

解放されました。 しかもプロセスは生きたままで、 health は OK を返し続けます。次のリクエストで自動的に復帰します。

自前で「12 時間経ったらプロセスを殺すタスク」を書くつもりでしたが、 フラグ 1 つで済みました。しかも殺さないので、復帰はモデルの再読み込みだけです。

運用値は 12 時間(43200 秒)にしました。

--sleep-idle-seconds 43200

ここでの教訓は「自分で書く前に --help を読む」に尽きます。 書き始めていたら、プロセス監視のタスクを作って、 殺すタイミングを考えて、復帰の 40 秒をまた作り直していました。

48 GB 占有していた

動くようになったので、停止と再起動を確認しました。そこで見つけました。

PID 105720  23.95 GB  ← ポート 8080 を保持(正常)
PID  94128  23.96 GB  ← ポートを取れなかった孤児

llama-server が 2 つ動いて、合わせて 48 GB。 半分は完全な無駄です。

原因はこれでした。

停止を待たずに返したせいで二重起動した流れ。-Stop 実行後、旧プロセスが終了しきる前に llm を実行すると health が応答せず「落ちている」と判断され、新プロセスが起動する。新プロセスはポートを取れず居座り、合計48GBになる

# 直す前
$p = Get-Process llama-server -ErrorAction SilentlyContinue
if ($p) { $p | Stop-Process -Force }   # ← 投げっぱなしで即座に返る

Stop-Process は終了を待ちません。24 GB を抱えたプロセスが片付くには数秒かかります。 その隙に llm を叩くと、

  1. health を叩く → まだ死にかけているので応答なし
  2. 「サーバは落ちている」と判断
  3. 2 つ目を起動
  4. 2 つ目はポートを取れない。でもモデルは読み込み済みなので居座る

ポートが取れなかった側は仕事をしないまま 24 GB を抱え続けます。 llm は 1 つ目(まだ生きている方)から答えを受け取れてしまうので、 成功しているように見えます。気づいたのは、たまたまプロセス一覧を見たからでした。

直し方

停止側は、消えるまで待つようにしました。

$procs | Stop-Process -Force
foreach ($i in 1..30) {
    Start-Sleep -Milliseconds 500
    if (-not (Get-Process llama-server -ErrorAction SilentlyContinue)) { return $true }
}

起動側は、「応答しないのにプロセスが居る」状態を疑うようにしました。 これは 2 つの場合がありえます。

  • 起動途中でまだモデルを読んでいる → 待てばよい
  • ポートを取れなかった孤児 → 片付けるべき

見分けはつかないので、まず 2 分待って、それでも応答しなければ孤児とみなします。

$existing = @(Get-Process llama-server -ErrorAction SilentlyContinue)
if ($existing.Count -gt 0) {
    # 起動途中かもしれないので、まず待つ
    ...
    # 2 分待って応答しないなら孤児
    [void](Stop-Server)
}

修正後、停止は 3.2 秒で完全終了を確認してから返り、 停止直後に llm を叩いてもプロセスは 1 個のままになりました。

「成功しているように見える失敗」が一番たちが悪い。 エラーも出ず、答えも返ってくる。 ただ静かにメモリが食われているだけ。 定期的にプロセス一覧を眺める習慣は、無駄ではありませんでした。

どこからでも llm と打てるようにする

PowerShell プロファイルに関数を 1 つ置きました。

function llm {
    $script = 'C:\Projects\local-llm\scripts\llm.ps1'
    if (-not (Test-Path $script)) {
        Write-Host "llm.ps1 が見つかりません: $script" -ForegroundColor Red
        return
    }
    & $script @args
}

@args で引数をそのまま渡しているので、 llm -Think "..." や llm -Stop もそのまま通ります。

プロファイルの場所は環境で違うので、$PROFILE.CurrentUserAllHosts で確認します。 うちは OneDrive 配下でした。他の PC にも同期されますが、 同期先にスクリプトが無ければ「見つかりません」と出るだけで実害はありません。

ストリーミングしないと体感が変わる

最初は応答が全部できてから表示していました。 20 t/s なので、300 トークンの回答だと 15 秒間なにも起きません。

速度は同じでも、これは「遅い」と感じます。 OpenAI 互換 API は SSE でストリーミングできるので、そちらに変えました。

while (-not $reader.EndOfStream) {
    $line = $reader.ReadLine()
    if (-not $line.StartsWith('data: ')) { continue }
    $data = $line.Substring(6)
    if ($data -eq '[DONE]') { break }
    $delta = ($data | ConvertFrom-Json).choices[0].delta
    if ($delta.content) { Write-Host $delta.content -NoNewline }
}

応答の最後に実測を出すようにもしました。

  [16.5 秒 / 約 17.3 tok/s]

自分の環境が遅くなったときに気づけます。

thinking は切っておく

Qwen3.6 は既定で thinking が有効です。 OpenAI 互換 API では、思考が reasoning_content、最終回答が content に分かれて返ります。

これがトークンを大量に食います。 「日本語で1文だけ自己紹介してください」という質問に対して、 思考に 1313 文字使っていました。max_tokens=100 だと思考だけで尽きて、 content が空で返ってきます。

CLI の即答用途では切りました。

{ "chat_template_kwargs": { "enable_thinking": false } }

必要なときだけ llm -Think "..." で有効にします。 思考部分はグレーで表示されるので、本文と混ざりません。

まとめ

  • 速くしただけでは使わない。 動線が長いと、速度は関係なく使わなくなる
  • 常駐か遅延起動かは、メモリと初回待ちのトレードオフ。 週に数回なら遅延起動のほうが無駄がない
  • --sleep-idle-seconds で 22.7 GB → 0.1 GB。 自前で監視タスクを書く前に --help を読むべきだった
  • 停止は終了を待つ。 待たずに返すと二重起動して 48 GB 食う。 しかも成功しているように見える
  • ストリーミングしないと、同じ速度でも遅く感じる

これで「PC を起動して llm と打つ」だけになりました。 速くした甲斐が、ようやく出てきたところです。

次は、3 枚のレビュアー(Claude / Codex / ローカル Qwen)を実際の開発で回してみて、 ローカル分がどれくらい当たるのかを記録していきます。 1 回の結果では判断できないので、しばらく貯めてから書きます。

関連記事 Related Posts

thinking を切っていいのか測ったら、採点器のほうが2回壊れていた thinking を切っていいのか測ったら、採点器のほうが2回壊れていた

ローカルLLMのCLIでthinkingを既定で切っていました。速度だけ見て決めた判断の裏を取るため正答率を測ったら、同率でした。ただしそこに至るまでに、自分で書いた採点スクリプトのバグを2回踏んでいます。 ローカルLLMのCLIでthinkingを既定で切っていました。速度だけ見て決めた判断の裏を取るため正答率を測ったら、同率でした。ただしそこに至るまでに、自分で書いた採点スクリプトのバグを2回踏んでいます。

自分が書いた1418行を3つのAIにレビューさせたら、実バグが11件出た 自分が書いた1418行を3つのAIにレビューさせたら、実バグが11件出た

Claude・Codex・ローカルQwenの3枚に、自分で書いて自分で動かしていたスクリプトをレビューさせました。3件は自力では気づけないもので、1件は「レビュー結果を集計するスクリプト自体」のバグでした。一方で、3枚とも見逃した欠陥もあります。 Claude・Codex・ローカルQwenの3枚に、自分で書いて自分で動かしていたスクリプトをレビューさせました。3件は自力では気づけないもので、1件は「レビュー結果を集計するスクリプト自体」のバグでした。一方で、3枚とも見逃した欠陥もあります。

Windows に Coreutils を入れてみた Windows に Coreutils を入れてみた

winget で Microsoft の GNU Coreutils を Windows にインストールし、PowerShell から cat や rm などの Unix コマンドを使えるようにする手順を紹介します。 winget で Microsoft の GNU Coreutils を Windows にインストールし、PowerShell から cat や rm などの Unix コマンドを使えるようにする手順を紹介します。