1. 필수 개념: 개발자에게 익숙한 것으로 이해하기¶
이 장에는 수학이 없습니다. 목표는 이후 코드 셀이 실행될 때마다 화면의 숫자가 무슨 뜻인지 아는 것입니다. 개념마다 일상적인 개발(데이터베이스, Git, 웹 서비스 등)에서 가져온 비유를 붙였습니다. 비유는 직관을 잡는 데 도움을 줄 뿐 엄밀한 정의는 아닙니다.
이 장 한눈에 보기
- 이 장에서 하는 일: "모델은 디스크 위의 디렉터리"라는 데서 출발해 토큰, 추론, 학습과 loss, SFT/LoRA/QLoRA, 스텝과 에폭, 일곱 가지 라이브러리의 역할 분담, VRAM을 차례로 다룹니다
- 필요한 것: 설치할 것은 없습니다. 웹 페이지를 열 수 있는 컴퓨터 한 대면 됩니다
- 끝나면 얻는 것: 이후 노트북에 나오는 모든 파라미터와 숫자를 짚어 낼 수 있고, 오류가 나면 어느 라이브러리를 봐야 할지 압니다
8개 절은 "먼저 모델이 있고, 그다음 텍스트를 어떻게 읽는지, 어떻게 답하는지, 어떻게 배우는지, VRAM을 어떻게 아끼는지"의 순서로 이어집니다. 각 절은 앞 절을 바탕으로 하므로 순서대로 읽으세요.
1.1 디스크 위의 모델은 디렉터리다¶
models/qwen3-4b/를 열어 보면, 물리적으로 "모델"이란 이 파일들이 전부입니다. 참고: 내 컴퓨터에서 --tokenizer-only로 내려받은 디렉터리(2.8절)에는 토크나이저와 설정만 있어 약 15 MB이며, 가중치 샤드 3개는 없습니다. 샤드는 GPU 호스트에서 전체 다운로드(3.4절)를 한 뒤에야 나타납니다. 이것은 정상이며 다운로드가 중단된 것이 아닙니다.
| 파일 | 무엇인가 | 비유 |
|---|---|---|
config.json |
구조 정의: 36개 층, 히든 크기 2560, 어휘 151,936개 등 | 데이터베이스의 테이블 생성 구문(스키마) |
model-00001-of-00003.safetensors ~ model-00003-of-00003.safetensors |
가중치. 약 40억 개의 숫자로 BF16 기준 8.0 GB이며 샤드 3개로 나뉨. 전체 다운로드 후에만 존재 | 데이터베이스의 데이터 파일 |
model.safetensors.index.json |
어떤 가중치가 어느 샤드에 있는지 | 샤드 라우팅 테이블 |
tokenizer.json, vocab.json, merges.txt |
토크나이저: 텍스트 ↔ 정수 ID 변환 규칙 | 직렬화 프로토콜 |
tokenizer_config.json |
chat_template 포함: system/user/assistant를 하나의 텍스트로 이어 붙이는 방법 |
요청 메시지 형식 |
generation_config.json |
기본 샘플링 파라미터 | 서버 측 기본 설정 |
model-lock.json |
이 튜토리얼이 기록한 버전(commit) | package-lock.json |
"모델을 불러온다"는 것은 config.json에 따라 빈 구조를 만든 뒤 safetensors의 숫자를 채워 VRAM에 올리는 일입니다. 이후 세 종류의 디렉터리를 만나게 됩니다. 베이스 모델(위의 것), adapter(LoRA 증분만 있으며 약 130 MB, 반드시 베이스 모델과 함께 써야 함), 병합 모델(베이스와 증분을 합쳐 계산한 완전한 디렉터리, 다시 8 GB)입니다. vLLM이 불러오는 것은 베이스 모델 또는 병합 모델 디렉터리입니다.
1.2 토큰: 모델이 보는 텍스트¶
모델은 글자를 읽지 않고 정수를 읽습니다. 토크나이저가 텍스트를 조각(토큰)으로 자르고, 각 조각은 어휘의 ID 하나에 대응합니다. 이 프로젝트의 토크나이저로 문장 하나를 잘라 보면 다음과 같습니다.
"为什么白天亮、晚上黑?"
→ ['为什么', '白天', '亮', '、', '晚上', '黑', '?']
→ [100678, 106772, 99538, 5373, 104030, 56652, 11319]
"The Earth rotates."
→ ['The', ' Earth', ' rotates', '.']
→ [785, 9237, 89059, 13]
(중국어 문장의 뜻은 "왜 낮에는 밝고 밤에는 어두워요?"입니다.)
흔한 단어는 토큰 하나지만 드문 글자는 여러 개로 쪼개질 수 있습니다. 영어에서는 공백이 뒤 단어에 붙습니다. 따라서 토큰 수는 글자 수와 같지 않으며, 중국어는 대략 토큰 1개 ≈ 한자 1.3~1.5자입니다. 이 튜토리얼에서 말하는 "길이 상한 4096"은 모두 토큰 수를 뜻합니다.
어휘에는 어떤 텍스트에도 대응하지 않고 구조 표시 역할을 하는 특수 토큰도 몇 개 있습니다. Qwen의 대화 형식(chat_template)은 요청 하나를 다음과 같이 이어 붙입니다.
<|im_start|>system
你是陪小学生探索科学的伙伴。……<|im_end|>
<|im_start|>user
为什么白天亮、晚上黑?<|im_end|>
<|im_start|>assistant
地球在不停转,我们住的地方转向太阳就是白天……<|im_end|>
(system: "너는 초등학생과 함께 과학을 탐구하는 친구야……", user: "왜 낮에는 밝고 밤에는 어두워요?", assistant: "지구는 쉬지 않고 돌고 있어. 우리가 사는 곳이 태양 쪽으로 돌아가면 낮이 돼……")
<|im_end|>(ID 151645)는 시퀀스 종료 표시, EOS입니다. 모델은 이것을 생성하면 멈춥니다. 학습할 때 모델이 답변 끝마다 이것을 출력하도록 배워야 하며, 그렇지 않으면 배포 후 답변이 멈추지 않습니다.
1.3 파라미터와 추론: 계수가 40억 개인 함수¶
모델은 본질적으로 하나의 함수입니다. 토큰 ID 시퀀스를 입력하면 "다음 토큰이 어휘의 각 항목일 확률"을 출력합니다. 이 함수에는 조정 가능한 계수가 약 40억 개 있으며, 이를 파라미터(또는 가중치)라고 부릅니다. 바로 safetensors 안의 숫자들입니다.
추론은 이 함수를 반복 호출하는 것입니다. 프롬프트를 주고, 확률이 가장 높은 토큰을 고르고(그리디 디코딩, do_sample=False), 그것을 입력 끝에 덧붙여 다시 호출하기를 EOS가 출력되거나 max_new_tokens에 도달할 때까지 반복합니다. 추론 중에는 파라미터가 하나도 바뀌지 않습니다.
temperature=0은 그리디 디코딩처럼 동작해 항상 확률이 가장 높은 토큰을 고르므로, 같은 입력에 같은 출력이 나와 비교하기에 편합니다. 0보다 크면 확률에 따라 무작위로 뽑으므로 답변이 더 다양해지지만 재현할 수 없습니다.
1.4 학습: 오차로 파라미터 고치기¶
학습은 그 반대 방향입니다. 모델에게 "맥락 + 참고 답변"을 보여 주고, 참고 답변의 모든 위치에서 다음 토큰을 예측하게 한 뒤, 예측과 참고 답변의 차이를 loss라는 숫자 하나로 바꿉니다.
| 대략적인 loss | 의미 |
|---|---|
| 약 3 이상 | 참고 답변의 모든 토큰이 모델에게 "의외"입니다. 무작위로 찍으면 ln(151936) ≈ 11.9입니다 |
| 1~2 | 좋은 프롬프트를 쓴 베이스 모델이 이 프로젝트의 참고 답변에서 보이는 전형적인 출발점 |
| 0.5 안팎 | 모델이 이미 참고 답변의 표현에 매우 가까움 |
| 0에 가까움 | 거의 한 글자도 틀리지 않고 외운 상태. 이때 검증 loss가 함께 오르고 있다면 과적합입니다 |
loss는 "모델이 참고 답변에 얼마나 놀라는가"를 재는 것이지 "과학 지식 점수"가 아닙니다. 모델이 학습 데이터의 말투를 배우고 있는지는 알려 주지만, 답변이 맞는지는 알려 주지 않습니다.
loss가 있으면 각 파라미터마다 "어느 방향으로 조금 움직이면 loss가 작아지는지"를 계산할 수 있는데, 이것이 그래디언트(역전파로 계산)입니다. 옵티마이저(AdamW)는 그래디언트에 따라 파라미터를 업데이트하고, 학습률(1e-4)은 한 번에 얼마나 크게 움직일지를 정합니다. 너무 크면 베이스 모델의 능력이 씻겨 나가고, 너무 작으면 배우지 못합니다. "loss 계산 → 그래디언트 계산 → 파라미터 업데이트" 한 바퀴를 스텝(step)이라고 합니다.
비유: loss는 CI에서 실패한 테스트 수, 그래디언트는 각 설정 항목을 어느 쪽으로 돌려야 하는지 알려 주는 것, 학습률은 한 번에 얼마나 과감하게 바꿀지, 스텝은 커밋 한 번과 같습니다.
1.5 SFT, LoRA, QLoRA: 서로 독립된 세 가지 선택¶
세 용어는 각각 다른 질문에 답하며 독립적으로 조합할 수 있습니다.
SFT(지도 파인튜닝)는 "무엇으로 가르치는가"에 답합니다. 모델에게 "입력 → 기대 출력" 쌍, 즉 2장의 대화 데이터를 보여 줍니다. 이 프로젝트는 "참고 답변 + EOS" 위치에서만 loss를 계산하고 맥락 부분에서는 계산하지 않습니다(이 마스크는 2.5절에서 직접 눈으로 확인합니다).
LoRA는 "어떤 파라미터를 바꾸는가"에 답합니다. 전체 파인튜닝은 40억 개 파라미터를 모두 업데이트해야 하고, 파라미터마다 그래디언트와 옵티마이저 상태가 따로 필요해 GPU 한 장에 들어가지 않습니다. 게다가 실험 버전마다 8 GB를 저장해야 합니다. LoRA는 원래 가중치 W를 전혀 건드리지 않고, 그 옆에 작은 행렬 A와 B를 추가해 그것만 학습합니다.
(즉, 실제로 쓰이는 가중치 = W(고정) + (alpha / r) × B × A 입니다.)
Git에 비유하면 베이스 모델은 main 브랜치, LoRA는 아주 작은 diff이며, 학습은 diff만 바꾸고 adapter/ 디렉터리에는 바로 그 diff가 저장됩니다. "병합"은 diff를 main 브랜치에 적용해 새로운 완전한 파일 사본을 만드는 것과 같습니다.
r(랭크)은 작은 행렬의 폭, 즉 diff의 용량을 정합니다. 이 프로젝트는 r=16을 쓰며, 7종류의 선형 층 × 36개 층을 합치면 학습 가능한 파라미터가 약 3,300만 개로 전체의 0.8%이고, adapter 파일은 약 130 MB입니다. alpha는 스케일링 계수이며, 이 프로젝트는 항상 alpha = 2 × r을 씁니다.
QLoRA는 "고정된 베이스 모델을 어떻게 VRAM에 넣는가"에 답합니다. 고정된 가중치는 어차피 바뀌지 않으므로, VRAM에서는 BF16(숫자 하나에 2바이트)에서 4비트(숫자 하나에 0.5바이트)로 압축해 두고 계산할 때만 잠시 BF16으로 되돌립니다. 4B 모델의 가중치가 8 GB에서 약 2.5 GB로 줄어들어, 24 GB 카드에 학습 중간 결과를 담을 여유가 생깁니다. NF4는 압축 형식의 이름이고, double_quant는 압축에 쓰는 보조 파라미터까지 한 번 더 압축한다는 뜻입니다. LoRA의 작은 행렬 자체는 압축하지 않고 BF16 그대로 둡니다.
더 읽을거리: LoRA 원 논문, QLoRA 원 논문.
1.6 샘플, 배치, 스텝, 에폭¶
| 개념 | 이 프로젝트의 값 | 의미 |
|---|---|---|
| 샘플 | 1150 | "맥락 + 참고 답변 하나" 묶음 하나. 2턴짜리 대화는 샘플 2개로 펼쳐집니다 |
| 마이크로 배치 | 1 | 한 번의 순전파/역전파에서 VRAM에 올리는 샘플 수 |
| 그래디언트 누적 | 16 | 샘플 16개의 그래디언트를 모은 뒤 파라미터를 한 번 업데이트합니다. VRAM에는 샘플 1개만 올리면서 batch=16에 가까운 효과를 냅니다 |
| 스텝 | 72 | 옵티마이저 업데이트 횟수 = ⌈1150 ÷ 16⌉. 진행 표시줄과 loss 곡선의 가로축 |
| 에폭 | 1 | 모든 샘플을 한 번 훑는 것 |
| 체크포인트 | 25스텝마다 | 학습을 이어서 재개할 수 있는 스냅샷: adapter + 옵티마이저 상태 + 진행 상황 |
비유: 샘플은 메시지 하나, 그래디언트 누적은 메시지 16개를 모았다가 데이터베이스에 일괄 쓰기, 스텝은 데이터베이스 쓰기 한 번, 에폭은 큐 전체를 한 번 소비하는 것입니다.
1.7 일곱 가지 라이브러리가 각각 하는 일¶
학습 환경에는 일곱 가지 라이브러리가 설치되며, 오류 메시지에 이들의 이름이 나타납니다.
| 라이브러리 | 담당 | 어디서 보게 되는가 |
|---|---|---|
| PyTorch | 텐서 연산, GPU 스케줄링, 자동 미분. 다른 라이브러리는 모두 이 위에 만들어짐 | torch.cuda.*, VRAM 통계 |
| Transformers | config.json으로 모델 구조 구성, 가중치 로드, 토크나이저, 채팅 템플릿, generate() |
AutoModelForCausalLM, AutoTokenizer |
| PEFT | 모델에 LoRA 층 추가, 원래 파라미터 고정, adapter 저장/로드, 병합 | LoraConfig, get_peft_model, PeftModel |
| TRL | SFT에 맞춘 트레이너: 마스크에 따라 loss 계산, 스텝/에폭/체크포인트 관리 | SFTConfig, SFTTrainer |
| bitsandbytes | 4비트 양자화의 저장 형식과 GPU 연산 커널 | BitsAndBytesConfig, NVIDIA GPU 전용 |
| accelerate | 모델을 지정한 장치에 배치, 혼합 정밀도. TRL이 내부적으로 의존 | device_map |
| datasets | 트레이너가 요구하는 데이터 컨테이너 | Dataset.from_list |
배포할 때는 다른 라이브러리로 바뀝니다. vLLM은 자체 추론 엔진과 HTTP 서버를 갖추고 있어 모델 디렉터리만 있으면 되며, 위 일곱 가지 중 PEFT와 TRL은 필요 없습니다.
호출 관계: Transformers가 모델 로드 → bitsandbytes가 가중치를 4비트로 압축 → PEFT가 LoRA 층 추가 → datasets가 샘플 보관 → TRL이 학습 루프 실행 → PEFT가 adapter 저장 → PEFT가 병합 → vLLM이 배포.
1.8 RAM, VRAM, 디스크¶
| 자원 | 주요 용도 | 흔한 오해 |
|---|---|---|
| CPU | 텍스트 처리, 토큰화, 로드와 스케줄링 | 코어가 많다고 대형 모델 학습에 적합한 것은 아닙니다 |
| RAM(호스트 메모리) | 가중치를 로드할 때의 임시 보관, CPU에서 모델 병합 | 100 GB RAM으로 24 GB VRAM을 대신할 수 없습니다. 학습 연산은 GPU에서 일어나므로 데이터가 VRAM에 있어야 합니다 |
| GPU | 방대한 행렬 연산 | CUDA 메인 경로는 NVIDIA가 필요합니다. Mac의 Apple MPS는 다른 경로이며 이 튜토리얼에서는 지원하지 않습니다 |
| VRAM | 가중치, 활성화 값, 그래디언트, 옵티마이저 상태, 추론용 KV 캐시 | 8 GB 모델 파일이 VRAM 8 GB를 뜻하지는 않습니다. 학습에는 중간 결과가, 추론에는 KV 캐시가 더해집니다 |
| 디스크 | 베이스 모델, 환경, 체크포인트, 병합 모델 | 체크포인트마다 옵티마이저 상태가 포함되고, 병합할 때마다 8 GB가 또 늘어납니다 |
학습 VRAM의 구성과 실제 측정값은 4.3절에, 추론 VRAM 추정은 5.3절에 있습니다.
점검: 모델 디렉터리의 각 파일이 무엇인지 말할 수 있습니다. loss가 2에서 0.6으로 떨어졌다는 것이 무슨 뜻인지, 그리고 그것만으로는 왜 답변이 좋아졌다고 말할 수 없는지 설명할 수 있습니다. 샘플 1150개가 왜 72스텝이 되는지 말할 수 있습니다. 일곱 가지 라이브러리 중 어느 것이 LoRA를 맡고 어느 것이 학습 루프를 맡는지 말할 수 있습니다.