Databricks Dolly 2.0 と OpenVINO を使用した手順

この Jupyter ノートブックは、ローカルへのインストール後にのみ起動できます。

GitHub

指示追従は、現世代の大規模言語モデル (LLM) の基礎の 1 つです。人間の好みによる強化学習 (RLHF) や InstructGPT などの技術は、ChatGPT や GPT-4 などのブレークスルーの中核基盤となっています。ただし、これらの強力なモデルは API の背後に隠されたままであり、その基礎となるアーキテクチャーについてはほとんど分かっていません。指示従うモデルは、プロンプトに応じてテキストを生成でき、執筆支援、チャットボット、コンテンツ生成などのタスクによく使用されます。現在、多くのユーザーがこれらのモデルを定期的に操作し、仕事にも使用していますが、そのようなモデルの大部分はクローズドソースのままであり、実験には大量の計算リソースが必要です。

Dolly 2.0 は、透過で自由に利用できるデータセットを基に Databricks によって微調整された、初のオープンソースの指示追従 LLM であり、商業目的での使用もオープンソース化されています。つまり、Dolly 2.0 は、API アクセスの料金を支払ったり、サードパーティーとデータを共有したりすることなく、商用アプリケーションで利用できるということです。Dolly 2.0 は、はるかに小さいにもかかわらず、ChatGPT と比較して同様の特性を示します。

このチュートリアルでは、Dolly 2.0 と OpenVINO を使用して、指示追従テキスト生成パイプラインを実行する方法を検討します。Hugging Face Transformers ライブラリーの事前トレーニング済みモデルを使用します。ユーザー・エクスペリエンスを簡素化するために、Hugging Face Optimum Intel ライブラリーを使用してモデルを OpenVINO™ IR 形式に変換します。

このチュートリアルは次のステップで構成されます。

  • 前提条件をインストールします。

  • OpenVINO と Hugging Face Optimum の統合を使用して、パブリックソースからモデルをダウンロードして変換します。

  • OpenVINO NNCF を使用してモデルの重みを INT8 に圧縮します。

  • 指示追従推論パイプラインを作成します。

  • 指示追従パイプラインを実行します。

Dolly 2.0 について

Dolly 2.0 は、商用利用が許可されている Databricks マシンラーニング・プラットフォームでトレーニングされた指示追従 LLM です。Pythia は、ブレインストーミング、分類、クローズド QA、生成、情報抽出、オープン QA、要約など、さまざまな機能ドメインで Databricks の従業員によって生成された約 15,000 の指示/応答の微調整レコードに基づいてトレーニングされています。Dolly 2.0 は、自然言語命令を処理し、指定された指示に追従する応答を生成することによって機能します。クローズド質疑応答、要約、生成など幅広い用途に使用できます。

モデルのトレーニング・プロセスは InstructGPT からインスピレーションを受けました。InstructGPT モデルをトレーニングする中心的な手法は、ヒューマン・フィードバックからの強化学習 (RLHF) です。この手法は、人間の好みを報酬信号として使用してモデルを微調整します。これは、解決する必要がある安全性と調整の問題が複雑で主観的なものであるため重要です。単純な自動メトリクスでは完全にはキャプチャーされません。InstructGPT アプローチの詳細については、OpenAI ブログ投稿を参照してください。 InstructGPT による画期的な発見は、言語モデルには大規模なトレーニング・セットが必要ないということです。人間が評価した質疑応答トレーニングを使用することにより、著者は、以前のモデルより 100 倍少ないパラメーターを使用して、より優れた言語モデルをトレーニングすることができました。Databricks は同様のアプローチを使用して、databricks-dolly-15k と呼ばれるプロンプトおよび応答データセットを作成しました。これは、数千人の Databricks 従業員によって生成された 15,000 を超えるレコードのコーパスであり、大規模な言語モデルが InstructGPT の魔法のような対話性を示すことができるようになります。モデルとデータセットの詳細については、Databricks のブログ投稿リポジトリーを参照してください。

必要条件

