→ Craft Plus and Family が40%オフ

BlurHashとMetalの融合:アプリの画像読み込み体験を飛躍的に向上させる

BlurHashがMetalによってパワーアップしました。即時プレビュー、ゼロラグ、そして圧倒的なパフォーマンス——画像デコードとGPUアクセラレーションが出会ったとき、何が可能になるかを示します。

公開日
6/4/2025
Building Craft
BlurHashとMetalの融合:アプリの画像読み込み体験を飛躍的に向上させる

Craftの開発者として、私の仕事はアイデアを現実にすることです。それは、最小限のUIトランジションの調整から、GPU Compute Shaderを使った画像圧縮アルゴリズムの最適化に至るまで、構築するすべてのものに卓越したレベルの丁寧さと精度をもたらすことを意味します。優れたデザインはパフォーマンスを犠牲にする必要はないからです。Craft v3では、スタイルギャラリーをリリースしました。厳選されたリストや独自のスタイルから選択できる、洗練された新機能です。

Style Gallery
Style Gallery

ボトルネック:大規模なBlurHash

私たちは、Woltが開発した独創的なアルゴリズムであるBlurHashを使って、視覚的に魅力的なプレースホルダー画像を生成しています。BlurHashは大きな画像を小さな文字列に変換し、実際の画像のダウンロードURLと一緒に転送することができます。画像が読み込まれている間、プレースホルダーが表示され、ダウンロードが完了した最終画像へとスムーズにクロスフェードします。

Original Image → Encoded String → Decoded Placeholder
Original Image → Encoded String → Decoded Placeholder

しかし、スタイルギャラリーが最大9つのスタイルを同時に表示し、それぞれに最大3つのプレースホルダー画像を持つまでに成長すると、単一のCPUコアで動作するBlurHashがすぐにパフォーマンスのボトルネックになりました。これは、なかなか理想のスタイルが見つからないときの激しいスクロール中に顕著に現れました。

Style Gallery with Placeholders
Style Gallery with Placeholders

BlurHashをさらに掘り下げる前に、プレースホルダー画像を画像のダウンロードと同時に生成し、画像が届く前に完成していれば挿入するというソリューションを実装しました。その結果、複雑なマルチスレッドのコードになり、競合状態に細心の注意を払う必要がありました。しかし、さらに気になったのは、BlurHashがCPUリソースをすべて使い切っていることでした。プレースホルダー画像の生成は軽量であるべきで、BlurHashは非効率だと感じていました。

スタイルギャラリーは1月にCraftに参加して以来、私のお気に入りの機能で、その可能性を最大限に引き出したいと強く思っていました。まだ満足できていませんでした。もっと速いものが必要でした。はるかに速いものが。

調査:BlurHashの仕組みを理解する

先ほど触れた小さな文字列は、実際には2つ*の要素から構成されています。1つ目はコンポーネントの数、2つ目は各コンポーネントの値です。BlurHashは、おそらくJPEG圧縮アルゴリズムからインスピレーションを得て、画像を(最大)9つの水平および9つの垂直コサイン波の組み合わせに分解します。各波には異なる重みがあり、その重みが文字列内のコンポーネントです。画像を分解するために、アルゴリズムは画像全体を走査し、離散コサイン変換(DCT)を実行してコンポーネントをBase83でエンコードします。

*技術的には4つで、3種類のコンポーネントがあります:最大AC値、DCコンポーネント、ACコンポーネント
64 components combining 8 horizontal and 8 vertical cosine waves.
64 components combining 8 horizontal and 8 vertical cosine waves.

各コンポーネントの重みは画像全体を対象に計算され、コンポーネント間に依存関係はありません。

これが気になる点です。これは大規模に並列化できる画像処理タスクなのに、なぜGPUで実行されていないのでしょうか?

MetalへようこそEmma:AppleのGPUを活用したソリューション

1月に、私はローカルAI統合のためのデバイス上推論を実装していました。推論はMLXによって行われ、MetalをつかってGPUを完全に活用し、驚異的なパフォーマンスを実現しています。それが私にひらめきを与えました。LLM推論も並列化できるタスクなのに、なぜMetalを使ってBlurHashも高速化できないのか?

Metalのドキュメントに飛び込み、実験を開始しました。強化されたエンコードおよびデコードアルゴリズムが元の実装と一致する(または5%の許容誤差内に収まる)ことを確認するためのテストを作成しました。そして、アルゴリズムの核心部分をMetal Compute Shaderに移行する実験を素早く始めました。

重要なエンコードおよびデコードルーティンをMetalで書き直したところ、初期ベンチマークに驚かされました。エンコードは200倍以上高速になり、デコードはさらに劇的に改善されました。最初は信じがたいほどでした。実際に動作しているのかを確認するために、元の実装を削除してデコードされた画像をディスクに保存しなければならないほどでした!

こうして、MetalBlurHashが誕生しました。

MetalBlurHash:ドロップイン対応のGPUアクセラレーションソリューション

MetalBlurHashはWoltのオリジナルBlurHash実装との完全な互換性を保っています。ドロップイン置き換えが可能で、統合が簡単、手間いらずです。ライブラリを切り替えるだけでパフォーマンスの恩恵を享受できます:

タスクオリジナルMetalBlurHash高速化
エンコード32.212s0.154s210x
デコード3.267s0.013s25x
MetalBlurHashはオリジナル実装より200倍以上高速です
タスク解像度コンポーネント
エンコード3648 × 54729x9
デコード3840 × 25609x9
テストパラメータ(M1 MAXでXCTestパフォーマンステストを使用してベンチマーク実施)

スタイルギャラリーでは、MetalBlurHashが実装をシンプルにしながら、あらゆる遅延を即座に解消しました。実際の画像の読み込みを開始する前にプレースホルダー画像を生成できるようになり、同時画像読み込みによる競合状態のデバッグが不要になりました。

もう一つのお知らせ

MetalBlurHashは本日より、寛容なMITライセンスのもとで公開されています。

Click here to check it out!
Click here to check it out!

⚠️ 制限事項

MetalBlurHashは、Metalの初期化が失敗した場合は常にnilを返します。この場合に備えた処理を準備しておいてください。

バックグラウンドからのコマンドバッファの実行が許可されていないため、BlurHashの生成はウィジェット内では動作しません:

Execution of the command buffer was aborted due to an error during execution.
Insufficient Permission (to submit GPU work from background)
(00000006:kIOGPUCommandBufferCallbackErrorBackgroundExecutionNotPermitted)

MetalBlurHashをプロジェクトに統合する方法

Swift Package Managerを使用して、プロジェクト内のBlurHashをMetalBlurHashに置き換えるだけです。

はい、それだけです。

MetalBlurHashの使用を始めたい方は、リポジトリ内のドキュメントをご参照ください。

まとめ:好奇心がパフォーマンスを駆動する

お気に入りの機能を磨きたいという欲求から始まったことが、画像処理アルゴリズムとGPUアクセラレーションへの深い探求に発展しました。その過程で、従来のボトルネックを再考し、GPUコンピューティングを活用することで、いかに大きなパフォーマンスの可能性が引き出せるかを学びました。

難しいことがかつてないほど簡単にできるようになっています。今回の場合、Metalアルゴリズムの出力が元の出力の誤差範囲内に収まるという明確な合否基準があれば、LLMを活用して真の10倍エンジニアになることができます。

ぜひGPUにタスクを移してみてください。実験してみてください。デフォルトを疑ってみてください。適切なツールと少しの好奇心があれば、自分でも驚くような可能性が見えてくるかもしれません。

MetalBlurHashに関するご質問は、zsombor.szenyan@craft.doまでご連絡ください。

ブログをもっと読む