0. 前言
波蘭小哥Adam Paszke從15年的Torch開始,到現在發表了關於PyTorch的Neurips2019論文(令我驚訝的是隻中了Poster?而不是Spotlight?)。中間經歷了漫長的過程。這裏,把原文進行翻譯放出來,以供讀者瞭解這幾個問題:
- 爲什麼要設計PyTorch?
- PyTorch與之前的深度學習framework的區別是什麼?
- PyTorch有什麼設計準則?
- 是什麼導致了PyTorch在研究者社區的流行?
相信讀者在看完下面的譯文,就能瞭解這4點乃至更多的疑問了。
1. Abstract (摘要)
深度學習框架通常在易用性和速度只能2選其1, 很難兼顧. PyTorch是一個兼具易用性和速度的機器學習庫: 提供了交互式和pythonic的編程風格,容易debug,並與一些流行的科學計算庫保持兼容和風格一致. 同時, 還能保持efficient和支持硬件加速.
本文中,我們詳細的介紹了實現PyTorch的原則以及這些原則如何反應在架構設計中. 在PyTorch中, 每個aspect都是一個完全由用戶控制的常規Python程序. 我們還解釋了runtime的核心組件的設計以及其是如何達成compelling performance的.
我們在一個常見benchmark上驗證了PyTorch的獨立子系統的efficiency和整體的速度.
2. Introduction(介紹)
隨着deep learning在近些年的流行,有很多的機器學習工具出現: 如Caffe, CNTK, Tensorflow, Theano, 這些框架都是創建一個靜態數據流圖,使得操作可以重複的作用於批數據(batch data). 這種靜態圖的方式,理論上可以提升性能(performance)和可擴展性(scalability).
然而,這種靜態圖的框架難以debug,使用成本高(對用戶有更高的要求),計算類型的靈活性受到限制.
Chainer, Torch和DyNet等框架意識到了在深度學習框架中, 動態eager執行的價值, 即實現了define-by-run的方法(定義即執行), 但是問題是性能不佳,或者使用的語言不夠expressive, 折現值了他們的應用.
然而, 隨着小心的工程實現和設計選擇, dynamic eager execution的實現可以沒有太多的性能損失.
PyTorch, 1個由自動微分賦能, 並支持GPU加速的動態執行Tensor計算Python庫. 與現有的最快的DL框架的速度相當的情況下, 在research community中, 得到了廣泛的使用. ICLR2019就有296篇提交文章提到了PyTorch.
3. Background(背景)
在科學計算領域, 有4個主流趨勢在深度學習中變得越來越重要.
① 基於數組的編程array-based programming
從1960s發展起來的DSL(domain specific languages), 如APL, MATLAB, R, Julia等, 將多維數組轉換爲由完整數學原語(primitive)(或算子operator)支持的first-class object來操作.
類似Numpy, Torch, Eigen, Lush等的出現, 使得**基於數組的編程(array-based programming)**在Python、Lisp、c++和Lua等通用語言中變得富有成效(productive)。
② 自動微分(automatic differentiation)
自動微分(automatic differentiation) 的發展使得計算derivatives的完全自動化(fully automate)成爲可能. 在允許使用高效的基於梯度的優化方案下, 顯著降低了實驗不同機器學習方法的難度.
autograd這個包推廣了這種技術在NumPy數組中的使用,像Chainer, DyNet, Lush, Jax等框架中也使用了類似的方法.
③ 開源生態
隨着免費軟件的飛速發展, 科學社區慢慢開始從閉源(closed propritary, 以MATLAB爲代表)走向開源Python生態(open-source Python ecosystem), 像Numpy, Scipy和Pandas這些包就得到了廣泛的使用,因爲他們能滿足大多數數值分析的需要, 包括並不限於: 數據集處理, 統計分析,繪圖等.
此外, 開放, 互操作性(interoperability)和靈活性,使得開源軟件的社區非常活躍, 人們在上面可以很快的交流並解決新出現的問題,這也是閉源軟件無法比擬的優勢(人多力量大).
像Python這樣的大型生態系統的網絡效應使其成爲研究者啓動研究的基本技能. 因此, 從2014年起,大多數的DL框架都涵蓋了Python接口.
④ hardware accelerators(硬件加速器)
hardware accelerators(硬件加速器), 隨着硬件的發展和商用, 如GPU, 提供了deep learning所需的算力. 如cuDNN等特定庫, 提供了一系列高性能, 可重用的深度學習kernel. Caffe, Torch7和Tensorflow都使用了這些硬件加速器的特性來加速計算.
PyTorch基於這些趨勢, 提供了array-based的編程模型(GPU+auto differentiation賦能), 集成在Python生態環境中.
4. Design principles(設計原則)
PyTorch的成功是源於將前人的想法編織到1個平衡速度和易用性的設計中. 我們的選擇背後有四個主要原則:
- ① Be Pythonic
數據科學家對Python語言比較熟悉, 包括其編程模型和工具. 而PyTorch應該是這個生態系統中的一流成員。它遵循通常建立的保持接口簡單和一致的設計目標,最理想的是使用一種符合python語言習慣的方式來做事情.
PyTorch與標準的畫圖,調試以及數據處理模塊自然地集成到一起.
- ② Put researchers first
PyTorch試圖讓模型編寫,數據加載和優化變得儘可能的簡單和高效. 機器學習內部的複雜性應該在PyTorch內部庫中進行處理, 對用戶隱藏.
同時, 對外提供的API要沒有副作用( free of side-effects)和意想不到的性能驟降(unexpected
performance cliffs).
- ③ Provide pragmatic performance
爲了更有用, PyTorch需要呈現令人信服的性能,與此同時不能增加使用的複雜度和門檻. 用10%的性能去換1個簡單的多的使用模型的方式是可接受的, 但是用100%的性能去換是不能接收的. 因此, PyTorch的確實爲了保證性能而增加了使用的複雜度. 額外的, 我們爲研究人員提供了工具, 使得他們可以手動控制其代碼的執行過程, 這對他們找到自己代碼的性能瓶頸提供了很大的幫助.
- ④ Worse is better
在給定的工程資源的條件下,其它條件相同時, 由於PyTorch內部實現的簡潔性而節省下來的時間,對研究者來說,可以被用來實現其它feature, 並跟隨AI領域的最新進展.
因此, it is better to have a simple but slightly incomplete solution than a comprehensive but complex and hard to maintain design.
5. Usability centric design(易用性爲中心的設計)
- 5.1 Deep learning models are just Python programs
在相當短的時間內, 機器學習就從單獨的數字識別發展到了自動打星際爭霸戰勝職業玩家的程度.
這其中, 神經網絡的發展非常非常迅速, 從簡單的feed forward layers變成由很多loop和recursive組成的複雜結構.
爲了支持這種變態的複雜度(後面還可能更復雜), PyTorch放棄了基於圖元編程(graph-metaprogramming)的方法的潛在好處,從而保留了Python的命令式編程模型。
這種方式是由Chainer和Dynet率先使用的,PyTorch將這種方式延伸到深度學習workflow的所有方面: 定義層結構, 組成模型, 加載數據, 跑優化器, 並行訓練等.
這種方式使得任何新的神經網絡架構都可以很容易的通過PyTorch實現.
List1展示了一個完整的模型是如何通過2d卷積,矩陣乘法,dropout以及softmax來創建的,
此外, 作者還以GAN的訓練爲例, 說明"everthing is just a program"的哲學.
在最基礎的GAN中需要設計2個獨立的模型(生成器和判別器),2個損失函數, 對rigid的APIs,這會使得設置變得非常複雜. 而在PyTorch中, 一切都變得簡單起來.
由於PyTorch程序是動態執行的, 那麼Python的所有特性在PyTorch中都可以使用: 比如 print語句, 標準debugger, 常見的可視化工具如matplotlib等.
用戶不需要等待編譯後再執行, 他們可以很容易的獲取模型的中間輸出來判斷模型是否正確工作.
- 5.2 Interoperability and extensibility 互操作性和可擴展性
簡單且高效的 互操作性(Interoperability) 是PyTorch的最高優先級目標之一, 因爲這可以使得PyTorch使用Python語言豐富的生態庫.
因此,PyTorch允許與外部庫進行雙向數據交換(bidrectional exchange of data).
比如說,允許通過torch.from_numpy()
和.numpy()
2個方法,將numpy array和PyTorch tensors進行互相轉換. 類似的功能也可用於交換使用DLPack[29]格式存儲的數據。
需要注意: 這些例子中,都不涉及數據拷貝(without any data copying)
即: 對象只描述如何解析內存區域(通過stride), 數據本身對應的內存不變化,或者說不頻繁的變化.
所以, 這些操作的代價非常小, 無論數組的大小, 所需的時間都是常數.
此外, PyTorch內部的許多關鍵部分都是爲了**可擴展性(extensible)**而特殊設計.
舉例, 自動微分系統使得用戶可以增加自己的自定義的可微分函數(custom differentiable function).
通過繼承torch.autograd.Function
類, 實現forward
和backward
成員函數即可.
這些內容的實際工作,完全取決於定義這些方法的人本身, 比如, 很多人, 使用python內置的包來進行data loading的操作.
DataLoader
類使用符合這個接口的對象,並在數據上提供一個迭代器(iterator),負責對 固定的CUDA內存(pinned CUDA memory) 進行變換、批處理、並行化和管理,以提高吞吐量(improve throughput)。
此外, 用戶可以隨意的替換PyTorch中任何不滿足其需要或性能要求的組件. 他們都被設計爲完全可替換的(completely interchangeable),
而且, PyTorch非常小心,不強加任何特定的解決方案。
- 5.3 Automatic differentiation 自動微分
由於基於梯度的優化對深度學習非常重要,PyTorch必須可以自動的計算被用戶定義的模型的梯度, 而這些模型可能是任意的Python程序.
然而, Python是一門動態語言, 即Python允許在runtime(運行時)修改信息和行爲. 這使得ahead of time的source2source的微分變得非常麻煩.
PyTorch用 運算符重載(operator overloading approach) 的方法,其在每次執行computed function時, 都構建對該function的representation.
在PyTorch現在的實現中, 實現了reverse-mode的automatic differentiation. 其計算一個標量輸出關於多變量輸出(multivariate)的梯度.
同樣地, PyTorch也可以很容易的擴展到forward-mode的automatic differentiation(通過使用array-level dual number): 數組級對偶數.
另一個在PyTorch中有趣且不常見的特徵是: 可以對發生變化的Tensor進行微分(inplace操作).
爲了保證安全, PyTorch爲Tensor實現了1個版本控制系統(versioning system), 這個系統可以使得我們追蹤Tensor的修改歷程並保證我們總是在使用我們所期待的數據.
1個有意思的tradeoff是當我們可以使用像copy-on-write這種技術,我們並沒有選擇這條路(not go down this path),
就性能而言(performance-wise), 用戶重寫代碼以確保不執行任何副本通常是有益的. 自此, 當大多數的mutation是良性且可以自動處理時, 真正複雜的情況會導致一個用戶錯誤,這讓他們知道他們可能想要重構程序.
這使得我們有效的避免引入subtle且難以尋找的performance cliffs.
6. Performance focused implementation(聚焦性能的實現)
基於Python解釋器來跑深度學習算法是臭名昭著的有挑戰性的事情: 比如, GIL的存在,使得在給定的時間(at any given time)內,任意數量的併發線程中只有一個正在運行。
基於靜態圖的解決方案通過將計算求解 deferring to 自定義的解釋器來回避這個問題.(sidestep this problem)
PyTorch用不同的方式來解決這個問題, 通過對執行過程的每個aspect進行小心的優化, 同時讓用戶可以容易的使用額外的優化策略.
- 6.1 高效的C++內核
儘管與Python生態集成的非常好, 但是, PyTorch的大部分代碼都是用C++來完成的(爲了達到更好的性能).
libtorch
: 實現了tensor的數據結構, GPU or CPU的運算符(operator), 以及基本的並行原語(parallel primitives). 此外, 它還提出了自動微分系統, 包括大多數內置函數的梯度公式(gradient formulas for most built-in functions).
這確保了由核心PyTorch操作符組成的函數的導數的計算完全在多線程求值程序中執行( executed entirely in a multithreaded
evaluator ), 這不需要持有Python的GIL鎖.
此外, 我們通過使用YAML源meta-data文件來binding C++和python (PYBIND11_MODULE, https://blog.csdn.net/g11d111/article/details/104407384
).
這種方法的一個有趣的副作用是,它允許我們的社區快速創建到多種其他語言的綁定,從而產生諸如NimTorch、hasktorch等項目。
隨着PyTorch 1.0的發佈, 我們可以使用通過Python來定義的模型, 用TorchScript引擎來運行這個模型(完全拋開Python).
- 6.2 Separate control and data flow 獨立的控制和數據流
PyTorch在其控制(即程序分支、循環)和數據流(即張量和對其執行的操作)之間保持嚴格的分離。其中, control flow由Python解析, 並在CPU上執行優化後的C++代碼, 並在設備上產生一個操作符調用的線性序列(linear sequence of operator invocations). 這些運算符既可以在CPU上執行,也可以在GPU上執行.
PyTorch通過利用CUDA stream機制來異步執行運算符: 將CUDA內核調用(kernel invocations)排進GPU硬件的FIFO隊列中. 這使得系統可以同時在CPU上執行Python代碼和在GPU上執行Tensor運算.
由於Tensor運算通常會消耗大量的時間, 這使得我們可以在一個有相當大overhead(開銷)的解釋型語言(Python)中達到/挖掘出GPU的巔峯性能. 需要注意的是, 這個機制對用戶幾乎是不可見的. 除非他們需要實現自己的multi-stream原語, 否則所有的CPU-GPU同步都由PyTorch庫來處理。
PyTorch可以利用類似的機制在CPU上進行異步執行Tensor運算. 然而, 跨線程通信和同步會抵消(negate)這種性能增益.
- 6.3 Custom caching tensor allocator 自定義的緩存分配器
幾乎所有的運算符都必須在執行過程中動態分配output tensor以保存結果. 因此, 對dynamic memory allocator(動態內存分配)的調優就變得極度重要. PyTorch可以靠[1-3]
這種優化庫解決在CPU上的這種動態內存分配的問題. 但是, GPU上的如cudaFree的慣例模式可能會阻塞直到所有的之前的GPU隊列裏的工作完成.
爲了避免性能瓶頸, PyTorch實現了custom allocator: 增量的構建了CUDA memory的緩存(cache), 並將cache重分配給後面的allocation, 而繞開了CUDA原生的API. 這種增量的分配對互操作性(interoperability)同樣非常重要, 因爲提前佔用所有GPU內存會阻止用戶使用其他支持GPU的Python包。
爲了進一步的提升其有效性, 我們的custom allocator針對深度學習使用的特定內存模式進行了調優(tuned). 比如:
-
將每個分配(allocation)都rounds up爲512字節的倍數, 以避免碎片問題(avoid fragmentation issues) .
-
對每一個CUDA stream都維護一個獨立的內存池.
這種設計叫做one-pool-per-stream. one-pool-per-stream設計似乎是有限制的,因爲分配結果是每個流碎片化,但實際上PyTorch幾乎從不使用多個流。衆所周知,以一種讓CUDA內核協同共享GPU的方式來編寫CUDA內核是非常困難的,因爲精確的調度是由硬件控制的。在實踐中,CUDA kernel編寫人員通常求助於結合多個任務的單片內核.
Data loading
和distributed computing utilities
是單一流設計的例外,它們小心地插入額外的同步以避免與分配器(allocator)的不良交互.
隨着one-pool-per-stream這種設計容易受到某些特殊情況的影響, 但是在實際代碼中,它幾乎從不顯示不需要的行爲。我們的大多數用戶並不知道它的存在。
- 6.4 Multiprocessing 多進程
由於GIL鎖的存在, Python默認的實現是不能多線程並行執行的. 爲了緩解這個問題, Python社區提出了標準的multiprocessing模塊, 這個模塊包含了一系列的utilities來使得用戶容易的從父進程派發出子進程並進行進程間通信.
然而, 這種實現使用與 磁盤上持久性(on-disk persistence) 相同的序列化形式, 對處理大數組來說效率很低. 因此, 我們將multiprocessing擴展到torch中,
這個擴展是將tensor數據通過shared memory傳給其它進程,而非通過communication channel進行傳遞.
這個設計極大的提高了性能並使得進程隔離性變弱, 即使得多進程的編程模型更像常規的多線程模型一樣.
用戶可以很容易地在獨立的gpu上實現大量的並行程序,但是稍後會使用all-reduce的原語來同步梯度。
這個系統的另一個獨特的特性是它透明地處理CUDA張量的共享,使得像Hogwild這樣的技術易於實現
- 6.5 Reference counting 引用計數
用戶通常的設計是跑滿顯存, 提升batch size以加速訓練. 因此, 爲了達到更高的性能, PyTorch需要將內存視爲scarce resource來進行小心管理.
GC有好的 攤消性能(amortized performance), 因此可以用來管理tensor memory.
runtime 週期性的分析系統的狀態, 枚舉所有的used objects並釋放其它對象. 但是,由於延遲釋放, 會導致程序總體上使用更多的內存. 由於GPU內存的有限性, 這些overheads是不可接受的.
Torch7使用Lua的GC,而PyTorch使用引用計數機制來計算每個tensor的計數, 一旦一個tensor的計數爲零,就立即釋放底層內存.
需要說明的是, PyTorch既track libtorch庫的內部引用, 也track用戶Python code中的外部引用(通過Python自己的引用計數機制) . 這使得當tensor不再被用到的時候即被釋放掉, 很高效.
我們只能保證在Cpython
和Swift
這種用引用計數的語言上能夠達到desired performance. 而在pypy
和lua
上不行.
7. Evaluation 評估
本節, 我們將PyTorch與其它常用的深度學習庫進行比較. 可以達到PyTorch能夠達到competitve的性能.
所用設備是2個Intel Xeon E5-2698 v4 和1個Quadro GP100 GPU。
Note:這裏我只列出Benchmark和Adoption,關於顯存管理和異步數據流,請參看原文。
-
7.1 Asynchronous dataflow (略)
-
7.2 Memory management (略)
-
7.3 Benchmarks
這裏, 與3個流行的graph-based的深度學習框架(CNTK, MXNet and TensorFlow),1個define-by-run的框架(Chainer)以及生產優先平臺(PaddlePaddle)進行比較,
由上表可以看出,PyTorch的性能是最快的框架的17%以內。我們將這個結果歸因於這樣一個事實: 即這些工具將大部分計算工作轉移到同一個版本的cuDNN和cuBLAS庫中。
- 7.4 Adoption
爲了分析PyTorch
的易用性和影響,我們分析了自2017年1月份最初發布的PyTorch
在arXiv e-Prints上面的被提到的次數。
下圖可以看出,PyTorch
在全部的深度學習工具和框架中的提到比率逐步上漲,已經逼近50%大關。注意:一篇文章我們只記1次。
8. Conclusion and future work 結論
PyTorch由於兼顧了易用性和高效計算性的特點, 在深度學習研究社區裏非常流行. 在未來, 我們想要繼續提高PyTorch的速度和可擴展性(可伸縮性).
最明顯的是, 我們開始聚焦PyTorch JIT
: 使得PyTorch程序可以在Python解釋器外執行, 以獲得更好的效率和優化.
同樣, 我們還打算通過提供高效的數據並行原語和基於遠程過程調用(RPC
)的模型並行化python庫來改進對分佈式計算的支持。
參考資料
[1] Emery D. Berger, Kathryn S.
A scalable memory allocator for multithreaded applications.
[2] S. Ghemawat and P. Menage. Tcmalloc: Thread-caching malloc.
[3] J. Evans. A scalable concurrent malloc(3) implementation for freebsd.