最初に、OpenVINO 統合によって高速化された Hugging Face Optimum ライブラリーをインストールする必要があります。Hugging Face Optimum Intel API は、Hugging Face Transformers ライブラリーのモデルを OpenVINO™ IR 形式に変換および量子化できる高レベル API です。詳細については、Hugging Face Optimum Intel のドキュメントを参照してください。

%pip install -q "diffusers>=0.16.1" "transformers>=4.33.0" "openvino>=2023.2.0" "nncf>=2.6.0" onnx gradio --extra-index-url https://download.pytorch.org/whl/cpu
%pip install -q --upgrade "git+https://github.com/huggingface/optimum-intel.git"

OpenVINO を使用して推論を実行するためにドロップダウン・リストからデバイスを選択します。

import ipywidgets as widgets
import openvino as ov

core = ov.Core()

device = widgets.Dropdown(
    options=core.available_devices + ["AUTO"],
    value='CPU',
    description='Device:',
    disabled=False,
)

device
Dropdown(description='Device:', options=('CPU', 'GPU', 'AUTO'), value='CPU')

モデルのダウンロードと変換

Optimum Intel を使用すると、Hugging Face Hub から最適化されたモデルをロードし、Hugging Face API を使用して OpenVINO ランタイムで推論を実行するパイプラインを作成できます。Optimum 推論モデルは、Hugging Face Transformers モデルと API の互換性があります。つまり、AutoModelForXxx クラスを対応する OVModelForXxx クラスに置き換えるだけで済みます。

以下は Dolly モデルの例です。

-from transformers import AutoModelForCausalLM
+from optimum.intel.openvino import OVModelForCausalLM
from transformers import AutoTokenizer, pipeline

model_id = "databricks/dolly-v2-3b"
-model = AutoModelForCausalLM.from_pretrained(model_id)
+model = OVModelForCausalLM.from_pretrained(model_id, from_transformers=True)

モデルクラスの初期化は、from_pretrained メソッドの呼び出しから始まります。Transformers モデルをダウンロードして変換する場合は、パラメーター export=True を追加する必要があります。サイズが大きいモデルの場合、save_pretrained メソッドを使用して、変換されたモデルを次回使用するために保存できます。トークナイザー・クラスとパイプライン API は Optimum モデルと互換性があります。

from pathlib import Path
from transformers import AutoTokenizer
from optimum.intel.openvino import OVModelForCausalLM

model_id = "databricks/dolly-v2-3b"
model_path = Path("dolly-v2-3b")

tokenizer = AutoTokenizer.from_pretrained(model_id)

current_device = device.value

ov_config = {'PERFORMANCE_HINT': 'LATENCY', 'NUM_STREAMS': '1', "CACHE_DIR": ""}

if model_path.exists():
    ov_model = OVModelForCausalLM.from_pretrained(model_path, device=current_device, ov_config=ov_config)
else:
    ov_model = OVModelForCausalLM.from_pretrained(model_id, device=current_device, export=True, ov_config=ov_config, load_in_8bit=False)
    ov_model.half()
    ov_model.save_pretrained(model_path)
INFO:nncf:NNCF initialized successfully. Supported frameworks detected: torch, tensorflow, onnx, openvino
No CUDA runtime is found, using CUDA_HOME='/usr/local/cuda'
2023-11-17 13:10:43.359093: I tensorflow/core/util/port.cc:110] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable TF_ENABLE_ONEDNN_OPTS=0.
2023-11-17 13:10:43.398436: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2023-11-17 13:10:44.026743: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
Compiling the model to CPU ...

NNCF 重み圧縮アルゴリズムは、モデルの重みを INT8 に圧縮します。これは、重みとアクティベーションの両方を圧縮する量子化アルゴリズムの代替です。重み圧縮は、Dolly 2.0 などの大規模言語モデル (LLM) など、重みのサイズがアクティベーションのサイズより大幅に大きい大規模モデルのフットプリントとパフォーマンスを最適化するのに効果的です。さらに、重み圧縮は通常、精度の低下をほとんど引き起こしません。

