Open4

「Diffusers」を試す 7. Adapters

kun432kun432

以下の続き(というかまだやってるけど)

https://zenn.dev/kun432/scraps/0ddbf1d8bac9f0

ここまでの流れで、Diffusersのパイプラインは、トランスフォーマー・テキストエンコーダ・VAEなど、いろいろなコンポーネントで構成されていると認識した。Adapterもそういうコンポーネントの一部なのかな?と思いきやちょっと違うらしい。

Dia に聞いてみた。

「adapter」は、既存のAIモデルに新しい機能やスタイルを追加するための拡張パーツみたいなものだよ!

「adapter」って、マジで便利なやつでさ、AIモデル(例えば画像生成モデルとか)に新しい能力をサクッと追加したい時に使うんだよね。
ウチ的には「拡張パーツ」ってイメージが一番しっくりくる!だって、元のモデルの重み(パラメータ)はそのままにして、追加でちょっとだけ新しいパラメータ(これがadapterの中身)をくっつけるだけで、モデルが新しいことできるようになるんだもん。

具体的にどう使うの?

  • 例えば「LoRA」っていう手法だと、adapterを使ってモデルに新しい絵柄とかスタイルを追加できるんだよ。
  • 元のモデルは「凍結」して、adapter部分だけ学習するから、めっちゃ軽いし速いし、コスパ最強って感じ!
  • しかも、adapterは数百MBくらいで済むから、保存もラクだし、いろんなスタイルを切り替えて遊べるのがテンション上がるポイント!

どんな場面で使う?

  • 画像生成モデルに新しいキャラや絵柄を追加したい時
  • キスト生成モデルに新しい話し方や知識を追加したい時
  • 既存のモデルを壊さずに、ちょっとだけカスタムしたい時

要は、「adapter」って、AIモデルの「着せ替えアイテム」みたいな存在で、ノリがいいし、リア充な使い方ができるって感じだし!
ウケるくらい簡単に新しいことできるから、AIの世界じゃ超人気だよ!

「adapter」はパイプラインのコンポーネントそのものじゃなくて、コンポーネントに追加する拡張機能だよ!

ウチ的に言うと、「パイプラインのコンポーネント」っていうのは、例えばDiffusersなら「UNet」「Text Encoder」「VAE」みたいな、画像生成の流れを作る本体パーツのことだよね。
で、「adapter」はその本体パーツ(コンポーネント)に後付けでくっつける“追加機能”みたいな感じ!

どういう関係?

  • パイプラインのコンポーネント(UNetとかText Encoder)は、モデルの基本性能を担ってる部分。
  • 「adapter」は、そのコンポーネントに新しい能力やスタイルを追加するための“拡張パーツ”。
  • 例えば「UNet」にLoRA adapterを追加すると、UNetが新しい絵柄を生成できるようになる、みたいなノリ!

例えるなら…

  • ゲームのキャラに新しいスキルを装備する感じ!
  • 本体(コンポーネント)は変わらず、adapterを付け外しして色んな能力を試せるってわけ。

技術的には?

  • Diffusersのパイプラインは、複数のコンポーネントで構成されてる。
  • adapterは、その中の特定のコンポーネント(例えばUNetやText Encoder)にだけ追加できる。
  • adapter自体は「コンポーネントの一部」ってより「コンポーネントにくっつける拡張」って立ち位置!

だから、「adapter=パイプラインのコンポーネント」っていうより、「adapter=コンポーネントの拡張機能」って考えるのがマジでしっくりくるし、ウチ的にはその方が理解しやすいと思う!

ということで引き続き、Dia と対話しながら、Colaboratory L4で試しながら進める。

kun432kun432

LoRA

https://huggingface.co/docs/diffusers/v0.35.1/en/tutorials/using_peft_for_inference

ウチ、最初の章だけめっちゃ分かりやすく説明するね!

LoRA(Low-Rank Adaptation)って何?

LoRAは、既存のモデルを新しいタスクにサクッと適応させるための手法だよ。元のモデルの重みはそのまま凍結して、追加でちょっとだけ新しい学習可能なパラメータ(重み)を足す感じ。だから、学習が超速いし、コストも安いのがウケるポイント!

