본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 15. 07:41

Turing 세대의 GPU 「RTX 2070」 1대로 로컬 QLoRA 파인튜닝에 도전하기 (진행 중)

요약

RTX 2070(VRAM 8GB) 환경에서 개인 데이터를 활용해 로컬 QLoRA 파인튜닝을 시도하는 시행착오 기록입니다. 데이터 준비의 중요성, 베이스라인 측정의 효능, 그리고 구형 GPU 환경에서의 기술적 제약 사항을 다룹니다.

핵심 포인트

  • VRAM 8GB 제한을 극복하기 위한 4bit 양자화 및 소형 LoRA 어댑터 구성
  • 알고리즘보다 중요한 교사 데이터(Training Data) 준비 과정의 중요성
  • 학습 전 베이스라인 측정을 통한 성능 변화 확인의 필요성
  • Turing 세대 GPU에서 bf16 미지원 등 하드웨어 환경에 따른 제약 사항
  • VRAM 8GB의 RTX 2070 (Turing 세대) 1대로, 개인 데이터를 로컬 상태 그대로 QLoRA 파인튜닝(Fine-tuning)하는 이야기 - 파인튜닝에서 정말로 시간을 잡아먹는 것은 알고리즘보다 교사 데이터(Training Data)의 준비라는 것 - 학습 전에 **「개선 전의 수치」를 측정하는 것 (베이스라인 측정)**의 효능 - 8GB에 맞추기 위한 **QLoRA의 구성 (4bit 양자화 (Quantization) + 작은 LoRA 어댑터 (Adapter))**을 실제 코드의 요점에서 - 그리고 본론—— 넘어진 것은 코드가 아니라 「환경」. Turing 세대에서 빠졌던 함정을 겪은 순서대로 - 학습은 돌아갔지만 「시간」의 벽에 부딪혀, 일단 중단한다는 현실적인 판단까지

  • 수중에 있는 게이밍 PC/노트북으로, 로컬 LLM의 파인튜닝을 시도해보고 싶은 사람 - QLoRA를 구동하려다 환경 구축에서 헤맸던 (헤맬 것 같은) 사람

  • 조금 오래된 세대의 GPU (Turing 등)에서, bf16을 사용할 수 없어 막혔던 경험이 있는 사람 - 「해봤다」 계열의, 미화하지 않은 시행착오 기록을 읽고 싶은 사람

이 기사는

진행 중입니다. 지금 서 있는 지점 (환경 구축 → 사전 측정 → 학습이 돌아가는 곳까지)을, 끝난 뒤에 미화하지 않고 기록하겠습니다. 학습 후의 정밀도 비교는 학습을 마친 후 추가하겠습니다.

자신을 위해 키우고 있는 개인 RAG 시스템이 있습니다. 집어넣은 가공되지 않은 메모를, 교훈·지식·가치관·설계 판단 등의 종류로 자동 분류하고, 요약하여 구조화합니다. 그것을 벡터 검색(Vector Search)과 키워드 검색의 하이브리드 + 재순위화(Re-ranking)로 불러올 수 있도록 색인화하고, 게다가—— 여기가 핵심입니다만—— 쌓인 지식을 AI 어시스턴트 (Claude Code 등)에게, 매 턴마다 해당 시점에 관련된 것만 자동으로 삽입합니다. 주고받은 대화도 그대로 흡수되어 다시 분류되어 돌아옵니다. **어질러진 메모를 AI가 사용할 수 있는 장기 기억 (제2의 뇌)으로 계속 바꾸어 나가는 폐쇄 루프 (Closed Loop)**이며, 검색 정밀도는 수치로 측정하며 키워왔습니다.

그 루프의 입구에 있는 것이, 방금 언급한 「분류 (종류 나누기)」 공정입니다. 이번에 만드는 것은, 이 역할을 담당하는 분류기를 로컬에서 동작하는 파인튜닝된 소형 모델로서 직접 제작한다는 이야기입니다.

동기는 세 가지입니다.

파인튜닝 (가중치를 학습하는 수법)을 실제로 끝까지 직접 손을 움직여 익히고 싶다. - 그 분류는 현재 범용 대규모 모델에 매번 부탁하고 있으므로, 전용 소형 모델로 교체하여 저렴하고, 빠르고, 안정시키고 싶다. - 그리고 무엇보다—— 교사 데이터가 자신의 개인 메모이므로, 외부 클라우드에 올리고 싶지 않다. 그래서 「수중에 있는 노트북만으로 완결시킨다」를 첫 번째 제약 조건으로 두었습니다.

수중의 장비는, **Turing 세대의 GPU 「RTX 2070」 (VRAM 8GB)**을 탑재한 게이밍 노트북 1대. 학습용으로는 최신 세대에 뒤처지지만, 「지금 있는 도구로 어디까지 할 수 있는가」를 확인하는 것도 이번 도전의 목적이었습니다.