to_compress = widgets.Checkbox(
    value=True,
    description='INT8 Compression',
    disabled=False,
)
print("Click on checkbox for enabling / disabling weights compression")
to_compress
Click on checkbox for enabling / disabling weights compression
Checkbox(value=True, description='INT8 Compression')
import gc
from optimum.intel import OVQuantizer

compressed_model_path = Path(f'{model_path}_compressed')

def calculate_compression_rate(model_path_ov, model_path_ov_compressed):
    model_size_original = model_path_ov.with_suffix(".bin").stat().st_size / 2 ** 20
    model_size_compressed = model_path_ov_compressed.with_suffix(".bin").stat().st_size / 2 ** 20
    print(f"* Original IR model size: {model_size_original:.2f} MB")
    print(f"* Compressed IR model size: {model_size_compressed:.2f} MB")
    print(f"* Model compression rate: {model_size_original / model_size_compressed:.3f}")

if to_compress.value:
    if not compressed_model_path.exists():
        quantizer = OVQuantizer.from_pretrained(ov_model)
        quantizer.quantize(save_directory=compressed_model_path, weights_only=True)
        del quantizer
        gc.collect()

    calculate_compression_rate(model_path / 'openvino_model.xml', compressed_model_path / 'openvino_model.xml')
    ov_model = OVModelForCausalLM.from_pretrained(compressed_model_path, device=current_device, ov_config=ov_config)
* Original IR model size: 5297.21 MB
* Compressed IR model size: 2657.89 MB
* Model compression rate: 1.993
Compiling the model to CPU ...

指示追従推論パイプラインを作成

run_generation 関数は、ユーザー指定のテキスト入力を受け入れ、それをトークン化して、生成プロセスを実行します。テキスト生成は反復プロセスであり、トークンの最大数または生成停止条件に達するまで、次のトークンは以前に生成されたものに依存します。生成が完了するまで待たずに中間生成結果を取得するには、Hugging Face ストリーミング API の一部として提供される TextIteratorStreamer を使用します。

以下の図は、指示追従パイプラインがどのように動作するかを示しています

generation pipeline)

生成パイプライン

最初の反復では、ユーザーはトークナイザーを使用してトークン ID に変換された指示を提供し、その後、モデルに提供される準備された入力を提供しました。モデルは、すべてのトークンの確率をロジット形式で生成します。予測された確率に基づいて次のトークンが選択される方法は、選択されたデコード方法によって決まります。最も一般的なデコード方法の詳細については、このブログをご覧ください。

テキスト生成の品質を制御できるパラメーターがいくつかあります。

  • Temperature は、AI が生成したテキストの創造性のレベルを制御するパラメーターです。temperature を調整することで、AI モデルの確率分布に影響を与え、テキストの焦点を絞ったり、多様にしたりできます。
    次の例を考えてみましょう。AI モデルは次のトークンの確率で “猫は ____ です” という文を完成させる必要があります。
    遊んでいる: 0.5
    眠っている: 0.25
    食べている: 0.15
    ドライブしている: 0.05
    飛んでいる: 0.05
    • 低温 (例: 0.2): AI モデルはより集中的かつ決定的になり、最も高い確率のトークン (“遊んでいる”など) を選択します。

    • 中温 (例: 1.0): AI モデルは創造性と集中力のバランスを維持し、大きな偏りのない確率に基づいてトークン (“遊んでいる”、“眠っている”、“食べている”など) を選択します。

    • 高温 (例: 2.0): AI モデルはより冒険的になり、確率の低いトークン (“ドライブしている” や “飛んでいる”など) を選択する可能性が高くなります。

  • Top-p (核サンプリングとも呼ばれる) は、累積確率に基づいて AI モデルによって考慮される、トークンの範囲を制御するために使用されるパラメーターです。top-p 値を調整することで、AI モデルのトークン選択に影響を与え、焦点を絞ったり、多様性を持たせることができます。猫と同じ例を使用して、次の top_p 設定を検討してください。

    • 低 top_p (例: 0.5): AI モデルは、累積確率が最も高いトークン (“遊んでいる”など) のみを考慮します。

    • 中 top_p (例: 0.8): AI モデルは、累積確率がより高いトークン (“遊んでいる”、“眠っている”、“食べている”など) を考慮します。

    • 高 top_p (例: 1.0): I モデルは、確率の低いトークン (“ドライブしている” や “飛んでいる”) を含むすべてのトークンを考慮します。

  • Top-k は、人気のあるサンプリング戦略です。累積確率が確率 P を超える最小の単語のセットから選択する Top-p と比較して、Top-k サンプリングでは、確率の最も高い K 個の単語がフィルタリングされ、確率の集合が K 個の次の単語のみに再分配されます。猫の例では、k=3 の場合、“遊んでいる”、“眠っている”、および“食べている”だけが次の単語として考慮されます。