例えば、画像生成モデルに新しいスタイルを覚えさせたいとき、全部の重みを学習し直すんじゃなくて、LoRAの小さい重みだけ追加して学習するんだ。これなら、LoRAのチェックポイント(学習済みデータ)は数百MBくらいで済むし、保存もラクだし、持ち運びも余裕!

使い方のイメージ

LoRAの重みは、既存のベースモデルに load_lora_weights() って関数で読み込むだけ。ファイル名を指定すればOKだし、めっちゃシンプル!

まとめ

  • 元のモデルはそのまま
  • 追加の重みだけ学習
  • 学習速いし、コスト安い
  • チェックポイントは軽い
  • load_lora_weights() で簡単に使える

マジで、LoRA使うと新しいスタイルとかタスクにモデルを適応させるのが超ラクになるんだよね!テンション上がるでしょ?

テキストから画像を生成。この時、ostris/super-cereal-sdxl-lora というシリアルのパッケージ画像スタイルを適用するLoRAを適用。

import torch
from diffusers import AutoPipelineForText2Image

pipeline = AutoPipelineForText2Image.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")

pipeline.load_lora_weights(
    "ostris/super-cereal-sdxl-lora",
    weight_name="cereal_box_sdxl_v1.safetensors",
    adapter_name="cereal"
)

pipeline("bears, pizza bites").images[0]

load_lora_weights() は、LoRAの重み(パラメータ)をUNetとテキストエンコーダーに読み込むための一番オススメな方法なんだ。なんでかっていうと、LoRAの重みが

  • UNetとテキストエンコーダー用に分かれてない場合でも
  • それぞれ分かれてる場合でも

どっちのパターンでもちゃんと対応してくれるから、マジで使い勝手いいんだよね!

load_lora_adapter()は、モデル全体(DiffusersのモデルでPeftAdapterMixinを継承してるやつ)にLoRAアダプターを直接読み込むためのメソッドだよ。これを使うと、必要なモデル設定も自動で準備してくれるし、UNetにもLoRAアダプターをセットしてくれる。

もしUNetだけにLoRAを入れたいなら、load_lora_adapter() を使うとテキストエンコーダーのキーは無視してくれるんだ。つまり、UNet専用でLoRAをセットできるってこと!

prefixってパラメータを使うと、どの部分のstate dict(重みデータ)を読み込むか指定できるよ。UNetに入れたいならprefix=“unet”って感じでOK!

import torch
from diffusers import AutoPipelineForText2Image

pipeline = AutoPipelineForText2Image.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")

pipeline.unet.load_lora_adapter(
    "jbilcke-hf/sdxl-cinematic-1",
    weight_name="pytorch_lora_weights.safetensors",
    adapter_name="cinematic",
    prefix="unet"
)
# プロンプトに cnmt を使って、LoRAをトリガー
pipeline("A cute cnmt eating a slice of pizza, stunning color scheme, masterpiece, illustration").images[0]


torch.compile

torch.compileは、PyTorchのモデルを最適化して、推論(inference)をめっちゃ速くしてくれる新機能だよ。普通にモデルを動かすより、内部でカーネルとかを自動で最適化してくれるから、特に大きいモデルや計算が重い部分(例えばUNetとか)で効果バツグン!

Diffusersでの使い方 

LoRAを使ってる場合、まずLoRAの重みを fuse_lora() でベースモデルに統合してから、unload_lora_weights() でLoRAの重みを外す必要があるんだ。その後で torch.compile を使うと、最適化された状態で推論できる!

import torch
from diffusers import DiffusionPipeline

# ベースモデルモデルとLoRAをロード
pipeline = DiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
    "ostris/ikea-instructions-lora-sdxl",
    weight_name="ikea_instructions_xl_v1_5.safetensors",
    adapter_name="ikea"
)

# LoRAを有効化してアダプタに重みを適応
pipeline.set_adapters("ikea", adapter_weights=0.7)

# LoRAをfuseして、重みをアンロード
pipeline.fuse_lora(adapter_names=["ikea"], lora_scale=1.0)
pipeline.unload_lora_weights()

