Stable Diffusion XL on macOS: Efficient Testing with BDD and safetensors

Learn how to optimize Stable Diffusion XL (SDXL) for macOS using Metal Performance Shaders, BDD testing, and safetensors. Step-by-step guide with practical code examples and performance insights.

この記事で解説する実装例はGitHubで公開しています。

MacOSでStable Diffusion XL (SDXL) の実装例を開発した記録を共有します。テストと品質管理にBDDアプローチを採用し、safetensorsによるモデル管理、Metal Performance Shadersによる最適化など、MacOSで効率的に動作させるためのポイントを解説します。

SDXL Turboの技術解説

SDXL Turboは、Stable Diffusion XLの最適化モデルで、Adversarial Diffusion Distillation (ADD)と呼ばれる新しい蒸留技術を採用しています。この技術により、従来50ステップ必要だった処理を1-4ステップまで削減しながら、高品質な画像生成を実現しています。

技術アーキテクチャ

従来のStable DiffusionモデルとSDXL Turboの違いは、Adversarial Diffusion Distillation (ADD)技術にあります。50ステップの処理をなんと1ステップに削減しながら、高品質な画像生成を実現しています。この最適化により、処理時間を大幅に短縮しつつ、画質を維持することが可能になりました。

処理フロー

このシーケンス図は、SDXL Turboの画像生成プロセスを示しています。特筆すべき点は、UNetでの単一ステップ処理による高速化です。A100 GPUを使用した場合、全体の処理時間は約207msと非常に高速に生成できるそうです。

パフォーマンスの内訳を見ると、全体の処理時間約207msの中で、UNet推論に67ms、残りをプロンプト処理とデコード処理で使用しています。

プロジェクト概要

本プロジェクトの目的は、MacOS環境でSDXL Turboを効率的に動作させることです。具体的には以下を実現しています:

  1. 高速な推論 - SDXL Turboの特性を活かした1ステップでの生成
  2. MacOS最適化 - Metal Performance Shadersによる処理の最適化
  3. メモリ効率 - safetensorsとattention slicingによる効率的なメモリ使用

これらの実現にBDD(Behavior Driven Development)アプローチを採用し、要件定義からテスト、実装までを一貫して管理しています。BDDを採用することで、以下のメリットを得ています:

  1. 要件の明確化 - 振る舞いを自然言語で記述し、全員が理解しやすい形式で管理
  2. 品質の確保 - テストと実装の整合性を常に保持
  3. 段階的な開発 - 要件から実装まで、体系的なアプローチで開発を進行

実装の詳細

SDXL Turboの実装では、MacOS環境での最適なパフォーマンスを実現するため、以下の点に注力しました。

パイプラインの設計と実装

SDXLパイプラインの中心となる実装では、デバイス管理、モデルのロード、そして最適化設定を適切に行います。MacOS環境で効率的に実行するため、MPSデバイスの活用とメモリ管理の最適化に特に注意を払っています。

core/pipeline.py
1from pathlib import Path
2from typing import Optional, Union
3from PIL import Image
4import logging
5from diffusers import StableDiffusionXLPipeline, EulerAncestralDiscreteScheduler
6import torch
7
8
9class SDXLPipeline:
10    def __init__(self, model_path: Union[str, Path]):
11        """Initialize SDXL pipeline.
12        
13        Args:
14            model_path: Path to the local SDXL model file (.safetensors)
15        """
16        self.pipeline = None
17        self.device = None
18        self.dtype = None
19        
20        self.model_path = Path(model_path)
21        if not self.model_path.exists():
22            raise FileNotFoundError(f"Model file not found: {self.model_path}")
23        
24        self._setup_device()
25
26    def _setup_device(self) -> None:
27        """Configure device settings for optimal performance.
28        
29        Automatically selects MPS if available, otherwise falls back to CPU.
30        """
31        # Metal Performance Shaders devices auto-detection
32        self.device = "mps" if torch.backends.mps.is_available() else "cpu"
33        self.dtype = torch.float32  # Optimal for MacOS
34        logger.info(f"Using device: {self.device}")
35
36    def load(self) -> bool:
37        """Load the SDXL model with optimal settings."""
38        try:
39            # Use safetensors for efficient loading
40            self.pipeline = StableDiffusionXLPipeline.from_single_file(
41                str(self.model_path),
42                torch_dtype=self.dtype,
43                use_safetensors=True
44            )
45            self.pipeline.to(self.device)
46            self.pipeline.enable_attention_slicing()  # Memory optimization
47            
48            # SDXL Turbo specific settings
49            self.pipeline.scheduler = EulerAncestralDiscreteScheduler.from_config(
50                self.pipeline.scheduler.config,
51                timestep_spacing="trailing"  # Optimal for Turbo models
52            )
53            return True
54        except Exception as e:
55            logger.error(f"Error loading pipeline: {str(e)}")
56            return False

デバイスとメモリの最適化

MacOS環境での最適なパフォーマンスを実現するため、以下の最適化を実装しています:

  1. MPSデバイスの活用:

    • Metal Performance Shadersの自動検出
    • float32データ型の使用
    • デバイス非依存のフォールバック処理
  2. メモリ管理の最適化:

    • attention_slicingの有効化
    • safetensorsフォーマットの使用
    • 適切なリソースクリーンアップ

SDXL Turbo固有の最適化

SDXL Turboモデルの特性を最大限活用するため、以下の設定を実装しています:

  1. スケジューラの最適化:

    • EulerAncestralDiscreteSchedulerの使用
    • trailingタイムステップスペーシング
  2. 推論設定の最適化:

    • シングルステップ推論
    • guidance_scale = 0.0
    • 512x512の最適解像度

実装の使用例

examples/generate.py
1from sdxl_mac_diffusers_guide import SDXLPipeline
2from pathlib import Path
3
4def main():
5    # Initialize pipeline with model path
6    model_path = "models/sdxl_turbo.safetensors"
7    pipeline = SDXLPipeline(model_path)
8    
9    if not pipeline.load():
10        print("Failed to load pipeline")
11        return
12    
13    # Generate image with optimal settings for SDXL Turbo
14    prompt = "A photorealistic image of a white cat in a garden"
15    image = pipeline.generate(
16        prompt,
17        num_inference_steps=1,    # Single step for Turbo
18        guidance_scale=0.0,      # Optimal for Turbo
19        width=512,               # Optimal resolution
20        height=512
21    )
22    
23    # Save the generated image
24    if image:
25        output_dir = Path("output")
26        output_dir.mkdir(exist_ok=True)
27        image.save(output_dir / "generated_cat.png")

テスト戦略と実装

本プロジェクトでは、BDDアプローチによるテストを採用し、pytest-bddを使用して実装しています。このセクションでは、テストの構造と実装方法を、図を交えて詳しく説明します。

テストの基本フロー

BDDテストの基本的な流れを以下の図で示します。テストは「Given」「When」「Then」の3つのステップで構成され、それぞれが明確な役割を持っています。

テスト環境のセットアップ

pytest-bddを使用したテスト環境の構築手順を説明します。このフレームワークを選択した理由は:

  1. Pytestとの統合

    • 既存のPytest機能やプラグインを活用可能
    • フィクスチャーの再利用が容易
    • デバッグとレポート機能が充実
  2. BDDの完全サポート

    • Gherkin記法による要件定義
    • シナリオのパラメータ化
    • タグによる柔軟なテスト管理
  3. 開発効率の向上

    • コード生成機能でボイラープレートを削減
    • シナリオの自動検出と実行
    • 豊富なテストユーティリティ

プロジェクト構造

テストコードは以下の構造で組織化しています:

tests/
├── features/          # Gherkinシナリオ
│   ├── pipeline/      # パイプライン関連のfeature
│   │   ├── initialization.feature
│   │   ├── optimization.feature
│   │   └── generation.feature
│   └── device/        # デバイス管理のfeature
│       ├── mps.feature
│       └── memory.feature
├── step_definitions/ # ステップの実装
│   ├── test_pipeline.py
│   └── test_device.py
├── fixtures/         # 共通フィクスチャー
│   ├── model.py
│   └── device.py
└── conftest.py      # テスト全体の設定

この構造により:

  • 機能ごとのテストを整理
  • コードの再利用を促進
  • メンテナンス性を向上

pytest-bddの設定

  1. 基本設定
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
markers =
    sdxl: SDXL関連のテスト
    device: デバイス管理のテスト
    memory: メモリ管理のテスト
    generation: 画像生成のテスト
bdd_features_base_dir = tests/features
  1. フィクスチャーの設定
# conftest.py
import pytest
from pathlib import Path

@pytest.fixture
def model_path(tmp_path) -> Path:
    """テスト用のモデルファイルパス"""
    return tmp_path / 'test_model.safetensors'

@pytest.fixture
def device_config() -> dict:
    """デバイス設定"""
    return {
        'device': 'mps',
        'dtype': 'float32',
        'attention_slicing': True
    }

基本的な使用例

tests/features/pipeline/initialization.feature
1Feature: Pipeline Initialization
2  As an AI developer
3  I want to initialize the SDXL pipeline correctly
4  So that I can ensure stable model execution
5
6  Background:
7    Given the test model file exists
8
9  @initialization
10  Scenario: Basic pipeline initialization
11    When I create a new pipeline instance
12    Then the pipeline should be configured correctly
13    And the device settings should match the system
14
15  @error @initialization
16  Scenario: Handle missing model file
17    Given the model file does not exist
18    When I try to create a pipeline instance
19    Then it should raise a FileNotFoundError
20    And provide a clear error message
tests/step_definitions/test_pipeline.py
1from pathlib import Path
2import pytest
3from pytest_bdd import scenarios, given, when, then, parsers
4from diffusers import StableDiffusionXLPipeline
5
6# シナリオの読み込み
7scenarios('pipeline/initialization.feature')
8
9# Background steps
10@given('the test model file exists', target_fixture='model_path')
11def model_file_exists(tmp_path):
12    path = tmp_path / 'test_model.safetensors'
13    path.touch()
14    return path
15
16# Scenario: Basic pipeline initialization
17@when('I create a new pipeline instance')
18def create_pipeline(model_path):
19    return SDXLPipeline(model_path)
20
21@then('the pipeline should be configured correctly')
22def verify_pipeline_configuration(pipeline):
23    assert isinstance(pipeline, SDXLPipeline)
24    assert pipeline.model_path is not None
25
26@then('the device settings should match the system')
27def verify_device_settings(pipeline):
28    import torch
29    expected_device = 'mps' if torch.backends.mps.is_available() else 'cpu'
30    assert pipeline.device == expected_device
31
32# Scenario: Handle missing model file
33@given('the model file does not exist')
34def no_model_file(tmp_path):
35    return tmp_path / 'nonexistent.safetensors'
36
37@when('I try to create a pipeline instance')
38def try_create_pipeline(model_path):
39    with pytest.raises(FileNotFoundError) as exc_info:
40        SDXLPipeline(model_path)
41    return exc_info
42
43@then('it should raise a FileNotFoundError')
44def verify_error_type(exc_info):
45    assert exc_info.type == FileNotFoundError
46
47@then('provide a clear error message')
48def verify_error_message(exc_info):
49    assert 'Model file not found' in str(exc_info.value)

テストアーキテクチャ

