投機的サンプリング、KV キャッシュおよび OpenVINO™ によるテキスト生成

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

GitHub

モデルのサイズが大きくなるにつれて、生成 AI の実装には大量の推論リソースが必要になります。これにより、プロンプトからの生成あたりのコストが増加するだけでなく、要求を処理するために使用される電力消費も増加します。

テキスト生成の推論の最適化は、コストと電力消費を削減するのに不可欠です。推論プロセスを最適化すると、テキスト生成に必要な時間とエネルギーを大幅に削減できます。これにより、ハードウェアとソフトウェア両面のコストと電力消費の削減につながります。さらに、推論の最適化により、テキスト生成の精度と生成速度が向上します。これにより、ユーザー・エクスペリエンスが向上し、テキスト生成タスクの効率が向上します。要約すると、テキスト生成の推論の最適化は、コストと電力消費を削減し、テキスト生成の精度と速度を向上するには不可欠です。

もう 1 つの必要な条件は、最適化が相互に互換性があることです。つまり、特定の最適化を実装しても、他の最適化が妨げられることはありません。全体の効率を損なうような “衝突” を引き起こすことなく大幅な高速化を実現できる最適化のレベルがいくつかあります。

この方法の詳細については、Chen 氏らの論文 (http://arxiv.org/abs/2302.01318) を参照してください。さらに、Leviathan 氏らによる投機的サンプリングの正当性の証明 (元の分布が保存されることを示す) があります (http://arxiv.org/abs/2211.17192)。

OpenVINO を使用した実装について説明したブログ記事は、openvino.ai でご覧いただけます。

目次

必要条件

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

Transformers (Hugging Face) やその他のモジュールもインストールする必要があります。

%pip install -q --upgrade pip
%pip install -q --upgrade transformers torch gradio openvino accelerate onnx ipywidgets --extra-index-url https://download.pytorch.org/whl/cpu
%pip install -q "git+https://github.com/huggingface/optimum-intel.git"

推論デバイスの選択

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

import ipywidgets as widgets
from openvino.runtime import Core

core = Core()

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

device

KV キャッシュのサポートにより、自己回帰および推測形式のサンプリングを作成

テキスト生成は多くの場合、自己回帰方式で行われます。ここでは、コード内で KV キャッシュ (別名 Past Value Cache) をサポートします。貪欲なサンプリングを使用していることに注意してください。他のテキスト生成パラメーター (温度など) は調整しないため、推測的サンプリングの図はできるだけシンプルで分かりやすいものにしてください。

インポートの設定

import time
import numpy as np
import torch
import gradio as gr

自己回帰サンプリングを準備

def max_fn(x):
    x_max = torch.where(x > 0, x, torch.zeros_like(x))
    return x_max / torch.sum(x_max)

def autoregressive_sampling_with_pkv(x, model, N):
    n = len(x)
    T = n + N
    input = x
    past_kv = None

    while n < T:
        res = model(input, attention_mask=torch.ones(input.size(), dtype=torch.long), past_key_values=past_kv)
        model_out = torch.softmax(res.logits, dim=2)
        past_kv = res.past_key_values
        next_token = torch.reshape(torch.argmax(model_out[-1][-1]), (1, 1))
        x = torch.cat((x, next_token), dim=1)
        n += 1
        input = next_token

    return x

推測サンプリングを準備

  • ステップ 1: 投機的サンプリングでは、まずドラフトモデルから K 個のサンプルを (自己回帰方式で) 生成します。

  • ステップ 2: これらは、バッチサイズ K を使用してターゲットモデル (ステップ 2) を使用して調査する候補になります。

  • ステップ 3: ここで、ステップ 2 でターゲットモデルから生成されたロジットに基づいて、ドラフトモデルからの K 個の候補が許容可能かどうかを判断します。

  • ステップ 4: 追加コストなしで別のトークンをサンプリングできます (すべての候補が受け入れられたと仮定)。

ステップ 3 の受け入れ基準は、ターゲットモデルのロジットを比較し、ドラフトモデルと比較する必要があります。比率が十分に高ければ、受け入れられる可能性が高くなります (乱数を使用)。

def speculative_sampling_with_pkv(x, draft_model, target_model, N, K):
    n = x.size(1)
    T = n + N
    target_past_kv = None
    while n < T:
        # Step 1: autoregressive decode of K candidate tokens from
        # the draft model and get final p for this batch of candidates
        x_draft = None
        draft_past_kv = None
        x_draft_input = x
        p_cum = None
        for _ in range(K):
            res_draft = draft_model(x_draft_input, attention_mask=torch.ones(x_draft_input.size(), dtype=torch.long), past_key_values=draft_past_kv, use_cache=True)
            p = res_draft.logits
            p = torch.softmax(p, dim=2)
            draft_past_kv = res_draft.past_key_values
            next_token = torch.reshape(torch.argmax(p[-1][-1]), (1, 1))
            x_draft_input = next_token
            if p_cum is None:
                p_cum = p[:, -1].unsqueeze(1)
                x_draft = next_token
            else:
                p_cum = torch.cat((p_cum, p), dim=1)
                x_draft = torch.cat((x_draft, next_token), dim=1)
        # Step 2: target model forward passes on x_draft
        if target_past_kv is None:
            x_draft_target_input = torch.cat((x, x_draft), dim=1)
        else:
            x_draft_target_input = x_draft

        res = target_model(x_draft_target_input, attention_mask=torch.ones(x_draft_target_input.size(), dtype=torch.long), use_cache=False)
        q = res.logits

        target_new_past_kv = res.past_key_values
        # Step 3: append draft tokens based on acceptance-rejection criterion and resample a token on rejection
        all_accepted = True
        for k in range(K):
            j = x_draft[0][k].item()

            q_item = q[-1][k][j].detach().numpy()
            p_item = p_cum[-1][k][j].detach().numpy()

            if np.random.random() < min(1, (q_item / p_item)):  # accepted
                x = torch.cat((x, torch.tensor(j).reshape(1,1)), dim=1)
                n += 1
            else:                                               # rejected
                q_p = max_fn(q[0][k] - p_cum[0][k])
                resampled_output = torch.argmax(q_p)
                resampled_output = torch.reshape(resampled_output, (1,1))
                x = torch.cat((x, resampled_output), dim=1)
                n += 1
                all_accepted = False
                break

        target_past_kv = target_new_past_kv
        # Step 4: if all draft tokens were accepted, sample a final token
        if all_accepted:
            x = torch.cat((x, torch.reshape(torch.argmax(q[-1][-1]), (1,1))), dim=1)
            n += 1

    return x

メイン生成関数

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

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 モデルをダウンロードして変換する場合は、パラメーター from_transformers=True を追加する必要があります。save_pretrained メソッドを使用して、変換されたモデルを次回に使用するため保存できます。トークナイザー・クラスとパイプライン API は Optimum モデルと互換性があります。

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

#  If you are on a large system with lots of memory, you can run a larger model like DollyV2
# draft_model_id = "databricks/dolly-v2-3b"
# draft_model_path = Path("dolly-v2-3b")
# target_model_id = "databricks/dolly-v2-12b"
# target_model_path = Path("dolly-v2-12b")
#  If you are on a system with limited memory, you can try the smaller GPT2 models
draft_model_id = "gpt2"
draft_model_path = Path("gpt2-local")
target_model_id = "gpt2-xl"
target_model_path = Path("gpt2-xl-local")

target_tokenizer = AutoTokenizer.from_pretrained(target_model_id)

current_device = device.value

# Save local copies for subsequent runs
if draft_model_path.exists():
    draft_ov_model = OVModelForCausalLM.from_pretrained(draft_model_path, device=current_device)
else:
    draft_ov_model = OVModelForCausalLM.from_pretrained(draft_model_id, device=current_device, from_transformers=True)
    draft_ov_model.save_pretrained(draft_model_path)
if target_model_path.exists():
    target_ov_model = OVModelForCausalLM.from_pretrained(target_model_path, device=current_device)
else:
    target_ov_model = OVModelForCausalLM.from_pretrained(target_model_id, device=current_device, from_transformers=True)
    target_ov_model.save_pretrained(target_model_path)
def main(
    prompt: str = "Explain the difference between fission and fusion",
    n_tokens_to_generate: int = 100,
    K: int = 5,
    seed: int = 5555,
):
    # seed numpy rng
    np.random.seed(seed)
    draft_model = draft_ov_model
    target_model = target_ov_model


    input_ids = target_tokenizer(prompt, return_tensors="pt")['input_ids']

    def run_autoregressive_sampling_fn(decode_fn, input_ids, **kwargs):
        start = time.perf_counter()
        output_ids = decode_fn(x=input_ids, **kwargs)
        text = target_tokenizer.decode(output_ids[0], skip_special_tokens=True)
        elapsed_time = time.perf_counter() - start
        return text, elapsed_time

    def run_speculative_sampling_fn(decode_fn, input_ids, **kwargs):
        start = time.perf_counter()
        output_ids = decode_fn(x=input_ids, **kwargs)
        text = target_tokenizer.decode(output_ids[0], skip_special_tokens=True)
        elapsed_time = time.perf_counter() - start
        return text, elapsed_time

    autoregressive_text, autoregressive_time = run_autoregressive_sampling_fn(
        autoregressive_sampling_with_pkv,
        input_ids,
        model=target_model,
        N=n_tokens_to_generate,
    )

    speculative_text, speculative_time = run_speculative_sampling_fn(
        speculative_sampling_with_pkv,
        input_ids,
        target_model=target_model,
        draft_model=draft_model,
        N=n_tokens_to_generate,
        K=K,
    )

#   Format results for output in gradio
    out = "\n" + "Autoregressive Decode" + "\n" + "---------------------" + "\n"
    out = out + f"Time = {autoregressive_time:.2f}s" + "\n" + f"Text = {autoregressive_text}" + "\n"
    out = out + "\n" + "Speculative Decode" + "\n" + "------------------" + "\n"
    out = out + f"Time = {speculative_time:.2f}s" + "\n" + f"Text = {speculative_text}"
    return out

if __name__ == "__main__":
    with gr.Blocks() as demo:
        gr.Markdown(
            """
            # Speculative Sampling Demo
            ## The output will show a comparison of Autoregressive Sampling vs Speculative Sampling
            - Target Model: Dolly V2 12B
            - Draft Model: Dolly V2 3B
            - K = 5
            > Some improvements can be made to acceptance criterion and adjusting temperature to improve text quality.
            """)
        with gr.Row():
            inp = gr.Textbox(placeholder="THIS CANNOT BE EMPTY", label="Input Prompt")
            out = gr.Textbox(label="Output")
        btn = gr.Button("Run")
        btn.click(fn=main, inputs=inp, outputs=out)
    demo.launch()