# UNetをchannels_lastにしてからcompile(最も計算処理が多い)
pipeline.unet.to(memory_format=torch.channels_last)
pipeline.unet = torch.compile(pipeline.unet, mode="reduce-overhead", fullgraph=True)

# 推論
pipeline("A bowl of ramen shaped like a cute kawaii bear").images[0]

ポイントまとめ 

  • torch.compile は推論を高速化する最強の機能!
  • LoRAを使う場合は、fuse/unloadしてからcompileするのがコツ!
  • UNetみたいな重い部分に使うと、マジで体感変わるレベルで速くなる!

あと、コンパイル済みモデル(torch.compileしたやつ)で複数のLoRAを使うとき、毎回再コンパイル(recompilation)しなくて済むようにするテクとして hotswapping があるよ。普通はLoRAを切り替えるたびにモデルが再コンパイルされて、時間もメモリも無駄にかかるんだけど、hotswappingを使えばその手間が省けるの!hotswapping セクションを参考にすれば、再コンパイルを回避する方法が詳しく載ってるから、マジでチェックしてみて!やり方を知ってると、複数LoRAをサクサク切り替えられて、開発のテンションも爆上がりだし!
こういう最適化テク、知ってるとめっちゃ得だし、AI開発のノリも上がるでしょ!


Weight scale

Weight scale(重みスケール)は、「LoRAをどれくらい効かせるか」を調整するためのパラメータだよ。
値を0にするとベースモデルだけ、1にするとLoRAをフルで適用、0.5なら半分だけLoRAを効かせる、みたいな感じでコントロールできる!

使い方

シンプルに使うなら、‎cross_attention_kwargs={"scale": 1.0} みたいにパイプラインに渡すだけでOK!