生成プロセスを最適化し、メモリーをより効率的に使用するには、use_cache=True オプションを有効にします。出力側は自動回帰であるため、出力トークンの非表示状態は、その後の生成ステップごとに計算されると同じままになります。したがって、新しいトークンを生成するたびに再計算するのは無駄であるように思えます。キャッシュを使用すると、モデルは計算後に非表示の状態を保存します。モデルは各タイムステップで最後に生成された出力トークンのみを計算し、保存された出力トークンを非表示のトークンに再利用します。これにより、変圧器モデルの生成の複雑さが O(n^3) から O(n^2) に軽減されます。仕組みの詳細については、この記事を参照してください。このオプションを使用すると、モデルは前のステップの非表示状態 (キャッシュされたアテンション・キーと値) を入力として取得し、さらに現在のステップの非表示状態を出力として提供します。これは、次のすべての反復では、前のステップから取得した新しいトークンと、次のトークン予測を取得するためのキャッシュされたキー値のみを提供するだけで十分であることを意味します。

生成サイクルは、シーケンストークンの終わりに達するまで繰り返されます。または、最大のトークンが生成されるときに中断されることもあります。すでに述べたように、生成全体がストリーミング API を使用し、新しいトークンを出力キューに追加し、準備ができたらそれらをプリントするまで待つことなく、現在生成されたトークンをプリントできるようになります。

from threading import Thread
from time import perf_counter
from typing import List
import gradio as gr
from transformers import AutoTokenizer, TextIteratorStreamer
import numpy as np

効果的に生成するために、モデルは特定の形式での入力を期待します。以下のコードは、追加のコンテキストを提供してユーザー指示をモデルに渡すためのテンプレートを準備します。

INSTRUCTION_KEY = "### Instruction:"
RESPONSE_KEY = "### Response:"
END_KEY = "### End"
INTRO_BLURB = (
    "Below is an instruction that describes a task. Write a response that appropriately completes the request."
)

# This is the prompt that is used for generating responses using an already trained model.  It ends with the response
# key, where the job of the model is to provide the completion that follows it (i.e. the response itself).
PROMPT_FOR_GENERATION_FORMAT = """{intro}

{instruction_key}
{instruction}

{response_key}
""".format(
    intro=INTRO_BLURB,
    instruction_key=INSTRUCTION_KEY,
    instruction="{instruction}",
    response_key=RESPONSE_KEY,
)

特別なトークンを使用して生成を終了するためにモデルが再トレーニングされました ### End 以下のコードは、生成停止基準として使用する ID を見つけます。

def get_special_token_id(tokenizer: AutoTokenizer, key: str) -> int:
    """
    Gets the token ID for a given string that has been added to the tokenizer as a special token.

    When training, we configure the tokenizer so that the sequences like "### Instruction:" and "### End" are
    treated specially and converted to a single, new token.  This retrieves the token ID each of these keys map to.

    Args:
        tokenizer (PreTrainedTokenizer): the tokenizer
        key (str): the key to convert to a single token

    Raises:
        RuntimeError: if more than one ID was generated

    Returns:
        int: the token ID for the given key
    """
    token_ids = tokenizer.encode(key)
    if len(token_ids) > 1:
        raise ValueError(f"Expected only a single token for '{key}' but found {token_ids}")
    return token_ids[0]

tokenizer_response_key = next((token for token in tokenizer.additional_special_tokens if token.startswith(RESPONSE_KEY)), None)

