DatabricksのLLM最適化モデルサービングにLlama2モデルをデプロイしてみる

こちらのサンプルノートブックをウォークスルーします。

https://qiita.com/taka_yayoi/items/f0ef1fae82197ec51725#llama2%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B%E6%9C%80%E9%81%A9%E5%8C%96llm%E3%82%B5%E3%83%BC%E3%83%93%E3%83%B3%E3%82%B0%E3%83%8E%E3%83%BC%E3%83%88%E3%83%96%E3%83%83%E3%82%AF

# 最適化Llama2サービングのサンプル

最適化LLMサービングによって、最先端のOSSのLLMを活用し、GPUによって改善されたレーテンシーとスループットを提供する自動最適化機能を持つDatabricksモデルサービングにデプロイすることができます。現時点では、Llama2とMosaic MPTクラスのモデルの最適化をサポートしています。

このサンプルでは以下をウォークスルーします:
1. Hugging Face `transformers`からモデルをダウンロード
1. DatabricksのUnity Catalogあるいはワークスペースレジストリに、最適化されたサービングサポートフォーマットでモデルを記録
1. モデルの最適化サービングの有効化

## 前提条件

- ノートブックを十分なメモリーを持つクラスターにアタッチ
- MLflowバージョン2.7.0以降がインストールされていること
- 7B以上のサイズのモデルを取り扱う際には特に**UCでのモデル管理**を行うこと

# ステップ1: 最適化LLMサービングのためにモデルを記録

```py
# 必要な依存関係を更新/インストール
!pip install -U mlflow
!pip install -U transformers
!pip install -U accelerate
!pip install -U flash_attn # DBR 13.3 MLでエラーになったので追加
dbutils.library.restartPython()
```

