この記事は tk3.biz のブログ からの転載です。
はじめに
前回、ローカルLLMを7倍速くしました。生成 20 t/s 前後。数字としては満足でした。
そしてほとんど使いませんでした。
理由は単純で、使うまでが面倒だったからです。
pwsh -File C:\Projects\local-llm\scripts\serve.ps1
# …40秒待つ…
# ブラウザで localhost:8080 を開く
ターミナルで作業している最中に、これをやる気にはなりません。速いかどうか以前に、動線が長すぎました。
今回はここを潰します。目標は「PC を起動して llm と打ったら答えが返る」。
完成形
llm "PowerShellでファイル一覧を取得するには?"
llm # 引数なしで対話モード
サーバの起動は気にしません。止まっていれば勝手に立ち上がります。
面倒さをどこに隠すか
「使う前にサーバを起動する」という手順が要るなら、選択肢は 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.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。 半分は完全な無駄です。
原因はこれでした。
# 直す前
$p = Get-Process llama-server -ErrorAction SilentlyContinue
if ($p) { $p | Stop-Process -Force } # ← 投げっぱなしで即座に返る
Stop-Process は終了を待ちません。24 GB を抱えたプロセスが片付くには数秒かかります。その隙に llm を叩くと、
-
healthを叩く → まだ死にかけているので応答なし - 「サーバは落ちている」と判断
- 2 つ目を起動
- 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 回の結果では判断できないので、しばらく貯めてから書きます。