M5Stack Module LLMでFunction Callingを実行～StackFlow編


[前回の記事](https://qiita.com/motoh_qiita/items/1b0882e507e803982753)で、Hugging Faceで見つけたFunction Calling対応の軽量なLLMモデルをModule LLMで実行できるようにしましたが、Ubuntuにログインして直接実行ファイルをたたく形式だったため、M5Stack Coreから使用できる形にはなっていませんでした。今回はStackFlowというModule LLM側のフレームワークにモデルを取り込み、M5Stack Coreからシリアル通信経由で使用できるようにします。なお、StackFlowは修正・ビルドすることなく実現できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624001/097130f4-a507-4c4d-917c-b56b592e752c.png)
実行した様子

## 実行環境
- Module LLM : 2ndロット (M5_LLM_ubuntu_v1.3_20241203-mini)
- M5ModuleLLMライブラリ : Ver.1.3.0


## 手順
基本的には前回使用したファイルを決まったディレクトリに配置するのが主ですが、一部ファイルの中身を修正したり、M5Stack Core側のライブラリ（M5ModuleLLM）を修正したりする必要があります。順を追って解説します。

### LLMモデル(axmodel)を配置
前回pulsar2 llm_buildで得られたモデルファイル一式をフォルダにまとめて /opt/m5stack/data に配置します。800MB程度ありストレージを圧迫するので、私はSDカードに保存してシンボリックリンクを貼りました。

```
cd /opt/m5stack/data
ln -s /mnt/mmcblk1p1/SmolLM-360M-Instruct-fncl SmolLM-360M-Instruct-fncl
```

ここで、このフォルダに新たなJSONファイルを追加します。
```
SmolLM-360M-Instruct-fncl
|-- SmolLM-360M-Instruct-fncl.json　※新たに追加
|-- llama_p512_l0_together.axmodel
|-- llama_p512_l1_together.axmodel
|-- llama_p512_l2_together.axmodel
|-- 　　　・・・
|-- llama_p512_l31_together.axmodel
|-- llama_post.axmodel
|-- model.embed_tokens.weight.bfloat16.bin
|-- model.embed_tokens.weight.float32.bin
`-- model.embed_tokens.weight.npy
```

JSONファイルの内容は以下の通りです。これは、前回作成したmain_prefill実行用スクリプトrun_smoilm_fncl_ax630c.shの内容に相当します。
>このJSONファイル名やこの後登場するモデル名は、ここで配置したフォルダ名と完全に一致させる必要があります。

```json:SmolLM-360M-Instruct-fncl.json
{
    "mode":"SmolLM-360M-Instruct-fncl",
    "type":"llm",
    "capabilities":[
        "text_generation",
        "chat"
    ],
    "input_type":[
        "llm.chat_completion",
        "llm.chat_completion.stream"
    ],
    "output_type":[
        "llm.utf-8",
        "llm.utf-8.stream"
    ],
    "mode_param":{
        "tokenizer_type":2,
        "filename_tokenizer_model":"http://localhost:8080",
        "filename_tokens_embed":"model.embed_tokens.weight.bfloat16.bin",
        "filename_post_axmodel":"llama_post.axmodel",
        "template_filename_axmodel":"llama_p512_l%d_together.axmodel",
        "b_use_topk":false,
        "b_bos":false,
        "b_eos":false,
        "axmodel_num":32,
        "tokens_embed_num":49152,
        "tokens_embed_size":960,
        "b_use_mmap_load_embed":true,
        "b_dynamic_load_axmodel_layer":false
    }
}
```

### トークナイザを配置
/opt/m5stack/scripts に前回使用したトークナイザのJSONとPythonスクリプトを配置します。フォルダ、ファイル名は以下のようにモデル名に合わせます。


```
scripts/
|-- SmolLM-360M-Instruct-fncl
|   |-- tokenizer.json
|   `-- tokenizer_config.json
|-- SmolLM-360M-Instruct-fncl_tokenizer.py
```
ここで、Pythonスクリプトは前回のものから2点変更が必要です。

(1) model_idをトークナイザのJSONを置いたフォルダの**絶対パス**に変更。
```python
class Tokenizer_Http():

    def __init__(self):
        model_id = "/opt/m5stack/scripts/SmolLM-360M-Instruct-fncl"
        self.tokenizer = AutoTokenizer.from_pretrained(model_id)

```
(2) コマンドライン引数に--model_idと--contentを追加。
　（これらの引数は使わないが、StackFlowのmain_llm/src/main.cppからスクリプトを実行するときに指定されるため、引数として受け取れるようにしておかないとエラー終了してしまう）
```python
if __name__ == "__main__":

    args = argparse.ArgumentParser()
    args.add_argument('--host', type=str, default='localhost')
    args.add_argument('--port', type=int, default=8080)
    args.add_argument('--model_id', type=str, default='')
    args.add_argument('--content', type=str, default='')
    args = args.parse_args()
```
### M5Stack Core側のプログラムを作成
M5ModuleLLMライブラリのサンプルスケッチTextAssistantを利用しました。LLMモデルの設定を以下のように変更します。

```c++:TextAssistant.ino
void setup()
{
    ...
    
    /* Setup LLM module and save returned work id */
    M5.Display.printf(">> Setup llm..\n");
    //llm_work_id = module_llm.llm.setup();
    m5_module_llm::ApiLlmSetupConfig_t config;
    config.model = "SmolLM-360M-Instruct-fncl";   //モデル名を指定
    config.max_token_len = 511;    //Function Callingのプロンプトの
                                   //サイズを考慮したトークン長上限を設定
    llm_work_id = module_llm.llm.setup(config);

    //Note: setup()でタイムアウトすると返ってくるIDがnullになる
    M5.Display.printf("LLM work ID: %s\n", llm_work_id.c_str());
    Serial.print("Initialized...");
}
```

早速M5Stack Coreにダウンロードして実行したいところですが、このままだとllm.setup()内でのModule LLM側のトークナイザとLLMプログラムの起動完了を待つ処理でタイムアウトが発生し、正しく実行することができません（上記コメントの通り、返ってくるIDがnullになります）。タイムアウト待ち時間を延ばすために、M5ModuleLLMライブラリのapi_llm.cppを以下のように修正する必要があります。
> Arduino IDEの場合、ライブラリはDocuments\Arduino\libraries\にあります。

```c++:api_llm.cpp
String ApiLlm::setup(ApiLlmSetupConfig_t config, String request_id)
{
    ...
    
    String llm_work_id;
    _module_msg->sendCmdAndWaitToTakeMsg(
        cmd.c_str(), request_id,
        [&llm_work_id](ResponseMsg_t& msg) {
            // Copy work id
            llm_work_id = msg.work_id;
        },
        60000);    //10000 -> 60000に修正
    return llm_work_id;
}
```
以上で実行可能になります。Arduino IDEのシリアルモニタ等でテキストを送信すると冒頭の画像のような結果を得られます。