NPUを使ってローカル生成AI環境を作ってみましょう

はじめに

プロダクト技術本部の加藤(剛)と申します。

BIGLOBEは以前から生成AIを積極的に業務に活用しています。現状はクラウドサービスの生成AIを主軸に据えていますが、新しい技術や環境にも追随すべく、さまざまな検証を行っています。 先日、こちらの取り組みを通じて日本HP社から、「NPU(Neural Processing Unit)処理が45TOPS以上の性能」を持つノートPC「HP EliteBook X G1i 14 AI」をお借りする機会をいただきました。

最近では、CPUメーカー各社もAI機能に注力しており、ノートPCでも「NPU処理が45TOPS以上の性能」を持つ製品が市場に出ています。では、この性能を持つNPUでローカルLLM(生成AI)を動作させることは可能なのか?もし動作するなら、どの程度実用的なのか?といった点は、エンジニアとして非常に興味を引かれます。 そこで今回は、手元のNPU搭載ノートPCを使い、今後の生成AI環境の選択肢となるであろう「NPU&ローカルLLM」の組み合わせで、実際に生成AI環境を構築してみました。

なお、以下の内容は「2025年6月末時点での情報」をもとに作成しています。ご存知の通り、生成AI分野の技術進歩は目覚ましく、本日動作が確認できたとしても明日には状況が一変し、動作しなくなる可能性もあります。そのため、このような注意書きを記載させていただきます。

やりたいこと

 まず、今回の「やりたいこと」は、先述の通り「NPUとローカルLLMの組み合わせの評価」なので、次の2つを目標に設定しました。

  • NPUでローカルLLM(生成AI)を動作させることは可能なのか? その手法は? を確認
  • NPUでローカルLLM(生成AI)を動かしたときのパフォーマンスはどの程度なのかを確認
    →「NPUとローカルLLM」の組み合わせは、実際にどの程度利用可能なのか?

 ところで、「NPU」と言ってもCPUを製造している各社が、それぞれの仕様でNPUを提供しています。今回はお借りしたノートPCに搭載されている「Intel Core Ultra 7 プロセッサー 258V」のNPUをターゲットとして話を進めます。

NPUでの開発環境

2025年6月時点でPCを使ってローカルLLMを試そうとした場合、最も手軽なのは「ollama」を利用することになるでしょう。しかし、調べたところollamaはNPUに対応していないとのことでした。そのため、別の方法を探す必要がありました。

調べた結果、Intel のNPUを利用してローカルLLMを稼働させる環境としてはいくつか存在していることがわかりました。例えば、以下のような環境が存在します。

  • intel-npu-acceleration-library
  • OpenVINO
  • DirectML

 最初の”intel-npu-acceleration-library”は、WindowsでPythonを利用する環境です。一方、”OpenVINO”は複数のOSに対応し、開発言語はPythonとC++を利用します。この2つはIntel社製の環境で、最後の"DirectML"は、名前の通りMicrosoft社が提供しています。

 これらのうち、私としては使い慣れた開発言語を利用したいので、Pythonが使える ”intel-npu-acceleration-library” か “OpenVINO” のどちらかでと考えました。ネットで少し調べると、”intel-npu-acceleration-library” のほうが簡単そうに見えたので、手始めに ”intel-npu-acceleration-library” から試してみようと思った… のですが、これが苦悩の日々を始める原因にもなります。

実装・その1:”intel-npu-acceleration-library” を利用

 上で述べたとおりに、最初に ”intel-npu-acceleration-library” を利用してNPUでローカルLLMを稼働させようとしました。”intel-npu-acceleration-library”については以下のページで公開されています。

  • intel-npu-acceleration-library (GitHub)

 このページの下部に「Run a Tiny-llama model on the NPU」というサンプルコードがあります。ネットで ”intel-npu-acceleration-library” を調べると、日本語で書かれているものは大概、このサンプルコードを使っています。当然、私も最初にこのサンプルコードを動作させようと試みました。結果は… 動きません!。このサンプルコードは、そのままでは動きません。GitHubのissueにも私と同じ状況になって困っている人がいました。

 では、このサンプルコードを動かすにはどうしたら良いのかというと

  1. サンプルコードを書き換える
  2. ”intel-npu-acceleration-library” ライブラリのコードを書き換える

