Unsloth 적용기
27B 모델을 V100 32GB에서 굴리려니 속도와 메모리 양쪽이 빠듯했다. unsloth를 적용하면서 왜 쓰는지, 그리고 직접 batch size를 1·4·8로 바꿔보다 batch 1이 제일 빨랐던 이상한 결과를 추적했던 걸 정리해봤다.
왜 unsloth였나#
unsloth는 같은 모델을 더 빠르게, 더 적은 메모리로 학습하게 해주는 라이브러리다. 공식 표에서 모델별로 1.5x2x faster, 메모리 3080% less 식으로 정리돼 있는데, Gemma 3 계열도 1.5x faster / 50% less로 잡혀 있었다.
핵심은 Gradient Checkpointing이었다. 역전파를 하려면 forward 때 나온 중간 activation 값들이 필요한데, 이걸 전부 VRAM에 들고 있으면 메모리가 터진다. Gradient Checkpointing은 이 중간값을 다 저장하는 대신 필요할 때 다시 계산하는 방식으로, 연산을 조금 더 하는 대신 VRAM 사용량을 크게 줄인다. unsloth가 이 activation offloading을 잘 처리해줘서 OOM 없이 파인튜닝 파이프라인을 세울 수 있었다.
batch 1이 batch 8보다 빨랐다#
unsloth를 켜고 batch size를 1, 4, 8로 바꿔가며 속도를 재봤는데 결과가 직관과 반대였다.
- 8일 때가 가장 느리고
- 1일 때가 가장 빨랐다
보통 배치를 키우면 GPU를 더 채워 쓰니 throughput이 올라간다고 생각하는데 정반대가 나와서 한참 들여다봤다. 원인은 padding이었다.
padding이 범인이었던 이유#
배치로 여러 데이터를 묶으면, 길이가 제각각인 시퀀스를 가장 긴 것에 맞춰 짧은 것들에 padding을 채워야 한다. 그런데 이때 설정한 max_seq_length가 10,000으로 매우 컸다. 배치 안에 긴 데이터가 하나라도 끼면 나머지 짧은 데이터도 그 길이에 맞춰 끌려가서, padding token을 처리하는 불필요한 연산이 급증한다.
- batch size = 1: 데이터 하나만 처리하니 그 데이터 길이만큼만 연산한다. padding이 사실상 0%.
- batch size = 4, 8: 배치 내 가장 긴 데이터에 맞춰 패딩이 붙고, max_seq_length가 클수록 이 낭비가 커진다.
게다가 unsloth는 batch size = 1일 때 padding-free 최적화가 가장 완벽하게 작동한다. 그래서 1이 제일 빨랐던 것이다. 큰 배치가 빠를 거라는 일반론이 padding 비용 앞에서 뒤집힌 셈이다.
해결 방향#
padding 낭비를 줄이는 방향은 결국 max_seq_length를 데이터에 맞게 줄이는 것이었다. 10,000은 거의 쓰이지 않는 길이까지 잡아둔 값이라, 실제 데이터의 토큰 길이를 보고 적정선으로 낮추면 큰 배치에서의 padding 낭비도 줄어든다. 실제 학습에서 max_seq_length를 얼마로 정했고 effective batch를 어떻게 맞췄는지는 하이퍼파라미터 튜닝에 이어서 적었다.
- unsloth의 효율은 gradient checkpointing(activation 재계산)으로 VRAM을 줄이는 데서 온다
- 큰 배치가 항상 빠른 건 아니다. max_seq_length가 크면 padding token 연산이 배치를 키울수록 늘어난다
- unsloth는 batch 1에서 padding-free 최적화가 가장 잘 먹는다
댓글