深度學習框架如何優(yōu)雅的做算子對齊任務?

極市導讀
?本文介紹了OneFlow的算子AutoTest框架看一下OneFlow深度學習框架在算子開發(fā)過程中是如何優(yōu)雅的做算子對齊任務。?>>加入極市CV技術(shù)交流群,走在計算機視覺的最前沿
本文介紹OneFlow的算子AutoTest框架看一下OneFlow深度學習框架在算子開發(fā)過程中是如何優(yōu)雅的做算子對齊任務的(由@大缺弦 開發(fā),后經(jīng)我和其它同事進行擴展和豐富功能形成今天的形態(tài))。這個AutoTest框架也可以很輕易移植到其它深度學習訓練框架使用。
0. 前言
之前回答過「如何為PyTorch做貢獻的知乎問題」,原貼見:https://www.zhihu.com/question/502301777/answer/2248950419 ?;卮鹛岬搅巳ツ暝贠neFlow開發(fā)一些算子時,基于算子AutoTest框架找到了一些PyTorch算子的bug,并給PyTorch做出了反饋或修復。但這個回答沒有介紹這個AutoTest框架長什么樣子,以及它背后的原理。因此,這篇文章就用來介紹OneFlow的算子AutoTest框架看一下OneFlow深度學習框架在算子開發(fā)過程中是如何優(yōu)雅的做算子對齊任務的(由@大缺弦 開發(fā),后經(jīng)我和其它同事進行擴展和豐富功能形成今天的形態(tài))。這個AutoTest框架也可以很輕易移植到其它深度學習訓練框架使用,代碼實現(xiàn)在 https://github.com/Oneflow-Inc/oneflow/blob/v0.6.0/python/oneflow/test_utils/automated_test_util/torch_flow_dual_object.py。
1. 傳統(tǒng)的算子對齊方式
不局限于OneFlow,任何組織或者個人編寫的深度學習訓練框架都需要驗證算子的實現(xiàn)正確性。那么,深度學習框架中驗證算子正確性的一般做法是什么呢?以百度的PaddlePaddle為例,在驗證算子正確性時一般是根據(jù)調(diào)用其它標準庫獲得的結(jié)果(比如卷積算子的驗證就調(diào)用cudnn的卷積,erf算子的驗證就調(diào)用了scipy的erf)或者直接使用numpy模擬的計算結(jié)果來進行驗證(比如full算子的驗證即為numpy模擬)。在PyTorch的測試中還有硬編碼一些測試樣例的方式,也即將固定輸入樣例的標準答案和算子計算的結(jié)果進行對比,以此判斷算子實現(xiàn)的正確性。
這些方法都沒有什么問題,但在編寫測試時需要不少的人力并且在算子開發(fā)初期可能有一些corner case會容易想不到。以OneFlow為例,由于算子的行為是對齊PyTorch,如果要驗證轉(zhuǎn)置卷積Op在各種情況下的正確性,那么什么樣的測試代碼才可以全面驗證呢?一種做法是將每個參數(shù)都枚舉出來:
import?torch
import?numpy?as?np
import?oneflow?as?flow
for?N?in?range(1,?5):
????for?C_in?in?range(1,?10):
????????for?L_in?in?range(1,?10):
????????????for?H_in?in?range(1,?10):
????????????????for?C_out?in?range(1,?10):
????????????????????for?Ksize?in?range(1,?10):
????????????????????????for?Pad?in?range(1,?10):
????????????????????????????for?Dilation?in?range(1,?10):
????????????????????????????????for?Stride?in?range(1,?min(L_in,?H_in)):
????????????????????????????????????for?OutPad?in?range(1,?min(Dilation,?Stride)):
????????????????????????????????????????try:
????????????????????????????????????????????torch_input?=?torch.randn(N,?C_in,?L_in,?H_in)
????????????????????????????????????????????flow_input?=?flow.tensor(torch_input.numpy())
????????????????????????????????????????????torch_input.requires_grad?=?True
????????????????????????????????????????????flow_input.requires_grad?=?True
????????????????????????????????????????????torch_m?=?torch.nn.ConvTranspose2d(in_channels=C_in,?out_channels=C_out,?kernel_size=Ksize,?padding=Pad,?stride=Stride,
????????????????????????????????????????????????output_padding=(OutPad),?dilation=Dilation,?bias=False)
????????????????????????????????????????????flow_m?=?flow.nn.ConvTranspose2d(in_channels=C_in,?out_channels=C_out,?kernel_size=Ksize,?padding=Pad,?stride=Stride,
????????????????????????????????????????????????output_padding=(OutPad),?dilation=Dilation,?bias=False)
????????????????????????????????????????????flow_m.weight.data?=?flow.tensor(torch_m.weight.data.detach().numpy(),?requires_grad=True)
????????????????????????????????????????????torch_out?=?torch_m(torch_input)
????????????????????????????????????????????flow_out?=?flow_m(flow_input)
????????????????????????????????????????????torch_out?=?torch_out.sum()
????????????????????????????????????????????flow_out?=?flow_out.sum()
????????????????????????????????????????????assert(np.allclose(torch_out.detach().numpy(),?flow_out.detach().numpy(),?1e-06,?1e-06)),?"forward?not?equal"
????????????????????????????????????????????torch_out.backward()
????????????????????????????????????????????flow_out.backward()
????????????????????????????????????????????print(torch_input.grad.detach().numpy())
????????????????????????????????????????????print(flow_input.grad.detach()[:N,?:C_in,?:L_in,?:H_in].numpy())
????????????????????????????????????????????assert(np.allclose(torch_input.grad.detach().numpy(),?flow_input.grad.detach()[:N,?:C_in,?:L_in,?:H_in].numpy(),?1e-03,?1e-03)),?"backward?not?equal"
????????????????????????????????????????except?Exception?as?e:
????????????????????????????????????????????print('Input?Param?Error')
但這種做法雖然驗證得比較全面但同樣有缺點。首先枚舉的上界如何確定?如果給了一個大的上界,那么這個算子的驗證時間會非常長,不利于在 CI 流程中使用。如果上界很小就可能忽略一些 corner case,導致測試仍然不會全面并增加算子出 bug 的風險。
基于算子測試的這些問題,同事 @大缺弦 開發(fā)了一個算子 AutoTest 框架,用于解決 OneFlow 算子和 PyTorch 算子對齊的問題。后來我在此基礎上又為這個 AutoTest 框架豐富了其它的一些功能,感覺目前已經(jīng)比較好使,接下里做一個全面介紹。
整個 AutoTest 框架只有2個Python文件,即 https://github.com/Oneflow-Inc/oneflow/blob/v0.6.0/python/oneflow/test_utils/automated_test_util/torch_flow_dual_object.py ?和 https://github.com/Oneflow-Inc/oneflow/blob/v0.6.0/python/oneflow/test_utils/automated_test_util/generators.py 。并且這個AutoTest框架可以輕易移植到其它任何深度學習框架去做算子對齊任務。
2. 算子AutoTest框架用法
在介紹原理之前,我們先看一下 AutoTest 框架的用法。以上面的反卷積算子為例,使用了 AutoTest 框架之后就可以用下面的代碼來完成算子對齊測試:
@autotest()
def?test_deconv2d_with_random_data(test_case):
????channels?=?random(1,?6)
????m?=?torch.nn.ConvTranspose2d(
????????in_channels=channels,
????????out_channels=random(1,?20),
????????kernel_size=random(1,?4),
????????stride=random()?|?nothing(),
????????padding=random(1,?3).to(int)?|?nothing(),
????????dilation=random(1,?5)?|?nothing(),
????????groups=random(1,?5)?|?nothing(),
????????padding_mode=constant("zeros")?|?nothing(),
????)
????m.train(random())
????device?=?random_device()
????m.to(device)
????x?=?random_pytorch_tensor(ndim=4,?dim1=channels).to(device)
????y?=?m(x)
????return?y
熟悉PyTorch的小伙伴可以發(fā)現(xiàn)這個算子測試代碼和PyTorch的代碼風格基本一樣。的確,AutoTest框架相當于是一個high level的PyTorch,它的接口和PyTorch一樣,但對于給定的輸入會分別用OneFlow和PyTorch運行一遍,記錄運行過程中得到的每個tensor以及對應梯度tensor的值,再對這些OneFlow和PyTorch分別產(chǎn)生的tensor檢查一遍數(shù)值形狀是否完全相同,以完成自動測試工作,我們后面會細講。
我們可以再看一個測試matmul算子的例子:
?@autotest()
?def?test_flow_matmul_with_random_data(test_case):
?????k?=?random(1,?6)
?????x?=?random_pytorch_tensor(ndim=2,?dim1=k)
?????y?=?random_pytorch_tensor(ndim=2,?dim0=k)
?????z?=?torch.matmul(x,?y)
??return?z
我們基于random_pytorch_tensor方法構(gòu)造了兩個隨機tensor x和y,它們的維度分別是[m, k]和[k, n],這些維度的值都是隨機生成的。
執(zhí)行上述兩個測試例子,自動測試框架會自動幫我們隨機出各種合法參數(shù)組合成的Op,并基于數(shù)值和類型完全相同的輸入Tensor(PyTorch和OneFlow各有一份)分別運行PyTorch和OneFlow的代碼,并完成算子的自動測試。由于自動測試框架的用法對齊了PyTorch用法,我們在開發(fā)算子之后編寫測試樣例將非常簡單。不用再引入其它的標準庫或者使用Numpy去模擬一遍算子的前向反向計算過程等,解放了生產(chǎn)力。
并且測試的時候只要次數(shù)足夠多,就可以很大概率的覆蓋到一些OneFlow算子和PyTorch算子無法對齊的樣例,這個時候如果能拿到對應的復現(xiàn)樣例就可以幫助我們確定OneFlow算子實現(xiàn)是否存在問題。
3. 算子AutoTest框架實現(xiàn)思路
了解了AutoTest框架的使用方法之后,這里來講解一下AutoTest框架的實現(xiàn)思路。從上面的用法可以大概可以猜到AutoTest框架在實現(xiàn)時會分成兩部分,一部分是如何產(chǎn)生隨機數(shù)據(jù),另外一部分是運AutoTest部分的程序并記錄和比較中間tensor以及對應的梯度tensor的形狀和數(shù)值。
3.1 如何產(chǎn)生隨機數(shù)據(jù)?
這里說的隨機數(shù)據(jù)不僅指的是隨機的輸入tensor,還包含Op的屬性參數(shù)比如上面反卷積Op測試例子中的kernel_size=random(1, 4)就實現(xiàn)了指定kernel_size將會在[1, 4)這個區(qū)間進行取值。
這部分實現(xiàn)在 https://github.com/Oneflow-Inc/oneflow/blob/v0.6.0/python/oneflow/test_utils/automated_test_util/generators.py 這個文件里。首先我們看一下這個文件導出了哪些接口:
__all__?=?[
????"random_tensor",
????"random_bool",
????"random_device",
????"random",
????"random_or_nothing",
????"oneof",
????"constant",
????"nothing"
]
這些接口都是繼承了generator基類用來產(chǎn)生隨機數(shù)據(jù)結(jié)構(gòu)的類,這里的數(shù)據(jù)結(jié)構(gòu)既可以是內(nèi)置類型如int,也可以是自定義數(shù)據(jù)類型比如tensor。AutoTest框架所有的參數(shù)的隨機性都是基于這些方法來做到的,我們看一下generator基類的實現(xiàn):
class?generator:
????def?__init__(self,?children):
????????self.children?=?children
????????self._value?=?None
????def?_init(self):
????????self._value?=?None
????????for?x?in?self.children:
????????????x._init()
????def?eval(self):
????????self._init()
????????return?self.value()
????def?_calc_value(self):
????????raise?NotImplementedError()
????def?value(self):
????????if?self._value?is?None:
????????????self._value?=?self._calc_value()
????????return?self._value
????def?size(self):
????????return?1
????def?__or__(self,?other):
????????other?=?pack(other)
????????return?oneof(
????????????self,?other,?possibility=self.size()?/?(self.size()?+?other.size())
????????)
????def?__ror__(self,?other):
????????return?self?|?other
????def?__add__(self,?other):
????????return?add(self,?other)
????def?__radd__(self,?other):
????????return?self?+?other
????def?__sub__(self,?other):
????????return?self?+?neg(other)
????def?__rsub__(self,?other):
????????return?neg(self?-?other)
????def?__mul__(self,?other):
????????return?mul(self,?other)
????def?__rmul__(self,?other):
????????return?self?*?other
????def?to(self,?annotation):
????????self._to(annotation)
????????for?x?in?self.children:
????????????x.to(annotation)
????????return?self
????def?_to(self,?annotation):
????????pass
這個類不僅持有了_calc_value,value,eval等和取值有關(guān)的函數(shù),還持有size這個反應生成數(shù)據(jù)個數(shù)的函數(shù)。另外還持有了一系列的魔法函數(shù),讓不同的generator子類可以互相組合,提升了自動測試框架書寫的靈活性。最后還有一個to成員函數(shù),這個函數(shù)被繼承generator基類的類重寫,用來確定這個隨機數(shù)據(jù)結(jié)構(gòu)的數(shù)值類型。
所有的generator派生類都繼承了generator基類,并重寫其中的__init__,__calc_value,size,_to等成員函數(shù)。比如nothing這個generator的派生類就是直接重寫_calc_value函數(shù),并在其中返回一個什么都不做的類的實體。
class?Nothing:
????pass
class?nothing(generator):
????def?__init__(self):
????????super().__init__([])
????def?_calc_value(self):
????????return?Nothing()
再例如,random這個generator的派生類的定義如下:
class?random(generator):
????def?__init__(self,?low=1,?high=6):
????????self.low?=?pack(low)
????????self.high?=?pack(high)
????????super().__init__([self.low,?self.high])
????????self.annotation?=?None
????def?_to(self,?annotation):
????????if?self.annotation?is?not?None:
????????????return
????????if?hasattr(annotation,?"__origin__"):
????????????#?PyTorch?_size_2_t?and?similar?types?are?defined?by?type?variables,
????????????#?leading?to?unexpected?__args__?and?__origin__
????????????#
????????????#?>>>?_size_2_t?=?Union[T,?Tuple[T,?T]][int]
????????????#?>>>?_size_2_t.__origin__
????????????#?typing.Union[~T,?typing.Tuple[~T,?~T]]
????????????#
????????????#?So?recreate?a?new?annotation?object?by?repr?and?eval
????????????#
????????????#?>>>?_size_2_t
????????????#?typing.Union[int,?typing.Tuple[int,?int]]
????????????#?>>>?_size_2_t_new?=?eval(repr(annotation))
????????????#?>>>?_size_2_t_new.__origin__
????????????#?typing.Union
????????????annotation?=?eval(repr(annotation))
????????self.annotation?=?annotation
????def?_generate(self,?annotation):
????????if?hasattr(annotation,?"__origin__"):
????????????if?annotation.__origin__?is?Union:
????????????????x?=?random_util.choice(annotation.__args__)
????????????????return?self._generate(x)
????????????if?annotation.__origin__?is?Tuple?or?annotation.__origin__?is?py_tuple:
????????????????return?[self._generate(x)?for?x?in?annotation.__args__]
????????????else:
????????????????raise?NotImplementedError(
????????????????????f"Not?implemented?annotation?{annotation}?in?random,?type(annotation.__origin__)?is?{type(annotation.__origin__)}"
????????????????)
????????low,?high?=?self.low.value(),?self.high.value()
????????if?annotation?==?int:
????????????val?=?int(rng.integers(low,?high))
????????elif?annotation?==?float:
????????????val?=?float(rng.random()?*?(high?-?low)?+?low)
????????elif?annotation?==?bool:
????????????val?=?random_util.choice([True,?False])
????????else:
????????????raise?NotImplementedError(
????????????????f"Not?implemented?annotation?{annotation}?in?random"
????????????)
????????return?val
????def?_calc_value(self):
????????return?self._generate(self.annotation)
def?random_or_nothing(low,?high):
????return?oneof(random(low,?high),?nothing(),?possibility=2?/?3)
這里需要注意的一點是,持有annotation屬性的generator派生類的可以通過to來更新annotation屬性(如random類),也可以忽略這個annotation直接在_calc_value構(gòu)造相應類型的隨機結(jié)果(如random_device類)。
3.2 AutoTest核心實現(xiàn)
AutoTest框架的核心實現(xiàn)在 https://github.com/Oneflow-Inc/oneflow/blob/v0.6.0/python/oneflow/test_utils/automated_test_util/torch_flow_dual_object.py 這個文件。這個文件最后2行代碼是:
torch?=?GetDualObject("",?torch_original,?flow)
__all__?=?["autotest",?"random_pytorch_tensor"]
這行代碼torch = GetDualObject("", torch_original, flow) 里面的torch_original表示原始的PyTorch框架,而使用GetDualObject獲得的torch表示是對原始的PyTorch和OneFlow進行了一個封裝,變成了一個high level的PyTorch。因此,這里最關(guān)鍵的實現(xiàn)就是GetDualObject這個函數(shù),我們先不關(guān)注這個函數(shù)具體在做什么,而是它返回了什么。查看代碼可以發(fā)現(xiàn)這個函數(shù)返回了一個DualObject類對象,我們先研究一下這個類:
class?DualObject:
????def?__init__(self,?name,?pytorch,?oneflow):
????????self.name?=?name
????????self.pytorch?=?pytorch
????????self.oneflow?=?oneflow
????????if?isinstance(pytorch,?torch_original.nn.Module):
????????????state_dict?=?pytorch.state_dict()
????????????state_dict?=?{k:?v.detach().cpu().numpy()?for?(k,?v)?in?state_dict.items()}
????????????oneflow.load_state_dict(state_dict,?strict=False)
????????????if?testing:
????????????????dual_modules_to_test.append(self)
????????if?isinstance(pytorch,?torch_original.Tensor):
????????????if?testing:
????????????????dual_objects_to_test.append(self)
????def?__repr__(self):
????????return?f"PyTorch?object:\n{self.pytorch}\n\nOneFlow?object:\n{self.oneflow}"
????def?__getattr__(self,?key):
????????pytorch_attr?=?getattr(self.pytorch,?key)
????????oneflow_attr?=?getattr(self.oneflow,?key)
????????new_name?=?f"{self.name}.{key}"
????????global?call_pytorch
????????call_pytorch?=?self.pytorch
????????return?GetDualObject(new_name,?pytorch_attr,?oneflow_attr)
在__init__中傳入了類對象名和 pytorch/oneflow 兩個對象,在導出 high level 的 PyTorch的時候傳入的是torch_original和flow,而在導出random_pytorch_tensor 接口時傳入的是pytorch_tensor和oneflow_tensor。這里不妨先看一下random_pytorch_tensor這個函數(shù)的實現(xiàn):
def?random_pytorch_tensor(
????ndim=None,
????dim0=1,
????dim1=None,
????dim2=None,
????dim3=None,
????dim4=None,
????low=0,
????high=1,
????dtype=float,
????requires_grad=True,
):
????if?isinstance(requires_grad,?generator):
????????requires_grad?=?requires_grad.value()
????pytorch_tensor?=?(
????????random_tensor(ndim,?dim0,?dim1,?dim2,?dim3,?dim4,?low,?high,?dtype)
????????.value()
????????.requires_grad_(requires_grad?and?dtype?!=?int)
????)
????flow_tensor?=?flow.tensor(
????????pytorch_tensor.detach().cpu().numpy(),
????????requires_grad=(requires_grad?and?dtype?!=?int),
????)
????return?GetDualObject("unused",?pytorch_tensor,?flow_tensor)
可以看到它和導出high level PyTorch的實現(xiàn)一樣,也是通過調(diào)用GetDualObject來獲得了一個對象。再回到DualObject類的實現(xiàn),可以發(fā)現(xiàn)這里分別使用了dual_modules_to_test和dual_objects_to_test這兩個list來分別記錄OneFlow和PyTorch的nn.Module和tensor對象。另外DualObject類還重寫了__getattr__這個魔法方法,這里以Flatten為例來看看這個魔法方法獲取了AutoTest程序中的那些屬性:
def?__getattr__(self,?key):
????????pytorch_attr?=?getattr(self.pytorch,?key)
????????oneflow_attr?=?getattr(self.oneflow,?key)
????????print(key)
????????#?print(pytorch_attr)
????????#?print(oneflow_attr)
????????new_name?=?f"{self.name}.{key}"
????????return?GetDualObject(new_name,?pytorch_attr,?oneflow_attr)
#?flatten的AutoTest程序
@autotest(auto_backward=False)
def?test_against_pytorch(test_case):
????m?=?torch.nn.Flatten(
????????start_dim=random(1,?6)?|?nothing(),?end_dim=random(1,?6)?|?nothing()
????)
????m.train(random())
????device?=?random_device()
????m.to(device)
????x?=?random_pytorch_tensor().to(device)
????y?=?m(x)
????return?y
然后看一下__getattr__中key的打印結(jié)果:
nn
Flatten
train
to
to
可以看到被autotest()裝飾器修飾的測試程序中的PyTorch或者OneFlow的nn.Module或者其它函數(shù)都重寫了這個方法,它將這些nn.Module或者其它函數(shù)的參數(shù)和屬性都取出來并同樣使用GetDualObject返回一個新的DualObject對象,我們可以打印一下Flatten這個nn.Module對應的DualObject對象是什么:
PyTorch?object:
1,?end_dim=-1)>
OneFlow?object:
1,?end_dim=-1)>
GetDualObject這個函數(shù)就是根據(jù)傳入的Pytorch以及OneFlow對象和它們的名字來生成一個DualObject對象。GetDualObject這個函數(shù)會為high level的PyTorch重寫傳入的原始PyTorch以及OneFlow對象的__call__魔法函數(shù),最后返回一個DualObject對象,這個過程還包含了跳過一些不需要關(guān)注的魔法函數(shù)以及檢查傳入對象的屬性是否合法和基于nn.Module和其它API默認參數(shù)的類型對generator繼承類產(chǎn)生的隨機數(shù)據(jù)綁定特定類型的工作(get_args函數(shù)中完成)。這里還有一句對于Tensor方法的特判,因為Tensor方法的調(diào)用方式(通過getattr)和其它Module和函數(shù)不同(通過__call__)。
GetDualObject的實現(xiàn)思路大致就是這樣,代碼比較長這里就不貼了,感興趣可以在這里查看:https://github.com/Oneflow-Inc/oneflow/blob/v0.6.0/python/oneflow/test_utils/automated_test_util/torch_flow_dual_object.py#L195-L401 。
最后,我們看一下autotest()裝飾器的實現(xiàn):
def?autotest(
????n=20,
????auto_backward=True,
????rtol=0.0001,
????atol=1e-05,
????check_graph=True,
????check_allclose=True,
):
????verbose?=?os.getenv("ONEFLOW_TEST_VERBOSE")?is?not?None
????def?deco(f):
[email protected](f)
????????def?new_f(test_case):
????????????nonlocal?n
????????????loop_limit?=?n?*?20
????????????loop?=?0
????????????while?n?>?0:
????????????????clear_note_fake_program()
????????????????if?loop?>?loop_limit:
????????????????????raise?ValueError("autotest?stuck?in?an?endless?loop!")
????????????????dual_modules_to_test.clear()
????????????????dual_objects_to_test.clear()
????????????????try:
????????????????????global?testing
????????????????????testing?=?True
????????????????????global?testing_graph
????????????????????if?check_graph:
????????????????????????testing_graph?=?True
????????????????????res?=?f(test_case)
????????????????????testing?=?False
????????????????????testing_graph?=?False
????????????????except?(PyTorchDoesNotSupportError,?BothDoNotSupportError)?as?e:
????????????????????if?verbose:
????????????????????????print(f"{f.__name__}")
????????????????????????print(e)
????????????????????loop?+=?1
????????????????????continue
????????????????if?res?is?not?None:
????????????????????if?not?isinstance(res,?collections.abc.Sequence):
????????????????????????res?=?[res]
????????????????????func_outputs?=?res
????????????????????for?x?in?res:
????????????????????????if?auto_backward:
????????????????????????????if?isinstance(x.pytorch,?torch_original.Tensor):
????????????????????????????????call_tensor_id.append(id(x.pytorch))
????????????????????????????????x.sum().backward()
????????????????????????dual_objects_to_test.append(x)
????????????????for?x?in?dual_modules_to_test:
????????????????????for?key?in?x.pytorch.state_dict().keys():
????????????????????????if?key?not?in?x.oneflow.state_dict().keys():
????????????????????????????warnings.warn(f"oneflow?module?don't?have?`{key}`")
????????????????????????????continue
????????????????????????vis_parameters[key]?=?x.pytorch.state_dict()[key]
????????????????????????dual_objects_to_test.append(
????????????????????????????GetDualObject(
????????????????????????????????"unused",
????????????????????????????????getattr(x.pytorch,?key),
????????????????????????????????getattr(x.oneflow,?key),
????????????????????????????)
????????????????????????)
????????????????????????call_tensor_id.append(id(getattr(x.pytorch,?key)))
????????????????????????dual_objects_to_test.append(
????????????????????????????GetDualObject(
????????????????????????????????"unused",
????????????????????????????????getattr(x.pytorch,?key).grad,
????????????????????????????????getattr(x.oneflow,?key).grad,
????????????????????????????)
????????????????????????)
????????????????????????call_tensor_id.append(id(getattr(x.pytorch,?key).grad))
????????????????for?x?in?dual_objects_to_test:
????????????????????if?(
????????????????????????isinstance(x.pytorch,?torch_original.Tensor)
????????????????????????and?id(x.pytorch)?not?in?call_tensor_id
????????????????????):
????????????????????????vis_tensor.append(x.pytorch)
????????????????#?check?eager
????????????????for?x?in?dual_objects_to_test:
????????????????????if?check_allclose:
????????????????????????test_case.assertTrue(check_equality(x,?rtol=rtol,?atol=atol),?x)
????????????????????if?verbose:
????????????????????????print(f"{f.__name__}?test?eager?passed.")
????????????????????
????????????????n?-=?1
????????????????loop?+=?1
????????return?new_f
????return?deco
這個裝飾器的res = f(test_case)這行代碼會執(zhí)行這個裝飾器修飾的自動測試程序,會在給定輸入的情況下去分別運行PyTorch和OneFlow的程序獲得所有中間的輸出tensor,包括tensor的梯度,并將它們記錄到dual_modules_to_test這個列表。再遍歷這個列表里面的每個tensor,比較數(shù)值和shape是否完全一樣。比較函數(shù)實現(xiàn)在 https://github.com/Oneflow-Inc/oneflow/blob/v0.6.0/python/oneflow/test_utils/automated_test_util/torch_flow_dual_object.py#L565-L599 。原理就是拿到tensor的numpy數(shù)據(jù)進行比較。autotest() 裝飾器還有幾個參數(shù)可以調(diào)整,可以控制測試是否執(zhí)行反向,執(zhí)行次數(shù),以及最后結(jié)果對比的精度閾值。
4. 自動生成出BUG的程序和數(shù)據(jù)
上面介紹完了AutoTest框架的原理和使用方法,這里再展示一下基于AutoTest框架如何拿到可復現(xiàn)BUG的程序以及對應的輸入tensor和參數(shù)等。原理很簡單,就是把GetDualObject過程中使用的 api 記錄下來拼起來就構(gòu)成一個完整的程序,這里展示一下在 CI 中的效果。https://github.com/Oneflow-Inc/oneflow/runs/4760189461?check_suite_focus=true 這個例子展示了在某次CI過程中,OneFlow的 conv_transpose2d 算子和 PyTorch 的 conv_transpose2d 算子在某個 case 下沒有對齊,那么 CI 在報告這個錯誤時也輸出了對應的復現(xiàn)代碼和數(shù)據(jù),可以方便框架開發(fā)者進行定位和判斷:

