20
6

背景

好奇心と物欲に負けて、最近Intel Core Ultra7のCPUを搭載したPCを購入しました。しかし、AIプロセッサー(NPU)など最新機能をいまだに体感できておりません。

なにかできないかと考えていた際、Intel社のOpenVINOを使えばIntelのCPU内蔵GPUで、ディープラーニングが可能であることを思い出しました。

そこで、LLM(大規模言語モデル)のような高い要求スペックのモデルをOpenVINOで動かすことで、Intel Core Ultra7の最新機能を体感できるのではないかと思い、調査と検証してみました。

検証環境

  • 検証環境①
    • CPU:Intel® Core™ Ultra 7 155H
    • GPU:Intel® Arc™ graphics
    • NPU:Intel® AI Boost
    • 分析環境
      • Anaconda
        • JupyterLAB

用語解説

OpenVINO

ELYZA-japanese-Llama-2-7b

NPU

NPUは、CPUやGPUと比較して、AI計算に特化したアーキテクチャを持っているため、特定のAIタスクに対しては非常に高い性能を発揮します。これにより、AIモデルの推論やトレーニングが効率的に行えるため、スマートフォンや自動運転車、IoTデバイスなど、さまざまな応用分野で利用されています。

検証概要

LLMをローカルPCのNPUで演算させることを目的であり、性能を検証するものではありません。
モデルは、ELYZA-japanese-Llama-2-7bを利用します。

今回の検証では、以下のライブラリを試しています。

  1. OpenVINO
  2. intel-npu-acceleration-library

なお、OpenVINO形式のモデルでLLMをNPU演算させるのはエラーの解消が困難と判断し、挫折しました。
その代替手段として、2の手段を実施しています。

OpenVINO

以下、ドキュメントを参考に実装。ただし、下記の実装ではNPU演算がうまくいきません。
そのため、本項目は、CPUとGPUで演算させる実装となります。

モデル取得

ELYZA-japanese-Llama-2-7bをOpenVINO形式のモデルに変換して保存する。

from optimum.intel import OVModelForCausalLM, OVWeightQuantizationConfig
from huggingface_hub import login
import nncf

# Hugging Face Hubにログイン
login("Hugging Faceから取得")

# モデルIDの設定
model_id = "elyza/ELYZA-japanese-Llama-2-7b-instruct"

# 量子化の設定を指定
quantization_config = OVWeightQuantizationConfig(
    bits=4,  # 4ビットに量子化する(GPUがメモリリークするため軽量化)
)

# モデルをHugging Faceから読み込み、OpenVINO形式に変換し、量子化設定を適用
model = OVModelForCausalLM.from_pretrained(
    model_id,  # 使用するモデルのID
    export=True,  # OpenVINO形式に変換するかどうか
    quantization_config=quantization_config  # 量子化設定を指定
)

# 保存先のパスを設定
optimized_model_path = "./optimized_model"

# 最適化後のモデルを指定したパスに保存
model.save_pretrained(optimized_model_path)

推論

推論のコードは以下の実装となります。
なお、出力の性能を評価する目的ではないため、プロンプトは固定しています。

from optimum.intel import OVModelForCausalLM, OVConfig
from huggingface_hub import login
from transformers import AutoTokenizer