end_key_token_id = None
if tokenizer_response_key:
    try:
        end_key_token_id = get_special_token_id(tokenizer, END_KEY)
        # Ensure generation stops once it generates "### End"
    except ValueError:
        pass

前述したように、run_generation 関数は生成を開始するエントリーポイントです。指定された入力命令をパラメーターとして取得し、モデル応答を返します。

def run_generation(user_text:str, top_p:float, temperature:float, top_k:int, max_new_tokens:int, perf_text:str):
    """
    Text generation function

    Parameters:
      user_text (str): User-provided instruction for a generation.
      top_p (float):  Nucleus sampling. If set to < 1, only the smallest set of most probable tokens with probabilities that add up to top_p or higher are kept for a generation.
      temperature (float): The value used to module the logits distribution.
      top_k (int): The number of highest probability vocabulary tokens to keep for top-k-filtering.
      max_new_tokens (int): Maximum length of generated sequence.
      perf_text (str): Content of text field for printing performance results.
    Returns:
      model_output (str) - model-generated text
      perf_text (str) - updated perf text filed content
    """

    # Prepare input prompt according to model expected template
    prompt_text = PROMPT_FOR_GENERATION_FORMAT.format(instruction=user_text)

    # Tokenize the user text.
    model_inputs = tokenizer(prompt_text, return_tensors="pt")

    # Start generation on a separate thread, so that we don't block the UI. The text is pulled from the streamer
    # in the main thread. Adds timeout to the streamer to handle exceptions in the generation thread.
    streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
    generate_kwargs = dict(
        model_inputs,
        streamer=streamer,
        max_new_tokens=max_new_tokens,
        do_sample=True,
        top_p=top_p,
        temperature=float(temperature),
        top_k=top_k,
        eos_token_id=end_key_token_id
    )
    t = Thread(target=ov_model.generate, kwargs=generate_kwargs)
    t.start()

    # Pull the generated text from the streamer, and update the model output.
    model_output = ""
    per_token_time = []
    num_tokens = 0
    start = perf_counter()
    for new_text in streamer:
        current_time = perf_counter() - start
        model_output += new_text
        perf_text, num_tokens = estimate_latency(current_time, perf_text, new_text, per_token_time, num_tokens)
        yield model_output, perf_text
        start = perf_counter()
    return model_output, perf_text

インタラクティブなユーザー・インターフェイスを作成するには、Gradio ライブラリーを使用します。以下のコードは、UI 要素との通信に使用される便利な関数を提供します。

def estimate_latency(current_time:float, current_perf_text:str, new_gen_text:str, per_token_time:List[float], num_tokens:int):
    """
    Helper function for performance estimation

    Parameters:
      current_time (float): This step time in seconds.
      current_perf_text (str): Current content of performance UI field.
      new_gen_text (str): New generated text.
      per_token_time (List[float]): history of performance from previous steps.
      num_tokens (int): Total number of generated tokens.

    Returns:
      update for performance text field
      update for a total number of tokens
    """
    num_current_toks = len(tokenizer.encode(new_gen_text))
    num_tokens += num_current_toks
    per_token_time.append(num_current_toks / current_time)
    if len(per_token_time) > 10 and len(per_token_time) % 4 == 0:
        current_bucket = per_token_time[:-10]
        return f"Average generation speed: {np.mean(current_bucket):.2f} tokens/s. Total generated tokens: {num_tokens}", num_tokens
    return current_perf_text, num_tokens

def reset_textbox(instruction:str, response:str, perf:str):
    """
    Helper function for resetting content of all text fields

    Parameters:
      instruction (str): Content of user instruction field.
      response (str): Content of model response field.
      perf (str): Content of performance info filed

    Returns:
      empty string for each placeholder
    """
    return "", "", ""