전체 흐름은 다음과 같습니다. 하고 있는 일은 자신의 RAG에서 평소 돌리고 있는 「측정 → 변경 → 다시 측정」과 같은 방식입니다.

┌─────────────┐ ┌───────────┐ ┌─────────┐ ┌─────────┐ ┌──────────────┐
│ ① 데이터 준비 │ → │ ② 기준 측정 │ → │ ③ 학습 │ → │ ④ 평가 │ → │ ⑤ 비교 │
│ │ │ (before) │ │ (QLoRA) │ │ (after) │ │ before/after │
...

파인튜닝이라고 하면 학습 알고리즘을 떠올리지만, 실제로 시간을 잡아먹는 것은 교사 데이터의 준비였습니다. 수중의 데이터를 솔직하게 점검해 보면 다음과 같습니다.

  • 애초에 양이 적고 편향되어 있음 (많은 종류와 십수 건밖에 없는 종류가 혼재)
  • 「어디에도 해당하지 않음」 분류의 정답 데이터가 존재하지 않으므로, 직접 만들어야 함
  • 정제된 메모와 실제로 분류시키고 싶은 가공되지 않은 메모 사이의 문장 질감이 어긋나 있음

깨끗한 데이터가 저절로 내려오는 일은 없습니다. 이 투박한 전처리(Preprocessing)야말로 본체이며, 이 부분을 어떻게 받아들이느냐가 그대로 결과를 좌우합니다. 최종적으로 다룰 수 있는 형태로 정리하여 수백 건 규모의 학습 데이터를 준비하고, 평가용으로 분리했습니다.

데이터가 갖춰졌다고 해서 바로 학습하는 것은 아닙니다. 무언가를 개선하려면 먼저 개선 전의 수치가 필요합니다. 이것이 없으면 「좋아졌다」도 「나빠졌다」도 말할 수 없습니다.

그래서 방금 분리해낸 평가용 데이터로, 범용 대규모 모델(Large Model)에 그대로 분류를 시켰을 때의 정답률을 측정했습니다. 결과는 전체적으로 약 48%. 게다가 클래스별로 살펴보니, 특정 종류의 메모를 거의 놓치고 있다는 사실도 알 수 있었습니다. "개선할 여지가 충분하다"라는 출발점이 인상이 아닌 숫자로 보였다——이것만으로도 앞으로의 판단이 흔들리지 않게 됩니다.

8GB라는 제약 안에 담기 위한 핵심이 이것입니다. 본체는 4bit로 압축하여 동결(Freeze)하고, 작은 LoRA 어댑터(Adapter)만을 학습합니다.