class TextGenerator:
    def __init__(self, optimized_model_path, target_device="CPU"):
        # Hugging Face Hubにログイン
        login("Hugging Faceのマイページから取得")
        self.optimized_model_path = optimized_model_path
        self.target_device = target_device
        self.model, self.tokenizer = self.load_model_and_tokenizer()

    def load_model_and_tokenizer(self):
        # モデルの読み込みとデバイス設定
        config = OVConfig(device=self.target_device)
        model = OVModelForCausalLM.from_pretrained(self.optimized_model_path, config=config)
        model.to(self.target_device)
        
        # トークナイザーの読み込み
        model_name = "elyza/ELYZA-japanese-Llama-2-7b-instruct"
        tokenizer = AutoTokenizer.from_pretrained(model_name)
        return model, tokenizer

    def create_prompt(self):
        # インストラクションの開始と終了を示すトークン
        B_INST, E_INST = "[INST]", "[/INST]"
        # システムプロンプトの開始と終了を示すトークン
        B_SYS, E_SYS = "<<SYS>>\n", "\n<</SYS>>\n\n"
        # デフォルトのシステムプロンプト
        DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人のアシスタントです。"
        # ユーザーがアシスタントに指示する具体的なプロンプトの内容
        text = "クマが海辺に行ってアザラシと友達になり、最終的には家に帰るというプロットの短編小説を書いてください。"
        # トークンを組み合わせて最終的なプロンプトを生成
        prompt = "{bos_token}{b_inst} {system}{prompt} {e_inst} ".format(
            bos_token=self.tokenizer.bos_token,
            b_inst=B_INST,
            system=f"{B_SYS}{DEFAULT_SYSTEM_PROMPT}{E_SYS}",
            prompt=text,
            e_inst=E_INST,
        )
        return prompt

    def generate_text(self, prompt):

        token_ids = self.tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
        
        output_ids = self.model.generate(
            token_ids.to(self.model.device),
            max_new_tokens=256,
            pad_token_id=self.tokenizer.pad_token_id,
            eos_token_id=self.tokenizer.eos_token_id,
        )
        output = self.tokenizer.decode(output_ids.tolist()[0][token_ids.size(1) :], skip_special_tokens=True)

        # 生成されたテキストをデコードし、特殊トークンをスキップして出力
        return output
CPU結果

4bitに量子化したものの多少時間がかかるように思えます。

text_generator = TextGenerator(optimized_model_path="./optimized_model", target_device="CPU")
%%time
prompt = text_generator.create_prompt()
generated_text = text_generator.generate_text(prompt)
print(generated_text)

>>> 承知しました以下にクマが海辺に行ってアザラシと友達になり最終的には家に帰るというプロットの短編小説を記述します
>>> 
>>> クマは家の外に出ると海辺にやってきましたクマは海辺に来るとアザラシがい ることが多いため今回もアザラシがいるのではないかと思いその場所に行きましたクマが海辺に着いたところではアザラシはいませんでしたクマはアザラシがいないことに少々がっかりしましたがそこでクマはアザラシと友達になることを決めその日を待ちわびるようになり
>>> CPU times: total: 1min 3s
>>> Wall time: 2min 4s
GPU結果

CPU演算より高速であることが確認できました。

text_generator = TextGenerator(optimized_model_path="./optimized_model", target_device="GPU")
%%time
prompt = text_generator.create_prompt()
generated_text = text_generator.generate_text(prompt)
print(generated_text)

>>> 承知しましたお伝えしたいことは、「短編小説を書くことは容易ではないことについてです
>>>
>>> 短編小説とは物語のあ る一断面を描写したものであり長い時間や空間を要する物語と比べて物語の舞台を絞ることができますしかし短編小説であるがゆえに物語を絞ることは容易である反 物語を膨らませることは難しくなるのです
>>> CPU times: total: 39.9 s
>>> Wall time: 1min 18s
NPU結果

エラーが発生しました。解決を試みましたが、こちら解消できませんでした。

text_generator = TextGenerator(optimized_model_path="./optimized_model", target_device="NPU")
%%time
prompt = text_generator.create_prompt()
generated_text = text_generator.generate_text(prompt)
print(generated_text)

>>> RuntimeError                              Traceback (most recent call last)
>>> File <timed exec>:2
>>> ・・・
>>> RuntimeError: Exception from src\inference\src\cpp\core.cpp:109:
>>> Exception from src\inference\src\dev\plugin.cpp:54:
>>> Exception from src\plugins\intel_npu\src\plugin\src\plugin.cpp:513:
>>> get_shape was called on a descriptor::Tensor with dynamic shape

intel-npu-acceleration-libraryを利用

下記サイトを参考に、intel-npu-acceleration-libraryを利用して、NPU演算で実行を試みる。