def select_device(device_str:str, current_text:str = "", progress:gr.Progress = gr.Progress()):
    """
    Helper function for uploading model on the device.

    Parameters:
      device_str (str): Device name.
      current_text (str): Current content of user instruction field (used only for backup purposes, temporally replacing it on the progress bar during model loading).
      progress (gr.Progress): gradio progress tracker
    Returns:
      current_text
    """
    if device_str != ov_model._device:
        ov_model.request = None
        ov_model._device = device_str

        for i in progress.tqdm(range(1), desc=f"Model loading on {device_str}"):
            ov_model.compile()
    return current_text

指示追従パイプラインを実行

これで、モデルの機能を調べる準備が整いました。このデモは、テキスト命令を使用してモデルと通信できるシンプルなインターフェイスを提供します。[ユーザー指示] フィールドに指示を入力するか、事前定義された例から 1 つを選択し、[送信] ボタンをクリックして生成を開始します。さらに、高度な生成パラメーターに変更できます。

  • Device - 推論デバイスを切り替えることができます。新しいデバイスが選択されるたびにモデルが再コンパイルされるため、時間がかかることに注意してください。

  • Max New Tokens - 生成されるテキストの最大サイズ。

  • Top-p (nucleus sampling) - < 1 に設定すると、合計が top_p 以上になる確率が高いトークンの最小セットのみが 1 回の生成にわたって保持されます。

  • Top-k - top-k フィルター処理のために保持する最も確率の高い語彙トークンの数。

  • Temperature - ロジット分布をモジュール化するために使用される値。

available_devices = ov.Core().available_devices + ["AUTO"]

examples = [
    "Give me recipe for pizza with pineapple",
    "Write me a tweet about new OpenVINO release",
    "Explain difference between CPU and GPU",
    "Give five ideas for great weekend with family",
    "Do Androids dream of Electric sheep?",
    "Who is Dolly?",
    "Please give me advice how to write resume?",
    "Name 3 advantages to be a cat",
    "Write instructions on how to become a good AI engineer",
    "Write a love letter to my best friend",
]

with gr.Blocks() as demo:
    gr.Markdown(
        "# Instruction following using Databricks Dolly 2.0 and OpenVINO.\n"
        "Provide insturction which describes a task below or select among predefined examples and model writes response that performs requested task."
    )

    with gr.Row():
        with gr.Column(scale=4):
            user_text = gr.Textbox(
                placeholder="Write an email about an alpaca that likes flan",
                label="User instruction"
            )
            model_output = gr.Textbox(label="Model response", interactive=False)
            performance = gr.Textbox(label="Performance", lines=1, interactive=False)
            with gr.Column(scale=1):
                button_clear = gr.Button(value="Clear")
                button_submit = gr.Button(value="Submit")
            gr.Examples(examples, user_text)
        with gr.Column(scale=1):
            device = gr.Dropdown(choices=available_devices, value=current_device, label="Device")
            max_new_tokens = gr.Slider(
                minimum=1, maximum=1000, value=256, step=1, interactive=True, label="Max New Tokens",
            )
            top_p = gr.Slider(
                minimum=0.05, maximum=1.0, value=0.92, step=0.05, interactive=True, label="Top-p (nucleus sampling)",
            )
            top_k = gr.Slider(
                minimum=0, maximum=50, value=0, step=1, interactive=True, label="Top-k",
            )
            temperature = gr.Slider(
                minimum=0.1, maximum=5.0, value=0.8, step=0.1, interactive=True, label="Temperature",
            )

    user_text.submit(run_generation, [user_text, top_p, temperature, top_k, max_new_tokens, performance], [model_output, performance])
    button_submit.click(select_device, [device, user_text], [user_text])
    button_submit.click(run_generation, [user_text, top_p, temperature, top_k, max_new_tokens, performance], [model_output, performance])
    button_clear.click(reset_textbox, [user_text, model_output, performance], [user_text, model_output, performance])
    device.change(select_device, [device, user_text], [user_text])

if __name__ == "__main__":
    try:
        demo.queue().launch(debug=False, height=800)
    except Exception:
        demo.queue().launch(debug=False, share=True, height=800)

# If you are launching remotely, specify server_name and server_port
# EXAMPLE: `demo.launch(server_name='your server name', server_port='server port in int')`
# To learn more please refer to the Gradio docs: https://gradio.app/docs/