の2つの対応が必要になります。最初のは良いとして、2つめは「?」と思いますよね。実は、”intel-npu-acceleration-library”のページ(英文)を読むと

Important

Intel® NPU Acceleration Library – End of Life Announcement We are
announcing the end of life for the Intel® NPU Acceleration Library.

という記述があります。また、ページのトップには

This repository was archived by the owner on Apr 25, 2025. It is now read-only.

という記述があります。
要するに、”intel-npu-acceleration-library”は終了扱いで、2025年4月下旬からはメンテナンスもされていないということでした。このブログの最初に「いつ時点」かの注意を記載したのは、まさにこのためです。
私がPCを借りて”intel-npu-acceleration-library”を動作させようとした2025年6月時点では、すでに開発が終了したライブラリだったのです。一方で、ネットにある日本語の ”intel-npu-acceleration-library” の技術系記事は2025年4月よりもずっと前のものが多いので、これを参考にしても「うまくいかない」となります。

 結果として、サンプルコードが動かず、メンテナンス終了に気づくまでに週末の2日間を費やしてしまいました。 しかし、”intel-npu-acceleration-library”がこのような状況だと分かっただけでも収穫だと気を取り直し、別の環境で再挑戦することにしました。

実装・その2:”OpenVINO” を利用

 上記の通り”intel-npu-acceleration-library”が利用できなかったため、次に試したのは”OpenVINO”です。この環境は、2025年6月時点での IntelのCPU紹介ページに「OpenVINO」と名称が記載されているので、問題なく使えるだろうという予想で手を進めました。”OpenVINO” については以下のページで公開されています。

1)環境の構築

 とっかかりで使いだすのであれば、「OpenVINO Documentation」の「Install OpenVINO」から順にやっていく形になります。2025年6月時点で、”OpenVINO 2025.2” がリリースされていますので、こちらを使います。

 次にOpenVINOで生成AIを扱う場合には、「Generative AI workflow」あたりから読み進めていくことになります。うえで、‘‘OpenVINO 2025.2” の環境が出来上がっているかと思うので、ここでは "optimum-cli" コマンドを準備します。pipを使って以下のように実行します。

$ pip install optimum-intel[openvino]

余談ですが、実は私が勘違いして「pip install optimum-intel」と実行してしまい、正しいコマンドに気づくまでにかなり苦労しました。

2)LLM の準備

 環境の構築が終わったら、次は生成AIのモデルの準備をします。「Generative Model Preparation」の中の「Convert and Optimize Generative Models」セクションに書いてある通りに、optimum-cli コマンドを使って目的の生成AIのモデルの変換をします。 optimum-cli コマンド自体が hugging face サイトから変換するLLMの元データのダウンロードをしてくれますし、事前に hugging face サイトからLLMのデータをダウンロードしているようなら、それを利用することもできます。いずれのケースでも、LLMの元データは
(自分のホームディレクトリ) \.cache\huggingface\hub
の下にあります。
optimum-cli コマンドのパラメータのうちで「weight-format」は「int4」を指定します。本当は「nf4」も試してみたかったのですが、NPUではnf4で変換したモデルが動かなかったため、早々に諦めました。

なお、OpenVINOで扱える生成AIのモデルで「最新&日本語の精度が良い」のは「Phi-4」や「Qwen3」あたりになります。個人的には「Gemmaシリーズ」を試したかったのですが、動作対象ではない(optimum-cliのコードに記述がない)ようです。