本ライブラリはOpenVINO形式のモデルに対応していないかつ、量子化する手段がないため、生成するトークンの最大数を大幅に落として検証しました。

実装

intel-npu-acceleration-libraryはoptimum.intelと共存ができないみたいですので、
参考にされる方いれば、環境にご注意ください。

from intel_npu_acceleration_library import NPUModelForCausalLM
from huggingface_hub import login
from transformers import AutoTokenizer, TextStreamer
import torch

class TextGenerator:
    def __init__(self):
        # Hugging Face Hubにログイン
        login("Hugging Faceのマイページから取得")
        self.model, self.tokenizer = self.load_model_and_tokenizer()

    def load_model_and_tokenizer(self):
        # トークナイザーの読み込み
        model_id = "elyza/ELYZA-japanese-Llama-2-7b-instruct"
        # モデルの読み込みとデバイス設定
        model = NPUModelForCausalLM.from_pretrained(model_id, use_cache=False, dtype=torch.int8).eval()
        tokenizer = AutoTokenizer.from_pretrained(model_id)
        return model, tokenizer

    def create_prompt(self):
        # インストラクションの開始と終了を示すトークン
        B_INST, E_INST = "[INST]", "[/INST]"
        # システムプロンプトの開始と終了を示すトークン
        B_SYS, E_SYS = "<<SYS>>\n", "\n<</SYS>>\n\n"
        # デフォルトのシステムプロンプト
        DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人のアシスタントです。"
        # ユーザーがアシスタントに指示する具体的なプロンプトの内容
        text = "クマが海辺に行ってアザラシと友達になり、最終的には家に帰るというプロットの短編小説を書いてください。"
        # トークンを組み合わせて最終的なプロンプトを生成
        prompt = "{bos_token}{b_inst} {system}{prompt} {e_inst} ".format(
            bos_token=self.tokenizer.bos_token,
            b_inst=B_INST,
            system=f"{B_SYS}{DEFAULT_SYSTEM_PROMPT}{E_SYS}",
            prompt=text,
            e_inst=E_INST,
        )
        return prompt

    def generate_text(self, prompt):

        token_ids = self.tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
        
        output_ids = self.model.generate(
            token_ids.to(self.model.device),
            max_new_tokens=4, # 4より大きくするとスペックが足りずエラーになります
            pad_token_id=self.tokenizer.pad_token_id,
            eos_token_id=self.tokenizer.eos_token_id,
        )
        output = self.tokenizer.decode(output_ids.tolist()[0][token_ids.size(1) :], skip_special_tokens=True)

        # 生成されたテキストをデコードし、特殊トークンをスキップして出力
        return output
text_generator = TextGenerator()
prompt = text_generator.create_prompt()
generated_text = text_generator.generate_text(prompt)
print(generated_text)

NPUの実行状況の抜粋

演算中にNPUが動いていることを確認しました。

image.png

総括

intel-npu-acceleration-libraryはまだ開発途中のライブラリで、NPUの機能の探索目的のもののようです。

今回、PCのスペックが不足しており、モデルの量子化や生成するトークンの最大数を大幅に下げる対応が必要でした。

TinyLlamaのような軽量なモデルであれば、Intel NPU Acceleration Libraryを使用することで、今回のような極端な対応をしなくても問題なく動作することが確認できています。

また、OpenVINOを利用してNPU上でLLMを実行できれば、モデルを量子化するなどの手段で、今回使用したPCのスペックでも違和感のないレベルの出力も得られたと思います。

最後に

最近になって、Copilot+PCなど最近よくニュースを見ますね、
NPU搭載したCPUに手を出すのはもう少し待ったほうがよかったかも?

また、エッジデバイスでText2Textのモデルを実行させたいユースケースは果たしてあるのか?
もし、ユースケースがあったとしても、個人的にはNVIDIA jetsonでいいじゃんって感じです。

多少ニッチな検証でしたが、少しでもお役に立てたなら、いいねやコメントをいただけると嬉しいです。

20
6
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
20
6