NNCF を使用した OpenAI BLIP モデルのトレーニング後の量子化と重み圧縮

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

GitHub

このチュートリアルの目的は、NNCF (ニューラル・ネットワーク圧縮フレームワーク) から OpenVINO IR モデルに 8 ビットのトレーニング後の量子化とデータフリー int8 重み圧縮を適用してモデルを高速化して、OpenVINO™ ツールキットにより最適化された BLIP モデルを推論する方法を実証することです。最適化プロセスには次の手順が含まれます。

  1. 量子化のためにデータセットをダウンロードして前処理します。

  2. NNCF を使用して、ノートブックから変換されたビジョンおよびテキスト・エンコーダー OpenVINO モデルを量子化します。

  3. NNCF を使用してノートブックから OpenVINO テキスト・デコーダー・モデルの重みを圧縮します。

  4. ノートブックから同じ入力データを使用してモデルの結果を確認します。

  5. 変換および最適化されたモデルのモデルサイズを比較します。

  6. 変換および最適化されたモデルのパフォーマンスを比較します。

注: 最初に 233-blip-convert ノートブックを実行して、最適化に使用される OpenVINO IR モデルを生成する必要があります。

目次

必要条件

%pip install -q datasets
from pathlib import Path

VISION_MODEL_OV = Path("blip_vision_model.xml")
TEXT_ENCODER_OV = Path("blip_text_encoder.xml")
TEXT_DECODER_OV = Path("blip_text_decoder_with_past.xml")

if not (VISION_MODEL_OV.exists() and TEXT_ENCODER_OV.exists() and TEXT_DECODER_OV.exists()):
    raise RuntimeError('This notebook should be run after 233-blip-convert notebook')
from transformers import BlipProcessor

processor = BlipProcessor.from_pretrained("Salesforce/blip-vqa-base")

量子化

NNCF は、モデルグラフに量子化レイヤーを追加し、トレーニング・データセットのサブセットを使用してこれらの追加の量子化レイヤーのパラメーターを初期化することで、トレーニング後の量子化を可能にします。このフレームワークは、元のトレーニング・コードへの変更が最小限になるように設計されています。

最適化プロセスには次の手順が含まれます。

  1. 量子化用のデータセットを作成します。

  2. nncf.quantize を実行して、事前トレーニングされた FP16 モデルから量子化されたモデルを取得します。

  3. openvino.save_model 関数を使用して INT8 モデルをシリアル化します。

注: 量子化は時間とメモリーを消費する操作です。以下の量子化コードの実行には時間がかかる場合があります。

データセットの準備

VQAv2 は、画像に関する自由形式の質問を含むデータセットです。これらの問に答えるには、視覚、言語、常識的な知識の理解が必要です。

import numpy as np
from datasets import load_dataset
from tqdm.notebook import tqdm

def preprocess_batch(batch, vision_model, inputs_info):
    """
    Preprocesses a dataset batch by loading and transforming image and text data.
    VQAv2 dataset contains multiple questions to image.
    To reduce dataset preparation time we will store preprocessed images in `inputs_info`.
    """
    image_id = batch["image_id"]
    if image_id in inputs_info:
        inputs = processor(text=batch['question'], return_tensors="np")
        pixel_values = inputs_info[image_id]["pixel_values"]
        encoder_hidden_states = inputs_info[image_id]["encoder_hidden_states"]
    else:
        inputs = processor(images=batch["image"], text=batch["question"], return_tensors="np")
        pixel_values = inputs["pixel_values"]
        encoder_hidden_states = vision_model(pixel_values)[vision_model.output(0)]
        inputs_info[image_id] = {
            "pixel_values": pixel_values,
            "encoder_hidden_states": encoder_hidden_states,
            "text_encoder_inputs": []
        }

    text_encoder_inputs = {
        "input_ids": inputs["input_ids"],
        "attention_mask": inputs["attention_mask"]
    }
    inputs_info[image_id]["text_encoder_inputs"].append(text_encoder_inputs)


def prepare_input_data(dataloader, vision_model, opt_init_steps):
    """
    Store calibration subset in List to reduce quantization time.
    """
    inputs_info = {}
    for batch in tqdm(dataloader, total=opt_init_steps, desc="Prepare calibration data"):
        preprocess_batch(batch, vision_model, inputs_info)

    calibration_subset = []
    for image_id in inputs_info:
        pixel_values = inputs_info[image_id]["pixel_values"]
        encoder_hidden_states = inputs_info[image_id]["encoder_hidden_states"]
        encoder_attention_mask = np.ones(encoder_hidden_states.shape[:-1], dtype=int)
        for text_encoder_inputs in inputs_info[image_id]["text_encoder_inputs"]:
            text_encoder_inputs["encoder_hidden_states"] = encoder_hidden_states
            text_encoder_inputs["encoder_attention_mask"] = encoder_attention_mask
            blip_inputs = {
                "vision_model_inputs": {"pixel_values": pixel_values},
                "text_encoder_inputs": text_encoder_inputs,
            }
            calibration_subset.append(blip_inputs)
    return calibration_subset


