부록 부록
A.2 (부록) 실습 환경 셋업
본문은 어텐션을 직접 계산하고(1.4), 추론이 왜 연산이 아니라 메모리에서 병목이 되는지 산식으로 세우고(5.1), LoRA로 작은 모델을 미세조정하는 원리를 풀었다(3.8). 그런데 그것을 화면 위의 수식이 아니라 실제 GPU에서 실행해 보려는 순간, 이야기가 물리 세계로 내려온다. CUDA out of memory가 표시되고, torch.cuda.is_available()이 아무 설명 없이 False를 반환하고, 어제까지 정상 동작하던 스크립트가 오늘은 임포트 한 줄에서 실패한다. 이 부록은 그 물리 세계를 다룬다 — 본문의 예제와 A.3의 엔드투엔드 실습을 직접 따라가려는 독자가 자기 실습 컴퓨터를 세우는 일.
먼저 한 가지를 명시해 둔다. 이 부록은 설치 명령을 적지 않는다. 이 영역은 이 책에서 가장 빨리 노후화된다 — 라이브러리 버전, 패키지 이름, 설치 명령, 신형 GPU, 클라우드 시간당 요금, CUDA 호환 매트릭스는 몇 주 단위로 바뀐다. pip install torch==2.x.y 같은 줄을 그대로 베껴 두면 몇 달 뒤 반드시 깨지고, 그것을 종이에 기록해 두는 순간 그 종이는 독자를 잘못된 길로 안내하는 함정이 된다. 그래서 여기서는 특정 버전·패키지명·가격·GPU 모델명을 사실로 고정하지 않는다. 대신 무엇을 왜 세우고, 무엇이 무엇과 맞아야 하고, 어떻게 확인하는지 — 반년 뒤에도 그대로일 구조만 적는다. 구체 값이 필요한 자리에서는 공식 문서(PyTorch 설치 선택기, 각 라이브러리의 호환표, 벤더 API 문서, Anthropic·Claude는 claude-api)를 1차 출처로 삼으라고 말할 것이다. 이 부록에서 "이 버전을 쓰라"는 문장은 하나도 없다.
A.2.1 스택은 여섯 층이다 — 아래부터 세운다
실습 환경은 서로 뒷받침하는 여섯 층으로 되어 있다. 층마다 들어가는 구체적인 이름과 버전은 바뀌어도, 층이 여섯이라는 구조와 그들이 아래에서 위로 의존한다는 사실은 바뀌지 않는다. 이 그림 하나가 이 부록에서 가장 오래 살아남을 내용이다.
여기서 얻어 갈 통찰은 둘이다. 첫째, 아래부터 맞춘다. 초보가 겪는 셋업 고통의 대부분은 ⑤ 라이브러리 층에서 에러 메시지로 실패하지만, 원인은 거의 언제나 그 아래 ①~③에 있다 — VRAM이 모자라거나, 드라이버가 구식이거나, CUDA 버전이 어긋났거나. 그런데 사람들은 에러가 뜬 그 층에서 pip install을 반복하며 헤맨다. 위층에서 증상을 붙잡고 씨름하는 대신 아래 층이 성립하는지부터 확인하면 대부분의 시간을 아낀다. 이 부록의 마지막 절(A.2.12)이 그 확인을 정확히 아래에서 위로 밟는다.
둘째, 지금 하려는 일이 어디까지 세울지를 정한다. 남의 모델을 벤더 API로만 호출하는 애플리케이션 개발자(제6·7부 경로)에게는 ①~③이 아예 필요 없다. GPU도, 드라이버도, CUDA도 없이 키와 파이썬 환경만 있으면 된다. "실습 환경 셋업"이라는 말이 자동으로 "GPU 세팅"을 뜻하지는 않는다는 얘기다. 그러니 첫 질문은 언제나 "나는 모델을 직접 돌리려는가, 남의 모델을 부르려는가"여야 한다 — 이 답 하나로 세워야 할 층의 절반이 갈린다.
A.2.2 GPU를 고르는 실제 기준은 VRAM이다
GPU를 처음 고르는 사람은 대개 연산 성능(TFLOPS)부터 본다. 그런데 LLM 실습에서 먼저 부딪히는 장벽은 거의 언제나 연산이 아니라 VRAM(GPU 메모리)이다. 이유는 단순하다. 연산이 느리면 결과가 늦게 나올 뿐이지만, 메모리가 모자라면 프로그램이 CUDA out of memory로 종료되어 아예 시작조차 못 한다. "느리다"는 참을 수 있어도 "안 돈다"는 참을 수 없다.
무엇이 VRAM을 차지하는지 나눠 보면 GPU 선택 기준이 분명해진다. GPU 메모리에는 (1) 모델 가중치, (2) 어텐션의 KV 캐시, (3) 순전파·역전파 중의 활성값, (4) 옵티마이저 상태(학습할 때만), (5) CUDA 컨텍스트와 프레임워크 오버헤드가 함께 올라간다. 이 중 무엇이 지배적인지가 작업마다 완전히 다르다 — 추론에서는 가중치가, 긴 컨텍스트를 배치로 서빙할 때는 KV 캐시가(5.1), 전체 파인튜닝에서는 옵티마이저 상태가 메모리를 소모한다. 그래서 "이 GPU면 되나?"는 "무엇을 하려는가?"를 먼저 답해야 대답할 수 있는 질문이고, 그 답은 다음 절의 계산으로 나온다.
VRAM 등급은 대략 두 세계로 나뉜다. 소비자용 카드는 대체로 8·12·16·24 GB 구간에 있고, 데이터센터용 가속기는 40·80 GB, 그 위로 더 간다(구체 모델명·용량은 세대마다 바뀌므로 확인 필요). 이 등급 하나가 "무엇까지 할 수 있나"를 상당 부분 정한다. 소비자 24 GB급이면 상당한 크기의 모델 추론과 QLoRA 미세조정까지가 현실적이고, 그 위의 전체 파인튜닝이나 대형 모델은 데이터센터 카드 여러 장으로 넘어간다. 왜 그 경계가 거기 그어지는지를 이제 직접 계산한다.
A.2.3 작업별 VRAM을 직접 어림한다
정확한 소요량은 시퀀스 길이·배치 크기·구현 디테일에 따라 변동하지만, 바이트/파라미터라는 어림자 하나로 자릿수는 정확히 맞는다. 파라미터 수를 $N$, 정밀도를 원소당 $b$ 바이트라 하자. FP32는 $b=4$, FP16·BF16은 $b=2$, INT8은 $b=1$, INT4는 $b\approx0.5$다(5.2). 이 $b$ 하나가 메모리를 몇 배로 교란한다.
A.2.3.1 추론 — 가중치가 지배한다
추론에서 메모리를 지배하는 것은 모델 가중치 전체를 높이는 일이다. 활성값은 디코드 스텝마다 토큰 하나 분량이라 작고, KV 캐시는 컨텍스트·배치가 커질 때만 문제가 된다(5.1). 그래서 추론 VRAM은 대략
$$M_{\text{추론}} \approx \underbrace{N \times b}_{\text{가중치}} + \underbrace{(\text{토큰당 KV}) \times L \times B}_{\text{KV 캐시}} + \underbrace{C}_{\text{컨텍스트·오버헤드}}$$
로 쓴다. 70억(7B) 파라미터 모델을 FP16으로 적재하는 경우를 직접 밟아 보자. 가중치는
$$7\times10^{9} \times 2\ \text{B} = 14\times10^{9}\ \text{B} = 14\ \text{GB}$$
이다. 여기에 CUDA 컨텍스트와 프레임워크 오버헤드로 보통 1~2 GB가 상시 추가된다. KV 캐시는 컨텍스트가 짧고 배치가 1이면 작다 — 7B급을 $n_{\text{layers}}=32$, KV 헤드 32, head_dim 128, FP16으로 두면 토큰당 KV가 $2\times32\times32\times128\times2 = 512$ KB이고(5.1의 산식 그대로), 컨텍스트 2048에 배치 1이면 $512\text{KB}\times2048 \approx 1$ GB에 불과하다. 전부 더하면
$$14\ (\text{가중치}) + 1.5\ (\text{오버헤드}) + 1\ (\text{KV}) \approx 16.5\ \text{GB}$$
로, 24 GB 카드에는 여유 있게, 16 GB 카드에는 아슬아슬하게 들어간다. 여기서 정밀도 $b$를 낮추면 그림이 바뀐다. INT8이면 가중치가 7 GB, INT4면 3.5 GB로 내려가(5.2) 오버헤드를 더해도 8~12 GB급 소비자 GPU에 적재하힌다. 양자화가 실습·엣지에서 판을 바꾸는 지점이 바로 이 계산이다 — 정밀도 한 단계가 카드 등급 하나를 내려 준다.
주의할 것은 이 그림이 "배치 1, 짧은 컨텍스트"에서만 명료하다는 점이다. 서빙하듯 배치를 키우거나 컨텍스트를 수만 토큰으로 늘리면 KV 캐시가 가중치를 넘어설 만큼 커져 추론조차 메모리 장벽에 부딪힌다. 그 정밀한 계산은 5.1에서 이미 했다 — 여기서는 "혼자 하나씩 돌려 보는 실습"의 추론은 가중치가 지배한다는 것만 붙들면 된다.
A.2.3.2 전체 파인튜닝 — 왜 16바이트/파라미터인가
전체 파인튜닝(full fine-tuning)에서 메모리가 급증하는 이유는 파라미터 하나마다 여러 개의 복사본이 겹겹이 쌓이기 때문이다. 혼합정밀도(2.6)에서 Adam으로 학습하는 표준 구성을 원소별로 세어 보면 이렇다.
$$M_{\text{상태}} \approx N \times \big(\underbrace{2}_{\text{FP16 가중치}} + \underbrace{2}_{\text{FP16 기울기}} + \underbrace{4}_{\text{FP32 마스터}} + \underbrace{4}_{\text{Adam } m} + \underbrace{4}_{\text{Adam } v}\big) = 16\,N\ \text{바이트}$$
각 항이 왜 필요한지가 중요하다. 순전파·역전파는 속도를 위해 FP16으로 실행하니 FP16 가중치와 그에 대한 FP16 기울기가 있어야 한다. 그런데 FP16으로 파라미터를 갱신하면 작은 갱신량이 반올림에 먹혀 사라지므로(2.6), 갱신은 별도의 FP32 마스터 가중치에 누적한다 — 그래서 FP32 복사본 4바이트가 더 붙는다. 여기에 Adam은 파라미터마다 1차·2차 모멘트($m$, $v$)를 FP32로 들고 다니니 4+4바이트가 또 붙는다. 다 합쳐 파라미터당 16바이트다. 7B에 넣으면
$$7\times10^{9} \times 16\ \text{B} = 112\times10^{9}\ \text{B} = 112\ \text{GB}$$
로, 활성값을 더하기도 전에 이미 80 GB 카드 하나를 넘긴다. 이것이 전체 파인튜닝을 단일 GPU로 못 하는 이유이고, ZeRO나 FSDP가 정확히 이 16바이트/파라미터를 여러 GPU에 분할해 나눠 지는 기법인 이유다(2.4). 실습에서 전체 파인튜닝을 수천만~수억 파라미터 규모의 소형 모델로만 권하는 것도 같은 계산에서 나온다 — 1억 파라미터라면 상태가 1.6 GB라 소비자 카드에 들어가지만, 7B는 계산부터 단일 카드를 초과한다.
이 16바이트를 깎는 수단도 산식에서 바로 읽힌다. 8비트 Adam(bitsandbytes, 5.2)은 모멘트 두 개를 FP32 8바이트에서 약 2바이트로 줄여 16을 10 근처로 낮추고, 활성값은 그래디언트 체크포인팅으로 재계산과 맞바꿔 줄인다. 그래도 7B 전체 파인튜닝이 단일 소비자 카드에 들어오지는 않는다 — 그래서 다음 절의 LoRA가 등장한다.
A.2.3.3 LoRA·QLoRA — 파인튜닝을 추론 크기로 되돌린다
LoRA(3.8)는 베이스 가중치를 얼린 채 두고, 작은 저랭크 어댑터만 학습한다. 그러면 방금 본 16바이트/파라미터가 베이스 전체가 아니라 어댑터에만 붙는다 — 그리고 어댑터는 베이스에 비해 터무니없이 작다. 얼마나 작은지 세어 보자. 랭크 $r$의 LoRA는 원래 $d\times d$ 가중치 곁에 $A\in\mathbb{R}^{d\times r}$와 $B\in\mathbb{R}^{r\times d}$ 둘을 붙이므로 행렬당 $2dr$ 파라미터를 더한다. $d=4096$, $r=16$, 어텐션의 q·k·v·o 네 투영, 32개 층에 적용하면
$$2 \times 4096 \times 16 \times 4\ (\text{투영}) \times 32\ (\text{층}) \approx 1.7\times10^{7}\ \text{파라미터}$$
로, 베이스 7B의 0.24%에 불과하다. 이 1,700만 파라미터에만 16바이트/파라미터를 물려도 옵티마이저 상태가 300 MB가 채 안 된다. 학습 대상이 사실상 사라진 셈이다.
QLoRA는 여기서 한 걸음 더 간다. 얼려 둔 베이스를 4비트로 양자화해 적재한다. 7B 베이스가 $7\times10^{9}\times0.5\ \text{B}=3.5$ GB로 내려가고, 그 위에 방금 본 300 MB 크기의 어댑터 학습을 추가한다. 이 조합 덕분에 파인튜닝인데도 메모리 소요가 추론에 가깝게 떨어져, 24 GB급 소비자 카드에서 — 때로는 그보다 작은 카드에서도 — 7B 미세조정이 동작한다. QLoRA가 실습의 판을 바꾼 지점이 정확히 이 계산이다.
다만 한 항을 빼먹으면 안 된다. 어댑터만 학습해도 역전파는 얼린 베이스 층들을 거꾸로 통과해야 하므로 순전파 활성값을 저장해 둬야 한다. 그래서 QLoRA에서 실제로 메모리를 좌우하는 것은 어댑터가 아니라 이 활성값이고, 그것은 다음 절의 주제다. QLoRA가 "3.5 GB면 되겠네"가 아니라 "시퀀스를 짧게 잡으면 12 GB에도 되고 길게 잡으면 24 GB도 빠듯하다"인 이유가 여기 있다.
A.2.3.4 활성 메모리라는 숨은 항, 그리고 요약
앞의 세 계산은 가중치·기울기·옵티마이저만 셌다. 그런데 사람들을 가장 자주 놀라게 하는 것은 장부에 안 보이던 활성 메모리다. 역전파를 하려면 순전파에서 계산한 중간값(활성값)을 층마다 저장해 둬야 하고, 이 양은 대략 배치 크기 × 시퀀스 길이 × 은닉 차원에 층수와 층당 저장 텐서 수를 곱한 만큼 커진다. 배치 1, 시퀀스 2048, 은닉 4096, 32층이면 층당 활성 텐서 하나가 $2048\times4096\times2\ \text{B}\approx17$ MB이고, 층마다 여러 개를 여러 층에 걸쳐 쌓으면 금세 수 GB가 된다. 게다가 어텐션 점수 행렬은 시퀀스 길이의 제곱($L^2$)으로 자라므로, 긴 컨텍스트를 학습하면 이 항이 급증한다 — FlashAttention이 이 $L^2$ 활성값을 아예 물질화하지 않는 것이 학습 메모리에 그토록 중요한 이유다. 활성값을 다스리는 두 조정 변수가 그래디언트 체크포인팅(활성값을 버리고 역전파 때 재계산 — 메모리↓·연산↑)과 배치·시퀀스 축소다. "메모리가 모자라면 배치를 줄여라"라는 조언이 통하는 것이 바로 이 항 때문이다.
바이트/파라미터 어림을 한 표로 모으면 후보 GPU를 자르는 데는 충분하다.
| 작업 | 지배 요소 | 대략 바이트/파라미터 | 7B 예시(개략) | 실습 현실 | 연결 |
|---|---|---|---|---|---|
| 추론 FP16 | 가중치 (+KV) | ~2 | ~14 GB + α | 16~24 GB GPU | 5.1 |
| 추론 INT4 | 양자화 가중치 | ~0.5 | ~4 GB + α | 8~12 GB GPU | 5.2 |
| QLoRA 파인튜닝 | 4비트 베이스 + 활성 | ~0.5 + 활성 | 한 자릿수~십수 GB | 24 GB급 소비자 GPU | 3.8 |
| LoRA 파인튜닝(FP16 베이스) | FP16 베이스 + 활성 | ~2 + 활성 | ~16~24 GB+ | 24~40 GB GPU | 3.8 |
| 전체 파인튜닝(Adam) | 가중치+기울기+옵티마이저 | ~16 (+활성) | ~112 GB+ | 다중 GPU·ZeRO/FSDP | 2.4 |
이 숫자들은 자릿수 어림이다. 실제로는 활성 메모리, 프레임워크 오버헤드, 8비트 옵티마이저, KV 정밀도, 그래디언트 체크포인팅이 값을 다 교란한다. "내 GPU에 정말 들어가나?"는 결국 한 번 적재해 봐야 안다. 하지만 어림으로 후보를 자르는 일은 확실하게 된다 — 24 GB에 7B 전체 파인튜닝을 추가하려는 시도는 실측할 것도 없이 계산부터 틀렸고, 그것을 미리 아는 것만으로도 몇 시간의 시행착오를 줄인다.
A.2.4 어디서 실행하나 — 로컬·클라우드·무료 노트북
VRAM 요구량을 어림했으면 다음 질문은 "그 GPU를 어디서 구하나"다. 세 갈래가 있고, 각자 다른 것을 값으로 치른다. 강요할 답은 없다 — 하려는 일과 매칭하는 문제다.
무료·저가 클라우드 노트북(브라우저에서 GPU 딸린 주피터 세션을 주는 서비스들)은 시작 장벽이 가장 낮다. 드라이버·CUDA가 이미 설치되어 있어 다음 두 절의 고통을 건너뛰고, 추론·LoRA 실습이나 A.3의 첫 단계에 잘 맞는다. 대신 세션이 시간 제한과 유휴 종료로 끊긴다 — 그때 커널 안의 상태와 저장 안 한 파일 전체가 손실되므로, 체크포인트와 출력물은 반드시 외부 저장소에 내보내야 한다. 배정되는 GPU가 그때그때 다르고 큰 VRAM은 유료라, 장시간 학습이나 재현 실험에는 약하다. 구체 제공자·한도·GPU 종류는 수시로 바뀌니 확인이 필요하다.
로컬 GPU(내 PC·워크스테이션)는 반대 성격이다. 언제든 무제한으로 쓰고, 데이터가 밖으로 나가지 않으며(프라이버시·규제, 8.4·9.3), 시간당 과금의 공포가 없다. 대가는 초기 비용, VRAM이 하드웨어에 고정된다는 점, 그리고 드라이버·CUDA를 내가 직접 맞춰야 한다는 점이다 — 다음 절의 고통이 여기에 집중된다. 소비자 24 GB급으로는 추론과 QLoRA까지가 현실적이고, 전체 파인튜닝이나 대형 모델은 무리다(앞 절의 계산 그대로다).
클라우드 GPU(대여 인스턴스)는 큰 VRAM과 다중 GPU를 필요할 때만 빌린다 — 전체 파인튜닝이나 대형 모델 실습은 사실상 여기다. 함정은 성능이 아니라 비용이다. 인스턴스는 켜 있는 동안 계속 과금되므로, 실습이 끝난 뒤 끄지 않으면 밤새 유휴 GPU가 비용을 낭비한다. 스팟·프리엠터블 인스턴스는 저렴하지만 도중에 회수될 수 있어 체크포인트가 필수다. 비용 인식과 알람은 7.5에서 깊이 다룬다.
내 생각에는 대부분의 독자에게 "무료 노트북에서 시작해 막히면(VRAM이나 시간에) 클라우드로 옮기고, 데이터와 반복이 중요해지면 로컬로" 가는 경로가 무난하다. 처음부터 큰 GPU를 사거나 큰 클라우드 인스턴스를 켜 둘 필요가 없다 — 본문과 A.3 예제는 의도적으로 작은 모델로 설계해 소박한 환경에서도 동작하게 했다. 큰 하드웨어는 필요가 계산으로 증명된 다음에 불러도 늦지 않는다.
A.2.5 NVIDIA 밖의 선택지 — Apple·AMD·CPU
실습 생태계의 1급 시민은 여전히 NVIDIA다. 대부분의 튜토리얼·라이브러리·에러 해법이 CUDA를 가정하고 쓰였기 때문에, 다른 가속기를 쓰면 코드 자체보다 "이 경로가 CUDA 전용은 아닌가"를 의심하는 데 시간을 쓰게 된다. 대안이 없지는 않지만 성숙도와 문서가 제각각이라 초보 실습에서는 마찰이 더 크다(확인 필요).
Apple Silicon(Mac)은 PyTorch의 MPS 백엔드로 소형 모델 추론과 경량 실습을 실행한다. llama.cpp나 MLC 계열은 Apple에서 특히 잘 실행되는 편이라, 로컬 추론·엣지 실험(8.4)에는 쓸 만하다 — 다만 대형 학습에는 부적합하다. AMD는 ROCm으로 PyTorch·vLLM 등을 지원하지만 버전·모델별 성숙도 편차가 있다(확인 필요). CPU만 있는 환경이라면 소형 모델의 추론은 llama.cpp(GGUF 양자화)로 가능하지만, 학습은 사실상 비현실적으로 느리다. 요점 하나만 붙들면 된다 — 튜토리얼이 안 돌 때, 코드를 심층 분석하기 전에 "이게 NVIDIA 전용 경로인가"를 먼저 의심하라. 그 한 번의 의심이 잘못된 디버깅 몇 시간을 아낀다.
A.2.6 파이썬 환경을 격리한다 — venv·conda
하드웨어가 정해졌으면 소프트웨어를 추가한다. 여기서 초보를 좌초시키는 것이 버전 지옥(dependency hell) — 라이브러리들이 서로 다른 버전을 요구하며 충돌하는 상황이다. 이것을 예방하는 첫 위생이 환경 격리다. 규칙은 하나로 요약된다. 시스템 파이썬에 직접 pip install하지 마라. 프로젝트마다 가상환경을 따로 만들어, A 프로젝트의 라이브러리 업그레이드가 B 프로젝트를 깨뜨리지 못하게 장벽을 세운다.
# 표준 라이브러리 venv — 경량이고 어디에나 있다 (개념 예, 버전 고정 아님)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
# 이제 설치는 이 환경 안에서만 일어난다
두 갈래가 있다. venv + pip는 파이썬 표준이고 경량이라 순수 파이썬 의존성에는 충분하다. conda(또는 그 빠른 대체 구현 mamba)는 파이썬 패키지뿐 아니라 CUDA 툴킷·컴파일러 같은 비-파이썬 바이너리까지 환경 안에 담아 주므로, GPU 스택의 시스템 의존성 충돌을 줄이는 데 유리하다. 어느 쪽이든 핵심은 격리다. 그리고 되는 조합을 찾았으면 그것을 잠금 파일로 고정하라 — pip freeze > requirements.txt나 conda의 environment.yml이다. 이 잠금 파일이 없으면 "어제 되던 게 오늘 깨졌다"를 되돌릴 방법이 없고, 있으면 언제든 되는 상태로 복원한다. 재현성의 나머지는 A.2.11에서 이어 다룬다.
A.2.7 드라이버·CUDA·PyTorch 정합 — 버전 지옥의 진원지
GPU 스택이 안 실행되는 사연의 태반은 세 층의 버전이 서로 안 맞아서다. "설치는 됐는데 GPU를 못 잡는다", "임포트에서 죽는다"의 진원지가 여기다. 어떻게 물려 있는지를 알면 대부분 스스로 풀 수 있다.
첫째 원리는 드라이버가 CUDA의 상한을 정한다는 것이다. GPU 드라이버는 자기가 지원할 수 있는 CUDA 버전에 상한이 있다. 드라이버가 너무 구식이면 최신 CUDA로 빌드된 최신 PyTorch를 못 실행한다. 그래서 "GPU를 못 잡는다"의 가장 흔한 근본 원인이 구식 드라이버이고, 가장 흔한 해법이 드라이버 업데이트다. 반대 방향의 여유는 대체로 있다 — 새 드라이버가 옛 CUDA를 실행하는 하위 호환은 넓지만, 옛 드라이버가 새 CUDA를 실행하는 상위 호환은 좁다.
둘째 원리는 요즘 PyTorch 휠이 CUDA 런타임을 동봉해 온다는 것이다. 근래에는 pip로 GPU용 PyTorch를 설치하면 필요한 CUDA 런타임과 cuDNN이 휠 안에 들어와, 시스템 CUDA 툴킷을 따로 깔지 않아도 되는 경우가 많다(확인 필요). 덕분에 드라이버만 충분히 새것이면 위층은 대체로 저절로 풀린다. 그러니 버전을 머리로 지어내지 말고 PyTorch 공식 설치 선택기 — OS·패키지 관리자·CUDA 버전을 고르면 정확한 설치 명령을 만들어 주는 페이지 — 를 1차 출처로 삼아라. 이 한 곳이 "어떤 조합이 지금 유효한가"의 정답지다.
셋째 원리는 컴파일 확장이 훨씬 까다롭다는 것이다. bitsandbytes(5.2)·flash-attn·일부 vLLM 커널은 순수 파이썬이 아니라 CUDA 커널을 컴파일해 쓰는데, 이들은 세 가지에 동시에 민감하다. (1) CUDA 버전, (2) PyTorch의 ABI — PyTorch가 바뀌면 그 위에 빌드된 확장의 바이너리 호환이 깨져 임포트 에러가 난다, (3) GPU의 연산 능력(compute capability, SM 아키텍처) — 미리 빌드된 커널이 내 GPU 세대를 포함하지 않으면 커널을 못 찾거나 느린 대체 경로로 떨어진다. 그래서 이 확장들이 설치에서 실패하면 십중팔구 코드 문제가 아니라 궁합 문제다. 공식 호환표에서 "이 PyTorch·이 CUDA에 맞는 버전"을 먼저 확인하는 것이 시행착오를 줄이는 길이다.
증상과 원인을 이어 두면 진단이 빨라진다.
| 증상 | 흔한 원인 | 원리적 대응 |
|---|---|---|
torch.cuda.is_available() → False | 드라이버 구식·CPU 빌드가 설치됨 | 드라이버 갱신, GPU용 PyTorch 휠 재설치 |
CUDA out of memory | VRAM 초과 | 배치·시퀀스↓, 양자화(5.2)·LoRA(3.8), 그래디언트 체크포인팅 |
| 라이브러리 설치 충돌 | 전역 설치·버전 상충 | 새 가상환경에서 다시(A.2.6) |
bitsandbytes/flash-attn 임포트·빌드 실패 | CUDA·PyTorch ABI·GPU 아키텍처 궁합 | 공식 호환표대로 맞는 조합 설치 |
| 어제 되던 것이 오늘 깨짐 | 무심코 업그레이드됨 | 잠금 파일로 고정·복원(A.2.6) |
이 모든 것을 관통하는 원칙은 순서다. 프로젝트마다 새 가상환경을 만들고, PyTorch는 공식 선택기가 준 명령으로 먼저 설치하고, 나머지 라이브러리는 그 PyTorch에 맞춰 추가하고, 되는 조합을 잠금 파일로 고정한다. 화려한 최신 버전을 좇는 대신 되는 조합을 고정하는 것 — 이 한 문장이 실습 생산성의 대부분을 지킨다. 최신 버전은 대개 며칠 뒤에 다른 사람이 궁합 문제를 다 겪어 준 다음에 올라타도 늦지 않다.
A.2.8 핵심 라이브러리 지도 — 무엇을 왜 까나
LLM 실습 스택은 사실상 PyTorch 위의 Hugging Face 생태계에, 서빙·양자화·검색 도구가 붙는 형태로 수렴한다(대안도 많으니 확인 필요). 개별 API는 공식 문서를 보면 되고, 여기서 필요한 것은 각각이 무엇에 쓰이고 본문 어느 장의 구현인지를 파악하는 지도다.
| 라이브러리 | 무엇에 쓰나 | 이 책 연결 |
|---|---|---|
| PyTorch | 텐서·자동미분·GPU 연산의 토대. 거의 모든 것이 이 위에 선다 | 전반(1.3) |
| transformers (HF) | 사전학습 모델 로딩·추론·학습 API(AutoModel·AutoTokenizer·Trainer) | 1.4·3.2 |
| tokenizers (HF) | 빠른 BPE·서브워드 토크나이저(러스트 백엔드) | 1.2 |
| datasets (HF) | 데이터셋 로딩·스트리밍·매핑, 메모리 초과 없이 대용량 처리 | 2.1·3.2 |
| accelerate (HF) | 같은 코드를 CPU·단일/다중 GPU에서 실행하게 추상화, 분산·혼합정밀도 런처 | 2.4·2.6 |
| peft (HF) | LoRA·QLoRA·어댑터 등 파라미터 효율 파인튜닝 | 3.8 |
| trl (HF) | SFT·보상모델·PPO·DPO 등 사후학습·정렬 트레이너 | 3.2~3.5 |
| bitsandbytes | 8·4비트 양자화·8비트 옵티마이저 — QLoRA·저VRAM의 핵심 | 5.2·3.8 |
| vLLM (및 TGI·SGLang 등) | 고성능 서빙 — 연속 배칭·PagedAttention·OpenAI 호환 API | 5.4·5.3 |
| 벡터DB·검색 (FAISS 등) | 임베딩 색인·근사최근접(ANN) 검색 — RAG의 검색부 | 6.2·6.3 |
읽는 법은 이렇다. HF 생태계는 역할 분업이다 — transformers가 모델을, datasets가 데이터를, tokenizers가 토큰화를, peft가 파인튜닝 기법을, trl이 정렬 학습을, accelerate가 분산·정밀도 런처를 맡아 함께 물려 하나의 파이프라인을 이룬다. A.3가 실제로 이 조합으로 구성된다. 양자화와 서빙 도구는 본문 기법의 구현이다 — bitsandbytes는 5.2의 양자화를, vLLM은 5.3·5.4의 서빙 기법을 당신이 처음부터 구현하지 않아도 되도록 감싼 것이다. 원리는 본문에서 읽고, 사용은 여기서 한다.
한 가지 실무 습관을 붙여 두면 좋다. 위를 한꺼번에 다 설치하지 마라. 추론만 할 것이면 transformers(또는 vLLM)만, LoRA면 거기에 peft·trl·bitsandbytes, RAG면 벡터검색만 추가한다. 라이브러리를 많이 깔수록 A.2.7의 궁합 충돌 표면만 넓어진다 — 안 쓰는 컴파일 확장 하나가 임포트 단계에서 전체를 붕괴시키는 일이 흔하다. 필요할 때 필요한 것만 까는 것이 그 자체로 디버깅을 줄이는 전략이다.
A.2.9 모델·데이터 접근 — 허브·캐시·게이트·라이선스
코드가 준비돼도 모델 가중치와 데이터를 어디서 가져오느냐가 남는다. 실습의 표준 경로는 Hugging Face Hub다(다른 허브·저장소도 있으니 확인 필요). transformers·datasets로 모델·데이터셋을 이름으로 부르면 자동으로 내려받아 로컬에 캐시하고, 다음 실행부터는 캐시를 재사용한다.
# 개념 예 — 이름으로 로딩, 최초 1회 다운로드 후 캐시 (실제 모델 ID는 확인 필요)
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = AutoTokenizer.from_pretrained("<org>/<model>")
model = AutoModelForCausalLM.from_pretrained("<org>/<model>")
여기서 실무적으로 챙길 것이 셋이다. 캐시가 디스크를 채운다 — 대형 모델은 수~수십 GB이고, 클라우드 노트북의 작은 디스크에서는 이것만으로 저장 공간이 고갈된다. 캐시 디렉터리를 큰 볼륨으로 옮기는 환경변수가 있으니(확인 필요) 미리 지정해 두는 편이 낫다. 오프라인·에어갭 환경(8.4)에서는 한 번 받은 가중치를 로컬 경로로 직접 로딩해 네트워크 없이 실행한다. 그리고 재현이 중요하면 리비전(커밋 해시)을 고정하라 — 같은 이름이라도 허브의 리비전이 갱신되면 어제와 다른 가중치를 받을 수 있다(A.2.11·8.3).
그런데 "다운로드할 수 있다"와 "그 용도로 써도 된다"는 별개의 문제다. 두 관문이 있다. 하나는 게이트(gated) 모델이다 — 일부 모델은 약관 동의와 접근 신청을 거쳐야 받아진다. 허브 계정으로 페이지에서 동의하고 액세스 토큰(HF token)으로 인증해야 다운로드가 열리며, 승인 전에는 코드가 401·403으로 막힌다(이 토큰도 비밀이다 — 다음 절). 다른 하나는 라이선스다. 오픈 웨이트라고 다 같은 라이선스가 아니다. 상업적 사용·재배포·파생·출력물 사용 조건이 모델마다 다르고, "연구용만"이라거나 매출·사용자 규모 제한이 붙기도 한다. 실습을 넘어 제품·상업 배포로 갈 것이면 그 모델의 현재 라이선스를 반드시 확인해야 하며, 이것은 9.2에서 깊이 다룬다. 데이터셋도 마찬가지로 출처·라이선스·개인정보를 확인해야 한다(2.1·9.3). 특정 모델의 게이트 여부와 상업 사용 가부는 시간이 지나면 바뀌므로, "이 모델은 상업적으로 써도 된다"를 기억으로 단정하지 말고 그 모델 페이지의 현재 라이선스를 직접 읽는 것이 유일하게 안전한 길이다.
A.2.10 API로 쓸 때 — 키 위생이 곧 보안
남의 모델을 벤더 API로 호출하는 경로(제6·7부)는 GPU가 필요 없다. 대신 API 키가 관문이고, 그 키를 다루는 위생이 그대로 보안이 된다. 핵심은 하나다 — 키는 비밀이다.
절대 하지 말 것부터 분명히 하자. 키를 소스코드에 하드코딩해 커밋하는 것, 노트북 셀에 붙여 공유하는 것, 로그·에러 메시지·스크린샷에 노출하는 것, 공개 저장소에 푸시하는 것. 한 번 공개된 키는 유출로 간주하고 즉시 폐기·회전해야 한다 — 봇이 공개 저장소를 실시간으로 훑어 노출된 키를 줍기 때문에, "잠깐 올렸다 지웠다"는 통하지 않는다. 반대로 할 것은 키를 환경변수나 비밀 관리자(secret manager), 혹은 .env 파일(단, .gitignore에 반드시 추가)로 주입하고, 코드는 그것을 읽기만 하는 것이다. 최소 권한·사용량 한도·만료·회전을 적용해 두면 유출됐을 때의 피해도 줄인다(7.2·7.4).
export SOME_VENDOR_API_KEY="..." # 셸/CI 비밀에서 주입, 커밋 금지
import os
api_key = os.environ["SOME_VENDOR_API_KEY"] # 코드는 읽기만. 하드코딩·출력 금지
벤더 API의 스펙 — 모델 ID, 컨텍스트 길이, 토큰 단가, rate limit, 기능 지원 — 은 이 책이 지어내지 않는다. 이것들은 수시로 개정되므로 각 벤더의 공식 문서가 1차 출처이고, Anthropic·Claude API의 스펙은 claude-api를 단일 출처로 확인해야 한다(BK-05). 원리 자체는 벤더가 달라도 공통이다. 비용은 토큰으로 흐른다 — 입력·출력 토큰 수에 단가를 곱한 것이 과금의 골격이고(7.5), 실습 중에 무심코 큰 컨텍스트·긴 출력·반복 호출을 실행하면 예상 밖의 청구가 난다. 그러니 사용량 알림과 한도를 미리 걸어라. 또 하나 유용한 습관은 클라이언트를 OpenAI 호환 인터페이스 뒤에 두는 것이다 — 그러면 base_url만 바꿔 벤더와 로컬 서빙(5.4)을 갈아탈 수 있어 락인과 교체 비용을 낮춘다.
A.2.11 노트북 vs 스크립트, 재현성, 클라우드 비용
주피터 노트북과 스크립트는 경쟁하는 것이 아니라 서로 다른 국면에 맞는 도구다. 노트북은 탐색에 강하다 — 단계별 실행, 시각화, 설명과 코드의 혼합이 데이터를 훑고 예제를 따라가고 프로토타입을 만드는 데 잘 맞는다. 스크립트는 재현에 강하다 — 버전 관리·자동화·장시간 학습·배포에 맞는다.
| 주피터 노트북(.ipynb) | 스크립트(.py) | |
|---|---|---|
| 강점 | 탐색·시각화·단계별 실행·설명과 코드 혼합 | 재현·자동화·버전관리·장시간 학습·배포 |
| 약점 | 실행 순서 꼬임(숨은 상태), diff·리뷰 어려움 | 대화형 탐색엔 둔함 |
| 맞는 자리 | 데이터 훑기, 예제 따라가기, 프로토타입, 교육 | 파인튜닝 잡, 평가 파이프라인, 재현 실험, 서빙 |
노트북에는 고유한 함정이 하나 있다. 셀을 순서 없이 오가며 실행하면 커널 안에만 존재하는 숨은 상태가 생겨, "노트북대로 위에서 아래로 다시 돌리면 결과가 재현되지 않는" 사태가 흔하다. 어떤 변수는 지운 셀에서 정의됐고, 어떤 값은 세 번 실행한 셀의 누적이다. 결과를 확정하기 전 커널을 재시작해 전체를 처음부터 끝까지 실행해(Restart & Run All) 그대로 재현되는지 확인하는 습관이 이것을 막는다. 실험과 파인튜닝처럼 결과가 중요한 것은 아예 스크립트로 고정하는 편이 안전하다.
재현성은 시드 고정에서 시작하지만 거기서 끝나지 않는다. 같은 코드가 매번 같은 결과를 내게 하려면(디버깅·비교·회귀 방지) 무작위성을 시드로 묶는다.
# 개념 예 — 무작위성 시드 고정 (라이브러리별 API는 확인 필요)
import random, numpy as np, torch
SEED = 42
random.seed(SEED); np.random.seed(SEED)
torch.manual_seed(SEED); torch.cuda.manual_seed_all(SEED)
그런데 시드만으로는 부족하다. GPU 커널의 비결정적 연산, 라이브러리 버전, 하드웨어가 바뀌면 결과가 미세하게 달라진다(2.6). 완전한 비트 단위 재현은 어렵고, 실무의 목표는 "결론이 흔들리지 않을 만큼의 재현성"이다. 그것을 얻으려면 시드뿐 아니라 라이브러리 버전(잠금 파일)·모델 리비전·데이터 버전·하이퍼파라미터·코드 커밋 해시를 실험마다 함께 남겨야 한다. 이 실험 메타데이터 관리가 파인튜닝 운영과 모델 라이프사이클의 핵심이고(8.3), 실습 단계에서 습관을 들여 두면 나중에 제품으로 옮길 때 그대로 쓰인다.
마지막으로 클라우드 GPU 실습의 1번 사고는 성능이 아니라 비용이라는 점을 다시 명시한다. 인스턴스는 켜 있는 동안 과금된다 — 학습이 끝나도, 밤새 유휴여도, 끄지 않으면 계속 실행된다. 실습이 끝나면 인스턴스를 정지·삭제하는 습관을 들이고, 붙어 있는 디스크나 정지된 인스턴스의 볼륨 같은 유휴 자원도 과금될 수 있으니 정리하라. 예산 알림과 한도를 미리 걸어 임계치를 넘으면 알림이나 차단이 걸리게 하고, 스팟·프리엠터블을 쓴다면 회수에 대비해 체크포인트를 남긴다. 토큰 API든 GPU 대여든, 비용을 지속적으로 관측하는 이 습관이 그대로 유닛 이코노믹스 관점으로 자란다(7.5).
A.2.12 설치가 됐는지 확인하는 법 — 아래부터 위로
"설치했다"와 "실제로 된다"는 다른 말이다. 확인은 A.2.1의 층 구조 그대로 아래에서 위로 밟는다 — 그래야 어디서 처음 실패하는지로 원인을 좁힌다. 위에서부터 큰 모델을 실행해 보다 종료되면 원인이 다섯 층 중 어디인지 알 수 없지만, 아래부터 올라가면 실패한 그 지점이 곧 원인이다.
# 1) GPU가 OS에 보이나 — 드라이버·하드웨어 (NVIDIA 예; 명령은 환경별 상이)
nvidia-smi # GPU·VRAM·드라이버 버전이 표로 나오면 ①② OK. 안 나오면 드라이버부터.
# 2) 격리된 파이썬 환경에 들어와 있나
which python; python --version # .venv/conda 경로인지 확인 (시스템 파이썬 아님)
# 3) PyTorch가 GPU를 인식하나 — False면 대개 드라이버·CUDA·CPU빌드 문제 (A.2.7)
import torch
print(torch.__version__, "CUDA:", torch.cuda.is_available())
# 4) 실제로 GPU에서 연산이 실행되나 — 작은 텐서를 적재해 곱해 본다
dev = "cuda" if torch.cuda.is_available() else "cpu"
x = torch.randn(1024, 1024, device=dev)
print("matmul ok:", (x @ x).shape)
# 5) 필요한 라이브러리만 임포트되나 (다 설치하지 않는다)
import transformers, datasets
# import peft, trl, bitsandbytes # 파인튜닝을 할 때만
# import vllm # 서빙을 할 때만
# 6) 아주 작은 모델로 끝단까지 — 로딩→토크나이즈→1스텝 추론 (실제 ID는 확인 필요)
# 여기까지 통과하면 스택 전체가 살아 있다. 큰 모델은 그다음에.
이 여섯 단계가 통과하면 스택이 살아 있는 것이고, 어느 단계에서 멈추면 그 단계가 원인의 층을 정확히 가리킨다. 1에서 막히면 드라이버, 3에서 False면 드라이버·CUDA·잘못된 CPU 빌드, 6에서만 막히면 게이트 승인이나 토큰(A.2.9) 문제다. 특히 6단계 — 아주 작은 모델로 로딩부터 추론 한 스텝까지 끝단을 통과시키는 것 — 을 큰 모델을 실행하기 전에 반드시 먼저 하라. 작은 모델이 안 실행되는 환경에서 큰 모델은 절대 실행되지 않고, 작은 모델의 실패는 몇 초 만에 알려 주지만 큰 모델의 실패는 몇 분의 다운로드와 로딩 뒤에야 알려 준다. 여기에 API를 쓴다면 키가 환경변수·비밀로 주입되어 코드·저장소에 없는지(A.2.10), 되는 설치 조합을 잠금 파일로 고정했는지(A.2.6), 클라우드라면 예산 알림과 정지 습관을 세웠는지(A.2.11)를 더하면 실습 환경으로서 갖출 것은 갖춘 셈이다.
이 부록이 세운 것은 결국 A.3를 직접 따라갈 발판이다. 다음 장은 여기서 준비한 환경 위에서 작은 LLM 서비스 하나를 데이터에서 파인튜닝, 서빙, 제품까지 실제로 조립하며 — HF 생태계, peft·trl, vLLM, 키 관리, 재현성 — 이 부록의 조각들을 그대로 쓴다. 배경 수학이 막히면 A.1로, 용어가 걸리면 A.4으로 역참조하면 된다. 그리고 이 장에서 단 하나만 몸에 붙인다면 그것은 특정 버전이 아니라 태도다 — 버전·명령·가격은 반년이면 낡으니 외우지 말고, 층 구조와 계산과 확인 절차를 쥔 채 구체 값은 그때그때 공식 문서에서 길어 오는 것.