Hugging faceのトークンを入力します。詳細は[こちら](https://qiita.com/taka_yayoi/items/bc1da42144826da56ab4)。

```py
import huggingface_hub
# 既にhugging faceにログインしている際にはスキップ
huggingface_hub.login()
```

```py
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-chat-hf", 
    torch_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(
    "meta-llama/Llama-2-7b-chat-hf"
)
```

```py
import mlflow
from mlflow.models.signature import ModelSignature
from mlflow.types.schema import ColSpec, Schema
import numpy as np

# モデルの入出力スキーマの定義
input_schema = Schema([
    ColSpec("string", "prompt"),
    ColSpec("double", "temperature", optional=True),
    ColSpec("integer", "max_tokens", optional=True),
    ColSpec("string", "stop", optional=True),
    ColSpec("integer", "candidate_count", optional=True)
])

output_schema = Schema([
    ColSpec('string', 'predictions')
])

signature = ModelSignature(inputs=input_schema, outputs=output_schema)

# 入力サンプルの定義
input_example = {
    "prompt": np.array([
        "Below is an instruction that describes a task. "
        "Write a response that appropriately completes the request.\n\n"
        "### Instruction:\n"
        "What is Apache Spark?\n\n"
        "### Response:\n"
    ])
}
```

最適化サービングを有効化するには、モデルを記録する際に以下のように`mlflow.transformers.log_model`を呼び出して追加のメタデータのディクショナリーを含めます:

```
metadata = {"task": "llm/v1/completions"}
```

ここで、モデルサービングエンドポイントで使用されるAPIシグネチャを指定します。

```py
import mlflow

# UCでのモデル管理を行わない場合には以下の行をコメントアウトします
# 3レベルの名前空間ではなく、シンプルにモデル名を指定します
mlflow.set_registry_uri('databricks-uc')
CATALOG = "quickstart_catalog_taka"
SCHEMA = "llm-catalog"
registered_model_name = f"{CATALOG}.{SCHEMA}.llama2-7b"

# 新規MLflowランをスタート
with mlflow.start_run():
    components = {
        "model": model,
        "tokenizer": tokenizer,
    }
    mlflow.transformers.log_model(
        transformers_model=components,
        task = "text-generation",
        artifact_path="model",
        registered_model_name=registered_model_name,
        signature=signature,
        input_example=input_example,
        metadata={"task": "llm/v1/completions"}
    )
```

モデルが記録されます。4分弱かかります。
![Screenshot 2023-10-08 at 17.12.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/b5be9e26-2769-7b2e-89e1-8d6ca26504ff.png)

カタログエクスプローラからも確認できます。
![Screenshot 2023-10-08 at 17.11.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/8f723e93-54e8-e3fe-cbb0-3256730986b6.png)


# ステップ2: モデルサービングGPUエンドポイントの設定および作成

エンドポイント名を変更するには以下のセルを修正します。エンドポイントAPIを呼び出した後は、記録されたLlama2モデルは自動的に最適化LLMサービングにデプロイされます。`workload_type`は環境で利用できるスペックのGPUを指定する必要があります。

```py
# MLflowエンドポイント名の設定
endpoint_name = "taka-llama2-7b"

# 登録されたMLflowモデル名
model_name = registered_model_name

# MLflowモデルの最新バージョンの取得
model_version = 1

# 計算資源タイプの指定 (CPU, GPU_SMALL, GPU_MEDIUM, etc.)
workload_type = "GPU_MEDIUM"

# 計算資源のスケールアウトサイズの指定 (Small, Medium, Large, etc.)
workload_size = "Small" 

# ゼロへのスケールの指定 (CPUエンドポイントでのみサポート)
scale_to_zero = False 

# 現在のノートブックコンテキストからAPIエンドポイントとトークンを取得
API_ROOT = dbutils.notebook.entry_point.getDbutils().notebook().getContext().apiUrl().get() 
API_TOKEN = dbutils.notebook.entry_point.getDbutils().notebook().getContext().apiToken().get()
```

```py
import requests
import json

data = {
    "name": endpoint_name,
    "config": {
        "served_models": [
            {
                "model_name": model_name,
                "model_version": model_version,
                "workload_size": workload_size,
                "scale_to_zero_enabled": scale_to_zero,
                "workload_type": workload_type,
            }
        ]
    },
}

headers = {"Context-Type": "text/json", "Authorization": f"Bearer {API_TOKEN}"}

response = requests.post(url=f"{API_ROOT}/api/2.0/serving-endpoints", json=data, headers=headers)

print(json.dumps(response.json(), indent=4))
```


# エンドポイントの参照

エンドポイントのより詳細な情報を参照するには、左のナビゲーションバーの**Serving**にアクセスし、エンドポイント名で検索します。
![Screenshot 2023-10-09 at 15.00.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/046677f6-2e71-fae5-0954-b43861ec6b2c.png)

# ステップ3: エンドポイントへのクエリー

エンドポイントの準備ができたら、APIリクエストを行うことでクエリーできるようになります。モデルのサイズや複雑性に依存しますが、準備ができるまでに30分以上を要します。

:::note
**GPU Medium**の場合、30分程度でReadyになりました。
:::

```py
data = {
    "inputs": {
        "prompt": [
            "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nWhat is Apache Spark?\n\n### Response:\n"
        ]
    },
    "params": {
        "max_tokens": 100, 
        "temperature": 0.0
    }
}

headers = {
    "Context-Type": "text/json",
    "Authorization": f"Bearer {API_TOKEN}"
}

response = requests.post(
    url=f"{API_ROOT}/serving-endpoints/{endpoint_name}/invocations",
    json=data,
    headers=headers
)

print(json.dumps(response.json(), indent=4))
```

クエリーできました！

```json:JSON
{
    "predictions": [
        {
            "candidates": [
                {
                    "text": "Apache Spark is an open-source data processing engine that can handle large-scale data processing tasks. It was developed at the University of California, Berkeley and is now maintained by Apache Software Foundation. Spark provides high-level APIs in Java, Python, Scala, and R, and supports a wide range of data sources, including Hadoop Distributed File System (HDFS), Cassandra, HBase, and Hive. Spark's core features include in-memory computing, fault",
                    "metadata": {
                        "finish_reason": "length"
                    }
                }
            ],
            "metadata": {
                "input_tokens": 41,
                "output_tokens": 100,
                "total_tokens": 141
            }
        }
    ]
}
```

### Databricksクイックスタートガイド

[Databricksクイックスタートガイド](https://www.amazon.co.jp/dp/B09V1YXFVQ/)


### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)