def prepare_dataset(vision_model, opt_init_steps=300, streaming=True):
    """
    Prepares a vision-text dataset for quantization.
    """
    dataset = load_dataset("Hugging FaceM4/VQAv2", split="train", streaming=streaming)
    train_dataset = dataset.shuffle(seed=42).take(opt_init_steps)
    calibration_subset = prepare_input_data(train_dataset, vision_model, opt_init_steps)
    return calibration_subset

ストリーミング・モードでのデータセットの読み込みと処理には時間がかかりますが、インターネットの接続環境によって異なります。

import nncf
import openvino as ov

comp_vision_model = ov.compile_model(VISION_MODEL_OV)
calibration_data = prepare_dataset(comp_vision_model)
INFO:nncf:NNCF initialized successfully. Supported frameworks detected: torch, tensorflow, onnx, openvino
Repo card metadata block was not found. Setting CardData to empty.
Prepare calibration data:   0%|          | 0/300 [00:00<?, ?it/s]

ビジョンモデル

VISION_MODEL_OV_INT8 = Path(str(VISION_MODEL_OV).replace(".xml", "_int8.xml"))

core = ov.Core()
ov_vision_model = core.read_model(VISION_MODEL_OV)
vision_dataset = nncf.Dataset(calibration_data, lambda x: x["vision_model_inputs"])

quantized_model = nncf.quantize(
    model=ov_vision_model,
    calibration_dataset=vision_dataset,
    model_type=nncf.ModelType.TRANSFORMER
)

ov.save_model(quantized_model, VISION_MODEL_OV_INT8)
Statistics collection: 100%|██████████| 300/300 [00:21<00:00, 14.06it/s]
Applying Smooth Quant: 100%|██████████| 48/48 [00:01<00:00, 29.72it/s]
INFO:nncf:36 ignored nodes was found by name in the NNCFGraph
Statistics collection: 100%|██████████| 300/300 [01:17<00:00,  3.89it/s]
Applying Fast Bias correction: 100%|██████████| 49/49 [00:27<00:00,  1.80it/s]

テキスト・エンコーダー

TEXT_ENCODER_OV_INT8 = Path(str(TEXT_ENCODER_OV).replace(".xml", "_int8.xml"))

text_encoder_dataset = nncf.Dataset(calibration_data, lambda x: x["text_encoder_inputs"])
ov_text_encoder = core.read_model(TEXT_ENCODER_OV)
quantized_model = nncf.quantize(
    model=ov_text_encoder,
    calibration_dataset=text_encoder_dataset,
    model_type=nncf.ModelType.TRANSFORMER
)
ov.save_model(quantized_model, TEXT_ENCODER_OV_INT8)
Statistics collection: 100%|██████████| 300/300 [00:10<00:00, 28.70it/s]
Applying Smooth Quant: 100%|██████████| 73/73 [00:02<00:00, 28.87it/s]
INFO:nncf:72 ignored nodes was found by name in the NNCFGraph
Statistics collection: 100%|██████████| 300/300 [00:31<00:00,  9.54it/s]
Applying Fast Bias correction: 100%|██████████| 120/120 [00:38<00:00,  3.11it/s]

重みを圧縮

テキストデコーダーの量子化により、精度が大幅に低下します。トレーニング後の量子化の代わりに、データのフリーウェイト圧縮を使用してモデルのフットプリントを削減できます。

最適化プロセスには次の手順が含まれます。

  1. nncf.compress_weights を実行して、圧縮された重みを持つモデルを取得します。

  2. openvino.save_model 関数を使用して OpenVINO モデルをシリアル化します。

TEXT_DECODER_OV_INT8 = Path(str(TEXT_DECODER_OV).replace(".xml", "_int8.xml"))

text_decoder = core.read_model(TEXT_DECODER_OV)
compressed_text_decoder = nncf.compress_weights(text_decoder)
ov.save_model(compressed_text_decoder, str(TEXT_DECODER_OV_INT8))

最適化された OpenVINO モデルを実行

最適化された OpenVINO BLIP モデルを使用して予測を行う手順は、PyTorch モデルと似ています。1 番目のノートブックと同じ入力データを使用してモデルの結果を確認してみましょう。

q_ov_vision_model = ov.compile_model(VISION_MODEL_OV_INT8)
q_ov_text_encoder = ov.compile_model(TEXT_ENCODER_OV_INT8)
q_ov_text_decoder_with_past = ov.compile_model(TEXT_DECODER_OV_INT8)
from functools import partial
from transformers import BlipForQuestionAnswering
from blip_model import OVBlipModel, text_decoder_forward

model = BlipForQuestionAnswering.from_pretrained("Salesforce/blip-vqa-base")
text_decoder = model.text_decoder
text_decoder.eval()