ちなみに、「Phi-4」「Qwen3」が利用できるようになったのは、‘‘OpenVINO 2025.2”からなのですが、この ‘‘OpenVINO 2025.2”がリリースされたのが、2025年6月18日。ちょうど私が ‘‘OpenVINO” で悪戦苦闘していた期間だったりします。「optimum-cli のコードには「Phi-4」「Qwen3」とか書いてあるのに、‘‘OpenVINO”で動かんのは何故?」と悶々としていたら、たまたま見たリリースノートで「昨日の日付で‘‘OpenVINO 2025.2”がリリースされている?!」と気が付いたときに虚脱感と言ったら…。ということで、‘OpenVINO 2025.2”を入れたら、問題なく「Phi-4」「Qwen3」が動きました。ここにも、このブログの最初に「いつ時点」かを記載した理由があったりします。

さて、上記の通りにLLMの準備ができたら、次は実行コードを作成します。

3)実行コードを作成

 今回は非常にシンプルなコードです。単に「モデルデータを読み込んでクエリ渡して回答を求める」程度のものです。今回作成&実行した実際のコードはページの最後に記載しますが、単純に結果だけを求めるコードなら、この程度で済んでしまいます。

import openvino_genai as ov_genai
model_path = "TinyLlama"
pipe = ov_genai.LLMPipeline(model_path, "NPU")
print(pipe.generate("The Sun is yellow because", max_new_tokens=100))

ただし、ローカルLLMは出力に時間がかかるため、ストリーミングで出力しないと現実的ではないでしょう。

4)動かしてみる

 ページの最後に記載しているサンプルコードを、任意のファイル名で作成・保存します。
また、今回の評価で利用したモデルは「Phi-4:14b」になります。

サンプルコードは以下のようにいくつかのパラメータを渡すことで実行できます。「--device」でLLMの処理を CPU、GPU、NPU のいずれかで行うかを指定できます。「--debug」は true を指定すると決め打ちの質問をクエリにして実行します(処理時間計測用のオプションとして利用)。

$ python sample11.py phi-4 --device NPU --debug true

上記の指定の場合、あらかじめ用意された質問「20世紀の哲学者で著名な3名を理由も併せて回答して。」がクエリとして渡されます。実は、このフレーズは私のお気に入りで、ちょくちょく初めて使うローカルLLMでこのフレーズを使っています。

5)実行結果

 では、実際にサンプルコードを実行した時の結果をお見せします。

 上記のように「--debug」を true にして「20世紀の哲学者で著名な3名を理由も併せて回答して。」の質問を推論させたとき、Phi-4の回答は以下のような内容になります。それなりにボリュームある回答をしてきます。

20世紀には多くの著名な哲学者が登場し、それぞれが哲学のさまざまな分野に大きな影響を与えました。以下に、特に著名な3人の哲学者とその理由を挙げます。

1. ルートヴィヒ・ウィトゲンシュタイン

理由: ウィトゲンシュタインは20世紀の哲学において非常に影響力のある人物であり、特に言語哲学と論理学において重要な貢献をしました。彼の初期の著作『論理哲学論考』では、言語と現実の関係を分析し、論理的結果主義を提唱しました。後期の著作『哲学探究』では、言語ゲームの概念を導入し、言語の意味を社会的文脈に依存するものとして捉え直しました。彼の思想は、言語の役割と限界を理解する上で重要な基盤を提供しました。

2. ジャン=ポール・サルトル

理由: サルトルは存在論的人間主義の代表的な哲学者であり、特に存在主義の流れを決定づける役割を果たしました。彼の著作『存在と無』では、人間の自由と責任について深く掘り下げ、人間は自らの存在を通じて意味を創造するという考えを展開しました。また、サルトルは文学や政治にも深く関与し、哲学を超えて広範な影響を与えました。

3. マルティン・ハイデッガー