``cross_attention_kwargs={"scale": 1.0}`を指定して生成

import torch
from diffusers import AutoPipelineForText2Image

pipeline = AutoPipelineForText2Image.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
    "ostris/super-cereal-sdxl-lora",
    weight_name="cereal_box_sdxl_v1.safetensors",
    adapter_name="cereal"
)
pipeline(
    "bears, pizza bites",
    cross_attention_kwargs={"scale": 1.0}
).images[0]

cross_attention_kwargs={"scale": 0.5}だとこうなる

cross_attention_kwargs={"scale": 0.0}だとこうなる

もっと細かくコントロールしたい場合、UNetやテキストエンコーダーの各部分ごとに、LoRAのスケール値を辞書(dict)で渡すことができる!

import torch
from diffusers import AutoPipelineForText2Image

pipeline = AutoPipelineForText2Image.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
    "ostris/super-cereal-sdxl-lora",
    weight_name="cereal_box_sdxl_v1.safetensors",
    adapter_name="cereal"
)
scales = {
    "text_encoder": 0.5,
    "text_encoder_2": 0.5,
    "unet": {
        "down": 0.9,
        "up": {
            "block_0": 0.6,
            "block_1": [0.4, 0.8, 1.0],
        }
    }
}
pipeline.set_adapters("cereal", scales)
pipeline("bears, pizza bites").images[0]

上記の例では以下のような設定を行っている。

  • UNet の "down" ブロックは、LoRAのスケールを 0.9 に指定
  • さらに "up" ブロックは、その中の各トランスフォーマー層ごとに、"block_0"0.6"block_1"[0.4, 0.8, 1.0] を指定
  • "mid" は定義していないが、その場合はデフォルト値として 1.0になる

なお、以下の注意書きがある。

  • set_adapters() メソッドは、基本的にAttentionの重みだけスケールを調整する。
  • ただし、LoRA が ResNet や down/upサンプラーを含んでいる場合、それらの部分はスケール値が常に1.0(フル適用)になる。

Scale scheduling

Scale scheduling(スケールスケジューリング)って何? 

普通はLoRAのスケール(効き具合)をずっと同じ値で使うけど、Scale schedulingを使うと、画像生成の各ステップごとにスケールを変えられるんだ。
例えば、最初はLoRAを強めに効かせて、途中から弱めにする…みたいなことができる!

なんで便利なの?

  • 最初のステップでキャラの特徴をしっかり出したい
  • 後半はLoRAの影響を抑えて、背景や全体のバランスを整えたい

みたいな時に、めっちゃ役立つんだよね!

以下は、 black-forest-labs/FLUX.1-dev で ジブリキャラクターのスタイルを適用するLoRAを、最初の20ステップは高いスケールから徐々に下げていって、後半のステップではLoRAの効果がでないように スケールを 0.2 に設定する、というものになっている。なるほど、コールバックが使えるのね。

なお、Colaboratory L4だとOOMになるので、bitsandbytes で 4ビット量子化している。

import torch
from diffusers import FluxPipeline
from diffusers.quantizers import PipelineQuantizationConfig
from diffusers import BitsAndBytesConfig as DiffusersBitsAndBytesConfig
from transformers import BitsAndBytesConfig as TransformersBitsAndBytesConfig

pipeline_quant_config = PipelineQuantizationConfig(
    quant_mapping={
        "transformer": DiffusersBitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_compute_dtype=torch.bfloat16
        ),
        "text_encoder_2": TransformersBitsAndBytesConfig(
            load_in_4bit=True,
            compute_dtype=torch.bfloat16
        ),
    }
)

pipeline = FluxPipeline.from_pretrained(
    "black-forest-labs/FLUX.1-dev",
    quantization_config=pipeline_quant_config,
    torch_dtype=torch.bfloat16,
).to("cuda")

pipeline.load_lora_weights("alvarobartt/ghibli-characters-flux-lora", "lora")

num_inference_steps = 30
lora_steps = 20
lora_scales = torch.linspace(1.5, 0.7, lora_steps).tolist()
lora_scales += [0.2] * (num_inference_steps - lora_steps + 1)

pipeline.set_adapters("lora", lora_scales[0])

def callback(pipeline: FluxPipeline, step: int, timestep: torch.LongTensor, callback_kwargs: dict):
    pipeline.set_adapters("lora", lora_scales[step + 1])
    return callback_kwargs

prompt = """
Ghibli style The Grinch, a mischievous green creature with a sly grin, peeking out from behind a snow-covered tree while plotting his antics, 
in a quaint snowy village decorated for the holidays, warm light glowing from cozy homes, with playful snowflakes dancing in the air
"""
pipeline(
    prompt=prompt,
    guidance_scale=3.0,
    num_inference_steps=num_inference_steps,
    generator=torch.Generator().manual_seed(42),
    callback_on_step_end=callback,
).images[0]


LoRAのホットスワップ

hotswappingは、複数のLoRAを使うときに、毎回「load_lora_weights()」で重みを読み込むたびにメモリがどんどん増えちゃうのを防いでくれるテクだよ。
さらに、モデルをコンパイル(torch.compile)してる場合でも、再コンパイルの手間を減らせるから、マジで効率的!

まず、ベースモデルと最初のLoRAを読み込み。

import torch
from diffusers import DiffusionPipeline

# load base model and LoRAs
pipeline = DiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
    "ostris/ikea-instructions-lora-sdxl",
    weight_name="ikea_instructions_xl_v1_5.safetensors",
    adapter_name="ikea"
)

では別のLoRAに切り替える。この時、

  • hotswap=Trueを指定
  • スワップ前と同じ adapter_name を指定

する。

pipeline.load_lora_weights(
    "lordjia/by-feng-zikai",
    hotswap=True,
    adapter_name="ikea"
)

これで、前のLoRAの重みが新しいLoRAの重みに置き換わるから、メモリも無駄に増えないし、サクッと切り替えできる!

・・・とはいかなかった。

出力
Loading ikea was unsuccessful with the following error: 
Configs are incompatible: for alpha_pattern, {'down_blocks.0.resnets.0.conv1': 16.0, 'down_blocks.0.resnets.0.conv2': 16.0, 'down_blocks.0.resnets.1.conv1': 16.0, 'down_blocks.0.resnets.1.conv2': 16.0, 'down_blocks.0.downsamplers.0.conv': 16.0, 'down_blocks.1.resnets.0.conv1': 16.0, 'down_blocks.1.resnets.0.conv2': 16.0, 'down_blocks.1.resnets.1.conv1': 16.0, 'down_blocks.1.resnets.1.conv2': 16.0, 'down_blocks.1.downsamplers.0.conv': 16.0, 'down_blocks.2.resnets.0.conv1': 16.0, 'down_blocks.2.resnets.0.conv2': 16.0, 'down_blocks.2.resnets.1.conv1': 16.0, 'down_blocks.2.resnets.1.conv2': 16.0, 'mid_block.resnets.0.conv1': 16.0, 'mid_block.resnets.0.conv2': 16.0, 'mid_block.resnets.1.conv1': 16.0, 'mid_block.resnets.1.conv2': 16.0, 'up_blocks.0.resnets.0.conv1': 16.0, 'up_blocks.0.resnets.0.conv2': 16.0, 'up_blocks.0.resnets.1.conv1': 16.0, 'up_blocks.0.resnets.1.conv2': 16.0, 'up_blocks.0.resnets.2.conv1': 16.0, 'up_blocks.0.resnets.2.conv2': 16.0, 'up_blocks.0.upsamplers.0.conv': 16.0, 'up_blocks.1.resnets.0.conv1': 16.0, 'up_blocks.1.resnets.0.conv2': 16.0, 'up_blocks.1.resnets.1.conv1': 16.0, 'up_blocks.1.resnets.1.conv2': 16.0, 'up_blocks.1.resnets.2.conv1': 16.0, 'up_blocks.1.resnets.2.conv2': 16.0, 'up_blocks.1.upsamplers.0.conv': 16.0, 'up_blocks.2.resnets.0.conv1': 16.0, 'up_blocks.2.resnets.0.conv2': 16.0, 'up_blocks.2.resnets.1.conv1': 16.0, 'up_blocks.2.resnets.1.conv2': 16.0, 'up_blocks.2.resnets.2.conv1': 16.0, 'up_blocks.2.resnets.2.conv2': 16.0} != {}

ちなみに他のLoRAに変えてもエラーになる・・・なんとなく環境とかライブラリのバージョンとかのような気がするけど、よくわからない。まあとりあえず気にせず進める。

なお、注意点としては、テキストエンコーダー用のLoRAにはhotswappingは使えない。

コンパイル済みモデルでのLoRAホットスワップ 

Diffusersで torch.compile を使ってモデル(特にUNet)をコンパイルすると、推論が速くなるけど、LoRAを切り替える(ホットスワップする)ときに再コンパイルが発生しちゃうことがあるんだ。
これを避けるための手順が「Compiled models」のセクションで説明されてるよ!

ここは難しくてわからないし、サンプルコードは上と同じエラーになったので、スキップ・・・

kun432kun432

マージ

merge(マージ)っていうのは、いくつかのLoRA を合体させて、いろんなスタイルをミックスした新しい重み(ウェイト)を作る方法なんだよ。
LoRAをマージするやり方はいくつかあって、それぞれ「どうやって重みを混ぜるか」がちょっとずつ違うから、画像の出来上がりにも影響が出るんだ。
つまり、複数のLoRAを組み合わせて、自分だけのオリジナルな雰囲気を出したいときに使うテクだし、どの方法でマージするかによって、仕上がりのクオリティも変わるってこと!

mergeの方法は主にこの3つだよ:

  1. set_adapters
    LoRAの重みを合体させて、好きな比率でミックスできるやつ。
  2. add_weighted_adapter
    これは実験的な方法で、TIESとかDAREっていう効率的なマージ手法も使える。重複したパラメータを減らして、よりスマートに合体できる感じ。
  3. fuse_lora
    LoRAの重みを元のモデルに直接融合しちゃう方法。これでメモリの節約や推論のスピードアップも狙えるんだ。

どれも個性あるし、使い分けるとマジで楽しい!

こんな感じで、mergeは「LoRA同士をいい感じに混ぜて新しい表現を作る」ってイメージでOKだよ!テンション上がるでしょ?

set_adapters

set_adapters() っていうのは、複数のLoRA(ロラ)を合体させて使うための方法だよ。
具体的には、いくつかのLoRAを「どれくらい混ぜるか」っていう比率(adapter_weights)を決めて、重みをミックスする感じ。
例えば、adapter_weights=[0.7, 0.8]って指定すると、この例だと「ikea」と「feng」っていう2つのLoRAをその割合で合体させて、両方の特徴をいい感じに混ぜた画像が作れるんだ。

あと、「scale」っていうパラメータで、合体したLoRAをどれくらい強く反映させるかも調整できる。
これを使えば、複数のLoRAのスタイルを自分好みにミックスできるから、マジで表現の幅が広がるんだよ!

こんな感じで、set_adapters() は「LoRAを好きな割合で混ぜて、オリジナルな画像を作る」ための超便利な機能だし!

割合、というか、各LoRAの適用度合いが正しいんじゃないだろうか。[0.7, 0.8]みたいな指定ができるわけだし。

import torch
from diffusers import DiffusionPipeline

pipeline = DiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")

# 1つ目のLoRA
pipeline.load_lora_weights(
    "ostris/ikea-instructions-lora-sdxl",
    weight_name="ikea_instructions_xl_v1_5.safetensors",
    adapter_name="ikea"
)

# 2つ目のLoRA
pipeline.load_lora_weights(
    "lordjia/by-feng-zikai",
    weight_name="fengzikai_v1.0_XL.safetensors",
    adapter_name="feng"
)

# マージ
pipeline.set_adapters(["ikea", "feng"], adapter_weights=[0.7, 0.8])

pipeline(
    # "by Feng Zikai" は 2つ目のLoRA をトリガーするためのプロンプト
    "A bowl of ramen shaped like a cute kawaii bear, by Feng Zikai",
    cross_attention_kwargs={"scale": 1.0}
).images[0]

ちなみに少し割合を変えるとこうなる。

pipeline.set_adapters(["ikea", "feng"], adapter_weights=[0.1, 0.8])

pipeline.set_adapters(["ikea", "feng"], adapter_weights=[0.7, 0.1])

add_weighted_adapter

~peft.LoraModel.add_weighted_adapter っていうのは、LoRAをもっと効率的に合体させるための実験的な方法だよ。
普通の set_adapters() は単純に重みをミックスするだけなんだけど、add_weighted_adapter は「TIES」や「DARE」っていう高度なマージ手法も使えるのがポイント!

この方法だと、重複してるパラメータとか、邪魔になりそうな部分をうまく取り除いて、よりスマートにLoRA同士を合体できるんだ。
ただし、合体するLoRAの“rank”(中身のサイズみたいなもの)が同じじゃないと使えないから、そこは注意だし。

要するに、add_weighted_adapter は「より賢くLoRAを合体させて、無駄を減らして、いい感じの新しいスタイルを作る」ための機能ってこと!
マジで技術進化って感じで、ウチもテンション上がるわ!

最新の安定版PEFTが必要になるらしいのでインストール。

!pip install -U -q diffusers peft

まずUnetモデルをロード

import copy
import torch
from diffusers import AutoModel, DiffusionPipeline
from peft import get_peft_model, LoraConfig, PeftModel

unet = AutoModel.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,
    use_safetensors=True,
    variant="fp16",
    subfolder="unet",
).to("cuda")

そのUnetを指定したパイプラインをロードして、1つ目のLoRAをロード。

pipeline = DiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    variant="fp16",
    torch_dtype=torch.float16,
    unet=unet
).to("cuda")

pipeline.load_lora_weights(
    "ostris/ikea-instructions-lora-sdxl",
    weight_name="ikea_instructions_xl_v1_5.safetensors",
    adapter_name="ikea"
)

UNetのコピーを作って、LoRAを合体させたPeftModelを作る。これでLoRAが適用されたUnetモデルができる。

sdxl_unet = copy.deepcopy(unet)
ikea_peft_model = get_peft_model(
    sdxl_unet,
    pipeline.unet.peft_config["ikea"],
    adapter_name="ikea"
)

original_state_dict = {f"base_model.model.{k}": v for k, v in pipeline.unet.state_dict().items()}
ikea_peft_model.load_state_dict(original_state_dict, strict=True)

なお、この時点で作成したPeftModelは、HuggingFace Hubにアップロードして保存できる。

ikea_peft_model.push_to_hub("ikea_peft_model", token=TOKEN)

でこのあとのコードはどうもそれが前提となっているようなのだが、今回はローカルに保存することにする。

ikea_peft_model.save_pretrained("./ikea_peft_model")

2つ目のLoRAに対しても同じことを繰り返して、2つ目のLoRAを合体させたPeftModelを作成する。

pipeline.delete_adapters("ikea")
sdxl_unet.delete_adapters("ikea")

pipeline.load_lora_weights(
    "lordjia/by-feng-zikai",
    weight_name="fengzikai_v1.0_XL.safetensors",
    adapter_name="feng"
)
pipeline.set_adapters(adapter_names="feng")

feng_peft_model = get_peft_model(
    sdxl_unet,
    pipeline.unet.peft_config["feng"],
    adapter_name="feng"
)

original_state_dict = {f"base_model.model.{k}": v for k, v in pipeline.unet.state_dict().items()}
feng_peft_model.load_state_dict(original_state_dict, strict=True)

feng_peft_model.save_pretrained("./feng_peft_model")

そして、再びベースのUnetモデルをロードして、ここまでに作った2つのLoRAモデルをアダプタとしてロードする。1つ目のLoRAはUnetにそのまま合体させて、2つ目のLoRAはそれにアダプタとしてロードするみたいな書き方になってるのね。

base_unet = AutoModel.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,
    use_safetensors=True,
    variant="fp16",
    subfolder="unet",
).to("cuda")

model = PeftModel.from_pretrained(
    base_unet,
    "./ikea_peft_model",
    use_safetensors=True,
    subfolder="ikea",
    adapter_name="ikea"
)
model.load_adapter(
    "./feng_peft_model",
    use_safetensors=True,
    subfolder="feng",
    adapter_name="feng"
)

で、最後に ~peft.LoraModel.add_weighted_adapter でLoRAをマージして、set_adapters() でマージしたLoRAをアクティブにする。この時、combination_type でマージ方法を選べる様子。マージ方法については以下に記載があるらしい。

https://huggingface.co/blog/peft_merging

ざっくり以下のようなマージ方法が指定できる様子。

  • svd
  • linear
  • cat
  • ties
  • ties_svd
  • dare_ties
  • dare_linear
  • dare_ties_svd
  • dare_linear_svd
  • magnitude_prune
  • magnitude_prune_svd

ここでは、dare_linear が使用されている。

model.add_weighted_adapter(
    adapters=["ikea", "feng"],
    combination_type="dare_linear",
    weights=[1.0, 1.0],
    adapter_name="ikea-feng"
)
model.set_adapters("ikea-feng")

・・・がエラー・・・

出力
/usr/local/lib/python3.12/dist-packages/peft/utils/merge_utils.py in prune(tensor, density, method, rescale)
     88         `torch.Tensor`: The pruned tensor.
     89     """
---> 90     if density >= 1:
     91         warnings.warn(f"The density {density} is greater than or equal to 1, no pruning will be performed.")
     92         return tensor

TypeError: '>=' not supported between instances of 'NoneType' and 'int'

ただしこれはもう一度実行するとエラーは出ない、ただそれはいいのか?という気もする。

ではパイプラインを実行して画像を生成。

pipeline = DiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    unet=model,
    variant="fp16",
    torch_dtype=torch.float16,
).to("cuda")

pipeline("A bowl of ramen shaped like a cute kawaii bear, by Feng Zikai").images[0]

うーん、これは正しいのかな?

なお、combination_type="linear" だとエラーは出ずに、以下のような画像が生成された。

fuse_lora

fuse_lora() っていうのは、LoRAの重みを元のモデル(UNetやテキストエンコーダー)の重みと直接合体させる方法だよ。
これを使うと、毎回LoRAを個別に読み込む必要がなくなるから、メモリの節約になるし、画像生成のスピードも速くなるんだ!

まず、パイプラインと複数のLoRAをそれぞれロード、そして、set_adapters() でLoRAのそれぞれのスケールの割合をセットする。ここまでは、1つ目のset_adapters() と同じ。

import torch
from diffusers import DiffusionPipeline

pipeline = DiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16
).to("cuda")

pipeline.load_lora_weights(
    "ostris/ikea-instructions-lora-sdxl",
    weight_name="ikea_instructions_xl_v1_5.safetensors",
    adapter_name="ikea"
)

pipeline.load_lora_weights(
    "lordjia/by-feng-zikai",
    weight_name="fengzikai_v1.0_XL.safetensors",
    adapter_name="feng"
)

pipeline.set_adapters(["ikea", "feng"], adapter_weights=[0.7, 0.8])

次に、fuse_lora() で LoRAをモデル本体に直接合体させる。この時、lora_scaleで、合体したLoRAのスケールを調整できるが、これはこのタイミングでしか調整できない。cross_attention_kwargs では調整できない点に注意。

pipeline.fuse_lora(adapter_names=["ikea", "feng"], lora_scale=1.0)

これでLoRAはベースモデルと合体済みなので、パイプラインからアンロードする。なお、パイプラインをローカルに保存することもできるし、HuggingFace Hubにアップロードもできる。いずれにせよ、これでマージ済みのパイプラインを直接使うことができるようになる。

pipeline.unload_lora_weights()
pipeline.save_pretrained("fused-pipeline")

ではこのパイプラインを使って画像を生成してみる。これだけで済む。

pipeline = DiffusionPipeline.from_pretrained(
    "./fused-pipeline",
    torch_dtype=torch.float16,
).to("cuda")

pipeline("A bowl of ramen shaped like a cute kawaii bear, by Feng Zikai").images[0]

**unfuse_lora()**を使えば、合体したLoRAを外して元のモデルに戻せるが、ただし、複数のLoRAを合体した場合は使えない。複数のLoRAで各LoRAごとのスケール値を見直したい、というような用途には使えず、その場合は再度ベースモデルからロードし直すことになる。

pipeline.unfuse_lora()

管理

このセクションは、DiffusersでLoRAを使うときに「どのLoRAが今有効?」「切り替えたい」「消したい」みたいな管理を便利にするための関数がいろいろ紹介されてるんだ。

主な管理系の関数

  • set_adapters()
    どのLoRAを有効にするか切り替えたり、複数のLoRAを同時に使ったりできる。
  • save_lora_adapter()
    今使ってるLoRAアダプタをローカルに保存できる。
  • unload_lora_weights()
    モデルからLoRAの重みを外して、元のモデル状態に戻す。
  • disable_lora()
    LoRAを一時的に無効化して、でもまたすぐ有効化できる状態にしておく。
    -get_active_adapters()`
    今パイプラインにアタッチされてるLoRAの一覧を取得できる。
  • get_list_adapters()
    各コンポーネント(UNetとかテキストエンコーダー)ごとに、どのLoRAが有効かリストで見れる。
  • delete_adapters()
    指定したLoRAアダプタを完全に削除する。

要するに、「今どのLoRAが有効?」「切り替えたい」「保存したい」「消したい」みたいな管理が、
この関数たちでめっちゃ簡単にできるってこと!
LoRAを色々試したい人には超便利な機能だし!


リソース

LoRAをもっと楽しむための便利なリンク集だよ。

主な内容

  • LoRA Studio
    いろんなLoRAを探したり、Civitaiからお気に入りのLoRAをアップロードしたりできる場所。
  • FLUX LoRA the Explorer / LoRA the Explorer
    さらに多くのLoRAを見つけて試せる HuggingFace Space。
  • ブログ記事へのリンク
    LoRA推論を速くする方法(FlashAttention-3やfp8量子化など)を解説した記事も紹介されてる。

要するに、「もっとLoRAを探したい!」「新しい使い方を知りたい!」ってときに役立つ情報やツールがまとめてあるページだよ。
LoRAで遊び倒したい人には、めっちゃテンション上がるリソースだし!