text_decoder.forward = partial(text_decoder_forward, ov_text_decoder_with_past=q_ov_text_decoder_with_past)
int8_model = OVBlipModel(model.config, model.decoder_start_token_id, q_ov_vision_model, q_ov_text_encoder, text_decoder)
from PIL import Image

raw_image = Image.open("demo.jpg").convert('RGB')
question = "how many dogs are in the picture?"
# preprocess input data
inputs = processor(raw_image, question, return_tensors="pt")

画像のキャプション

from utils import visualize_results

out = int8_model.generate_caption(inputs["pixel_values"], max_length=20)
caption = processor.decode(out[0], skip_special_tokens=True)
fig = visualize_results(raw_image, caption)
../_images/233-blip-optimize-with-output_23_0.png

質問への回答

out = int8_model.generate_answer(**inputs, max_length=20)
answer = processor.decode(out[0], skip_special_tokens=True)
fig = visualize_results(raw_image, answer, question)
../_images/233-blip-optimize-with-output_25_0.png

ファイルサイズを比較

def calculate_compression_rate(ov_model_path):
    fp16_ir_model_size = Path(ov_model_path).with_suffix(".bin").stat().st_size / 1024
    int8_model_path = str(ov_model_path).replace(".xml", "_int8.xml")
    quantized_model_size = Path(int8_model_path).with_suffix(".bin").stat().st_size / 1024
    print(f'{ov_model_path.as_posix().split(".")[0]}')
    print(f"    * FP16 IR model size: {fp16_ir_model_size:.2f} KB")
    print(f"    * INT8 model size: {quantized_model_size:.2f} KB")
    print(f"    * Model compression rate: {fp16_ir_model_size / quantized_model_size:.3f}")
for model_path in [VISION_MODEL_OV, TEXT_ENCODER_OV, TEXT_DECODER_OV]:
                                            calculate_compression_rate(model_path)
blip_vision_model
    * FP16 IR model size: 168145.68 KB
    * INT8 model size: 84915.75 KB
    * Model compression rate: 1.980
blip_text_encoder
    * FP16 IR model size: 268087.17 KB
    * INT8 model size: 134677.23 KB
    * Model compression rate: 1.991
blip_text_decoder_with_past
    * FP16 IR model size: 269303.42 KB
    * INT8 model size: 135450.65 KB
    * Model compression rate: 1.988

FP16 と最適化モデルの推論時間を比較

FP16INT8 モデルの推論パフォーマンスを測定するには、キャリブレーション・データセットの 100 サンプルの推論時間の中央値を使用します。したがって、動的量子化モデルの速度向上を見積もることができます。

注: 最も正確なパフォーマンス推定を行うには、他のアプリケーションを閉じた後、ターミナル/コマンドプロンプトで benchmark_app を実行することを推奨します。

import time
import torch

def calculate_inference_time(blip_model, calibration_data, generate_caption):
    inference_time = []
    for inputs in calibration_data:
        pixel_values = torch.from_numpy(inputs["vision_model_inputs"]["pixel_values"])
        input_ids = torch.from_numpy(inputs["text_encoder_inputs"]["input_ids"])
        attention_mask = torch.from_numpy(inputs["text_encoder_inputs"]["attention_mask"])

        start = time.perf_counter()
        if generate_caption:
            _ = blip_model.generate_caption(pixel_values, max_length=20)
        else:
            _ = blip_model.generate_answer(pixel_values=pixel_values, input_ids=input_ids, attention_mask=attention_mask, max_length=20)
        end = time.perf_counter()
        delta = end - start
        inference_time.append(delta)
    return np.median(inference_time)
fp_original_model = BlipForQuestionAnswering.from_pretrained("Salesforce/blip-vqa-base")
fp_text_decoder = fp_original_model.text_decoder
fp_text_decoder.eval()

comp_text_encoder = ov.compile_model(TEXT_ENCODER_OV)
comp_text_decoder_with_past = ov.compile_model(TEXT_DECODER_OV)
fp_text_decoder.forward = partial(text_decoder_forward, ov_text_decoder_with_past=comp_text_decoder_with_past)
fp16_model = OVBlipModel(model.config, model.decoder_start_token_id, comp_vision_model, comp_text_encoder, fp_text_decoder)
validation_data = calibration_data[:100]

int8_caption_latency = calculate_inference_time(int8_model, validation_data, generate_caption=True)
fp16_caption_latency = calculate_inference_time(fp16_model, validation_data, generate_caption=True)

print(f"Image Captioning speed up: {fp16_caption_latency / int8_caption_latency:.3f}")
int8_generate_answer_latency = calculate_inference_time(int8_model, validation_data, generate_caption=False)
fp16_generate_answer_latency = calculate_inference_time(fp16_model, validation_data, generate_caption=False)
print(f"Question Answering speed up: {fp16_generate_answer_latency / int8_generate_answer_latency:.3f}")