理由: ハイデッガーは20世紀の哲学において最も影響力のある存在論者の一人とされています。彼の代表作『存在と時間』では、存在の問いを深く探求し、存在の「あり方」を「存在論的解釈」として捉え直しました。ハイデッガーの思想は、後の多くの哲学者や思想家に影響を与え、特に現象学や解釈学の発展に大きく寄与しました。

これらの哲学者は、それぞれ異なる分野で重要な理論を展開し、20世紀の哲学的議論において中心的な役割を果たしました。

 次にこの回答を出力するまでの経緯をお見せします。

 まず、サンプルコードを実行して、pipeを作成します。NPUで処理するための事前処理が実行されます。cacheが効いているときには、約1分程度の時間を必要とします。

pipeが作成されたら、それを利用してクエリを渡し、LLMの推論を実行します。このとき、NPUがどの程度稼働しているかをタスクマネージャーで確認したのが以下の図です。Phi-4程度のモデルサイズであれば、ローカルLLMの実行にはかなりのメモリが消費され、パフォーマンスも100%に近い値となります。なお、NPUのメモリはメインメモリと共有しているため、どちらかのメモリ利用量が大きいともう一方に影響が出ると考えられます。今回利用している 258V だとメモリ上限が32GBで、Phi-4のために必要なメモリが13GB弱となるので、その他のプログラムで利用するメモリはその辺の値を参考にして注意したほうがよろしいかと。

次に今回のPhi-4を使っての実行時間は以下の通りです。

  • 前処理に要する実行時間が約1分13秒
  • 推論に要する実行時間が約2分30秒

合計すると約3分40秒となり、正直なところ「終わるまで待つ」感覚は否めません。

 実は、これでも実行時間の短縮を図っていての値だったりします。上で「cacheが効いている」に下線を入れていますが、ここが実行時間の短縮を図っているところです。上の実行時間に記載した「前処理の実行」で行う内容をキャッシュできる機能があるそうです。以下はサンプルコードの一部を抜粋したものです。

pipe = ov_genai.LLMPipeline(model_path, device, CACHE_DIR=".npucache")

このように「CACHE_DIR」を設定すると、前処理の実行時間を短縮できます。では、このキャッシュが無い場合、どのくらいの差がでるのでしょうか。以下の図はキャッシュ無しの前処理を行った結果です。

上の通りに所要時間は2分ちょいとなります。キャッシュを効かせると、1回目の前処理は「2分ちょい」の時間を要しますが、2回目以降の実行で行われる前処理の所要時間は 「1分ちょい」と約半分近く短縮できることになります。とはいえ、前処理に1分以上かかるのは、実用的ではないと感じます。

ただ、これはPhi-4のサイズが、NPUで動かすには大きすぎるためかと思われます。以下は「Qwen3:4b」を実行した時の図です。

上のケースだと、キャッシュ無しでの実行となりますが、前処理の所要時間は 45秒 という値です。Phi-4のパラメータサイズは 14Bに対して、上の Qwen3のパラメータサイズは4bなので、この差によるものかと思います。Qwen3:4bは、起動から推論実行までを非常に軽快に動作させられます。

【おまけ】

今回の目的は「NPUでローカルLLMを動かす」ですが、同じコード、同じクエリをGPUで動かしたらどうなるのか? 以下がその結果です。

前処理で約22秒。

推論処理で1分5秒。

この結果を見ると、同じCPUに搭載されているGPUとNPUのどちらを使うかと聞かれれば、GPUを選んでしまいそうです。ただし、ここには「消費電力」という大事なパラメータが出てきていません。一般に NPUのメリットには「消費電力の低さ」があげられています。消費電力と処理時間とのかけ合わせで俯瞰したときに、NPUとGPUのどちらを選ぶのが最適になるのかは非常に気になるところだと思います。ただし、今回の評価環境ではNPUやGPUの稼働時の消費電力を調べる方法がなかったため、この点は評価から省かざるを得ませんでした。

6)その他