┌──────────────────────────────────────┐ ┌────────────────────────────┐
│ 베이스 모델: 동결·4bit 양자화 (Quantization) ❄ │ │ LoRA 어댑터 │
│ 수십억 개의 가중치 (업데이트 안 함 = VRAM 절약) │ ──► │ (학습함 · 매우 작음) │
...
# 본체는 4bit 양자화(NF4)하여 로드 = VRAM을 대폭 절약
bnb = BitsAndBytesConfig(
load_in_4bit=True,
...

포인트는 compute_dtype을 비롯하여, 수치 형식을 처음부터 명시하고 있다는 점입니다. 이 부분을 모호하게 두면, 나중에 GPU 세대 차이로 인해 발목을 잡히게 됩니다 (다음 절).

여기서부터가 본게임——일 터였으나, 발을 헛디딘 것은 알고리즘이 아니라 죄다 발밑의 환경이었습니다. 절차 자체는 공식 문서나 해설에 나와 있습니다. 하지만 자신의 머신에서 실행하면, 교과서에 적혀 있지 않은 벽이 차례차례 나타납니다. 기록한 순서대로 나열합니다.

#증상정체어떻게 고쳤나
1라이브러리 로드 시 알 수 없는 타입(Type) 에러핵심 라이브러리 간의 세대 차이 (한쪽은 너무 최신이고, 다른 한쪽은 요구하는 타입이 없음)토대 라이브러리를 상위 버전에 맞게 세대를 맞춰 다시 설치
2설정 파일 로드 시 글자 깨짐 현상일본어 Windows의 기본 문자 코드가 UTF-8 파일을 읽지 못함프로그램을 UTF-8 모드로 실행
3학습 API 인자(Argument)가 통하지 않음라이브러리 버전에 따른 API 변경 (인자 이름이 바뀌어 있었음)설치된 버전의 방식에 맞춰 호출을 수정
4학습 1스텝(Step)째에 정지Turing 세대(RTX 2070)에서 새로운 수치 형식(bf16) 연산이 미구현됨수치 취급을 명시하고, 혼합 정밀도(Mixed Precision) 메커니즘을 꺼서 회피

특히 4번은 까다로워서, "타입을 명시하는" 것만으로는 고쳐지지 않았고, 최종적으로 혼합 정밀도(Mixed Precision, 가속화 메커니즘) 자체를 끄는 것으로 겨우 학습 루프가 돌아가기 시작했습니다. Turing 세대에서는 흔히 발생하는 함정이라는 것을, 직접 겪고 나서야 알게 되었습니다.

코드로 작성하면 판단은 단 두 줄입니다. 하지만 "왜 여기서 이것을 끄는가"를 말할 수 있는 것이, 복사(Copy)와 자작의 차이가 됩니다.

args = SFTConfig(
per_device_train_batch_size=1, # 8GB에 맞추기 위해 최소값. 실효 배치(Effective Batch)는 그래디언트 누적(Gradient Accumulation)으로 확보
gradient_accumulation_steps=8,
...

하나를 넘으면 또 다음 것이 나옵니다. 마음이 꺾일 것 같기도 합니다. 하지만, 막힌 부분과 "왜 그렇게 고쳤는지"를 하나씩 기록해 나가다 보면, 그것이 사라지지 않는 자산으로 변해갔습니다. 같은 환경에서 다음에 도전할 사람——예를 들어 몇 달 후의 자신——에게 이 기록 자체가 지도가 됩니다.

걸림돌을 모두 넘어서, 마침내 학습 루프가 돌아가기 시작했습니다. 손실(Loss, 모델의 틀린 정도를 나타내는 수치)이 제대로 계산되며 진행됩니다. 그 순간의 안도감은 몇 번을 경험해도 기분 좋은 것입니다.

그런데 마지막에 가로막은 것은 속도였습니다. RTX 2070(Turing 세대)에서는 1스텝당 약 100초. 끝까지 돌리려면 6시간 정도 걸릴 것으로 예상됩니다. 게다가 그동안 GPU는 점유되고, 기계는 계속 열을 받습니다.

여기서 한 가지 현실적인 판단을 내렸습니다. 일단 중단한다. 그리고 다음에 돌릴 때는 다루는 문장의 길이나 학습 반복 횟수를 타협하여 현실적인 시간 안에 맞춘다. 완벽주의로 6시간을 억지로 돌리기보다, "우선 한 바퀴 돌려서 결과를 내고, 괜찮으면 본 게임을 돌린다"—그렇게 판단했습니다.

  • ✅ 학습용 환경을 실제 RAG에 영향을 주지 않고 별도로 구축 (GPU 인식 완료)
  • ✅ 학습 데이터를 정비하여 학습 및 평가용으로 분할
  • 개선 전 수치 (전체 약 48%) 측정
  • 학습 루프가 돌아가는 것을 확인 (모든 난관 돌파)
  • ⏳ 학습을 끝까지 완료하기 → 개선 후 수치를 산출하여 비교 ← 다음 회차에서 이어짐

즉, 아직 "진행 중"입니다. 하지만 가장 험난한 환경 구축의 산은 넘었습니다. 남은 것은 시간을 확보하여 끝까지 돌리는 것과, 결과를 정직하게 측정하는 것뿐입니다.

"AI를 사용하면 누구나 쉽다"라는 말을 자주 듣습니다. 절반은 사실입니다. 코드는 확실히 복사할 수 있습니다. 공식 절차도, 샘플도 검색하면 나옵니다.

하지만, 복사할 수 없는 것이 있습니다. 어디에서 막혔는지, 무엇을 버렸는지, 왜 그 설정을 선택했는지—그 판단입니다. 이번 6시간이라는 현실과, 그것을 마주했을 때 어떻게 타협할 것인가. Turing 세대의 GPU에서 어떤 일이 벌어지는가. 문자 인코딩(Character encoding)의 함정을 어떻게 피할 것인가. 이러한 "직접 겪어봐야만 알 수 있는 것들"은 매뉴얼에 적혀 있지 않습니다.

가치는 언제나 그곳에 남습니다. 그래서 저는 시행착오를 포함하여 기록합니다. 완주하게 되면, 개선된 수치와 함께 이 글의 뒷이야기를 쓰겠습니다.

용어 정리(프롬프트 튜닝 (Prompt Tuning)과 파인튜닝 (Fine-tuning)의 차이점)는 별도의 글로 정리해 두었습니다.

🔗 개념편: 「프롬프트 튜닝 (Prompt Tuning)」과 「파인튜닝 (Fine-tuning)」은 무엇이 다른가

용어의미
파인튜닝 (Fine-tuning)예시를 통해 모델의 가중치 (Weights)를 업데이트하여 동작을 학습시킴
...

🔗 개인 블로그에도 동일한 내용과 관련 글을 작성하고 있습니다: Turing 세대의 GPU 「RTX 2070」 1대로 로컬 QLoRA 파인튜닝에 도전하기 (진행 중)

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0