自動測試框架在算子和PyTorch沒對齊時會輸出復現(xiàn)程序和數(shù)據(jù)
除此之外,這個 AutoTest 框架目前不僅負責 Eager 算子的測試,還被我們擴展到支持 nn.Graph 和 Eager Consistent 等多種情況,極大的方便了框架開發(fā)者。
5. 總結(jié)
這篇文章介紹了 OneFlow 的算子 AutoTest 框架,提供了一個深度學習優(yōu)雅的做算子對齊的方法,使得開發(fā)者和用戶可以像寫 PyTorch 那樣方便寫測試程序。AutoTest 框架的靈活性和易用性都比較強,歡迎大家學習或者使用。
相關(guān)鏈接
https://github.com/Oneflow-Inc/oneflow https://github.com/pytorch/pytorch
如果覺得有用,就請分享到朋友圈吧!
公眾號后臺回復“transformer”獲取最新Transformer綜述論文下載~

#?CV技術(shù)社群邀請函?#

備注:姓名-學校/公司-研究方向-城市(如:小極-北大-目標檢測-深圳)
即可申請加入極市目標檢測/圖像分割/工業(yè)檢測/人臉/醫(yī)學影像/3D/SLAM/自動駕駛/超分辨率/姿態(tài)估計/ReID/GAN/圖像增強/OCR/視頻理解等技術(shù)交流群
每月大咖直播分享、真實項目需求對接、求職內(nèi)推、算法競賽、干貨資訊匯總、與?10000+來自港科大、北大、清華、中科院、CMU、騰訊、百度等名校名企視覺開發(fā)者互動交流~

