一、符號式編程
1、命令式編程和符號式編程
命令式:
def add(a, b): return a + b def fancy_func(a, b, c, d): e = add(a, b) f = add(c, d) g = add(e, f) return g fancy_func(1, 2, 3, 4)
符號式:
def add_str(): return ''' def add(a, b): return a + b ''' def fancy_func_str(): return ''' def fancy_func(a, b, c, d): e = add(a, b) f = add(c, d) g = add(e, f) return g ''' def evoke_str(): return add_str() + fancy_func_str() + ''' print(fancy_func(1, 2, 3, 4)) ''' prog = evoke_str() print(prog) y = compile(prog, '', 'exec') exec(y)
以上定義的三個函數都只是返回計算流程。最后,我們編譯完整的計算流程並運行。
由於在編譯時系統能夠完整地看到整個程序,因此有更多空間優化計算。例如,編譯的時候可以將程序改寫成print((1 + 2) + (3 + 4))
,甚至直接改寫成print(10)
。這樣不僅減少了函數調用,還節省了內存。
2.MXNet的符號式編程
Sequential類 -> HybridSequential類
Block類 -> HybridBlock類
使用上面兩個基於Hybrid的類構建的網絡實例會具有.hybridize()方法,進行.hybridize()聲明之后網絡的第一次運行會生成編譯好的C++代碼,之后再運行網絡實例不會運行python代碼,而回轉向C++代碼,也就是"靜態圖",同樣的,MXNet的靜態結構決定了其對python的動態控制流程不支持(同TensorFlow),但是效率大大提升。
def get_net(): net = nn.HybridSequential() net.add( nn.Dense(256, activation="relu"), nn.Dense(128, activation="relu"), nn.Dense(2) ) net.initialize() return net x = nd.random.normal(shape=(1, 512)) net = get_net() net(x)
我們可以通過調用hybridize
函數來編譯和優化HybridSequential實例中串聯的層的計算。模型的計算結果不變。
In [5]:
net.hybridize() net(x) Out[5]:
[[ 0.08827581 0.00505182]]
<NDArray 1x2 @cpu(0)>
需要注意的是,只有繼承HybridBlock的層才會被優化。例如,HybridSequential類和Gluon提供的Dense類都是HybridBlock的子類,它們都會被優化計算。如果一個層只是繼承自Block而不是HybridBlock類,那么它將不會被優化。我們接下會討論如何使用HybridBlock類。
class HybridNet(nn.HybridBlock): def __init__(self, **kwargs): super(HybridNet, self).__init__(**kwargs) self.hidden = nn.Dense(10) self.output = nn.Dense(2) def hybrid_forward(self, F, x): print('F: ', F) print('x: ', x) x = F.relu(self.hidden(x)) print('hidden: ', x) return self.output(x)
在繼承HybridBlock類時,我們需要在hybrid_forward
函數中添加額外的輸入F
。我們知道,MXNet既有基於命令式編程的NDArray類,又有基於符號式編程的Symbol類。由於這兩個類的函數基本一致,MXNet會根據輸入來決定F
使用NDArray或Symbol。
In [12]:
net.hybridize() net(x)F: <module 'mxnet.symbol' from '/var/lib/jenkins/miniconda3/envs/gluon_zh_docs/lib/python3.6/site-packages/mxnet/symbol/__init__.py'> x: <Symbol data> hidden: <Symbol hybridnet0_relu0> Out[12]: [[ 0.00370749 0.00134991]] <NDArray 1x2 @cpu(0)>
可以看到,F
變成了Symbol。而且,雖然輸入數據還是NDArray,但hybrid_forward
函數里,相同輸入和中間輸出全部變成了Symbol。
再運行一次看看。
In [13]:
net(x) Out[13]:
[[ 0.00370749 0.00134991]]
<NDArray 1x2 @cpu(0)>
可以看到hybrid_forward
函數里定義的三行打印語句都沒有打印任何東西。這是因為上一次在調用hybridize
函數后運行net(x)
的時候,符號式程序已經得到。之后再運行net(x)
的時候MXNet將不再訪問Python代碼,而是直接在C++后端執行符號式程序。這也是調用hybridize
后模型計算性能會提升的一個原因。但它可能的問題是我們損失了寫程序的靈活性。在上面這個例子中,如果我們希望使用那三行打印語句調試代碼,執行符號式程序時會跳過它們無法打印。
此外,對於少數Symbol不支持的函數,例如asnumpy
,我們是無法在hybrid_forward
函數中使用並在調用hybridize
函數后進行模型計算的(mxnet.sym類即為Symbol類,支持大部分Ndarray操作)。
二、惰性計算
可以使用不同的前端語言編寫MXNet程序,像Python、R、Scala和C++。無論使用何種前端編程語言,MXNet程序的執行主要都發生在C++實現的后端。換句話說,用戶寫好的前端MXNet程序會傳給后端執行計算。后端有自己的線程來不斷收集任務,構造、優化並執行計算圖。后端優化的方式有很多種,其中包括本章將介紹的惰性計算。
假設我們在前端調用以下四條語句。MXNet后端的線程會分析它們的依賴關系並構建出如下圖所示的計算圖。
In [3]:
a = nd.ones((1, 2))
b = nd.ones((1, 2))
c = a * b + 2
c
Out[3]:
[[ 3. 3.]]
<NDArray 1x2 @cpu(0)>
在惰性計算中,前端執行前三條語句的時候,僅僅是把任務放進后端的隊列里就返回了。當最后一條語句需要打印計算結果時,前端會等待后端線程把c
的結果計算完。此設計的一個好處是,這里的Python前端線程不需要做實際計算。因此,無論Python的性能如何,它對整個程序性能的影響會很小。只要C++后端足夠高效,那么不管前端語言性能如何,MXNet都可以提供一致的高性能。
用同步函數實際計算出結果
print()
nd數組.wait_to_read()
nd.waitall()
由於asnumpy
、asscalar(Python內置、numpy等數據結構並不支持惰性計算)
和print
函數會觸發讓前端等待后端計算結果的行為,我們通常把這類函數稱作同步函數。
三、自動並行
在“惰性計算”里我們提到MXNet后端會自動構建計算圖。通過計算圖,系統可以知道所有計算的依賴關系,並可以選擇將沒有依賴關系的多個任務並行執行來獲得性能的提升。以“惰性計算”一節中的計算圖為例。其中a=nd.ones((1,2))
和b=nd.ones((1,2))
這兩步計算之間並沒有依賴關系。因此,系統可以選擇並行執行它們。
通常一個運算符會用掉一個CPU/GPU上所有計算資源。例如,dot
操作符會用到所有CPU(即使是有多個CPU)或單個GPU上所有線程。因此在單CPU/GPU上並行運行多個運算符可能效果並不明顯。
MXNet通過自動並行計算提升計算性能,主要經由CPU和GPU的並行以及計算和通訊的並行實現,