原文: https://pytorch.org/docs/stable/notes/rref.html
警告
RRef API 是實驗性的,隨時可能更改。
本說明描述了遠程引用協(xié)議的設(shè)計細節(jié),并逐步介紹了不同情況下的消息流。 在繼續(xù)之前,請確保您熟悉分布式 RPC 框架。
RRef 代表遠程參考。 它是位于本地或遠程工作人員上的對象的引用,并且透明地在內(nèi)部進行引用計數(shù)。 從概念上講,它可以視為分布式共享指針。 應(yīng)用程序可以通過調(diào)用 remote()
創(chuàng)建 RRef。 每個 RRef 都由 remote()
呼叫的被調(diào)用方工作者(即所有者)擁有,并且可以由多個用戶使用。 所有者存儲實際數(shù)據(jù),并跟蹤全局參考計數(shù)。 每個 RRef 可以由全局RRefId
唯一標識,該全局RRefId
在創(chuàng)建時在 remote()
調(diào)用的調(diào)用方上分配。
在所有者工作程序中,只有一個OwnerRRef
實例包含真實數(shù)據(jù),而在用戶工作程序中,可以根據(jù)需要包含任意數(shù)量的UserRRefs
,而UserRRef
不保存數(shù)據(jù)。 所有者上的所有用法都將使用全局唯一的RRefId來檢索唯一的OwnerRRef實例。 在 rpc_sync()
, rpc_async()
或 remote()
調(diào)用中將UserRRef
用作參數(shù)或返回值時,將創(chuàng)建該UserRRef
將會根據(jù)更新的參考計數(shù)通知所有者。
如果全局沒有UserRRef
實例,并且所有者上也沒有對OwnerRRef
的引用,則OwnerRRef
及其數(shù)據(jù)將被刪除。
RRef 協(xié)議的設(shè)計基于以下假設(shè)。
rpc_sync()
, rpc_async()
或 remote()
的用戶功能(UDF) 不是冪等的,因此無法重試。 但是,內(nèi)部 RRef 控制消息將成為冪等且可重試。該協(xié)議的目標是在適當(dāng)?shù)臅r候刪除OwnerRRef
。 刪除OwnerRRef
的正確時機是在沒有活動UserRRef
實例且用戶代碼也沒有保存對OwnerRRef
的引用的情況下。 棘手的部分是確定是否存在任何活動的UserRRef
實例。
用戶可以在以下三種情況下獲得UserRRef
:
UserRRef
。UserRRef
。UserRRef
。情況 1 是最簡單的,所有者將其 RRef 傳遞給用戶,所有者調(diào)用 rpc_sync()
, rpc_async()
或 remote()
使用其 RRef 作為參數(shù)。 在這種情況下,將在用戶上創(chuàng)建一個新的UserRRef
。 由于所有者是調(diào)用者,因此可以輕松地在OwnerRRef
上更新其本地引用計數(shù)。
唯一的要求是任何UserRRef
必須在銷毀時通知所有者。 因此,我們需要第一個保證:
G1。 刪除任何“ UserRRef”時,都會通知所有者。
由于郵件可能會延遲或出現(xiàn)亂序,因此我們還需要一項保證,以確保刪除郵件不會過早處理。 如果 A 向 B 發(fā)送涉及 RRef 的消息,我們將 A 上的 RRef 稱為父 RRef,將 B 上的 RRef 稱為子 RRef。
G2。 在所有者確認子 RRef 之前,不會刪除父 RRef。
在情況 2 和 3 中,所有者可能僅對 RRef 分支圖有部分了解或根本不了解。 例如,可以在用戶上構(gòu)建 RRef,并且在所有者收到任何 RPC 調(diào)用之前,創(chuàng)建者用戶可能已經(jīng)與其他用戶共享了 RRef,并且這些用戶可以進一步共享 RRef。 一個不變性是,任何 RRef 的派生圖始終都是一棵樹,因為派生 RRef 總是在被調(diào)用方上創(chuàng)建一個新的UserRRef
實例(除非被調(diào)用方是所有者),因此每個 RRef 都有一個父級。
所有者對樹中任何UserRRef
的視圖分為三個階段:
1) unknown -> 2) known -> 3) deleted.
所有者對整棵樹的看法不斷變化。 擁有者認為沒有活動的UserRRef
實例時,即刪除OwnerRRef
實例時,所有UserRRef
實例都可能確實被刪除或未知,因此所有者刪除了其OwnerRRef
實例。 危險的情況是某些分叉未知,而另一些被刪除。
G2 簡單地保證在擁有者知道其所有子級UserRRef
實例之前,不能刪除任何父級UserRRef
。 但是,有可能在擁有者知道其父項UserRRef
之前刪除了子項UserRRef。
考慮下面的示例,其中OwnerRRef
分支到 A,然后 A 分支到 Y,Y 分支到 Z:
OwnerRRef -> A -> Y -> Z
如果所有者在處理所有來自 Z 的消息(包括刪除消息)之前先處理來自 Y 的所有消息,那么所有者將在知道 Y 之前就知道 Z 的刪除。但這不會造成任何問題。 因為,Y 的祖先中至少有一個還活著(在本例中為 A),這將阻止所有者刪除OwnerRRef
。 更具體地說,如果所有者不知道 Y,則由于 G2 而無法刪除 A,并且所有者知道 A,因為所有者是 A 的父母。
如果在用戶上創(chuàng)建 RRef,事情會變得有些棘手:
OwnerRRef
^
|
A -> Y -> Z
如果 Z 在UserRRef
上調(diào)用 to_here()
,則所有者至少知道刪除 Z 時的 A,否則, to_here()
不會結(jié)束。 如果 Z 沒有調(diào)用 to_here()
,則所有者可能在從 A 和 Y 發(fā)送任何消息之前就已從 Z 接收了所有消息。在這種情況下,由于尚未獲得OwnerRRef
的真實數(shù)據(jù) 創(chuàng)建后,也沒有要刪除的內(nèi)容。
就像 Z 根本不存在一樣。 因此,仍然可以。
G1 通過在UserRRef
析構(gòu)函數(shù)中發(fā)送刪除消息來實現(xiàn)。 為了提供 G2 ,無論何時將父級UserRRef
派生,都將其置于上下文中,并由新的ForkId
對其進行索引。 僅當(dāng)父級UserRRef
從子級收到確認消息(ACK)時,才會從上下文中刪除該父級UserRRef
,并且只有當(dāng)所有者確認后,該子級才會發(fā)出 ACK。
現(xiàn)在,讓我們討論以上設(shè)計如何在四種情況下轉(zhuǎn)換為協(xié)議。
import torch
import torch.distributed.rpc as rpc
## on worker A
rref = rpc.remote('B', torch.add, args=(torch.ones(2), 1))
## say the rref has RRefId 100 and ForkId 1
rref.to_here()
在這種情況下,在用戶工作程序 A 上創(chuàng)建UserRRef
,然后將其與遠程消息一起傳遞給所有者工作程序 B,然后 B 創(chuàng)建OwnerRRef
。 方法 remote()
立即返回,這意味著UserRRef
可以在所有者了解之前被分叉/使用。
在所有者上,當(dāng)接收到 remote()
調(diào)用時,它將創(chuàng)建OwnerRRef
,并返回一個 ACK 來確認{100, 1}
(RRefId
,ForkId
)。 僅在收到此 ACK 后,A 才能刪除其UserRRef
。 這涉及 G1 和 G2 。 G1 很明顯。 對于 G2 而言,OwnerRRef
是UserRRef
的子級,并且UserRRef
直到收到所有者的
ACK 才被刪除。
上圖顯示了消息流,其中實心箭頭包含用戶功能,而虛線箭頭是內(nèi)置消息。 請注意,從 A 到 B 的前兩個消息 (remote()
和 to_here()
)可以按任何順序到達 B,但最終的刪除消息僅在以下情況下發(fā)送: :
UserRRef {100, 1}
(G2),并且UserRRef
實例。 當(dāng) RRef 不再在范圍內(nèi)并且可以進行垃圾回收時,就會發(fā)生這種情況。import torch
import torch.distributed.rpc as rpc
## on worker A and worker B
def func(rref):
pass
## on worker A
rref = rpc.remote('B', torch.add, args=(torch.ones(2), 1))
## say the rref has RRefId 100 and ForkId 1
rpc.rpc_async('B', func, args=(rref, ))
在這種情況下,在 A 上創(chuàng)建UserRRef
后,A 會將其用作對 B 的后續(xù) RPC 調(diào)用中的參數(shù)。A 將使UserRRef {100, 1}
保持活動狀態(tài),直到收到 B 的確認 (G2 , 而不是 RPC 調(diào)用的返回值)。 這是必要的,因為在接收到所有先前的消息之前,A 不應(yīng)發(fā)出刪除消息,否則,OwnerRRef
可以在使用前刪除,因為我們不能保證消息的傳遞順序。 這是通過創(chuàng)建 RRef 的子項ForkId
并將其保存在地圖中,直到收到所有者確認該子項ForkId來完成的。
下圖顯示了消息流。
注意,UserRRef
可以在功能完成甚至啟動之前在 B 上刪除。 但是,這是可以的,因為在 B 向子ForkId
發(fā)送 ACK 時,它已經(jīng)獲取了OwnerRRef
實例,這將防止它被過早刪除。
所有者對用戶是最簡單的情況,所有者可以在本地更新引用計數(shù),并且不需要任何其他控制消息即可通知其他人。 關(guān)于 G2 ,因為父級是所有者,所以它與父級立即從所有者接收 ACK 相同。
import torch
import torch.distributed.rpc as RRef, rpc
## on worker B and worker C
def func(rref):
pass
## on worker B, creating a local RRef
rref = RRef("data")
## say the rref has RRefId 100
dist.rpc_async('C', func, args=(rref, ))
上圖顯示了消息流。 請注意,當(dāng)在 rpc_async 調(diào)用之后OwnerRRef
退出作用域時,將不會刪除它,因為如果存在任何已知的派生,則內(nèi)部會存在一個使它保持活動狀態(tài)的映射,在這種情況下為UserRRef {100, 1}
。 (G2 )
這是最復(fù)雜的情??況,其中調(diào)用者用戶(父級UserRRef
),被調(diào)用者用戶(子級UserRRef
)和所有者都需要參與。
import torch
import torch.distributed.rpc as rpc
## on worker A and worker C
def func(rref):
pass
## on worker A
rref = rpc.remote('B', torch.add, args=(torch.ones(2), 1))
## say the rref has RRefId 100 and ForkId 1
rpc.rpc_async('C', func, args=(rref, ))
當(dāng) C 從 A 接收到子項UserRRef
時,它向所有者 B 發(fā)送一個派生請求。稍后,當(dāng) B 確認 C 上的UserRRef
時,C 將并行執(zhí)行兩個操作:1)發(fā)送子項 ACK 到 A,然后 2)運行用戶提供的功能。 在這段時間中,親本(A)將保持其UserRRef {100, 1}
存活以實現(xiàn) G2 。
更多建議: