【M5 Stack Module LLM】NPU上で文章生成～音声生成をまるっと行う

# はじめに
こんにちは！

ノベルワークスでアルバイトしているザワッチ([@zawatti](https://x.com/zawattizawawa))です。

今回は、M5 Stack Module LLMというLinuxボードにNPUが搭載されている組み込みコンピュータ上で入力したテキストから、文章を生成し、生成した文章から音声生成をしていきます。

NPUを搭載している組み込みコンピュータを触ることが初めてで、わからないことも多いですが、

- 完全オフラインの状況下で、エッジAIはどれほど処理ができるのか
- NPU上で動くLLM・TTSモデルのレスポンス速度と処理性能

という視点を考慮に入れながら作っていきます。

# M5 Stack Module LLMとは
主に端末向けデバイスとして開発されたLLMを推論させるためのモジュールです。

Module LLMにはAX630CというNPUが内蔵されたシステムオンチップ(SoC)が搭載されており、そこにはプログラムを処理させるための一通りのパーツが集約されています。

NPUはCPU、GPUと異なり、ニューラルネットワークの構造のように多段かつ並列に演算ユニットを組んでおり、それらの演算ユニットが相互につながっているために演算効率が向上しています。

システムメモリ1GB、NPU用メモリ3GB、ストレージ容量32GBという制約もあります。

詳しいスペックは以下の公式サイトをご覧ください。

https://docs.m5stack.com/ja/module/Module-LLM

# 文章生成～音声生成の流れ
使用する文章生成モデルと音声生成モデルは以下の通りです。

<strong>文章生成モデル：qwen2.5_0.5B
</strong>
Qwen0.5 は約0.5B（5億）のパラメータを持つLLMです。

より大きなサイズ（1.5B、3B、7B、14B、32B、72B）と比べると非常に軽量であり、計算資源が限られた環境でも動作させやすいのが特徴です。

https://github.com/QwenLM/Qwen2.5

<strong>音声生成モデル：MeloTTS</strong>

MyShell.aiによる高品質な多言語音声合成ライブラリで、英語・日本語をはじめとしたテキストから音声を生成します。

https://github.com/myshell-ai/MeloTTS

学習方法もドキュメントあるようなので、いろいろ遊べそうです。

https://github.com/myshell-ai/MeloTTS/blob/main/docs/training.md

前提として、
- Module LLMはインターネットに接続済み
- モデルの構築は別PCで行う

とします。

::: note info
Module LLMのインターネットへの接続ですが、Module LLMのDebug Kitをルーターに有線で接続することで、プライベートIPが割り当てられます。
その状態でModule LLMとPCをUSB接続し、SSHでプライベートIPに接続しました。
:::

## 1. スピーカーの確認
M5 Stack LLM上でTTSをするために、音声デバイスが反応するかを確かめます。

まず、Linuxボード上に搭載されているオーディオデバイスのユーティリティ群がパッケージになっているALSAをダウンロードします。

```
sudo apt install alsa-utils
```
aplayコマンドでLinuxボード上に接続されているスピーカーを確認します。

```
aplay -l

**** List of PLAYBACK Hardware Devices ****
card 0: Audio [Axera Audio], device 1: 6050000.i2s_mst-actt 23f2000.audio_codec-1 [6050000.i2s_mst-actt 23f2000.audio_codec-1]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
```

上記の出力から、スピーカーが認識されていることを確認しました。

次に、スピーカーから音声が出ることを確認します。

```
aplay test.wav

Signed 16 bit Little Endian, Rate 44100 Hz, Mono
aplay: set_params:1416: Unable to install hw params:
...
```

:::note info
サンプルレートを48000Hzにして、録音・再生すると音が出ました
:::

## 2. Pulsar2の環境構築
モデルをAX630C上で動作可能な形式に変換するために、Pulsar2を使用します。

https://pulsar2-docs.readthedocs.io/en/latest/user_guides_quick/quick_start_prepare.html

Pulsar2はAxera社が独自に開発したオールインワン型ニューラルネットワークコンパイラです。

ONNX形式のニューラルネットワークモデル(.onnxファイル)を入力し、以下のステップでモデルを処理していきます。

<strong>1. モデルの変換</strong>
モデル内部の演算処理をターゲットとなるハードウェアプラットフォームに適した表現に変換します。

<strong>2. 量子化</strong>
モデルの精度を保ちながら、計算効率とメモリ使用量を最適化するために、モデルの重みなどで使用される数値の精度を荒くします。これによって、モデルの表現能力自体は低下しますが、計算回数、モデル容量が減り、推論が高速化します。

<strong>3. コンパイル</strong>
量子化されたモデルをターゲットチップ（今回は、AX630C)上で実行可能な形式にコンパイルします。

この過程で、チップに搭載されているNPUの特性に合わせた最適化が行われます。

上記のステップの実行された後の最終的な出力は、ターゲットチップ上で実行可能な形式のモデルファイル(.axmodel)です。

このファイルは、AXEngine APIを介して、チップ上でロード・実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478853/e5e6a12b-bf7b-53f4-a50e-d0a65ce833fe.png)

## 3. モデルを.onnx形式に変換
文章生成モデルに関しては、すでに以下のGoogle Driveに.axmodel形式への変換後のzipファイルがあったので、以下からダウンロードしました。

https://drive.google.com/drive/folders/14gYqz2SvpuWwoHY45M0KGOyRK4Ndo0rK

音声生成モデルに関しては、AXERA社の中の人が、MelottsモデルをAX630Cで動かすための方法を公開していたので、これをベースにモデルを構築していきます。

以下のREAD.MEを参考にレポジトリをクローン、仮想環境を作成し、必要なライブラリをインストールします。

https://github.com/ml-inory/melotts.axera/tree/main/python

以下のPythonファイルを実行することで、Melottsモデルをダウンロードし、モデルをロードして、Pulsar2が受け入れ可能な形である.onnx形式に変換します。

```
python convert.py -l JP
```

以下のQiita記事を参考にさせていただきました。

https://qiita.com/nnn112358/items/2e2e18e5fb4a3d3fd6b9

## 4. onnx → axmodelに変換
2.で構築したPulsar2の環境に入り、onnx形式からaxmodel形式に変換します。

```
sudo docker run -it --net host --rm -v $PWD:/data pulsar2:3.3
```

MeloTTSはEncoder、Decoderという大きな2つのネットワークで構成されています。

Encoderでは、入力されたシーケンスをAIが処理しやすい数値的な中間表現に変換します。

Decoderでは、その中間表現をもとに音声波形を生成していきます。

<strong>Encoder:</strong>
```
pulsar2 build --input encoder-jp.onnx --config config_encoder_u16.json --output_dir encoder --output_name encoder-jp.axmodel --target_hardware AX620E --npu_mode NPU2 --compiler.check 0
```
:::note info
PCのメモリ容量の都合上、encoder層は変換していません
:::

<strong>Decoder:</strong>
```
pulsar2 build --input decoder-jp.onnx --config config_decoder_u16.json --output_dir decoder --output_name decoder-jp.axmodel --target_hardware AX620E --npu_mode NPU2 --compiler.check 0
```

処理に成功すると、<strong>decoder-jp.axmodel</strong>というファイルが出力されています。

## 5. 文章生成モデル、音声生成モデルをそれぞれで実行してみる

### 文章生成モデルの実行
モデルを構築したPCのvscode上で、Module LLMにSSH接続し、コマンドライン上で操作していきます。

3.でダウンロードしたzipファイルを展開します。

展開されたファイルにある<strong>run_qwen2.5_0.5B_prefill_ax630c.sh</strong>を実行します。

すると、以下の動画のようにコマンドラインから文字を入力することができ、エンターを押すと、

qwen2.5_0.5Bモデルにテキストが入力され、文章を生成します。

