0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

0
Posted at

この記事は 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. 常駐させる — 起動時に立ち上げっぱなしにする
  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 回の結果では判断できないので、しばらく貯めてから書きます。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?