본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 28. · 15 Views
데이터 파이프라인과 DataLoader 완벽 가이드
딥러닝 모델 학습의 핵심인 데이터 파이프라인과 DataLoader를 초급 개발자도 이해할 수 있도록 설명합니다. FineWeb 데이터셋부터 멀티 GPU 분산 학습까지, 38B 토큰을 효율적으로 처리하는 방법을 다룹니다.
목차
1. FineWeb 데이터셋 분석
어느 날 김개발 씨는 대규모 언어 모델을 학습시키는 프로젝트에 투입되었습니다. 선배가 건네준 코드를 열어보니 dataset.py라는 파일이 눈에 들어왔습니다.
"이게 대체 어떻게 수십억 개의 토큰을 처리하는 거지?" 김개발 씨의 머릿속에는 물음표가 가득했습니다.
FineWeb은 대규모 언어 모델 학습을 위해 정제된 웹 크롤링 데이터셋입니다. 마치 도서관에서 수백만 권의 책 중에서 양질의 책만 골라 정리해 놓은 것과 같습니다.
이 데이터를 효율적으로 불러오는 방법을 이해하면 수십억 개의 토큰도 무리 없이 처리할 수 있게 됩니다.
다음 코드를 살펴봅시다.
import numpy as np
from datasets import load_dataset
# FineWeb 데이터셋 로드 - 100B 토큰 샘플 버전
def load_fineweb_dataset(split="train"):
dataset = load_dataset(
"HuggingFaceFW/fineweb-edu",
name="sample-100BT",
split=split,
streaming=True # 메모리 효율을 위한 스트리밍 모드
)
return dataset
# 데이터 샤드 파일 경로 생성
def get_shard_path(shard_id, data_dir="data/fineweb"):
return f"{data_dir}/fineweb_train_{shard_id:06d}.bin"
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 오늘 처음으로 대규모 언어 모델 학습 프로젝트에 참여하게 되었는데, 선배 박시니어 씨가 건네준 코드베이스를 열어보니 dataset.py라는 파일이 가장 먼저 눈에 들어왔습니다.
파일을 열어보니 FineWeb이라는 낯선 이름이 보였습니다. 김개발 씨가 고개를 갸웃거리자 박시니어 씨가 다가와 설명을 시작했습니다.
"FineWeb은 HuggingFace에서 공개한 대규모 웹 크롤링 데이터셋이야. 그냥 아무 웹 데이터가 아니라, 교육적 가치가 높은 콘텐츠만 필터링한 거지." 쉽게 비유하자면, FineWeb은 마치 거대한 도서관에서 사서가 양질의 책만 골라 별도의 서가에 정리해 둔 것과 같습니다.
인터넷에는 수없이 많은 텍스트가 있지만, 언어 모델 학습에 적합한 고품질 텍스트만 추려낸 것입니다. 이런 데이터 정제 과정이 없다면 모델은 쓸모없는 정보나 유해한 콘텐츠까지 학습하게 됩니다.
그런데 문제가 있습니다. FineWeb의 전체 크기는 무려 수백 테라바이트에 달합니다.
이걸 한 번에 메모리에 올리는 것은 불가능합니다. 여기서 스트리밍 모드가 등장합니다.
스트리밍 모드는 데이터를 한꺼번에 불러오지 않고, 필요할 때마다 조금씩 가져오는 방식입니다. 마치 넷플릭스에서 영화를 볼 때 전체 파일을 다운로드하지 않고 실시간으로 스트리밍하는 것과 같습니다.
이렇게 하면 아무리 큰 데이터셋이라도 메모리 걱정 없이 처리할 수 있습니다. 코드를 자세히 살펴보겠습니다.
load_dataset 함수는 HuggingFace의 datasets 라이브러리에서 제공하는 핵심 함수입니다. 여기서 **name="sample-100BT"**는 100B 토큰 샘플 버전을 의미합니다.
전체 데이터셋은 너무 크기 때문에 실험용으로 샘플 버전을 사용하는 것이 일반적입니다. streaming=True 옵션이 핵심입니다.
이 옵션을 켜면 데이터가 실제로 다운로드되지 않고, 반복자 형태로 제공됩니다. 필요한 순간에만 네트워크에서 데이터를 가져오기 때문에 로컬 디스크 공간도 절약됩니다.
실무에서는 학습 전에 데이터를 미리 토큰화하여 바이너리 파일로 저장해 둡니다. get_shard_path 함수가 바로 이런 샤드 파일의 경로를 생성하는 역할을 합니다.
샤드란 큰 데이터를 여러 조각으로 나눈 것을 말합니다. 김개발 씨가 고개를 끄덕였습니다.
"아, 그래서 data 폴더에 .bin 파일이 그렇게 많았군요!" 박시니어 씨가 웃으며 답했습니다. "맞아, 이제 다음 단계로 넘어가 볼까?"
실전 팁
💡 - FineWeb-edu 버전은 교육적 콘텐츠에 특화되어 있어 LLM 학습에 더 적합합니다
- 실험 단계에서는 sample 버전으로 시작하고, 본격 학습 시 전체 데이터셋을 사용하세요
2. 분산 데이터 로딩
김개발 씨는 다음으로 dataloader.py 파일을 열었습니다. GPU가 8개나 달린 서버에서 학습을 돌린다고 하는데, 어떻게 하면 각 GPU에 데이터를 효율적으로 나눠줄 수 있을까요?
단순히 똑같은 데이터를 8번 복사해서 주면 될까요?
분산 데이터 로딩은 여러 GPU에서 동시에 학습할 때 각 GPU가 서로 다른 데이터를 받아 처리하도록 하는 기술입니다. 마치 대형 공장에서 컨베이어 벨트가 각 작업자에게 서로 다른 부품을 전달하는 것과 같습니다.
이를 통해 학습 시간을 GPU 개수만큼 단축할 수 있습니다.
다음 코드를 살펴봅시다.
import torch
from torch.utils.data import DataLoader, DistributedSampler
class DistributedDataLoader:
def __init__(self, dataset, batch_size, rank, world_size):
self.rank = rank # 현재 GPU 번호
self.world_size = world_size # 전체 GPU 개수
# 분산 샘플러: 각 GPU가 다른 데이터를 받도록 보장
sampler = DistributedSampler(
dataset,
num_replicas=world_size,
rank=rank,
shuffle=True
)
self.loader = DataLoader(
dataset,
batch_size=batch_size,
sampler=sampler,
num_workers=4,
pin_memory=True # GPU 전송 속도 향상
)
박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "자, 우리 서버에는 GPU가 8개 있어.
만약 8개가 모두 똑같은 데이터로 학습하면 어떻게 될까?" 김개발 씨가 잠시 생각하다가 답했습니다. "음...
같은 계산을 8번 반복하는 거니까 시간 낭비 아닌가요?" 박시니어 씨가 고개를 끄덕였습니다. "정확해.
그래서 분산 데이터 로딩이 필요한 거야." 분산 데이터 로딩은 마치 피자 배달부 여러 명이 각자 다른 구역을 맡아 배달하는 것과 같습니다. 배달부 8명이 모두 같은 주소로 가면 의미가 없겠죠.
각자 다른 구역을 담당해야 전체 배달 시간이 줄어듭니다. PyTorch에서는 DistributedSampler가 이 역할을 담당합니다.
전체 데이터셋을 GPU 개수로 나누어 각 GPU에 할당합니다. 예를 들어 1000개의 데이터가 있고 GPU가 8개라면, 각 GPU는 125개씩 서로 다른 데이터를 받게 됩니다.
코드에서 rank는 현재 GPU의 번호를 의미합니다. 0번부터 시작해서 7번까지, 총 8개의 GPU가 있다면 각각 0~7의 rank를 갖습니다.
world_size는 전체 GPU 개수입니다. DistributedSampler를 생성할 때 num_replicas에 전체 GPU 개수를, rank에 현재 GPU 번호를 전달합니다.
그러면 샘플러가 자동으로 "나는 8개 중 3번이니까, 3, 11, 19, 27... 번째 데이터만 가져가야지"라고 계산합니다.
shuffle=True 옵션은 매 에폭마다 데이터 순서를 섞어줍니다. 하지만 분산 환경에서는 주의가 필요합니다.
모든 GPU가 같은 시드로 셔플해야 데이터가 겹치지 않습니다. 다행히 DistributedSampler가 이를 자동으로 처리해 줍니다.
num_workers=4는 데이터를 미리 준비하는 워커 프로세스 개수입니다. GPU가 학습하는 동안 CPU 워커들이 다음 배치를 미리 준비해 둡니다.
마치 요리사가 요리하는 동안 보조 직원이 다음 재료를 손질해 두는 것과 같습니다. pin_memory=True는 CPU 메모리에서 GPU 메모리로 데이터를 전송할 때 속도를 높여주는 옵션입니다.
기술적으로는 페이지 락 메모리를 사용하여 DMA 전송을 가능하게 합니다. 김개발 씨가 코드를 따라 치면서 질문했습니다.
"그럼 에폭이 바뀔 때마다 샘플러를 업데이트해야 하나요?" 박시니어 씨가 답했습니다. "좋은 질문이야.
sampler.set_epoch(epoch) 메서드를 호출해야 매 에폭마다 다르게 셔플돼."
실전 팁
💡 - 매 에폭 시작 시 sampler.set_epoch(epoch)을 호출하여 셔플 시드를 갱신하세요
- num_workers는 CPU 코어 수와 메모리를 고려하여 설정하되, 보통 4~8 정도가 적당합니다
3. 토큰화된 데이터 샤드
김개발 씨는 data 폴더를 열어보았습니다. fineweb_train_000000.bin, fineweb_train_000001.bin...
수백 개의 바이너리 파일이 줄지어 있었습니다. "이 파일들은 뭐죠?
그리고 왜 이렇게 많이 나눠져 있는 거예요?"
데이터 샤드는 거대한 데이터셋을 여러 개의 작은 파일로 나눈 것입니다. 마치 백과사전을 여러 권으로 나누어 출판하는 것과 같습니다.
각 샤드 파일에는 미리 토큰화된 정수 배열이 저장되어 있어, 학습 시 즉시 모델에 입력할 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
import os
def create_data_shard(tokens, shard_id, output_dir="data/fineweb"):
# 토큰 배열을 uint16으로 변환하여 저장
# GPT-2 어휘 크기(50257)는 uint16(65535)으로 충분
tokens_np = np.array(tokens, dtype=np.uint16)
shard_path = f"{output_dir}/fineweb_train_{shard_id:06d}.bin"
tokens_np.tofile(shard_path)
print(f"Saved shard {shard_id}: {len(tokens):,} tokens")
def load_data_shard(shard_path):
# 메모리 매핑으로 효율적인 읽기
tokens = np.memmap(shard_path, dtype=np.uint16, mode='r')
return tokens
# 샤드 크기 계산: 100M 토큰 * 2바이트 = 200MB per shard
SHARD_SIZE = 100_000_000 # 1억 토큰
박시니어 씨가 터미널을 열어 파일 목록을 보여주었습니다. "이 .bin 파일들이 바로 샤드야.
전체 학습 데이터를 1억 토큰 단위로 잘라 놓은 거지." 김개발 씨가 물었습니다. "왜 하나의 큰 파일로 만들지 않고 나누는 거죠?" 박시니어 씨가 설명을 이어갔습니다.
"몇 가지 이유가 있어. 첫째, 파일 하나가 너무 크면 읽고 쓰는 데 오래 걸려.
둘째, 분산 학습 시 각 노드가 서로 다른 샤드를 읽을 수 있어. 셋째, 일부가 손상되어도 전체를 잃지 않아." 비유하자면, 샤드는 마치 물류 창고에서 상품을 파렛트 단위로 나누어 보관하는 것과 같습니다.
창고 전체를 하나의 거대한 공간으로 쓰면 물건을 찾기 어렵지만, 파렛트 단위로 정리하면 필요한 것만 빠르게 꺼낼 수 있습니다. 코드에서 dtype=np.uint16이 중요합니다.
토큰은 결국 정수입니다. GPT-2 토크나이저의 어휘 크기는 50,257개이므로, 0부터 50,256까지의 정수로 표현됩니다.
uint16은 0부터 65,535까지 표현할 수 있으므로 충분합니다. uint16을 사용하면 토큰 하나당 2바이트만 사용합니다.
만약 int32를 사용했다면 4바이트가 필요했을 것입니다. 38B 토큰 기준으로 계산하면, uint16은 약 76GB, int32는 약 152GB가 필요합니다.
무려 76GB를 절약하는 셈입니다. np.memmap은 메모리 매핑이라는 기술을 사용합니다.
파일 전체를 메모리에 올리지 않고, 마치 메모리인 것처럼 접근할 수 있게 해줍니다. 실제로 접근하는 부분만 디스크에서 읽어오기 때문에 수백 GB 파일도 부담 없이 다룰 수 있습니다.
샤드 파일명의 {shard_id:06d} 부분은 6자리 숫자로 패딩하는 포맷입니다. 000000, 000001, 000002...
이렇게 정렬되면 파일 시스템에서 순서대로 나열되어 관리하기 편합니다. 실무에서 샤드 크기는 보통 1억 토큰(약 200MB) 정도로 설정합니다.
너무 작으면 파일 개수가 많아져 관리가 번거롭고, 너무 크면 읽기 성능이 떨어집니다. 이 균형점을 찾는 것이 중요합니다.
김개발 씨가 파일 크기를 확인했습니다. "정말 각 파일이 200MB 정도네요.
1억 토큰 곱하기 2바이트... 맞아떨어지네요!" 박시니어 씨가 흐뭇하게 웃었습니다.
"이제 감이 오기 시작했구나."
실전 팁
💡 - 샤드 크기는 100M~500M 토큰 사이가 적당하며, 디스크 I/O와 메모리 상황에 따라 조절하세요
- memmap을 사용할 때는 mode='r'로 읽기 전용으로 열어 실수로 데이터가 변경되는 것을 방지하세요
4. 38B 토큰 학습 데이터
프로젝트 문서를 보던 김개발 씨의 눈이 커졌습니다. "38B 토큰이요?
38억 개의 토큰을 어떻게 준비하는 거죠?" 숫자가 너무 커서 감이 오지 않았습니다. 책 한 권이 보통 5만 단어라고 하면, 대체 몇 권 분량일까요?
38B(380억) 토큰은 대규모 언어 모델 학습에 사용되는 데이터 규모입니다. 일반 책으로 치면 약 50만 권에 해당하는 엄청난 양입니다.
이 정도 규모의 데이터를 효율적으로 전처리하고 관리하려면 체계적인 파이프라인이 필수입니다.
다음 코드를 살펴봅시다.
from transformers import GPT2Tokenizer
import multiprocessing as mp
from tqdm import tqdm
def prepare_training_data(dataset, output_dir, target_tokens=38_000_000_000):
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
current_shard = []
shard_id = 0
total_tokens = 0
for sample in tqdm(dataset, desc="Tokenizing"):
# 텍스트를 토큰으로 변환
tokens = tokenizer.encode(sample["text"])
current_shard.extend(tokens)
# 샤드가 충분히 크면 저장
if len(current_shard) >= SHARD_SIZE:
create_data_shard(current_shard[:SHARD_SIZE], shard_id, output_dir)
current_shard = current_shard[SHARD_SIZE:]
shard_id += 1
total_tokens += SHARD_SIZE
if total_tokens >= target_tokens:
break
print(f"Total: {total_tokens:,} tokens in {shard_id} shards")
김개발 씨가 계산기를 두드렸습니다. "38,000,000,000...
0이 10개네요. 380억 토큰이라니." 박시니어 씨가 비유를 들어 설명했습니다.
"영어 단어 하나가 평균 1.3개의 토큰으로 변환된다고 치면, 약 290억 단어야. 일반 소설책 한 권이 5만 단어 정도니까..." 김개발 씨가 암산을 시도했습니다.
"290억 나누기 5만... 약 58만 권이요?" 박시니어 씨가 고개를 끄덕였습니다.
"그래, 도서관 하나를 통째로 학습시키는 거지." 이렇게 거대한 데이터를 준비하는 과정을 전처리 파이프라인이라고 합니다. 원본 텍스트 데이터를 토크나이저로 변환하고, 샤드 파일로 저장하는 일련의 과정입니다.
파이프라인이라는 이름처럼 데이터가 한쪽에서 들어가 다른 쪽으로 나오는 구조입니다. 코드에서 GPT2Tokenizer를 사용하는 이유가 있습니다.
GPT-2 토크나이저는 BPE(Byte Pair Encoding) 방식을 사용하며, 50,257개의 어휘를 갖습니다. 많은 현대 LLM이 이 토크나이저를 기반으로 하거나 유사한 방식을 사용합니다.
tokenizer.encode() 함수가 핵심입니다. "Hello, world!"라는 문자열을 [15496, 11, 995, 0]과 같은 정수 리스트로 변환합니다.
이 정수들이 바로 토큰 ID입니다. 모델은 문자열이 아닌 이 숫자들을 입력받아 학습합니다.
코드의 로직을 살펴보면, 데이터셋에서 텍스트를 하나씩 가져와 토큰화한 뒤 current_shard 리스트에 누적합니다. 이 리스트가 1억 개를 넘으면 샤드 파일로 저장하고 비웁니다.
마치 양동이에 물을 받다가 가득 차면 버리는 것과 같습니다. tqdm은 진행 상황을 시각적으로 보여주는 라이브러리입니다.
380억 토큰을 처리하는 데는 수 시간이 걸리기 때문에, 현재 얼마나 진행되었는지 알 수 있어야 마음이 편합니다. 진행률 바가 조금씩 차오르는 것을 보며 기다리는 것이 막연히 기다리는 것보다 낫습니다.
실제 프로덕션 환경에서는 multiprocessing을 활용하여 여러 CPU 코어에서 병렬로 토큰화합니다. 코어가 32개인 서버라면 32배 빠르게 처리할 수 있습니다.
이런 최적화 없이는 전처리에만 며칠이 걸릴 수 있습니다. 김개발 씨가 질문했습니다.
"그럼 한 번 전처리해 두면 계속 재사용하는 건가요?" 박시니어 씨가 답했습니다. "맞아.
토큰화는 시간이 오래 걸리니까 미리 해두고, 학습할 때는 샤드 파일만 읽으면 돼. 그래서 이 전처리 과정이 중요한 거야."
실전 팁
💡 - 전처리는 시간이 오래 걸리므로 멀티프로세싱을 활용하고, 중간 결과를 저장하여 재시작에 대비하세요
- 토크나이저 버전을 기록해 두어 나중에 동일한 토큰화 결과를 재현할 수 있도록 하세요
5. 멀티 GPU 데이터 분배
드디어 실제 학습을 시작할 시간입니다. 김개발 씨 앞에는 A100 GPU 8개가 장착된 서버가 있습니다.
이 8개의 GPU가 동시에 학습하려면 데이터를 어떻게 나눠줘야 할까요? 그리고 각 GPU가 서로의 학습 결과를 어떻게 공유할까요?
멀티 GPU 데이터 분배는 분산 학습의 핵심입니다. 각 GPU는 전체 데이터의 일부만 받아 독립적으로 학습하고, 그래디언트를 모아 모델을 업데이트합니다.
마치 그룹 과제에서 각자 다른 부분을 조사한 뒤 결과를 합치는 것과 같습니다.
다음 코드를 살펴봅시다.
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
def setup_distributed(rank, world_size):
# 분산 환경 초기화
dist.init_process_group(
backend="nccl", # GPU 간 통신에 최적화된 백엔드
rank=rank,
world_size=world_size
)
torch.cuda.set_device(rank)
class ShardedDataLoader:
def __init__(self, data_dir, batch_size, rank, world_size, seq_len=1024):
self.shards = sorted(glob.glob(f"{data_dir}/*.bin"))
self.batch_size = batch_size
self.seq_len = seq_len
# 각 GPU가 다른 샤드를 담당
self.my_shards = self.shards[rank::world_size]
self.current_shard_idx = 0
self.tokens = self._load_shard(0)
self.position = 0
def _load_shard(self, idx):
return np.memmap(self.my_shards[idx], dtype=np.uint16, mode='r')
박시니어 씨가 서버실로 김개발 씨를 데려갔습니다. "이게 우리 학습 서버야.
NVIDIA A100 GPU가 8개 들어 있지." 김개발 씨가 서버의 윙윙거리는 소리를 들으며 물었습니다. "8개가 어떻게 협력하는 거죠?" 박시니어 씨가 설명을 시작했습니다.
"분산 데이터 병렬 처리(DDP)라는 방식을 사용해. 쉽게 말하면, 8개의 GPU가 각자 다른 데이터로 학습하고, 결과를 모아서 하나의 모델을 업데이트하는 거야." 비유하자면 이렇습니다.
8명의 학생이 1000페이지짜리 책을 읽고 요약해야 한다고 합시다. 혼자 읽으면 오래 걸리지만, 각자 125페이지씩 나누어 읽고 요약을 합치면 8배 빨라집니다.
GPU도 마찬가지입니다. 코드에서 dist.init_process_group은 GPU들이 서로 통신할 수 있도록 초기화하는 함수입니다.
**backend="nccl"**은 NVIDIA가 만든 GPU 간 통신 라이브러리입니다. NCCL은 NVLink나 InfiniBand 같은 고속 연결을 활용하여 GPU 간에 데이터를 빠르게 주고받습니다.
ShardedDataLoader 클래스가 핵심입니다. 생성자에서 **self.shards[rank::world_size]**라는 슬라이싱을 볼 수 있습니다.
예를 들어 샤드가 400개이고 GPU가 8개라면, GPU 0번은 0, 8, 16, 24... 번째 샤드를, GPU 1번은 1, 9, 17, 25...
번째 샤드를 담당합니다. 이렇게 하면 GPU들이 절대 같은 데이터를 읽지 않습니다.
데이터 중복 없이 전체 데이터셋을 효율적으로 활용할 수 있습니다. 마치 배달 구역을 나누듯이 샤드를 나누는 것입니다.
seq_len=1024는 시퀀스 길이, 즉 한 번에 모델에 입력하는 토큰 개수입니다. GPT 계열 모델은 보통 1024나 2048 토큰을 한 번에 처리합니다.
문맥을 이해하려면 충분한 길이가 필요하지만, 너무 길면 메모리가 부족해집니다. 학습이 진행되면 각 GPU는 독립적으로 순전파와 역전파를 수행합니다.
그런 다음 AllReduce 연산을 통해 8개 GPU의 그래디언트를 평균냅니다. 이 평균 그래디언트로 모든 GPU의 모델 가중치를 동일하게 업데이트합니다.
김개발 씨가 감탄했습니다. "그래서 8개 GPU가 마치 하나의 큰 GPU처럼 동작하는 거군요!" 박시니어 씨가 덧붙였습니다.
"맞아, 그래서 유효 배치 크기가 8배가 되는 거야. 개별 GPU 배치 크기가 32라면, 전체로는 256이 되는 거지."
실전 팁
💡 - 분산 학습 시 반드시 모든 GPU에서 동일한 시드를 사용하여 모델 초기화를 동기화하세요
- GPU 간 통신 병목을 줄이려면 그래디언트 누적(gradient accumulation)을 고려하세요
6. 배치 사이즈와 메모리
학습을 시작하자마자 김개발 씨의 화면에 무시무시한 에러가 떴습니다. "CUDA out of memory".
GPU 메모리가 부족하다는 뜻입니다. 배치 사이즈를 조금만 줄이면 해결된다는데, 배치 사이즈와 메모리는 어떤 관계가 있을까요?
배치 사이즈는 한 번의 학습 스텝에서 처리하는 샘플 개수입니다. 배치 사이즈가 클수록 학습이 안정적이고 빠르지만, GPU 메모리를 더 많이 사용합니다.
마치 한 번에 많은 접시를 나르면 빠르지만, 들고 있기 힘든 것과 같습니다. 이 균형을 맞추는 것이 실무에서 중요합니다.
다음 코드를 살펴봅시다.
def calculate_memory_requirement(
batch_size, seq_len, vocab_size, hidden_dim, num_layers
):
# 모델 파라미터 메모리 (FP16 기준, 파라미터당 2바이트)
params = vocab_size * hidden_dim # 임베딩
params += num_layers * (4 * hidden_dim * hidden_dim) # 어텐션 + FFN
model_memory = params * 2 # FP16
# 활성화 메모리 (배치 크기에 비례)
activation_per_layer = batch_size * seq_len * hidden_dim * 4 # FP32
activation_memory = activation_per_layer * num_layers
# 그래디언트 메모리 (모델과 동일)
gradient_memory = model_memory
# 옵티마이저 상태 (Adam: 모멘텀 + 분산, 각각 FP32)
optimizer_memory = params * 4 * 2 # 2개 상태, 각 4바이트
total_gb = (model_memory + activation_memory +
gradient_memory + optimizer_memory) / (1024**3)
return total_gb
# A100 80GB에서 적절한 배치 사이즈 찾기
print(calculate_memory_requirement(32, 1024, 50257, 768, 12))
"CUDA out of memory"라는 에러를 본 김개발 씨는 당황했습니다. 박시니어 씨가 다가와 화면을 보더니 말했습니다.
"아, 배치 사이즈가 너무 크구나. GPU 메모리가 감당을 못 하는 거야." 김개발 씨가 물었습니다.
"배치 사이즈랑 메모리가 무슨 관계인가요?" 박시니어 씨가 화이트보드에 수식을 적기 시작했습니다. GPU 메모리는 크게 네 가지를 저장합니다.
첫째, 모델 파라미터입니다. 가중치 행렬들이죠.
이건 배치 사이즈와 무관하게 고정입니다. 12억 파라미터 모델이라면 FP16 기준 약 2.4GB가 필요합니다.
둘째, 활성화 값(activation)입니다. 순전파 과정에서 각 레이어의 출력을 저장해 두어야 역전파 때 사용할 수 있습니다.
이 부분이 배치 사이즈에 비례해서 커집니다. 배치 사이즈가 2배면 활성화 메모리도 2배입니다.
셋째, 그래디언트입니다. 역전파 과정에서 계산된 기울기를 저장합니다.
이건 모델 크기와 같습니다. 넷째, 옵티마이저 상태입니다.
Adam 옵티마이저는 각 파라미터에 대해 모멘텀과 분산을 저장하므로 모델 크기의 2배가 필요합니다. 코드에서 각 요소를 계산하는 방식을 볼 수 있습니다.
activation_per_layer가 batch_size에 곱해지는 것을 주목하세요. 이 부분이 배치 사이즈에 따라 급격히 증가하는 주범입니다.
예를 들어 GPT-2 Small(768 히든, 12 레이어)을 A100 80GB에서 학습한다고 합시다. 배치 사이즈 32에서는 약 45GB를 사용합니다.
64로 올리면 활성화 메모리가 2배가 되어 약 70GB가 됩니다. 128은 메모리 초과입니다.
실무에서는 그래디언트 체크포인팅이라는 기법을 사용하기도 합니다. 일부 활성화 값을 저장하지 않고, 역전파 시 다시 계산합니다.
메모리를 아끼는 대신 계산 시간이 늘어나는 트레이드오프입니다. 또 다른 방법은 그래디언트 누적(gradient accumulation)입니다.
배치 사이즈 32를 4번 누적하면 유효 배치 사이즈 128과 같은 효과를 얻습니다. 메모리는 32만큼만 사용하면서요.
김개발 씨가 배치 사이즈를 32에서 16으로 줄였더니 학습이 정상적으로 시작되었습니다. "그래디언트 누적을 2로 설정하면 유효 배치 사이즈는 32로 유지할 수 있겠네요!" 박시니어 씨가 엄지를 치켜들었습니다.
"정확해. 이제 진짜 딥러닝 엔지니어가 되어가고 있구나."
실전 팁
💡 - 메모리 부족 시 배치 사이즈를 줄이고 그래디언트 누적을 늘려 유효 배치 사이즈를 유지하세요
- 혼합 정밀도(FP16/BF16) 학습을 사용하면 메모리를 절반으로 줄이면서 속도도 향상됩니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.