PyTorch團隊重寫「分割一切」模型,比原始實現快8倍

機器之心報道

編輯:陳萍

從年初到現在,生成式 AI 發展迅猛。但很多時候,我們又不得不面臨一個難題:如何加快生成式 AI 的訓練、推理等,尤其是在使用 PyTorch 的情況下。

本文 PyTorch 團隊的研究者爲我們提供了一個解決方案。文章重點介紹瞭如何使用純原生 PyTorch 加速生成式 AI 模型,此外,文章還介紹了 PyTorch 新功能,以及如何組合這些功能的實際示例。

結果如何呢?PyTorch 團隊表示,他們重寫了 Meta 的「分割一切」 (SAM) 模型,從而使代碼比原始實現快 8 倍,並且沒有損失準確率,所有這些都是使用原生 PyTorch 進行優化的。

博客地址:https://pytorch.org/blog/accelerating-generative-ai/

看完本文,你將瞭解到:

PyTorch 原生特性所帶來的吞吐量增加以及減少的內存開銷。

SAM 由 Meta 提出,關於這項研究的更多內容請參考「CV 不存在了?Meta 發佈「分割一切」AI 模型,CV 或迎來 GPT-3 時刻」。

接下來,文章介紹了 SAM 優化過程,包括性能分析、瓶頸識別,以及如何將這些新功能整合進 PyTorch 以解決 SAM 面臨的這些問題。除此以外,本文還介紹了 PyTorch 的一些新特性:torch.compile、SDPA、Triton kernels、Nested Tensor 以及 semi-structured sparsity(半結構化稀疏)。

本文內容逐層深入,文章的最後會介紹快速版 SAM,感興趣的小夥伴可以去 GitHub 上下載,此外,本文還通過 Perfetto UI 對這些數據進行了可視化,以此來闡釋 PyTorch 每項特性的應用價值。

GitHub 地址:https://github.com/pytorch-labs/segment-anything-fast

對分割一切模型 SAM 的重寫

該研究表示,本文利用的 SAM 基線數據類型爲 float32 dtype、batch 大小爲 1,使用 PyTorch Profiler 查看內核跟蹤的結果如下:

本文發現 SAM 有兩個地方可以優化:

第一個是對 aten::index 的長調用,這是由張量索引操作(例如 [])產生的底層調用導致的。然而實際上 GPU 花費在 aten::index 上的時間相對較低,原因在於 aten::index 在啓動兩個內核的過程中,兩者之間發生了阻塞 cudaStreamSynchronize。這意味着 CPU 會等待 GPU 完成處理,直到啓動第二個內核。因而爲了優化 SAM,本文認爲應該致力於消除導致空閒時間的阻塞 GPU 同步。

第二個是 SAM 在矩陣乘法中花費了大量的 GPU 時間(上圖中的深綠色),這在 Transformers 中很常見。如果能夠減少 SAM 模型在矩陣乘法上花費的 GPU 時間,我們就可以顯着加快 SAM 的速度。

接下來本文用 SAM 的吞吐量 (img/s) 和內存開銷 (GiB) 來建立基線。之後就是優化過程了。

Bfloat16 半精度(加上 GPU 同步和批處理)

爲了解決上述問題,即讓矩陣乘法花費的時間更少,本文轉向 bfloat16。Bfloat16 是常用的半精度類型,通過降低每個參數和激活的精度,能夠節省大量的計算時間和內存。

用 bfloat16 替換 padding 類型

此外,爲了移除 GPU 同步,本文發現有兩個位置可以優化。

具體來說(參考上圖更容易理解,出現的變量名都在代碼中),該研究發現在 SAM 的圖像編碼器中,有充當座標縮放器(coordinate scalers)的變量 q_coords 和 k_coords,這些變量都是在 CPU 上分配和處理的。然而,一旦這些變量被用來在 rel_pos_resized 中建立索引,這些索引操作就會自動的將這些變量移動到 GPU 上,這種複製會導致 GPU 同步。爲了解決上述問題,該研究注意到可以使用 torch.where 重寫這部分內容來解決問題,如上所示。

內核跟蹤

在應用了這些更改之後,本文注意到單個內核調用之間有着顯著的時間間隔,尤其在小批量(這裡爲 1)時更爲突出。爲了更深入的瞭解這一現象,本文開始對批大小爲 8 的 SAM 推理進行性能分析:

