TorchScript 中的動態並行¶
建立日期:2020 年 7 月 28 日 | 最後更新:2024 年 12 月 02 日 | 最後驗證:2024 年 11 月 05 日
警告
TorchScript 不再積極開發中。
在本教程中,我們介紹在 TorchScript 中進行動態操作間並行的語法。這種並行具有以下特性:
動態 - 建立的並行任務數量及其工作負載取決於程式的控制流。
操作間 (inter-op) - 這種並行涉及並行執行 TorchScript 程式片段。這與操作內並行 (intra-op parallelism) 不同,後者涉及將單個運算子拆分並並行執行運算子的部分工作。
基本語法¶
動態並行的兩個重要 API 是:
torch.jit.fork(fn : Callable[..., T], *args, **kwargs) -> torch.jit.Future[T]torch.jit.wait(fut : torch.jit.Future[T]) -> T
展示它們如何工作的好方法是舉例說明:
import torch
def foo(x):
return torch.neg(x)
@torch.jit.script
def example(x):
# Call `foo` using parallelism:
# First, we "fork" off a task. This task will run `foo` with argument `x`
future = torch.jit.fork(foo, x)
# Call `foo` normally
x_normal = foo(x)
# Second, we "wait" on the task. Since the task may be running in
# parallel, we have to "wait" for its result to become available.
# Notice that by having lines of code between the "fork()" and "wait()"
# call for a given Future, we can overlap computations so that they
# run in parallel.
x_parallel = torch.jit.wait(future)
return x_normal, x_parallel
print(example(torch.ones(1))) # (-1., -1.)
fork() 接受可呼叫物件 fn 以及該可呼叫物件的引數 args 和 kwargs,併為 fn 的執行建立一個非同步任務。fn 可以是函式、方法或 Module 例項。fork() 返回對該執行結果值的引用,稱為 Future。因為 fork 在建立非同步任務後立即返回,所以在執行 fork() 呼叫之後的程式碼行時,fn 可能尚未執行。因此,使用 wait() 等待非同步任務完成並返回值。
這些構造可用於重疊函式內語句的執行(如工作示例部分所示),或與迴圈等其他語言構造結合使用
import torch
from typing import List
def foo(x):
return torch.neg(x)
@torch.jit.script
def example(x):
futures : List[torch.jit.Future[torch.Tensor]] = []
for _ in range(100):
futures.append(torch.jit.fork(foo, x))
results = []
for future in futures:
results.append(torch.jit.wait(future))
return torch.sum(torch.stack(results))
print(example(torch.ones([])))
注意
當我們初始化一個空的 Future 列表時,需要為 futures 新增顯式的型別註解。在 TorchScript 中,空容器預設假定它們包含 Tensor 值,因此我們將列表建構函式 # 註解為 List[torch.jit.Future[torch.Tensor]] 型別
此示例使用 fork() 啟動函式 foo 的 100 個例項,等待這 100 個任務完成,然後對結果求和,返回 -100.0。
應用示例:雙向 LSTM 整合¶
讓我們嘗試將並行應用於一個更實際的示例,看看能從中獲得什麼樣的效能。首先,我們定義基線模型:一個由雙向 LSTM 層組成的整合模型。
import torch, time
# In RNN parlance, the dimensions we care about are:
# # of time-steps (T)
# Batch size (B)
# Hidden size/number of "channels" (C)
T, B, C = 50, 50, 1024
# A module that defines a single "bidirectional LSTM". This is simply two
# LSTMs applied to the same sequence, but one in reverse
class BidirectionalRecurrentLSTM(torch.nn.Module):
def __init__(self):
super().__init__()
self.cell_f = torch.nn.LSTM(input_size=C, hidden_size=C)
self.cell_b = torch.nn.LSTM(input_size=C, hidden_size=C)
def forward(self, x : torch.Tensor) -> torch.Tensor:
# Forward layer
output_f, _ = self.cell_f(x)
# Backward layer. Flip input in the time dimension (dim 0), apply the
# layer, then flip the outputs in the time dimension
x_rev = torch.flip(x, dims=[0])
output_b, _ = self.cell_b(torch.flip(x, dims=[0]))
output_b_rev = torch.flip(output_b, dims=[0])
return torch.cat((output_f, output_b_rev), dim=2)
# An "ensemble" of `BidirectionalRecurrentLSTM` modules. The modules in the
# ensemble are run one-by-one on the same input then their results are
# stacked and summed together, returning the combined result.
class LSTMEnsemble(torch.nn.Module):
def __init__(self, n_models):
super().__init__()
self.n_models = n_models
self.models = torch.nn.ModuleList([
BidirectionalRecurrentLSTM() for _ in range(self.n_models)])
def forward(self, x : torch.Tensor) -> torch.Tensor:
results = []
for model in self.models:
results.append(model(x))
return torch.stack(results).sum(dim=0)
# For a head-to-head comparison to what we're going to do with fork/wait, let's
# instantiate the model and compile it with TorchScript
ens = torch.jit.script(LSTMEnsemble(n_models=4))
# Normally you would pull this input out of an embedding table, but for the
# purpose of this demo let's just use random data.
x = torch.rand(T, B, C)
# Let's run the model once to warm up things like the memory allocator
ens(x)
x = torch.rand(T, B, C)
# Let's see how fast it runs!
s = time.time()
ens(x)
print('Inference took', time.time() - s, ' seconds')
在我的機器上,這個網路執行需要 2.05 秒。我們可以做得更好!
並行化前向和反向層¶
我們可以做一件非常簡單的事情,就是在 BidirectionalRecurrentLSTM 中並行化前向和反向層。對於這種情況,計算結構是靜態的,因此我們甚至不需要任何迴圈。我們像這樣重寫 BidirectionalRecurrentLSTM 的 forward 方法:
def forward(self, x : torch.Tensor) -> torch.Tensor:
# Forward layer - fork() so this can run in parallel to the backward
# layer
future_f = torch.jit.fork(self.cell_f, x)
# Backward layer. Flip input in the time dimension (dim 0), apply the
# layer, then flip the outputs in the time dimension
x_rev = torch.flip(x, dims=[0])
output_b, _ = self.cell_b(torch.flip(x, dims=[0]))
output_b_rev = torch.flip(output_b, dims=[0])
# Retrieve the output from the forward layer. Note this needs to happen
# *after* the stuff we want to parallelize with
output_f, _ = torch.jit.wait(future_f)
return torch.cat((output_f, output_b_rev), dim=2)
在此示例中,forward() 將 cell_f 的執行委託給另一個執行緒,同時繼續執行 cell_b。這使得兩個 cell 的執行相互重疊。
再次執行帶有此簡單修改的指令碼,執行時長為 1.71 秒,提升了 17%!
附註:視覺化並行¶
我們還沒有完成模型最佳化,但值得介紹一下我們用於視覺化效能的工具。一個重要的工具是 PyTorch 效能分析器。
讓我們使用效能分析器以及 Chrome 跟蹤匯出功能來視覺化並行化模型的效能
with torch.autograd.profiler.profile() as prof:
ens(x)
prof.export_chrome_trace('parallel.json')
這段程式碼片段將寫入一個名為 parallel.json 的檔案。如果您在 Google Chrome 中導航到 chrome://tracing,點選 Load 按鈕,然後載入該 JSON 檔案,您應該會看到類似以下的 timeline:
timeline 的水平軸表示時間,垂直軸表示執行執行緒。正如我們所見,我們一次執行兩個 lstm 例項。這是我們努力並行化雙向層的成果!
並行化整合模型¶
您可能已經注意到,我們的程式碼中還有進一步的並行化機會:我們還可以並行執行 LSTMEnsemble 中包含的模型。實現這一點的方法非常簡單,我們應該這樣修改 LSTMEnsemble 的 forward 方法:
def forward(self, x : torch.Tensor) -> torch.Tensor:
# Launch tasks for each model
futures : List[torch.jit.Future[torch.Tensor]] = []
for model in self.models:
futures.append(torch.jit.fork(model, x))
# Collect the results from the launched tasks
results : List[torch.Tensor] = []
for future in futures:
results.append(torch.jit.wait(future))
return torch.stack(results).sum(dim=0)
或者,如果您喜歡簡潔,可以使用列表推導式:
def forward(self, x : torch.Tensor) -> torch.Tensor:
futures = [torch.jit.fork(model, x) for model in self.models]
results = [torch.jit.wait(fut) for fut in futures]
return torch.stack(results).sum(dim=0)
正如引言中所述,我們使用迴圈為整合中的每個模型分叉任務。然後,我們使用另一個迴圈等待所有任務完成。這進一步增加了計算的重疊。
透過這個小更新,指令碼在 1.4 秒內執行完成,總共提速 32%!對於兩行程式碼來說相當不錯了。
我們還可以再次使用 Chrome 跟蹤器來檢視發生了什麼:
現在我們可以看到所有 LSTM 例項都完全並行執行。
結論¶
在本教程中,我們學習了 fork() 和 wait(),這是在 TorchScript 中進行動態操作間並行的基本 API。我們看到了一些使用這些函式來並行化 TorchScript 程式碼中函式、方法或 Modules 執行的典型用法模式。最後,我們透過一個示例來使用這項技術最佳化模型,並探討了 PyTorch 中可用的效能測量和視覺化工具。