以下內容為結合李沐老師的課程和教材補充的學習筆記,以及對課后練習的一些思考,自留回顧,也供同學之人交流參考。
本節課程地址:無
本節教材地址:12.1. 編譯器和解釋器 — 動手學深度學習 2.0.0 documentation
本節開源代碼:...>d2l-zh>pytorch>chapter_optimization>hybridize.ipynb
編譯器和解釋器
目前為止,本書主要關注的是命令式編程(imperative programming)。 命令式編程使用諸如print
、“+
”和if
之類的語句來更改程序的狀態。 考慮下面這段簡單的命令式程序:
def add(a, b):return a + bdef fancy_func(a, b, c, d):e = add(a, b)f = add(c, d)g = add(e, f)return gprint(fancy_func(1, 2, 3, 4))
輸出結果:
10
Python是一種解釋型語言(interpreted language)。因此,當對上面的fancy_func
函數求值時,它按順序執行函數體的操作。也就是說,它將通過對e = add(a, b)
求值,并將結果存儲為變量e
,從而更改程序的狀態。接下來的兩個語句f = add(c, d)
和g = add(e, f)
也將執行類似地操作,即執行加法計算并將結果存儲為變量。 圖12.1.1 說明了數據流。
盡管命令式編程很方便,但可能效率不高。一方面原因,Python會單獨執行這三個函數的調用,而沒有考慮add
函數在fancy_func
中被重復調用。如果在一個GPU(甚至多個GPU)上執行這些命令,那么Python解釋器產生的開銷可能會非常大。此外,它需要保存e
和f
的變量值,直到fancy_func
中的所有語句都執行完畢。這是因為程序不知道在執行語句e = add(a, b)
和f = add(c, d)
之后,其他部分是否會使用變量e
和f
。
符號式編程
考慮另一種選擇符號式編程(symbolic programming),即代碼通常只在完全定義了過程之后才執行計算。這個策略被多個深度學習框架使用,包括Theano和TensorFlow(后者已經獲得了命令式編程的擴展)。一般包括以下步驟:
- 定義計算流程;
- 將流程編譯成可執行的程序;
- 給定輸入,調用編譯好的程序執行。
這將允許進行大量的優化。首先,在大多數情況下,我們可以跳過Python解釋器。從而消除因為多個更快的GPU與單個CPU上的單個Python線程搭配使用時產生的性能瓶頸。其次,編譯器可以將上述代碼優化和重寫為print((1 + 2) + (3 + 4))
甚至print(10)
。因為編譯器在將其轉換為機器指令之前可以看到完整的代碼,所以這種優化是可以實現的。例如,只要某個變量不再需要,編譯器就可以釋放內存(或者從不分配內存),或者將代碼轉換為一個完全等價的片段。下面,我們將通過模擬命令式編程來進一步了解符號式編程的概念。
def add_():return '''
def add(a, b):return a + b
'''def fancy_func_():return '''
def fancy_func(a, b, c, d):e = add(a, b)f = add(c, d)g = add(e, f)return g
'''def evoke_():return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))'prog = evoke_()
print(prog)
# compile函數將字符串prog編譯為代碼對象
# 第一個參數是代碼字符串,第二個參數是文件名(空字符串意味著代碼不是從文件中讀取的),
# 第三個參數是模式,'exec'表示代碼是一個可執行的程序
y = compile(prog, '', 'exec')
# 用exec函數執行編譯后的代碼對象
exec(y)
輸出結果:
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
print(fancy_func(1, 2, 3, 4))
10
命令式(解釋型)編程和符號式編程的區別如下:
- 命令式編程更容易使用。在Python中,命令式編程的大部分代碼都是簡單易懂的。命令式編程也更容易調試,這是因為無論是獲取和打印所有的中間變量值,或者使用Python的內置調試工具都更加簡單;
- 符號式編程運行效率更高,更易于移植。符號式編程更容易在編譯期間優化代碼,同時還能夠將程序移植到與Python無關的格式中,從而允許程序在非Python環境中運行,避免了任何潛在的與Python解釋器相關的性能問題。
混合式編程
歷史上,大部分深度學習框架都在命令式編程與符號式編程之間進行選擇。例如,Theano、TensorFlow(靈感來自前者)、Keras和CNTK采用了符號式編程。相反地,Chainer和PyTorch采取了命令式編程。在后來的版本更新中,TensorFlow2.0和Keras增加了命令式編程。
如上所述,PyTorch是基于命令式編程并且使用動態計算圖。為了能夠利用符號式編程的可移植性和效率,開發人員思考能否將這兩種編程模型的優點結合起來,于是就產生了torchscript。torchscript允許用戶使用純命令式編程進行開發和調試,同時能夠將大多數程序轉換為符號式程序,以便在需要產品級計算性能和部署時使用。
Sequential
的混合式編程
要了解混合式編程的工作原理,最簡單的方法是考慮具有多層的深層網絡。按照慣例,Python解釋器需要執行所有層的代碼來生成一條指令,然后將該指令轉發到CPU或GPU。對于單個的(快速的)計算設備,這不會導致任何重大問題。另一方面,如果我們使用先進的8-GPU服務器,比如AWS P3dn.24xlarge實例,Python將很難讓所有的GPU都保持忙碌。在這里,瓶頸是單線程的Python解釋器。讓我們看看如何通過將Sequential
替換為HybridSequential
來解決代碼中這個瓶頸。首先,我們定義一個簡單的多層感知機。
import torch
from torch import nn
from d2l import torch as d2l# 生產網絡的工廠模式
def get_net():net = nn.Sequential(nn.Linear(512, 256),nn.ReLU(),nn.Linear(256, 128),nn.ReLU(),nn.Linear(128, 2))return netx = torch.randn(size=(1, 512))
net = get_net()
net(x)
輸出結果:
tensor([[ 0.0913, -0.0081]], grad_fn=<AddmmBackward0>)
通過使用torch.jit.script
函數來轉換模型,我們就有能力編譯和優化多層感知機中的計算,而模型的計算結果保持不變。
net = torch.jit.script(net)
net(x)
輸出結果:
tensor([[ 0.0913, -0.0081]], grad_fn=<AddmmBackward0>)
我們編寫與之前相同的代碼,再使用torch.jit.script
簡單地轉換模型,當完成這些任務后,網絡就將得到優化(我們將在下面對性能進行基準測試)。
通過混合式編程加速
為了證明通過編譯獲得了性能改進,我們比較了混合編程前后執行net(x)
所需的時間。讓我們先定義一個度量時間的類,它在本章中在衡量(和改進)模型性能時將非常有用。
#@save
class Benchmark:"""用于測量運行時間"""def __init__(self, description='Done'):self.description = descriptiondef __enter__(self):self.timer = d2l.Timer()return selfdef __exit__(self, *args):print(f'{self.description}: {self.timer.stop():.4f} sec')
現在我們可以調用網絡兩次,一次使用torchscript,一次不使用torchscript。
net = get_net()
with Benchmark('無torchscript'):for i in range(1000): net(x)net = torch.jit.script(net)
with Benchmark('有torchscript'):for i in range(1000): net(x)
輸出結果:
無torchscript: 1.6907 sec
有torchscript: 1.6595 sec
如以上結果所示,在nn.Sequential
的實例被函數torch.jit.script
腳本化后,通過使用符號式編程提高了計算性能。
序列化
編譯模型的好處之一是我們可以將模型及其參數序列化(保存)到磁盤。這允許這些訓練好的模型部署到其他設備上,并且還能方便地使用其他前端編程語言。同時,通常編譯模型的代碼執行速度也比命令式編程更快。讓我們看看save
的實際功能。
net.save('my_mlp')
!ls -lh my_mlp*
輸出結果:
-rw-rw-r--. 1 huida SharedUsers 652K May 6 14:12 my_mlp
小結
- 命令式編程使得新模型的設計變得容易,因為可以依據控制流編寫代碼,并擁有相對成熟的Python軟件生態。
- 符號式編程要求我們先定義并且編譯程序,然后再執行程序,其好處是提高了計算性能。
練習
- 回顧前幾章中感興趣的模型,能提高它們的計算性能嗎?
解:
以LeNet模型為例,使用torch.jit.script函數可以提高其網絡計算性能。
代碼如下:
# LeNet
net = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Flatten(),nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),nn.Linear(120, 84), nn.Sigmoid(),nn.Linear(84, 10))X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)with Benchmark('無torchscript'):for i in range(1000): net(X)net = torch.jit.script(net)
with Benchmark('有torchscript'):for i in range(1000): net(X)
輸出結果:
無torchscript: 3.2212 sec
有torchscript: 2.8837 sec