テストコードは以下の構造に従って整理しています:

  1. 機能テスト

    • パイプラインの基本機能
    • デバイス管理とメモリ最適化
    • モデルのロードと初期化
    • 画像生成プロセス
  2. エラーハンドリング

    • 無効な入力のテスト
    • リソース不足の検出
    • 例外の適切な処理
  3. パフォーマンステスト

    • メモリ使用量の測定
    • 実行時間の計測
    • 最適化の効果検証

パイプラインのテスト実装

tests/features/pipeline.feature
1Feature: SDXL Pipeline Management
2  As an AI application developer
3  I want to manage SDXL pipeline efficiently
4  So that I can provide stable image generation service
5
6  Background:
7    Given the SDXL Turbo model is available
8
9  Scenario: Initialize pipeline with optimal settings
10    When I create a pipeline with default settings
11    Then the pipeline should be configured for MacOS
12    And MPS acceleration should be enabled if available
13
14  Scenario: Handle memory efficiently
15    Given I have an initialized pipeline
16    When I load the model with attention slicing
17    Then the memory usage should be optimized
18    And the model should be loaded successfully
19
20  Scenario: Generate images with optimal settings
21    Given I have a loaded pipeline
22    When I generate an image with the following settings:
23      | Parameter         | Value |
24      | num_inference_steps | 1     |
25      | guidance_scale    | 0.0   |
26    Then the generation should complete successfully
27    And the output image should be 512x512 pixels
tests/step_definitions/test_pipeline.py
1from pathlib import Path
2import pytest
3from pytest_bdd import scenario, given, when, then, parsers
4
5@scenario('pipeline.feature', 'Initialize pipeline with optimal settings')
6def test_pipeline_initialization():
7    pass
8
9@given('the SDXL Turbo model is available', target_fixture='model_path')
10def model_available(tmp_path):
11    model_path = tmp_path / 'sdxl_turbo.safetensors'
12    model_path.touch()
13    return model_path
14
15@when('I create a pipeline with default settings')
16def create_pipeline(model_path):
17    pipeline = SDXLPipeline(model_path)
18    return pipeline
19
20@then('the pipeline should be configured for MacOS')
21def verify_macos_config(pipeline):
22    assert pipeline.device in ['mps', 'cpu']
23    assert pipeline.dtype == torch.float32
24
25@then('MPS acceleration should be enabled if available')
26def verify_mps_acceleration(pipeline):
27    if torch.backends.mps.is_available():
28        assert pipeline.device == 'mps'
29    else:
30        assert pipeline.device == 'cpu'

最適化のテスト

tests/features/optimization.feature
1Feature: Pipeline Optimization
2  As a system developer
3  I want to optimize the pipeline performance
4  So that it runs efficiently on MacOS
5
6  Background:
7    Given I have an initialized pipeline
8
9  @memory
10  Scenario Outline: Memory optimization settings
11    When I enable <optimization> optimization
12    Then the memory usage should be less than <limit> GB
13    And the model should still function correctly
14
15    Examples:
16      | optimization      | limit |
17      | attention_slicing | 8     |
18      | fp16             | 6     |

テストパターンと実装例

テストの実装にあたり、以下のパターンを活用しています。特に各featureファイルをどの粒度で分割するべきかはまだ検討の余地があると考えています:

  1. 段階的なテスト:

    • 基本機能から複雑な機能へ
    • 依存関係を考慮したテストの順序
    • エッジケースの体系的な検証
  2. 再利用可能なステップ:

    • 共通の前提条件をBackgroundとして定義
    • フィクスチャーの効果的な活用
    • ステップ間でのコンテキスト共有
  3. エラー処理のテスト:

    • 例外の適切な処理
    • エラーメッセージの検証
    • リソースのクリーンアップ確認

高度なテスト例

tests/features/generation/batch_processing.feature
1Feature: Batch Image Generation
2  As a service provider
3  I want to generate multiple images efficiently
4  So that I can handle concurrent requests
5
6  Background:
7    Given the pipeline is initialized with optimal settings
8
9  @performance
10  Scenario Outline: Generate images in batch
11    When I generate <count> images in parallel
12    Then all images should be generated successfully
13    And memory usage should stay below <memory_limit> GB
14    And total processing time should be less than <time_limit> seconds
15
16    Examples:
17      | count | memory_limit | time_limit |
18      | 2     | 12          | 5          |
19      | 4     | 16          | 10         |
20
21  @error-handling
22  Scenario: Handle resource exhaustion
23    Given system memory is limited to 4GB
24    When I attempt to generate multiple images
25    Then it should fail gracefully
26    And release all allocated resources
27    And provide appropriate error information
tests/step_definitions/test_batch_generation.py
1import pytest
2from pytest_bdd import scenario, given, when, then, parsers
3from concurrent.futures import ThreadPoolExecutor
4import psutil
5import time
6
7@scenario('generation/batch_processing.feature', 'Generate images in batch')
8def test_batch_generation():
9    pass
10
11@given('the pipeline is initialized with optimal settings')
12def optimized_pipeline(model_path):
13    pipeline = SDXLPipeline(model_path)
14    pipeline.enable_attention_slicing()
15    pipeline.enable_sequential_cpu_offload()
16    return pipeline
17
18@when(parsers.parse('I generate {count:d} images in parallel'))
19def generate_batch(pipeline, count):
20    prompts = [f'Test prompt {i}' for i in range(count)]
21    start_time = time.time()
22    
23    with ThreadPoolExecutor(max_workers=count) as executor:
24        futures = [
25            executor.submit(pipeline.generate, prompt)
26            for prompt in prompts
27        ]
28        results = [f.result() for f in futures]
29    
30    return {
31        'results': results,
32        'time_taken': time.time() - start_time
33    }
34
35@then('all images should be generated successfully')
36def verify_generation(results):
37    assert all(img is not None for img in results['results'])
38    for img in results['results']:
39        assert img.size == (512, 512)
40
41@then(parsers.parse('memory usage should stay below {memory_limit:d} GB'))
42def verify_memory_usage(memory_limit):
43    memory_gb = psutil.Process().memory_info().rss / (1024 * 1024 * 1024)
44    assert memory_gb < memory_limit
45
46@then(parsers.parse('total processing time should be less than {time_limit:d} seconds'))
47def verify_processing_time(results, time_limit):
48    assert results['time_taken'] < time_limit

テストの実装状況

現在のテスト実装の状況は以下の通りです:

  1. 基本機能 (✅)

    • パイプラインの初期化
    • デバイス設定の管理
    • リソース管理の検証
  2. 最適化機能 (✅)

    • メモリ使用量の最適化
    • 処理速度の改善
    • デバイス固有の最適化
  3. 画像生成 (🚧)

    • 基本的な生成機能
    • バッチ処理
    • エラー処理
  4. パフォーマンス (🚧)

    • 負荷テスト
    • スケーラビリティ
    • リソース制限

実装状況と得られた知見

開発を通じて、以下の重要な知見が得られました:

  1. テストシナリオの設計

    • 適切な粒度設定の重要性
    • BDDシナリオの再利用性
    • テストデータの管理方法
  2. 実装とテストの統合

    • TDDとBDDの組み合わせ効果
    • CIパイプラインでの自動化
    • コードレビューとテストの関係
  3. パフォーマンスとメンテナンス

    • テストの実行速度の最適化
    • 並列実行時のリソース管理
    • テストコードの保守性向上

今後の展望

本プロジェクトの今後の計画:

  1. 機能の拡張

    • メモリ最適化オプションの追加
    • バッチ処理の効率化
    • クロスプラットフォーム対応
  2. テストの強化

    • パフォーマンステストの自動化
    • 負荷テストの実装
    • カバレッジの向上
  3. ドキュメントとツール

    • APIドキュメントの整備
    • CLIツールの提供
    • テスト用ユーティリティの公開

参考文献