콘텐츠로 이동

메모리 모델

newch가 PyTorch와 가장 다른 부분입니다. 작은 기기에서 메모리를 예측할 수 있게 하려고, 텐서를 임시 텐서와 영구 텐서 두 종류로 나눕니다.

임시 텐서 영구 텐서
무엇 연산 결과 대부분 (활성값, 중간값, 배치 데이터) 파라미터, 버퍼, persistent() 안에서 만든 텐서
메모리 스텝 아레나 (큰 블록에서 잘라 씀) 텐서마다 따로 할당
해제 step_end()가 한 번에 (O(1)) Python에서 마지막 참조가 사라질 때

step_end()

import newch as torch
import newch.net as nn
import newch.net.functional as F

model = nn.Linear(4, 2)
opt = torch.optim.SGD(model.parameters(), lr=0.1)
for step in range(3):
    x = torch.randn(8, 4)                    # 임시 텐서: 이번 스텝에서만 씀
    target = torch.randn(8, 2)
    opt.zero_grad()
    loss = F.mse_loss(model(x), target)
    loss.backward()
    opt.step()
    print(step, loss.item())                 # 값은 step_end() 전에 꺼냅니다
    torch.step_end()                         # 이 스텝의 임시 텐서를 모두 해제

step_end() 뒤에 그 스텝의 임시 텐서를 쓰면 해제된 메모리를 읽는 대신 분명한 에러가 납니다.

x = torch.randn(3)
torch.step_end()
try:
    x.sum()
except torch.NcError as e:
    print("에러:", e)

fit(), predict(), evaluate()는 배치마다 알아서 step_end()를 부릅니다.

persistent()

여러 스텝에 걸쳐 쓸 텐서는 persistent() 안에서 만듭니다.

with torch.persistent():
    dataset_x = torch.randn(100, 4)
    mean = dataset_x.mean(0)
torch.step_end()
print(mean.shape)            # 아직 살아 있음
torch.release(dataset_x)     # 다 썼으면 바로 해제할 수도 있습니다 (안 하면 참조가 사라질 때 해제)

파라미터(nn.Linear의 weight 등)와 옵티마이저 상태는 원래 영구입니다.

역전파와 추론 중의 해제

  • 역전파는 노드를 지날 때마다 다 쓴 활성값과 중간 그래디언트를 돌려줍니다. Python에서 아직 참조하는 텐서는 지우지 않습니다. MNIST CNN(배치 64)의 학습 스텝 메모리가 이 덕분에 29MiB에서 15MiB로 줄었습니다.
  • no_grad() 안에서는 Python이 더 이상 참조하지 않는 중간값을 즉시 해제하므로, 긴 추론도 한 스텝 안에서 메모리가 쌓이지 않습니다.

메모리 측정

torch.default_context().reset_peak()
with torch.no_grad():
    y = model(torch.randn(256, 4))
stats = torch.mem_stats()
print({k: v for k, v in stats.items()})
torch.step_end()
항목 뜻
arena_bytes_in_use 지금 쓰는 임시 메모리
arena_peak_bytes reset_peak() 이후 최대 임시 메모리
arena_bytes_reserved 아레나가 OS에서 받아 둔 메모리 (다음 스텝에 재사용)
persistent_bytes 영구 텐서 전체

유난히 큰 스텝이 지나간 뒤 받아 둔 메모리를 OS에 돌려주려면 torch.default_context().trim()을 부릅니다.

Context와 스레드

모든 메모리는 Context가 가집니다. 보통은 기본 컨텍스트 하나로 충분합니다.

ctx = torch.Context(seed=1, threads=2)       # threads=0(기본)은 모든 코어
torch.set_default_context(ctx)
print(ctx.threads, ctx.backend)              # 쓰는 스레드 수와 커널 (avx2, neon, ref)
  • 큰 행렬곱, 원소별 연산, 합성곱, 풀링을 여러 스레드로 나눕니다. 출력 원소 하나는 늘 한 스레드가 계산하므로 결과가 스레드 수와 무관하게 비트 단위로 같습니다.
  • 컨텍스트를 버리면(참조가 모두 사라지면) 엔진 메모리와 워커 스레드도 정리됩니다.
  • torch.manual_seed(n)은 기본 컨텍스트의 난수 생성기를 다시 시드합니다.