학습 · 2026-10-05
학습 준비 1: 저장소와 RunPod 환경
편별 학습 코드와 공통 실행 도구를 구분하고, 두 API 키로 GPU 실험을 실행한 뒤 로그·프로파일·결과를 회수하는 과정을 따라갑니다.
학습 시리즈에서는 같은 모델에 한 가지 변경을 적용하고, 계산·메모리·학습 결과가 어떻게 달라지는지 비교합니다. 이 비교를 하려면 모델 코드뿐 아니라 실행 환경, 데이터, 설정과 관측 결과를 함께 관리해야 합니다. 지난 결과의 숫자만 남고 어떤 코드로 실행했는지 모르면 다음 실험의 기준으로 사용하기 어렵습니다.
첫 준비편에서는 실험을 정의하고, GPU에서 실행하고, 결과를 가져오는 경로를 마련하겠습니다. 학습은 RunPod의 H100 한 장에서 실행하고, loss와 지표는 W&B에 기록합니다. 로컬 컴퓨터는 실행을 요청하고 로그와 결과를 회수합니다. 데이터에서 모델 입력을 만드는 과정은 다음 준비편에서 살펴보겠습니다.
한 실험에 무엇을 묶을까
“같은 학습 코드를 실행했다”는 것만으로 같은 실험이 되지는 않습니다. 문서의 선택이나 순서, tokenizer, 배치 크기, 계산 정밀도가 바뀌면 학습 결과나 실행 비용도 달라질 수 있습니다. 같은 설정 파일을 사용하더라도 실제로 설치된 라이브러리와 GPU가 다를 수 있습니다.
그래서 실행마다 다음 정보를 함께 보관합니다.
| 구분 | 남기는 정보 | 확인할 질문 |
|---|---|---|
| 코드 | Git commit, 전송한 소스의 hash | 어떤 구현을 실행했는가? |
| 환경 | 이미지 digest, 실제 라이브러리·CUDA 버전, GPU | 어디에서 무엇으로 실행했는가? |
| 데이터 | 원본·tokenizer revision, split 정책, 파일 hash | 같은 입력을 다시 만들 수 있는가? |
| 설정 | 모델, 배치, 정밀도, optimizer, 관측 스텝 | 무엇을 고정하고 무엇을 바꿨는가? |
| 결과 | loss, 지표, trace, snapshot, W&B run | 그 설정에서 무엇을 관찰했는가? |
Commit은 저장소에 기록된 버전을 가리킵니다. 아직 커밋하지 않은 변경이 있는 상태로 실험할 수도 있으므로, 실제로 GPU로 보낸 파일의 hash도 남깁니다. 결과를 설명할 때는 이 실행 기록을 기준으로 삼습니다. Seed도 함께 고정하지만, seed 하나만으로 서로 다른 환경의 연산 결과가 항상 같아지는 것은 아닙니다.
편별 학습 코드와 공통 실행 도구
학습 코드는 공개 블로그 저장소의 labs/training/에서 관리합니다. RunPod 실행과 자원 정리는 labs/runner/가 담당합니다. 현재 파일 구조의 주요 부분은 다음과 같습니다.
labs/
runner/
experiment_runner/ # Pod 생성·로그·결과 회수·종료
training/
training_lab/
data.py # 문서 cache·분할·packing
model.py # 공통 scratch Decoder
engine.py # 현재 기준선의 학습 루프
measurement.py # 지표·trace·snapshot 수집
runpod.py # 학습 작업을 공통 runner에 연결
remote.py # GPU 환경의 학습 진입점
experiments/
smoke.json
corrected.json
chapters/
01-forward-backward/
train.py # 1편의 독립 실행 예제
교육용 코드에서는 독자가 이번 편의 전체 실행 순서를 읽을 수 있어야 합니다. 따라서 각 편의 train.py와 실행 진입점은 독립적으로 유지하고, 학습 루프의 중복을 허용합니다. Checkpointing이나 offload 기능, 데이터·모델·측정 구성요소는 필요한 범위에서 공유하되, 계산·전송·대기·해제 순서는 해당 편의 코드에서 드러나게 합니다.
현재 engine.py는 먼저 만든 사전학습 기준선입니다. 앞으로 추가할 모든 편이 이미 독립 구현되어 있다는 뜻은 아닙니다. 새 기능을 구현할 때 이 원칙을 적용하고, 글에서 사용한 편별 코드와 공유 모듈의 조합을 commit이나 태그에 연결하겠습니다.
공통 runner는 모델이나 loss의 의미를 알 필요가 없습니다. 학습 쪽에서 필요한 파일 목록, 설치·실행 명령, 결과 경로를 전달하면 runner가 실행과 회수를 담당합니다. 이 경계 덕분에 추론이나 양자화 실험도 같은 실행 도구를 사용하면서 각자의 실험 코드에 집중할 수 있습니다.
로컬 컴퓨터와 GPU 환경의 역할
RunPod의 Pod는 GPU와 CPU, 메모리, 디스크가 붙은 실행 환경입니다. 이 실습에서는 로컬 컴퓨터에 GPU나 Docker를 설치하지 않아도 됩니다. 로컬의 실행 관리 프로그램은 Python 3.9 이상과 표준 라이브러리로 동작하고, 학습에 필요한 의존성은 Pod 안에 설치합니다.
제 RunPod 추천 링크로 새로 가입하고 플랫폼에서 10달러 이상 사용하면 가입자도 추가 크레딧을 받을 수 있습니다. 저도 추천 보상으로 크레딧을 받을 수 있으며, 이 크레딧은 이 시리즈의 GPU 실험을 이어 가는 데 큰 도움이 됩니다. 혜택과 적용 조건은 RunPod 공식 안내를 참고해 주세요.
| 위치 | 담당하는 작업 | 보관하는 것 |
|---|---|---|
| 로컬 컴퓨터 | 실행 요청, 상태·로그 확인, 결과 다운로드 | 코드, 두 키, 제어 상태, 회수한 결과 |
| RunPod | 의존성 설치, 데이터 준비, GPU 학습·검증 | 실행 소스, 데이터 cache, 실행 중 결과 |
| W&B | 실행별 설정·지표·loss 곡선 기록 | run과 선택한 실험 산출물 |
RunPod API 키는 로컬에서 Pod를 관리하는 데 사용합니다. W&B API 키는 Pod의 학습 프로세스가 실험 기록을 저장하는 데 사용합니다. RunPod 키는 Pod로 전달하지 않습니다. 소스도 폴더 전체를 복사하는 대신 학습에 필요한 파일의 허용 목록으로 전송합니다.
저장소 루트에서 다음과 같이 시작합니다.
cd labs/training
cp .env.example .env
편집기로 .env를 열어 두 값을 채웁니다. 아래 값은 형식 예시입니다.
RUNPOD_API_KEY=발급받은_RunPod_키
WANDB_API_KEY=발급받은_WandB_키
.env는 Git에서 제외합니다. W&B의 다른 팀이나 프로젝트를 사용하려면 WANDB_ENTITY, WANDB_PROJECT를 지정할 수 있습니다. 기본 프로젝트는 llm-training-lab입니다. 실제 실행에는 RunPod 계정의 크레딧과 요청한 GPU의 가용성도 필요합니다.
작은 실행부터 연결 확인하기
처음부터 긴 학습을 시작하기보다, 작은 모델로 전체 경로를 확인합니다. smoke는 4층·폭 256의 모델로 6번 업데이트하는 설정입니다. 먼저 실행 구성을 확인한 다음 Pod를 생성합니다.
python3 -m training_lab runpod --config smoke --dry-run
python3 -m training_lab runpod --config smoke
--dry-run은 설정과 전송할 코드의 구성을 확인하며 Pod를 만들지 않습니다. 실제 실행에서는 Pod 생성 → 소스 전송 → 의존성 설치 → 데이터 준비 → 학습·검증 → 결과 회수 → Pod 삭제 순서로 진행합니다. 로컬의 실행 관리 프로그램은 결과 회수와 종료가 끝날 때까지 유지합니다.
현재 작은 연결 확인용 smoke와 profile은 초기 구현의 FlexAttention·token-budget 경로를 사용합니다. 이후 배치와 loss 구현을 수정한 기준선은 corrected입니다. 이 설정은 12층·폭 768·12 heads의 직접 구현한 GPT 계열 Decoder를 사용하며, 파라미터는 126,716,160개입니다. GPT-2 tokenizer를 사용하지만 GPT-2 사전학습 가중치를 불러오지는 않습니다.
python3 -m training_lab runpod --config corrected
corrected는 명시적인 pack 배치, variable-length FlashAttention, Liger의 통합 LM head·CE를 사용합니다. 계산 원리와 각 최적화의 내부는 이후 편에서 설명하고, 여기서는 검증한 실행 설정의 이름과 결과를 연결하겠습니다. 1편의 SmolLM2 예제는 사전학습된 모델의 다음 토큰 예측을 관찰하기 위한 별도 실습입니다.
실행 환경은 CUDA 12.8·PyTorch 2.8 이미지의 digest를 고정하고 시작합니다. 다만 corrected는 그 안에서 PyTorch 2.14.1 CUDA 12.6 wheel과 Liger 0.8.4를 설치합니다. 따라서 이미지 이름만 보고 실제 실행 버전을 판단하지 않고 environment.json을 확인해야 합니다. 파라미터·gradient·AdamW 상태는 FP32이며, GPU 계산에는 BF16 autocast를 사용합니다. 별도의 master parameter 복제본은 없습니다.
결과를 가져온 뒤 자원 정리하기
실행이 끝나면 결과는 로컬의 .runs/<pod-id>/로 회수됩니다. report.html을 브라우저에서 열면 loss와 지표, 관측 파일을 확인할 수 있습니다.
| 결과 파일 | 읽을 내용 |
|---|---|
config.json, environment.json |
요청한 설정과 실제 실행 환경 |
data-manifest.json, data-stats.json |
데이터 identity, 길이·packing 통계 |
metrics.jsonl, summary.json |
스텝별 값과 측정 구간 요약 |
wandb.json |
loss 곡선을 확인할 W&B run 링크 |
timeline.json |
CPU 작업과 CUDA kernel의 시간 흐름 |
memory-snapshot.pickle, memory-events.json |
allocator 이력과 관측 시점 |
checkpoint.pt |
모델·optimizer·RNG·sampler 복원 상태 |
공통 runner는 다운로드한 결과의 SHA-256을 확인한 뒤 Pod를 삭제하고, API에서 삭제 여부도 확인합니다. 학습 성공과 자원 정리 성공은 별개의 상태로 기록합니다. 학습이 실패해도 로그 회수가 완료되면 자원을 정리할 수 있고, 학습이 성공했어도 결과 다운로드에 실패하면 먼저 복구가 필요합니다.
중단·시간 초과·다운로드 실패 때는 Pod 정지를 시도해 디스크를 보존합니다. 이 경우 정지된 디스크의 비용은 남을 수 있습니다. 로컬 연결이 끊겼다면 자동 정지를 보장할 수 없으므로 RunPod 콘솔에서 상태를 확인합니다. 제어 상태는 .runpod/에 저장하며 접근 정보를 포함하므로 Git에 넣지 않습니다.
상태 파일의 실제 경로를 사용해 모니터링과 회수를 이어갈 수 있습니다. 아래 STATE.json은 실행 때 만들어진 파일로 바꿉니다.
python3 -m training_lab monitor .runpod/STATE.json
python3 -m training_lab collect .runpod/STATE.json --output .runs/recovered
python3 -m training_lab terminate .runpod/STATE.json
정지된 Pod의 결과를 회수할 때는 먼저 콘솔에서 재시작합니다. Pod 삭제 후에는 그 디스크를 cache로 사용할 수 없습니다. 반복 실행에서 cache를 유지하려면 기존 network volume을 별도로 연결할 수 있지만, volume은 Pod 삭제와 별개로 관리합니다.
성능 측정과 상세 관찰을 구분하기
W&B의 loss 곡선은 업데이트가 진행되면서 학습·검증 결과가 어떻게 변하는지 보여 줍니다. W&B run마다 설정과 지표를 연결해 기준선과 변경안을 비교합니다. Timeline과 메모리 snapshot은 다른 질문에 답합니다. 어떤 계산이 오래 걸렸는지, 어떤 값이 언제 할당되고 해제되었는지를 살펴보는 도구입니다.
작은 모델의 상세 관찰만 실행하려면 다음 명령을 사용합니다.
python3 -m training_lab runpod --config profile
corrected에서는 21–22번째 업데이트의 CPU/CUDA timeline을 수집하고, 100번째 업데이트에서 메모리 이력을 기록합니다. Snapshot은 마지막 시점의 사용량 숫자 하나만 남기는 것이 아닙니다. 해당 업데이트의 zero_grad, forward, backward, optimizer 전후 시점과 할당·해제 이력을 함께 남깁니다. Timeline은 Perfetto, snapshot은 PyTorch memory visualizer에서 열 수 있습니다.
계측도 실행 비용을 추가합니다. 그래서 정상 처리량 요약에서는 초기 10번의 워밍업과 profile·snapshot 스텝을 제외합니다. 여기서 워밍업은 측정에서 제외하는 구간이며 학습률 스케줄을 뜻하지 않습니다.
| 지표 | 이 실습에서의 의미 |
|---|---|
tokens/s/GPU |
정상 측정 구간의 실제 정답 토큰 합을 update 시간 합으로 나눈 값. 현재는 GPU 한 장 |
| MFU 추정 | 모델의 forward·backward 행렬 연산량 추정치를 시간과 GPU 이론 peak로 나눈 비율 |
| Peak allocated | 측정 구간에서 PyTorch tensor에 할당된 메모리의 최대값 |
| Peak reserved | 같은 구간에서 PyTorch allocator가 확보한 메모리의 최대값 |
| 전체 경과 시간 | 설치·데이터 준비·검증·저장 등도 포함하는 별도 실행 시간 |
Update 시간에는 packing, H2D, forward, backward, clipping과 optimizer가 들어갑니다. 검증·logging·checkpoint·profile export는 그 측정 구간에서 제외합니다. 스텝별 처리량을 단순 평균하지 않고 총 토큰 ÷ 총 시간으로 계산합니다. 데이터 cache의 토큰 수와 학습 중 처리한 토큰 수는 다릅니다. 같은 cache를 반복해서 사용할 수 있기 때문입니다.
MFU는 실제 kernel FLOP counter가 아니라 추정치입니다. 문서 사이의 attention 쌍은 제외하고, 독립 조각별 행렬 연산량을 사용합니다. 이 기준선은 NVIDIA H100 사양의 sparse 수치와 구별해 H100 SXM의 dense BF16 peak 989 TFLOPS를 분모로 삼습니다. Normalization·optimizer 등 모든 연산을 포함하지 않으며, allocated/reserved도 allocator 밖의 GPU 메모리를 모두 설명하지는 않습니다.
실제 H100 실행에서 확인한 것
아래는 2026년 10월 4일 corrected 구성으로 수행한 한 번의 연결 확인 실행입니다. H100 한 장에서 100번 업데이트했고, 로그·timeline·snapshot·checkpoint를 회수한 뒤 Pod 삭제를 확인했습니다. Loss 곡선은 당시 W&B 실행에 연결됩니다.
| 항목 | 기록한 값 |
|---|---|
| 모델 파라미터 | 126,716,160 |
| 전체 학습에서 처리한 정답 토큰 | 14,276,156 |
| 고정 검증 CE, 학습 전 → 100 updates 뒤 | 10.981665 → 6.847176 |
| 정상 측정 구간 | 87 updates |
| 정상 처리량 | 222,427 tokens/s/GPU |
| MFU 추정 | 20.68% |
| 정상 구간 peak allocated / reserved | 47.88 / 49.49 GiB |
이 실행은 전체 경로가 동작한다는 근거입니다. 최적 배치나 최종 모델 품질을 확정한 결과는 아닙니다. corrected의 학습 배치는 35개 논리 pack이며 실제 토큰 수는 문서 조합에 따라 달라집니다. 이후 기법을 비교할 때는 초기 가중치·학습 토큰·검증 입력·측정 범위를 맞춰야 합니다.
첫 초기 실행에서는 결과 회수 중 로그 symlink 문제를 발견해 복구 절차를 거쳤습니다. 위 표는 그 뒤 수정한 엔진의 실행 기록입니다. 초기 구현, 수정한 코드, 서로 다른 배치의 결과를 하나의 실험처럼 섞지 않고 각각 남깁니다.
실행과 결과 보관 경로가 마련되었으므로, 다음 준비편에서는 FineWeb 문서가 어떤 토큰과 정답으로 바뀌어 모델에 들어가는지를 살펴보겠습니다.