理由はわからないのですが、NPUを使って推論する場合、指定した最大トークン数に満たない時点で推論が終わるケースがあります。特に Qwen3 だとデフォルトで Thinking Modeで動くのですが、この Thinking Mode が長いとその途中で終わってしまうことが多々ありました。同じモデルとクエリで GPU上で稼働させたときには途中で終わることはないので、何かしらNPU側に制限があるのかもしれません。

さいごに

 Phi-4のような比較的大きなローカルLLMでも、NPUで動作することが確認できました。実際にPhi-4を使うには、現時点のNPUではややパフォーマンス不足だと感じます。一方、Qwen3:4b程度であれば、ストレスのないレスポンスで回答を得られるため、モデルを選べば「NPUとローカルLLM」の組み合わせは十分に実用的なレベルに達していると言えるでしょう。

 NPUについても、Intel社だけでなく、AMD社もCPUにオンパッケージでNPUを搭載しているAI対応CPUをリリースしています。NPUの性能(TOPS)的にはAMD社のNPUのほうが勝っているようなので、こちらも機会があれば評価してみたいと考えています。

サンプルコード

# Generative LLM w/ Intel NPU
#---
import datetime, os, argparse, logging
from logging import FileHandler
import openvino_genai as ov_genai
logging.basicConfig(
    level=logging.DEBUG,
    format="%(message)s",
    datefmt="[%X]",
    handlers=[FileHandler(filename="log.txt")]
)
def streamer(subword: str) -> bool:
    print(subword, end="", flush=True)
def main(model_path: str, device: str, debug:bool):
    print("Start main function.", flush=True)
   pipeline_config = { "CACHE_DIR": ".npucache" }
    st = datetime.datetime.now()
    print(f"---> モデル等の初期準備・開始時刻: {st}")
    if (device=="NPU"):
        print("準備にしばらく時間がかかります。数分おまちください。\n---", flush=True)
        pipe = ov_genai.LLMPipeline(model_path, device, CACHE_DIR=".npucache")
    elif (device=="GPU"):
        print("準備にしばらく時間がかかります。\n---", flush=True)
        pipe = ov_genai.LLMPipeline(model_path, device, CACHE_DIR=".gpucache")
    else:
        print("準備にしばらく時間がかかります。\n---", flush=True)
        pipe = ov_genai.LLMPipeline(model_path, device, CACHE_DIR=".cpucache")
    et = datetime.datetime.now()
    t = et - st
    print(f"---> 準備処理・終了時刻: {et} (所要時間: {t})\n---")
    print("\n\n開始します...", flush=True)
    if (debug):
        read_msg = "20世紀の哲学者で著名な3名を理由も併せて回答して。"
        st = datetime.datetime.now()
        print(f"---> 推論・開始時刻: {st}")
        print("---\n質問:"+read_msg)
        print(pipe.generate(read_msg, max_new_tokens=32000, streamer=streamer))
        et = datetime.datetime.now()
        t = et - st
        print(f"---\n---> 推論・終了時刻: {et} (所要時間: {t})\n---")
        return
    while True:
        read_msg = input("質問を入力してください。: ")
        if read_msg == "\\q":
            print("\n\n終了します。")
            exit(0)
        else:
            rslt_msg = pipe.generate( read_msg, max_new_tokens=32000, streamer=streamer)
            print()
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("model_dir", help='model_dir')
    parser.add_argument("--device", default="CPU")
    parser.add_argument("--debug", default=False)
    args = parser.parse_args()
    main(args.model_dir, args.device, args.debug)
掲載されている文章、イラストおよび写真は、BIGLOBE又は第三者が著作権を有しており、
権利者の許諾なく利用できません。

「BIGLOBE Style」は「はてなブログ」にて運用しており、
ブログ内でのCookie情報は株式会社はてなが収集します。

詳細は同社のプライバシーポリシーをご確認ください。
本サイトの商標についてはこちら