로컬 그래디언트가 곱해짐
전방 패스
ANDREA-120M의 전방 패스는 입력을 일련의 연산으로 통과시킵니다:
x = embed(token_ids) # 토큰 임베딩
for layer in 12_layers:
x = x + attn(LN(x)) # 어텐션 서브레이어
x = x + mlp(LN(x)) # MLP 서브레이어
logits = LN(x) @ embed.T # 공유 출력 프로젝션
loss = cross_entropy(logits, targets)
각 연산은 입력 텐서를 읽고 출력 텐서를 생성합니다. 순전파는 이 배치에 대한 교차 엔트로피 손실이라는 단일 스칼라에서 종료됩니다.
역전파
훈련은 손실을 감소시키는 방향으로 가중치를 업데이트합니다. 업데이트 방향을 얻기 위해 엔진은 다음이 필요합니다:
모델의 모든 학습 가능한 W에 대한 dL/dW
연쇄 법칙이 이를 제공합니다. loss = f(g(h(x)))와 같은 연쇄에 대해:
dL/dx = (dL/df) * (df/dg) * (dg/dh) * (dh/dx)
각 요인은 로컬 그래디언트입니다: 하나의 연산 출력이 입력이 약간 변경될 때 어떻게 변하는지. 로컬 그래디언트를 그래프를 통해 역방향으로 곱하면 손실 신호가 모든 가중치로 전파됩니다.
역방향 미분
Backprop은 역순으로 그래디언트를 계산합니다: dL/dlogits = 1에서 시작하여 크로스-엔트로피, 출력 프로젝션, 레이어 노름, 12개의 트랜스포머 블록, 임베딩을 거쳐 역방향으로 진행합니다. 각 단계에서 들어오는 그래디언트를 로컬 야코비안으로 곱합니다.
역방향 모드는 출력이 단일 스칼라(손실)이고 입력(가중치)이 많을 때 효율적입니다. 한 번의 역방향 패스로 모델의 모든 가중치에 대한 그래디언트를 생성합니다. 전방향 모드는 가중치당 한 번의 패스가 필요합니다; ANDREA-120M(~120M 가중치)의 경우 전방향 모드는 불가능합니다.
왜 역방향 모드인가
모든 순방향 연산에 후방 쌍이 제공됩니다
쌍 매칭 규칙
microgpt_cuda.cu는 모든 연산에 대해 두 개의 CUDA 커널을 제공합니다: 순방향 출력을 계산하는 하나와 출력 그래디언트를 주어 입력 그래디언트를 계산하는 하나. 쌍 매칭은 일대일입니다:
| Forward kernel | Backward kernel | Operation |
|---|---|---|
k_embed_fwd | k_embed_bwd | 토큰 임베딩 조회 |
k_layernorm_fwd | k_layernorm_bwd | 레이어 정규화 |
k_attn_qkv_fwd | k_attn_qkv_bwd | Q, K, V 프로젝션 |
k_attn_fwd | k_attn_bwd | 스케일드 도트-프로덕트 어텐션 |
k_attn_out_fwd | k_attn_out_bwd | 출력 프로젝션 W_O |
k_mlp_fwd | k_mlp_bwd | MLP (GELU 포함) |
k_residual_add | k_residual_add_bwd | 잔차 연결 |
k_loss_fwd | k_loss_bwd | 교차 엔트로피 손실 |
여덟 개의 연산 쌍이 전체 트랜스포머를 다룹니다. 추가로 몇 개의 유틸리티 커널: k_grad_norm_partial, k_grad_norm_final, k_grad_scale (그래디언트 클리핑을 위해, 활동 75 참조).
Backward 커널의 역할
후속 레이어에서 유입되는 그래디언트(grad_output)를 주어, backward 커널은 다음을 계산합니다:
1. grad_input: 연산의 입력 텐서에 대한 그래디언트. 이것이 더 뒤로 전달됩니다.
2. grad_weight: 연산의 학습 가능한 매개변수에 대한 그래디언트. 이것이 옵티마이저 상태로 전달됩니다.
둘 다 단일 커널 런치에서 계산됩니다. CUDA 스레드가 그래디언트 텐서의 타일에서 병렬로 협력합니다.
저장된 텐서
역방향 계산은 종종 순방향 패스에서 계산된 값을 필요로 합니다. 예를 들어, k_layernorm_bwd는 순방향에서 계산된 평균과 분산이 필요하고, k_mlp_bwd는 GELU 사전 활성화가 필요합니다. 훈련 엔진은 순방향에서 전용 버퍼에 이를 저장한 후, 역방향에서 읽습니다.
메모리 비용: 각 저장된 텐서에 대해 forward 출력과 대략 동일한 형태. ANDREA-120M에서 batch=8, seq=1024, d_model=768일 때, 하나의 저장된 텐서는 8 × 1024 × 768 × 4 bytes = 25 MB. 12개 레이어에 걸쳐 & 레이어당 여러 저장된 텐서로, 학습 중 activations가 VRAM을 지배 (~24 GB 카드에서 ~5-10 GB).
하나의 Backward 단계 추적
메모리에서 그래디언트가 저장되는 위치
하나의 가중치 텐서당 하나의 그래디언트 텐서
ANDREA-120M의 모든 학습 가능한 가중치 텐서마다 동일한 형태의 그래디언트 텐서가 짝을 이룹니다. 각 블록마다:
W_Q [768, 768] ↔ grad_W_Q [768, 768]
W_K [768, 768] ↔ grad_W_K [768, 768]
W_V [768, 768] ↔ grad_W_V [768, 768]
W_O [768, 768] ↔ grad_W_O [768, 768]
W_1 [768, 3072] ↔ grad_W_1 [768, 3072]
W_2 [3072, 768] ↔ grad_W_2 [3072, 768]
LN1.gamma [768] ↔ grad_LN1.gamma [768]
LN1.beta [768] ↔ grad_LN1.beta [768]
LN2.gamma [768] ↔ grad_LN2.gamma [768]
LN2.beta [768] ↔ grad_LN2.beta [768]
플러스 토큰 임베딩, 위치 임베딩, & 최종 레이어 노름. 그래디언트 버퍼의 총 메모리는 가중치 메모리와 일치: ~120M 플로트, FP32에서 ~480 MB, FP16에서 ~240 MB.
마이크로배치 간 누적
ANDREA의 batch_size = 8은 FP16에서 VRAM에 맞음. 더 큰 효과적인 배치에는 그래디언트 누적이 필요: 작은 배치에 대해 여러 forward+backward 패스를 실행하고, 그래디언트를 동일한 버퍼에 합산한 후, 한 번의 옵티마이저 스텝을 수행.
for microbatch in range(n_microbatches):
forward(마이크로배치)
backward() # 그라디언트 버퍼에 추가, 덮어쓰지 않음
scale_grads(1.0 / n_microbatches) # 마이크로배치 간 평균 계산
optimizer_step()
zero_grads() # 다음 학습 단계 위해 초기화
Backward 커널은 += 의미론을 사용하며 =를 사용하지 않습니다. 각 호출은 기존 버퍼에 그라디언트 기여도를 추가합니다. 버퍼는 zero_grads()가 이를 지울 때까지 누적 합계를 유지합니다.
옵티마이저 상태
AdamW (활동 73)는 가중치당 두 개의 추가 버퍼를 유지합니다: 첫 번째 모멘트 m & 두 번째 모멘트 v. 총 훈련 시 메모리:
가중치: 1× 가중치 수
기울기: 1× 가중치 수
Adam m: 1× 가중치 수
Adam v: 1× 가중치 수
저장된 acts: 레이어 & 배치에 따라 ~2-4×
──────────────────────────────────────────
총합: ~6-8× 가중치 수
ANDREA-120M at FP16: ~240 MB × 4 buffers (weight, grad, m, v) + ~5-10 GB activations = ~10-12 GB total. RTX 4090의 24 GB 한도보다 충분히 낮음. ANDREA-12M은 1.4 GB로 훈련됨; 10× 매개변수 스케일링으로 ~10× 메모리 증가.
그라디언트 버퍼 크기 조정
메모리 & 정밀도에 대한 완전한 제어
범용 프레임워크의 비용
PyTorch & JAX는 autograd를 편리하게 만듭니다: Python 코드를 작성하면 그래디언트를 자동으로 얻습니다. 비용: 코드와 CUDA 사이의 범용 디스패치 레이어. 모든 연산이 Python 인터프리터 오버헤드, 프레임워크 북키핑, 및 동적 커널 선택을 거칩니다. 한 GPU에서 작은 언어 모델을 훈련할 때, 그 오버헤드가 중요합니다.
ANDREA가 피하는 구체적인 비용:
1. Python 인터프리터 지연. 모든 PyTorch 연산은 Python/C++ 경계를 넘습니다. 훈련 스텝당 ~100개의 커널 실행, 분당 ~9 스텝으로, 분당 ~900번의 경계 횡단이 발생합니다. C 수준 디스패치는 이를 제거합니다.
2. 프레임워크 할당기 예측 불가능성. PyTorch의 캐싱 할당기는 평균적으로 좋은 처리량을 제공하지만 피크 메모리가 예측 불가능합니다. ANDREA의 훈련 엔진은 시작 시 모든 버퍼를 미리 할당합니다. 훈련 중 재할당 없음, 조각화 없음, 100K 스텝에서 갑작스러운 OOM 없음.
3. 일반 커널 선택. PyTorch는 런타임에 휴리스틱으로 커널을 선택합니다. ANDREA는 컴파일 타임에 RTX 4090 텐서 코어 타일 크기에 맞게 조정된 커널을 선택합니다.
4. 혼합 정밀도 배관. ANDREA-120M의 FP16 cuBLAS 경로와 ANDREA의 FP8 E4M3 텐서 코어 실험은 어떤 텐서가 어떤 정밀도에서 존재할지에 대한 정밀한 제어를 요구합니다. 일반 프레임워크는 계층화된 API를 통해 이 제어를 노출하지만, 커스텀 CUDA는 이를 직접 작성합니다.
The Tradeoff
커스텀 CUDA의 비용: 작성할 코드가 더 많고, 찾을 버그가 더 많으며, 커뮤니티 생태계가 없습니다. ANDREA의 microgpt_cuda.cu는 디버깅에 몇 달이 걸린 ~6000줄의 손으로 작성된 CUDA입니다. 새로운 연산마다 forward 커널, backward 커널, 및 테스트를 작성해야 합니다.
ANDREA가 얻는 것:
- 완전한 재현성. 훈련 파이프라인은 하나의 C 바이너리와 하나의 Python 프록시로 구성됩니다. PyTorch 릴리스 간 버전 드리프트 없음, 프레임워크 휠과의 CUDA 버전 불일치 없음.
- 비트 정확한 재개. SIGTERM이 GPU가 보는 대로 모든 텐서를 정확히 캡처하는 체크포인트 쓰기를 트리거합니다. 재개는 실행 중이던 동일한 손실 궤적을 이어갑니다.
- 예측 가능한 메모리. ANDREA-120M이 200K 스텝 동안 OOM 없이 훈련되었습니다. 메모리는 엔진 시작 시에 이미 계상되었습니다.
- 직접 하드웨어 액세스. 텐서 코어 타일 크기, FP8 E4M3 설정, 비동기 메모리 복사: CUDA에서 직접 접근 가능하며, 일반 프레임워크에서는 불투명합니다.
재현성을 사명으로
ANDREA 백서 섹션 9에 전체 재현성 스택이 나열되어 있습니다:
훈련 엔진: microgpt/microgpt_cuda.cu
훈련 프록시: microgpt/training_proxy.py
실험 설정: experiments/ANDREA-*-TRAIN.json
데이터 파이프라인: scripts/pull-hermes3.py, scripts/prep-megachat.py
대시보드: scripts/live-loss-dashboard.html
밴디트 사양: docs/FIREHOSE-BANDIT.md
모델 문서: docs/ANDREA.md
하드웨어 요구사항: ≥8 GB VRAM을 가진 하나의 NVIDIA GPU (RTX 3060 이상). 누구나 이러한 아티팩트로부터 ANDREA-12M을 재현할 수 있습니다. 사용자 지정 CUDA 경로가 그 이유의 일부입니다: 프레임워크 버전 고정 없음, 5년 후에도 의존성 놀라움 없음.
신호 및 체크포인트
CUDA 훈련 루프는 두 개의 POSIX 신호에 응답합니다:
- SIGTERM: 즉시 체크포인트를 작성한 후 종료. 훈련을 깨끗하게 중지할 때 사용.
- SIGUSR1: 즉시 체크포인트를 작성하고 훈련을 계속합니다. v3의 폴리시 피벗 중에 실행을 중단하지 않고 상태를 캡처하는 데 사용됩니다.
체크포인트 형식: [int32 step][int32 n_params][n_params × float32 weights][n_params × float32 m][n_params × float32 v]. 스텝 카운터, 가중치 수, 그 다음 Adam 모멘트가 뒤따르는 가중치. 비트 정확하게 재개됩니다. 프록시는 폴리시에서 .samples.json & .state.json을 별도로 아카이빙합니다; .loss.json은 절대 아카이빙되지 않습니다 (전체 훈련 이력을 누적합니다).