콘텐츠로 이동

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

(즉, 실제로 쓰이는 가중치 = 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를 맡고 어느 것이 학습 루프를 맡는지 말할 수 있습니다.