『MXNet』第六彈_Gluon性能提升


一、符號式編程

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),但是效率大大提升。

HybridSequential類

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類。

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()

由於asnumpyasscalar(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的並行以及計算和通訊的並行實現

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM