Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 72 additions & 54 deletions intermediate_source/autograd_saved_tensors_hooks_tutorial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
미분자동화(autograd) 저장된 텐서를 위한 Hooks
미분자동화(autograd) 저장된 tensor를 위한 Hooks
================================================

"""
Expand All @@ -8,9 +8,11 @@
######################################################################
# 파이토치는 일반적으로 역전파를 사용하여 기울기를 계산합니다.
# 그러나 특정 작업에서는 역전파를 수행하기 위한 중간결과를 저장해야 합니다.
# 이번 튜토리얼에서는 이러한 텐서를 저장/검색하는 방법과 패킹/언패킹 프로세스를 제어하기위한 hooks을 정하는 방법을 안내합니다.
# 이번 튜토리얼에서는 이러한 tensor를 저장/검색하는 방법과
# 패킹/언패킹 절차를 제어하는 hooks을 정의하는 방법을 안내합니다.

# 이 튜토리얼에서는 사용자가 역전파가 이론상에서 작동하는 방식에 익숙하다고 가정합니다. 아니라면, 아래의 자료를 먼저 읽어보세요.
# 이 튜토리얼에서는 독자가 역전파가 어떻게 동작하는지를 이론적으로 잘 알고 있다고 가정합니다.
# 아니라면, 아래의 자료를 먼저 읽어보세요.
# https://colab.research.google.com/drive/1aWNdmYt7RcHMbUk-Xz2Cv5-cGFSWPXe0#scrollTo=AHcEJ6nXUb7W

######################################################################
Expand All @@ -19,13 +21,14 @@


######################################################################
# 모델의 훈련은 일반적으로 모델을 인퍼런스하는 것보다 학습하는 과정에서 메모리를 더 많이 사용합니다.
# 대략적으로 말하면 파이토치는 역전파를 호출하는데 필요한 계산 그래프를 저장해야하므로 추가 메모리 사용량이 있기 때문입니다.
# 이 튜토리얼의 한가지 목표는 이러한 이해에 대한 미세 조정을 실행하는 것입니다.
# 일반적으로 모델을 추론하는 것보다 학습하는 과정에서 메모리를 더 많이 사용합니다.
# 대략적으로 말하면 파이토치는 역전파를 호출하는데 필요한 계산 그래프를 저장해야하므로
# 추가 메모리 사용하기 때문입니다.
# 이 튜토리얼의 목표 중 하나는 이런 내용을 이해와 미세 조정을 제공하는 것입니다.
#
# 사실, 이 그래프 자체로는 가끔은 그다지 많은 메모리를 사용하진 않습니다. 여타의 텐서들을 복제하지 않는 것 처럼요.
# 그러나, 그래프는 *참조* 를 유지할 수 있습니다.
# 그렇지 않으면 범위를 벗어난 텐서를 : **저장된 텐서** 라고 명명할 수 있습니다.
# 실제로 때때로 (연산) 그래프 자체는 tensor들을 전혀 복제하지 않기 때문에 많은 메모리를 소모하지는 않습니다.
# 하지만, 그래프는 범위에서 벗어난 tensor들에 대한 *참조(reference)* 는 유지할 수 있습니다.
# 이러한 tensor들을 **저장된 tensor(saved tensor)** 라고 합니다.


######################################################################
Expand All @@ -34,8 +37,8 @@


######################################################################
# 간단한 예제를 시작해봅시다: :math:`y = a \cdot b` , 이것은 미분량(그래디언트)로 알고있는 :math: `y`,로 각각 :math: `a` and
# :math: `b`:로 상정합니다.
# 간단한 예제를 시작해봅시다: :math:`y = a \cdot b`, 변화도를 알고있는 :math: `y`,로 각각 :math: `a` and
# :math: `b`:로 합니다.

# .. math:: \frac{\partial y}{\partial a} = b
# .. math:: \frac{\partial y}{\partial b} = a
Expand All @@ -55,22 +58,25 @@


######################################################################
# 이 예제에서 파이토치는 중간 값 :math: `a` 및 :math: `b` 를 저장하여 역방향 동안 기울기를 계산합니다.
# 이 예제에서 파이토치는 중간 값 :math: `a` 및 :math: `b` 를 저장하여
# 역방향 동안 변화도를 계산합니다.
#
# .. figure:: https://user-images.githubusercontent.com/8019486/130124538-3da50977-6f0b-46d0-8909-5456ade9b598.png
# :width: 300
# :align: center


######################################################################
# 이러한 중간 값(위의 주황색)은 접두사 ``_saved``로 시작하는 ``y``의 ``grad_fn`` 속성을 찾아 (디버깅 목적으로) 액세스할 수 있습니다.
# 이러한 중간 값(위의 주황색)은 접두사 ``_saved``로 시작하는
# ``y``의 ``grad_fn`` 속성을 찾아 (디버깅 목적으로) 접근 할 수 있습니다.

print(y.grad_fn._saved_self)
print(y.grad_fn._saved_other)


######################################################################
# 계산 그래프가 깊어질수록 *저장된 텐서*가 더 많이 저장됩니다. 한편, 텐서는 그래프가 아니었다면 범위를 벗어나게 됩니다.
# 계산 그래프가 깊어질수록 *저장된 tensor*가 더 많이 저장됩니다.
# 한편, tensor는 그래프가 아니었다면 범위를 벗어나게 됩니다.

def f(x):
return x * x
Expand All @@ -86,17 +92,20 @@ def f(x):
######################################################################
# 위의 예제에서 미분(grad)없이 실행하면 범위내의 ``x`` 와 ``y`` 는 유지되지만
# 그래프에서는 ``f(x)`` 와 ``f(f(x))`` 가 추가로 저장됩니다.
# 따라서 훈련 중 정방향 경로를 실행하면 평가중에 (더 정확하게는 자동미분(auto grad)가 필요하지 않은 경우보다) 메모리 사용비중이 더 많아지게 됩니다.
# 따라서 훈련 중 정방향 경로를 실행하면 평가중에
# (더 정확하게는 자동미분(auto grad)가 필요하지 않은 경우보다)
# 메모리 사용량이 더 많아지게 됩니다.


######################################################################
# 패킹과 언패킹의 개념
# ~~~~~~~~~~~~~~~~~~~
# ~~~~~~~~~~~~~~~~~~~~



######################################################################
# 첫 번째 예제로 돌아가서 ``y.grad_fn._saved_self`` 와 ``y.grad_fn._saved_other`` 는 각각 원래 텐서 객체 ``a`` 와 ``b`` 를 가리킵니다.
# 첫 번째 예제로 돌아가서 ``y.grad_fn._saved_self`` 와 ``y.grad_fn._saved_other`` 는
# 각각 원래 tensor 객체 ``a`` 와 ``b`` 를 가리킵니다.

a = torch.randn(5, requires_grad=True)
b = torch.ones(5, requires_grad=True)
Expand All @@ -116,18 +125,21 @@ def f(x):


######################################################################
# 내부적으로는 파이토치는 참조주기를 방지하기 위해서 텐서 ``y`` 를 **패킹** 및 **언패킹** 했습니다
# 경험상, 역전파 저장된 텐서에 엑세스하면 원래 텐서와 동일한 텐서의 객체가 생성된다는 결과를 기대해서는 *안됩니다.*
# 그러나 동일한 *저장소*를 공유합니다.
# 내부적으로는 파이토치는 참조주기를 방지하기 위해서 tensor ``y`` 를
# **패킹** 및 **언패킹** 했습니다
# 경험상, 역전파 저장된 tensor에 엑세스하면 원래 tensor와
# 동일한 tensor의 객체가 생성된다는 결과를 기대해서는 *안됩니다*.
# 그러나 tensor는 동일한 *저장소*를 공유합니다.

######################################################################
# 저장된 텐서 hooks
# 저장된 tensors hooks
# -------------------



######################################################################
# 파이토치는 텐서들이 패킹되고 언패킹되는지 저장할 수 있는 조정가능한 API를 제공합니다.
# 파이토치는 tensor들이 어떻게 패킹되고 언패킹되는지
# 저장할 수 있는 제어 가능한 API를 제공합니다.

def pack_hook(x):
print("Packing", x)
Expand All @@ -146,9 +158,9 @@ def unpack_hook(x):


######################################################################
# ``pack_hook`` 함수는 작업이 역전파를 위해 텐서를 저장할 때 마다 호출됩니다. 그러면
# ``pack_hook`` 의 출력이 원래 텐서 대신 계산 그래프에 저장됩니다.
# ``unpack_hook`` 은 해당 반환 값을 사용하여 역방향 전달 중에 실제로 사용된 텐서를텐서로 계산합니다.
# ``pack_hook`` 함수는 작업이 역전파를 위해 tensor를 저장할 때 마다 호출됩니다. 그러면
# ``pack_hook`` 의 출력이 원래 tensor 대신 계산 그래프에 저장됩니다.
# ``unpack_hook`` 은 해당 반환 값을 사용하여 역방향 전달 중에 실제로 사용된 tensor를tensor로 계산합니다.
# 일반적으로 ``unpack_hook(pack_hook(t))`` 가 ``t`` 와 같길 기대합니다.


Expand All @@ -160,8 +172,8 @@ def unpack_hook(x):


######################################################################
# 한 가지 주의할 점은 ``unpack_hook`` 이 올바른 값을 가진 텐서를 파생할 수 있는 한
# ``pack_hook`` 의 출력은 *모든 파이썬 객체* 가 될 수 있다는 것입니다.
# 한 가지 주의할 점은 ``unpack_hook`` 이 올바른 값을 가진 tensor를 파생할 수 있는 한
# ``pack_hook`` 의 출력은 *any 파이썬 객체* 가 될 수 있다는 것입니다.

######################################################################
# 몇 가지 특이한 예제들
Expand All @@ -170,14 +182,13 @@ def unpack_hook(x):


######################################################################
# 먼저, 가능성을 설명하기 위해서 몇 가지 어리석어보이는 예가 있지만 아마 하고싶지 않을 수도 있습니다.
# 먼저, 가능은 하지만 바보같아서 누구도 하고 싶어하지 않는 예제 몇가지를 살펴보겠습니다.

######################################################################
# **반환 및 int**
#
# 파이썬 리스트의 인덱스반환
#
# 상대적으로는 상관없지만 논란의 여지가 있는 유용성

# 파이썬 리스트의 인덱스 반환
# 상대적으로는 위험하진 않지만 논란의 여지가 있는 유용성

storage = []

Expand All @@ -198,7 +209,7 @@ def unpack(x):
######################################################################
# **튜플의 반환**
#
# 일부 텐서와 함수를 반환하고 압축을 푸는 방법은 현재 형태로는 유용하지 않을 것입니다.
# 일부 tensor와 함수를 반환하고 패킹을 푸는 방법은 이런 형태로는 유용하지 않을 것입니다.

def pack(x):
delta = torch.randn(*x.size())
Expand All @@ -219,8 +230,8 @@ def unpack(packed):
######################################################################
# **str의 반환**
#
# 텐서의 __repr__ 반환은
# 아마도 절대 이렇게 하진 않을 것!
# tensor의 __repr__ 반환
# 아마도 이렇게 하지 말 것

x = torch.randn(5, requires_grad=True)
with torch.autograd.graph.saved_tensors_hooks(lambda x: repr(x), lambda x: eval("torch." + x)):
Expand All @@ -230,20 +241,23 @@ def unpack(packed):


######################################################################
# 이러한 예제는 실제로 유용하지 않을 것이지만 ``pack_hook`` 의 출력이 원래 텐서의 내용을 검색하기에 충분한 정보를 포함하는 한
# 실제로 모든 Python의 객체가 될 수 있음을 보여줍니다.
# 이러한 예제는 실제로 유용하지 않을 것이지만
# 원래 tensor의 내용을 가져오기에 충분한 정보를 가지고 있다면
# ``pack_hook`` 의 결과물이 어떤 파이썬 객체라도
# 될 수 있음을 보여줍니다.
# 다음 섹션에서는 더 유용한 응용프로그램에 중점을 두겠습니다.

######################################################################
# 텐서를 CPU에 저장하기
# tensor를 CPU에 저장하기
# ~~~~~~~~~~~~~~~~~~~~~~~~



######################################################################
# 매우 빈번하게, 계산 그래프를 수반하는 텐서는 GPU에 있습니다.
# 그래프에 이러한 텐서에 대한 참조를 유지하는 것은 대부분의 모델이 평가중에 정상적으로 수행되었을 때
# 훈련중에 GPU 메모리가 부족하게 만드는 원인이 됩니다.
# 매우 빈번하게, tensor 계산 그래프 GPU에 살아 있습니다.
# 대부분 경우에서 모델이 평가중에 정상적으로 수행되지만 훈련 중에 메모리가 부족하다면,
# 계산 그래프에서 tensor에 대한 참조 유지가
# GPU 메모리를 부족하게 만드는 원인이 됩니다.
#
# hooks는 이를 구현하는 매우 간단한 방법을 제공합니다.

Expand All @@ -265,7 +279,8 @@ def unpack_hook(packed):


######################################################################
# 실제로 파이토치는 이러한 hooks를 편리하게 사용할 수 있는 API를 제공합니다. (고정된 메모리를 사용하는 기능도 포함.)
# 실제로 파이토치는 이러한 hooks를 편리하게 사용할 수 있는 API를 제공합니다.
# (고정된 메모리를 사용하는 기능도 포함).

import torch.nn as nn

Expand All @@ -287,11 +302,11 @@ def forward(self, x):

######################################################################
# 실제로 A100 GPU에서 배치크기가 256인 resnet-152의 경우 이는 GPU메모리 사용량이
# 48G에서 5GB로 6배 느려지는 비용 감소에 해당합니다.
# 48G에서 5GB로 줄어들지만, 이는 6배 느려지는 댓가를 치뤄야합니다.
#
# 물론 네트워크 특정부분만 CPU에 저장하여 절충안을 조정할 수 있습니다.
#
# 예를 들어, 모든 모듈을 감싸두고 해당 텐서를 CPU에 저장하는 특별한
# 예를 들어, 어떤 모듈을 감싸두고 해당 tensor를 CPU에 저장하는 특별한
# ``nn.Module`` 을 정의할 수 있습니다.

class SaveToCpu(nn.Module):
Expand All @@ -315,18 +330,19 @@ def forward(self, *args, **kwargs):


######################################################################
# 텐서를 디스크에 저장하기
# tensor를 디스크에 저장하기
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~



######################################################################
# 마찬가지로, 이러한 텐서를 디스크에 저장하고 싶을 수 도 있습니다. 다시 말하지만 이것은 앞서말한 hooks로 달성할 수 있습니다.
# 비슷하게, 이러한 tensor를 디스크에 저장하고 싶을 수 도 있습니다.
# 다시 말하지만 이것은 앞서말한 hooks로 달성할 수 있습니다.

######################################################################
# 단순한 버전의 아마 이럴것입니다.
# 모자란 버전은 보통 이럴것입니다.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단순한과 모자란 사이의 어딘가쯤일거 같은데, naive version을 옆에 명시해주는건 어떨까요?

#
# 단순버전 - 힌트: 이렇게 하지 마시오.
# 모자란 버전(naive version) - 힌트: 이렇게 하지 마시오.

import uuid
tmp_dir = "temp"
Expand Down Expand Up @@ -365,7 +381,7 @@ def unpack_hook(name):

######################################################################
# 위의 코드가 작동하지 않는 이유는 ``unpack_hook`` 가 여러번 호출되기 때문입니다.
# 먼저 압축을 풀 때 파일을 삭제하면 처음에, 저장된 텐서에 엑세스시에 사용할 수 없습니다.
# 먼저 압축을 풀 때 파일을 삭제하면 처음에, 저장된 tensor에 접근시에 사용할 수 없습니다.
# 두 번째에는 오류가 발생합니다.

x = torch.ones(5, requires_grad=True)
Expand All @@ -380,8 +396,9 @@ def unpack_hook(name):


######################################################################
# 이문제를 해결하기위해 이점을 활용하는 hooks의 버전을 작성할 수 있습니다.
# 파이토치가 저장된 데이터를 자동으로 해제(삭제)한다는 사실이 더이상 필요하지 않을 때입니다.
# 이 문제를 해결하기 위해,
# 파이토치는 저장된 데이터를 더이상 필요하지 않을 때
# 자동으로 해제(삭제) 하는 이점을 활용하는 hooks의 버전을 작성할 수 있습니다.

class SelfDeletingTempFile():
def __init__(self):
Expand All @@ -405,7 +422,7 @@ def unpack_hook(temp_file):
#
# 다음과 같은 방식으로 모델에 사용할 수 있습니다.

# 사이즈 >=1000인 텐서만이 디스크에 저장될 수 있습니다.
# 사이즈 >=1000인 tensor만이 디스크에 저장될 수 있습니다.

SAVE_ON_DISK_THRESHOLD = 1000

Expand Down Expand Up @@ -434,10 +451,11 @@ def forward(self, *args, **kwargs):


######################################################################
# 이 마지막 예제에서는 저장해야 하는 (여기에서는 원소의 수가 1000 이상인) 텐서들을 골라내는 방법과
# 이 마지막 예제에서는 저장해야 하는 (여기에서는 원소의 수가 1000 이상인) tensor들을 골라내는 방법과
# 이 기능을 ``nn.DataParallel`` 과 함께 사용하는 방법을 살펴보았습니다.


######################################################################
# 여기까지 잘 따라오셨나요? 축하합니다!
# 이제 저장된 텐서 hooks을 어떻게 사용하는지와 연산 시 메모리 관리(trade-offs)에 유용하게 사용할 수 있는 방법을 알게 되셨습니다.
# 이제 저장된 tensor hooks를 어떻게 사용하는지와
# 연산 시 메모리 관리(trade-offs)에 유용하게 사용할 수 있는 방법을 알게 되셨습니다.