在查看每個內核所花費的時間時,本文觀察到 SAM 的大部分 GPU 時間都花費在逐元素內核(elementwise kernels)和 softmax 操作上。

現在可以看到矩陣乘法的相對開銷小了很多。

將 GPU 同步和 bfloat16 優化結合在一起,SAM 性能提高了 3 倍。

Torch.compile(+graph breaks 和 CUDA graphs)

本文發現在深入研究 SAM 的過程中有很多小的操作,他們認爲使用編譯器來融合操作有很大的好處,因而 PyTorch 對 torch.compile 做了以下優化:

通過這些優化,該研究減少了 GPU 全局內存往返次數(roundtrips),從而加快了推理速度。我們現在可以在 SAM 的圖像編碼器上嘗試 torch.compile。爲了最大限度地提高性能,本文使用了一些高級編譯技術:

內核跟蹤

結果顯示,torch.compile 工作得很好。

可以觀察到 softmax 佔了很大一部分時間,然後是各種 GEMM 變體。以下測量的是批大小爲 8 及以上的變化。

SDPA: scaled_dot_product_attention

接下來,本文又對 SDPA(scaled_dot_product_attention)進行了實驗,研究的重點是注意力機制。一般來講,原生注意力機制在時間和內存上隨序列長度呈二次方擴展。PyTorch 的 SDPA 操作基於 Flash Attention、FlashAttentionV2 和 xFormer 的內存高效注意力原理構建,可以顯着加快 GPU 注意力。與 torch.compile 相結合,這個操作允許在 MultiheadAttention 的變體中表達和融合一個共同的模式。經過一小部分更改後,現在模型可以使用 scaled_dot_product_attention。

內核跟蹤

現在可以看到內存高效的注意力內核佔用了 GPU 上大量的計算時間:

使用 PyTorch 的原生 scaled_dot_product_attention,可以顯著增加批處理大小。下圖爲批大小爲 32 及以上的變化。

之後,該研究又實驗了 Triton,NestedTensor 、批處理 Predict_torch, int8 量化,半結構化 (2:4) 稀疏性等操作。

例如本文使用自定義 positional Triton 內核,觀察到批大小爲 32 的測量結果。

使用 Nested Tensor,批大小爲 32 及以上的變化。

添加量化後,批大小爲 32 及以上變化的測量結果。

文章的最後是半結構化稀疏性。該研究表示,矩陣乘法仍然是需要面對的一個瓶頸。解決的辦法是使用稀疏化來近似矩陣乘法。通過稀疏矩陣(即將值歸零)可以使用更少的位來存儲權重和激活張量。該研究將張量中哪些權重設置爲零的過程稱爲剪枝。剪枝掉較小的權重可以潛在地減小模型大小,而不會顯着損失準確率。

剪枝的方法多種多樣,從完全非結構化到高度結構化。雖然非結構化剪枝理論上對精度的影響最小,但 GPU 在進行大型密集矩陣乘法方面儘管非常高效,然而在稀疏情況下可能還會遭受顯着的性能下降。PyTorch 最近支持的一種剪枝方法旨在尋求平衡,稱爲半結構化(或 2:4)稀疏性。這種稀疏存儲將原始張量減少了 50%,同時產生密集張量輸出。參見下圖的說明。

爲了使用這種稀疏存儲格式和相關的快速內核,接下來要做的是剪枝權重。本文在 2:4 的稀疏度下選擇最小的兩個權重進行剪枝,將權重從默認的 PyTorch(“strided”)佈局更改爲這種新的半結構化稀疏佈局很容易。要實現 apply_sparse (model),只需要 32 行 Python 代碼:

在 2:4 的稀疏度下,本文觀察到 vit_b 和批大小爲 32 時的 SAM 峰值性能:

最後,一句話總結這篇文章:本文介紹了迄今爲止在 PyTorch 上最快的 Segment Anything 實現方式,藉助官方發佈的一系列新功能,本文在純 PyTorch 中重寫了原始 SAM,並且沒有損失準確率。

感興趣的讀者可以查看原博客瞭解更多內容。

參考鏈接:https://pytorch.org/blog/accelerating-generative-ai/