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?

FastFlowLMでNPUにLLMを載せるときの詰まりどころ8つ(Ryzen AI / XDNA2)

0
Posted at

AMDのNPU(XDNA2)でローカルLLMを動かす FastFlowLM(flm を実機で回したときに、
実際に踏んだ罠をまとめます。Ryzen AI Max+ 395(Strix Halo)/Windows 11 での話です。

速度や電力の実測そのものは別記事に書いたので、こちらはエラーで止まったときの切り分け専用です。


結論(症状から引く)

症状 原因 対処
APIが404 パス接頭辞が違う FLMとLM Studioは /v1、Lemonade Serverは /api/v1
flm run が返ってこない 対話モードに入っている flm serve + API で叩く
TTFTが40秒超と出る 思考トークンを数えていない reasoning_content も1トークンとして扱う
ドライバ版数が要件より小さく見える 採番体系が変わった 大小比較せず flm validate に判定させる
NPUに2つ目のモデルが載らない 排他ロック仕様 同時に保持できるのは1つだけ
NPU使用率が取れない 専用カウンターが存在しない タスクマネージャで見る
大きいモデルがNPUに載らない NPUの共有メモリ上限 上限はOS側メモリの半分。BIOSのVRAM配分を見直す
.ps1 が文字化けして構文エラー PowerShell 5.1 の既定エンコード BOM付きUTF-8で保存する

0. 前提の確認(ここで弾かれる人がいる)

FastFlowLM が対応するのは XDNA2 世代です。

flm version    # FLM v1.0.4
flm validate

flm validate の出力。

[Windows]  NPU: XDNA2
[Windows]  NPU dirver version: 32.0.20102.3930

XDNA2 と出れば対応世代です。XDNA1世代(Ryzen AI 7000/8000/200番台)は非対応なので、
ここで弾かれる場合はソフト側では解決しません。

なお出力の dirver は FastFlowLM 側の誤字で、実害はありません。

インストール直後にコマンドが見つからない場合

同じセッションではPATHが更新されていないことがあります。

$env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' +
            [System.Environment]::GetEnvironmentVariable('Path','User')

1. ドライバ版数を要件と比較して悩まない

FastFlowLM は NPUドライバ 32.0.203.311 以降を要求すると書かれています。
一方、実機のドライバはこうでした。

Get-CimInstance Win32_PnPSignedDriver |
  Where-Object { $_.DeviceName -match 'NPU|Neural|AI Accel|IPU|XDNA' } |
  Select-Object DeviceName, DriverVersion, DriverDate

結果は NPU Compute Accelerator Device / 32.0.20102.3930 / 2026-05-07。

第3フィールドが要件の 203 に対して実機は 20102 です。
これはAMDがAdrenalinのビルド番号に揃えた新方式に移行したためで、
体系の違うバージョン文字列を大小比較しても意味がありません

「要件を満たしていないのでは」と悩む必要はなく、判定は flm validate に任せるのが正解です。
弾かれたときだけ、Windows Update → AMDサポートのRyzen AI用ドライバ、の順で対処します。


2. APIが404になる(/v1/api/v1 か)

OpenAI互換APIのパス接頭辞が実装ごとに違います。ここが混同しやすい。

実装 接頭辞
FastFlowLM(flm serve /v1 http://127.0.0.1:52625/v1/models
LM Studio /v1 http://127.0.0.1:1234/v1/models
Lemonade Server /api/v1 http://localhost:8000/api/v1/models

FLMに対して /api/v1 を叩くと404です。Lemonade Serverの記事を見ながらFLMを叩くと必ず踏みます。

ポート番号も確認しておきます。

flm port    # 実測環境では 52625

疎通確認はこれだけで足ります。

Invoke-RestMethod http://127.0.0.1:52625/v1/models | ConvertTo-Json -Depth 4

自分でクライアントを書くなら、両方試して通ったほうを使うのが楽です。

CANDIDATE_PATHS = ["/v1", "/api/v1", ""]   # LM Studioを先に見つけるため /v1 を先頭に置く

def find_base(port: int) -> str:
    for prefix in CANDIDATE_PATHS:
        url = f"http://127.0.0.1:{port}{prefix}/models"
        try:
            with urllib.request.urlopen(url, timeout=5) as res:
                if res.status == 200:
                    return f"http://127.0.0.1:{port}{prefix}"
        except (urllib.error.URLError, OSError):
            continue
    raise SystemExit(f"ポート {port} で models が見つかりません")

3. flm run <model> -i <file> が返ってこない

ファイルを渡して一発で流したいときに、これをやると対話モードに入って戻ってきません
バッチやベンチのスクリプトから呼ぶと固まったように見えます。

非対話で回すなら、サーバを立ててAPIを叩きます。

# 別ウィンドウで起動しておく
flm serve gemma4-it:e2b --ctx-len 4096

あとは通常のOpenAI互換クライアントで投げられます。


4. TTFTが40秒超と出る(gpt-oss の思考トークン)

これが一番はまりました。gpt-oss:20b で計測したら TTFT 42秒 / 12 tok/s という数字が出ます。
実際の正しい値は TTFT 3.76秒です。

原因は、FastFlowLM が思考と回答を別のフィールドで流すことです。

フィールド 中身
reasoning_content 思考(先に流れてくる)
content 回答本文(思考の後)

content だけを数えると、思考が終わるまでの時間がまるごとTTFTに乗ります
思考トークンは実測で200〜939トークンを占め、回答本文より多いこともあります

ストリームを解析する側は両方を見てください。

delta = obj.get("choices", [{}])[0].get("delta", {}) or {}
reasoning = delta.get("reasoning_content") or ""
content = delta.get("content") or ""
if not reasoning and not content:
    continue

now = time.perf_counter() - t0
if ttft is None:
    ttft = now          # 最初の「何らかの」トークン = 本当のTTFT
if content and ttfa is None:
    ttfa = now          # 回答本文が始まった時刻は別に持つと親切

TTFT(最初のトークン)と TTFA(回答本文の開始)を分けて持つと、
「反応はあるが答えが始まらない」体感を数字で説明できます。

生のストリームを一度覗いておくと確実です。フィールド名は実装で変わります。

with urllib.request.urlopen(req, timeout=1800) as res:
    for i, raw in enumerate(res):
        print(raw.decode("utf-8", "replace").strip())
        if i > 30:
            break

5. LM Studio と数字が揃わない

NPU(FLM)と iGPU(LM Studio)を比べるとき、出力形式が完全に同一ではありません

  • LM Studio は reasoning_content を返しません(思考トークン0と報告される)
  • 総トークン数は近い値になる(実測で971〜1025)が、内訳は違う

同一モデル名でも比較の前提が揃わないので、記事や社内報告に出すなら断り書きが必要です。

もう1点、1回目のTTFTはJITのモデルロードを含みます
実測では1回目14.7秒、ウォーム状態1.25秒でした。公平に比べるならウォーム側の値を使ってください。


6. NPUに2つ目のモデルが載らない

NPUは排他ロックです。同時に保持できるNPUモデルは1つだけで、
切り替えるとロードとアンロードの待ちが入ります。

「小型を常駐させつつ別の小型も」という構成は組めません。
一方で NPUに1つ+iGPUに1つは同時に動きます(こちらは別記事で実測しました)。


7. NPU使用率が数値で取れない

パフォーマンスカウンターを探しても見つかりません。

Get-Counter -ListSet *NPU*      # 空

WindowsにNPU専用のカウンターセットは存在しません(NPUはMCDMデバイスとして扱われます)。
\GPU Engine(*)\Utilization Percentage にもNPUは出てきません。

数値で確認したいなら タスクマネージャの「パフォーマンス」タブを使います。
NPUの項目を選ぶと使用率・共有メモリ・ドライバ版数まで出ます。
自動化したいログ用途には向かないので、ここは割り切るしかありません。

参考までに、消費電力のほうは標準カウンターで取れます

Get-Counter '\Energy Meter(RAPL_Package0_PKG)\Power'   # 値はミリワット

APUなのでCPU・iGPU・NPUを合わせたパッケージ全体の値です。HWiNFO等の導入は要りません。


8. 大きいモデルがNPUに載らない

タスクマネージャのNPUパネルに 「共有メモリ 5.7 / 15.8 GB」 のように出ます。
この 15.8GB が NPU が使えるメモリの上限で、実機ではOS側メモリ31.6GBのちょうど半分でした。

ここが効いてくるのは、統合GPU機でVRAMを大きく固定しているときです。

搭載128GB
├─ GPU専用 96GB(BIOSで固定)
└─ OS側 31.6GB
     └─ NPUが使えるのはこの半分 = 15.8GB

実測では gpt-oss:20b(13.48GB)をロードすると flm serve プロセスのワーキングセットが
16.08GB に達し、OS側の空きが1.3GBまで低下しました。
プロセスのワーキングセットとNPUの共有メモリ枠は厳密には別の指標ですが、
どちらから見ても20B級がこの構成の上限に近いという結論になります。

NPUに大きいモデルを載せたいなら、BIOSでGPU専用メモリを削ることになります。
NPUの枠がOS側メモリの半分だったので、96GBを64GBに削ればNPU側は約31.6GBまで広がる計算です
(筆者はBIOSを変えての検証はしていません。表示値からの推測です)。

「NPUに載らない」をflm側の設定で解こうとすると詰まります。メモリ配分の問題です。


9. 計測スクリプトを書くときのPowerShellの罠

本題から少し外れますが、切り分け用のスクリプトを書く段階で2つ踏みました。

.ps1 は BOM付き UTF-8 で保存する

PowerShell 5.1 は BOMなしUTF-8 の .ps1 を ANSI(CP932)として読みます。
日本語のコメントや文字列が壊れ、終端記号 " がありません のような構文エラーになります。

エディタで「UTF-8 with BOM」を選ぶか、Pythonから書くならこうします。

p.write_bytes(b"\xef\xbb\xbf" + text.encode("utf-8"))

PowerShell 7 では起きません。5.1 で動かす前提なら必須です。

Bitmap.Save には絶対パスを渡す

画面キャプチャを保存しようとして「GDI+ で汎用エラーが発生しました」と出る場合、
多くはパスの問題です。.NET のカレントディレクトリは PowerShell の現在位置とは別なので、
相対パスは意図した場所を指しません。

$OutDir = (Resolve-Path $OutDir).Path      # 絶対パスにしてから渡す
$bmp.Save((Join-Path $OutDir 'shot.png'), [System.Drawing.Imaging.ImageFormat]::Png)

切り分けチェックリスト

止まったら、この順で確認します。

  1. flm validateXDNA2 と出るか(出なければハード側の問題)
  2. flm port でポートを確認したか
  3. 叩いているパスは /v1 か(Lemonade Serverの記事を見ていないか)
  4. flm run ではなく flm serve を使っているか
  5. TTFTが異常に大きいなら reasoning_content を数えているか
  6. モデルが載らないなら タスクマネージャでNPUの共有メモリ上限を見たか
  7. スクリプトが構文エラーなら .ps1 のBOMを疑ったか

次の一歩


まとめ

  • 対応世代の判定はバージョン文字列の比較ではなく flm validate
  • APIは /v1/api/v1 は Lemonade Server 側
  • 非対話で回すなら flm serve + API
  • gpt-oss 系の計測では reasoning_content を数える。忘れるとTTFTが10倍以上ずれる
  • NPU使用率はタスクマネージャしかない。電力は標準カウンターで取れる
  • 「NPUに載らない」は flm の設定ではなく メモリ配分の問題
  • .ps1BOM付きUTF-8

エラーメッセージが出ずに固まる/数字がおかしいという形で出るものが多いので、
先に上のチェックリストを通したほうが早いです。


参考

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?