1秒間に10トークンとかなり速い速度で生成されていることがわかります。

が、LLMの中ではとても小さいモデルを使っているので、応答がちょっとあれなところもあります。

https://x.com/zawattizawawa/status/1886778497252876613


### 音声生成モデルの実行

5.で作成した音声生成モデルをModule LLMに移動させます。

以下のPythonファイルを実行し、コマンドラインに入力した文章の音声を生成させます。

```
python3 melotts.py -s "となりの客はよく柿食う客だ" -e ../models/encoder-jp.onnx -d ../models/decoder-jp.axmodel --language JP
```

生成に成功すると、生成された音声が録音されている<strong>output.wav</strong>が出力されます。



## 6. 文章生成～音声生成を行う
文章生成モデルと音声生成モデルの読み込み時間がかなりかかるため、事前にModule LLM上にサーバーを立ち上げ、ロードしておきます。

大雑把な処理の流れは以下の通りです。

1. サーバーへプロンプト含めたリクエスト
2. 文章生成モデルにプロンプトを入力し、実行
3. 生成されたテキストを文単位で分割
-----以下文単位でループ-----
4. 生成された文章をトークナイズし、シーケンスに変換
5. 音声生成モデルに入力
6. 生成された音声波形をリストに格納 
-----ループ終了後-------------
7. 各文ごとの音声波形を1つの連続した音声データに統合  

::: note info
音声生成処理まわりは以下のコードを参考にしています。

https://github.com/ml-inory/melotts.axera/blob/main/python/melotts.py

:::


以下が実際の動画になります。

https://x.com/zawattizawawa/status/1886751189087477793

## 思ったこと
- 複数文章になったときのレスポンスが遅さ
→ 音声生成モデルのEncoder層ののボトルネック感がすごいあるので、NPUに最適化するともっと早くなりそう

- 文章ごとに、先に音声生成モデルに入力させるバケツリレー方式であると、生成時間の遅さがカバーできてよさそう
→ 文章生成モデルと音声生成モデルをストリーミングみたいにつなげれたらいい。もしくは組み合わさったモデルを作るべきか。

- 文章生成がループしてしまう
→ 文章生成モデルは0.5Bでかなり小さいので致し方ない。

## 補足(whisperモデルの.axmodelへの変換について)

https://github.com/ml-inory/whisper.axera

対応しているバージョンはいろいろありますが、whisper-v3-large-turboは未対応なようです。(2025/01/29時点)

```python
choices=[
    "tiny", "tiny.en", "base", "base.en",
    "small", "small.en", "medium", "medium.en",
    "large", "large-v1", "large-v2", "large-v3",
    "distil-medium.en", "distil-small.en", "distil-large-v2",
    # "distil-large-v3", # distil-large-v3 is not supported!
    # for fine-tuned models from icefall
    "medium-aishell",
    ],
```

しかも、AX650でのみ対応しているらしく、AX630Cではまだ実行を確認できていません。

onnxのバージョンアップに追従しているようですが、対応していない演算を使っているモデルに関してはpulsar2で変換できなさそうです。

https://github.com/onnx/onnx/blob/main/docs/Operators.md

https://pulsar2-docs.readthedocs.io/en/latest/appendix/op_support_list_ax620e.html

whisperモデルもNPU上で動かすことができれば、STTからの文章生成からの音声生成をまるっとできる可能性が出てくるので楽しみです。（既にやってる人もいそうですが）

# さいごに
今回は、NPU上で生成AIに文章生成～音声生成をリアルタイムでとはいえないですが、実行することができました。

前回の記事で利用したRealtimeAPIのレイテンシの速さとしゃべりの流暢さの違いを目の当たりにしました。（ベースのモデルがかなり優秀ですが）

モデルをオフラインで実行しましたが、ネットワークに接続されている状況下では、入力に応じてクラウドのモデルを使うなりできたらハイブリッドにできていいのかなと思いました。

次は、音声認識の部分もNPU上で行ってみたいと思います。