---
title: "LoRA 원 논문 해부: 큰 모델을 얼리고 작은 저랭크 변화만 학습하는 이유"
slug: "lora-low-rank-adaptation-deep-dive"
category: "AI / LLM"
topic: "ai-llm"
subtopic: "fine-tuning"
tags: ["LoRA","Low-Rank Adaptation","PEFT","Fine-tuning","Transformer","PyTorch","Paper Review"]
status: "published"
created: "2026-07-24"
updated: "2026-07-24"
summary: "Hu 외의 LoRA 원 논문을 12세 독자 기준으로 해부한다. 동결한 사전학습 가중치 W₀에 저랭크 BA 변화만 더하는 수식, rank·scaling·초기화·병합의 내부 실행, 원 논문 실험 조건과 한계, 공식 loralib source·작은 행렬 직접 검증, full fine-tuning·adapter·prompt tuning·QLoRA의 선택 경계를 사실과 해석으로 분리한다."
kind: "Deep Dive"
evidence: "Hu et al. arXiv:2106.09685v2·ICLR 2022 paper·공식 microsoft/LoRA 고정 commit c4593f0·Python 3.9.6 표준 라이브러리 행렬 연산으로 Eq. (3) merge 동치와 parameter 수 직접 검산·공개 source compile"
series: "LLM 시스템 지도 — 2단계"
---

> **2단계 열세 번째 논문.** 이 글은 Edward J. Hu 외의 [*LoRA: Low-Rank Adaptation of Large Language Models*](https://arxiv.org/abs/2106.09685) ([arXiv v2 PDF](https://arxiv.org/pdf/2106.09685), 2021-10-16; ICLR 2022)을 직접 읽고 썼다. LoRA(Low-Rank Adaptation, 저랭크 적응)는 **사전학습한 큰 모델의 원래 가중치 `W₀`는 고정하고, 작업별 변화 `ΔW`를 작은 두 행렬 `B`와 `A`의 곱으로만 학습해 `W₀ + BA`처럼 쓰는 fine-tuning(미세 조정) 방법**이다. 이 글의 조사 기준일은 2026-07-24다. 논문 수치와 공식 구현의 당시·현재 상태를 섞지 않는다.

## 이 글에서 얻을 답과 범위

앞 글의 ReAct는 model이 외부 도구를 쓰며 행동을 고르는 방법이었다. 그런데 회사별 요약, SQL 생성, 고객 상담, 사내 문서 질의처럼 같은 base model(기본 모델)을 여러 업무에 맞추려면 또 다른 문제가 생긴다.

```text
full fine-tuning
  업무 A용으로 70억 개 가중치를 모두 복사·학습·저장
  업무 B용으로 다시 70억 개 가중치를 모두 복사·학습·저장
  → 품질은 좋을 수 있지만 model·gradient·optimizer state 비용이 업무마다 반복된다.

LoRA
  공유 base model W₀ 하나는 그대로 둔다.
  업무 A에는 작은 Aₐ, Bₐ만 저장한다.
  업무 B에는 작은 Aᵦ, Bᵦ만 저장한다.
  → 실행 때 W₀에 선택한 변화만 더한다.
```

이 글을 다 읽으면 아래 질문에 답할 수 있어야 한다.

1. 왜 `ΔW`를 통째로 학습하지 않고 `BA`로 제한하면 저장·학습 메모리가 줄어드는가?
2. `rank r`, `lora_alpha α`, `A`, `B`, `W₀`는 각각 무슨 모양이고 첫 forward(순전파)에서 어떤 값이 되는가?
3. 학습 중 `W₀x + (α/r)BAx`를 계산하는 것과 배포 전에 `W₀ + (α/r)BA`로 합치는 것이 왜 같은가?
4. “추론 지연이 없다”, “rank 1이면 충분하다”, “LoRA는 양자화다”라는 말은 어디까지 사실인가?
5. 원 논문의 GPT-3 175B·GLUE·GPT-2 결과는 어떤 실험 조건의 숫자이며, 내 데이터와 모델에 그대로 약속되는가?
6. production(운영)에서 base model revision, tokenizer(토크나이저), target module, adapter version을 왜 하나의 배포 단위로 관리해야 하는가?
7. backend·DBA(Database Administrator, 데이터베이스 관리자) 경험자는 LoRA를 단순 `pip install`이 아니라 어떤 학습·평가·배포 파이프라인으로 다뤄야 하는가?

**범위:** 원 논문 v2의 §1–8·Appendix, [공식 `microsoft/LoRA` repository](https://github.com/microsoft/LoRA) 고정 commit [`c4593f0`](https://github.com/microsoft/LoRA/tree/c4593f060e6a368d7bb5af5273b8e42810cdef90)의 `loralib/layers.py`, `loralib/utils.py`, GPT-2 예제를 다룬다. 이 글은 Transformer의 self-attention(자기-어텐션) 전체 수식, GPU kernel 최적화, dataset 수집, 하이퍼파라미터 자동 탐색을 새로 증명하지 않는다. QLoRA(Quantized LoRA, 양자화 LoRA)는 LoRA와 혼동하지 않기 위한 경계까지만 다룬다.

### 먼저 외울 한 문장

> **LoRA는 거대한 책 `W₀`의 모든 글자를 다시 쓰지 않는다. 업무마다 필요한 수정 방향을 최대 `r`개로 제한한 작은 메모 `A`, `B`만 학습한다. 배포 때는 그 메모를 책에 합쳐 원래와 같은 한 번의 dense matrix multiplication(밀집 행렬 곱)으로 읽을 수 있지만, 어떤 메모가 맞는지는 데이터·평가·모델 버전이 결정한다.**

## 왜 필요한가: full fine-tuning은 업무가 늘수록 모델 사본과 학습 상태를 반복한다

### 12살 비유: 공용 백과사전과 업무별 수정 스티커

아주 두꺼운 공용 백과사전이 있다. 여행 안내 업무는 “교통” 문단을 더 잘 쓰게 하고 싶고, 고객 지원 업무는 “환불” 문단을 더 잘 쓰게 하고 싶다.

```text
full fine-tuning = 업무별로 백과사전 전체를 다시 인쇄한다.
LoRA              = 원본 백과사전은 공용으로 두고,
                    업무별로 필요한 문장을 바꾸는 작은 수정 스티커만 만든다.
```

비유에서 반드시 버릴 부분이 하나 있다. LoRA의 스티커는 사람이 읽는 문장 조각이 아니다. 수천 차원 숫자로 된 선형층의 **변화 행렬**이다. 그리고 원본을 실제로 영구 수정하지 않아도, 요청마다 `W₀`와 선택한 스티커를 함께 계산할 수 있다.

### full fine-tuning이 만드는 비용

사전학습(pre-training)은 일반 text에서 다음 token을 맞히며 `W₀`를 만든다. fine-tuning은 특정 업무의 `(입력, 정답)` 쌍으로 `W₀`의 거의 모든 parameter(학습 가능한 숫자)를 바꾼다. 원 논문 §2는 autoregressive language model의 조건부 log-likelihood(로그 우도)를 다음처럼 쓴다.

\[
\max_{\Phi}\sum_{(x,y)\in Z}\sum_{t=1}^{|y|}\log P_{\Phi}(y_t\mid x,y_{<t})
\]

| 기호 | 뜻 |
| --- | --- |
| `Z` | 업무 데이터 집합. 한 원소는 입력 `x`, 정답 token 열 `y`다. |
| `y_t` | 정답의 `t`번째 token이다. |
| `y_{<t}` | `t`번째보다 앞에 놓인 정답 token들이다. |
| `Φ` | 모델 전체 parameter다. full fine-tuning에서는 거의 전부를 갱신한다. |

Adam 또는 AdamW 같은 adaptive optimizer(적응형 최적화기)로 모든 parameter를 학습하면, 단순 checkpoint뿐 아니라 parameter의 gradient(기울기), Adam의 1차·2차 moment(누적 상태)도 크게 잡는다. 정확한 GPU byte 수는 dtype(자료형), sharding(분할), activation checkpointing(활성값 재계산), batch·sequence length에 따라 달라진다. 다만 **동결한 parameter에는 gradient와 optimizer state를 만들 필요가 없다**는 방향은 변하지 않는다.

원 논문은 GPT-3 175B 사례에서 query·value projection만 rank 4로 적응할 때, task checkpoint를 약 350 GB에서 35 MB로 줄인다고 보고했다. 이는 base model을 없앤다는 뜻이 아니다. 논문 각주가 명시하듯 serving(서빙)에는 여전히 약 350 GB base model이 필요하다. 100개 업무를 보관할 때의 비교는 `100 × 350 GB` 대 `350 GB + 100 × 35 MB`라는 **원 논문의 특정 가정**이다. [논문 §4.2](https://arxiv.org/pdf/2106.09685#page=6)를 “내 7B 모델도 항상 이만큼 절약한다”로 읽으면 안 된다.

### 기존 효율화 방법의 빈틈

LoRA는 PEFT(Parameter-Efficient Fine-Tuning, 파라미터 효율 미세 조정) 계열이다. 원 논문이 비교한 대표 경로는 다음이다.

| 방법 | 바꾸는 곳 | 장점 | 원 논문이 짚은 대가 |
| --- | --- | --- | --- |
| full fine-tuning | 원래 가중치 전체 | 최대 자유도 | 업무마다 큰 checkpoint·학습 상태 |
| adapter | Transformer block 사이에 새 작은 층 | 적은 task parameter | 추가 층을 순차 실행하므로 online inference latency가 늘 수 있음 |
| prefix/prompt tuning | 입력 또는 layer activation의 학습용 token | base를 고정 | prompt 길이가 context window를 차지하고 최적화가 불안정할 수 있음 |
| LoRA | 기존 선형층 변화 `ΔW` | `BA`를 원래 가중치와 합칠 수 있음 | rank·target module을 잘못 고르면 품질이 떨어지고 base 자체 비용은 남음 |

논문 Table 1은 Quadro RTX8000 한 장, GPT-2 medium, 100회 forward 평균이라는 좁은 조건에서 adapter가 batch 1·sequence length 128일 때 fine-tuning/LoRA 기준 19.8±2.7 ms보다 20.7% 또는 30.3% 느렸다고 보고한다. 이 수치는 adapter가 항상 느리다는 법칙이 아니다. batch, hardware, kernel, model parallelism(모델 병렬화)이 바뀌면 값도 바뀐다. LoRA의 설계상 강점은 **합친 뒤 추가 matrix multiplication을 남기지 않을 수 있다**는 점이다.

## 무엇인가: 작은 모델도, 양자화도 아닌 저랭크 update의 재매개변수화

### 원 논문의 정확한 정의

원 논문 §4.1에서 사전학습 weight matrix(가중치 행렬)는 `W₀ ∈ ℝ^{d×k}`다. 입력 `x ∈ ℝ^k`를 출력 `h ∈ ℝ^d`로 보내는 선형층을 생각하자. full fine-tuning은 같은 크기 `d×k`의 변화 `ΔW`를 자유롭게 학습한다.

LoRA는 대신 다음 제약을 둔다.

\[
\Delta W = BA,\qquad
A\in\mathbb{R}^{r\times k},\quad
B\in\mathbb{R}^{d\times r},\quad
r\ll\min(d,k)
\]

\[
h = W_0x + \frac{\alpha}{r}BAx
\]

| 기호 | 모양 | 하는 일 |
| --- | --- | --- |
| `W₀` | `d×k` | 사전학습 모델에서 가져와 **동결**한 원래 weight다. |
| `x` | `k×1` | 한 token의 hidden state 같은 입력 vector다. |
| `A` | `r×k` | `k`개 입력 방향을 `r`개 좁은 통로로 압축하는 학습 parameter다. |
| `B` | `d×r` | 좁은 `r`개 통로를 `d`개 출력 방향으로 되돌리는 학습 parameter다. |
| `r` | 정수 | rank(랭크, 독립적인 변화 방향의 최대 수)다. 작을수록 작고 덜 자유롭다. |
| `α` | 상수 | update 크기를 `α/r`로 조절하는 `lora_alpha`다. |
| `BA` | `d×k` | `W₀`와 같은 모양의 update지만 `rank(BA) ≤ r`다. |

`B`와 `A`가 모두 넓은 행렬처럼 보이지만, trainable parameter 수는 `dk`가 아니라 `r(d+k)`다. `r`가 작을 때만 절약이 생긴다. `r=d=k` 근처까지 키우면 LoRA는 full fine-tuning에 가까운 표현력을 얻는 대신 작은 adapter라는 이점을 잃는다. 원 논문도 모든 matrix에 충분히 큰 rank와 bias를 학습하면 full fine-tuning의 표현력에 대략 접근한다고 설명한다.

### LoRA와 QLoRA, SFT를 같은 말로 부르면 안 된다

```text
SFT(Supervised Fine-Tuning, 지도 미세 조정)
  = 정답 예시로 model 행동을 학습하는 '목적/학습 방식'이다.

LoRA
  = 그 학습에서 어떤 parameter를 어떤 저랭크 모양으로 바꿀지 정하는 '적응 방식'이다.

QLoRA
  = 동결한 base model을 4-bit로 양자화하고, 그 위의 LoRA adapter로 gradient를 흘리는
    후속 방법이다. LoRA 자체가 base model을 4-bit로 만들지는 않는다.
```

[QLoRA 원 논문](https://arxiv.org/abs/2305.14314)은 4-bit NormalFloat, double quantization, paged optimizer를 추가 요소로 제시한다. 그러므로 “VRAM이 부족하니 LoRA를 썼다”와 “4-bit base 위에 QLoRA를 썼다”는 서로 다른 실험·운영 조건이다. checkpoint 크기, training dtype, inference precision, numerical error를 같은 표에 섞지 말아야 한다.

## 선행 개념: 행렬 곱, rank, 선형층, 동결과 gradient

### 선형층은 입력 숫자 묶음을 다른 숫자 묶음으로 바꾼다

`W₀x`는 `x`의 각 숫자에 `W₀`의 각 줄을 곱해 더한다. `3×4` weight와 `4×1` 입력이면 출력은 `3×1`이다. Transformer에서는 이런 선형층이 token 표현에서 query(`Wq`), key(`Wk`), value(`Wv`), output(`Wo`)를 만들고 MLP(Multi-Layer Perceptron, 다층 퍼셉트론) 안에서도 동작한다.

### rank는 “몇 개의 독립 방향으로 바꿀 수 있는가”다

아래는 색깔을 바꾸는 예가 아니라 좌표 이동 예다.

```text
rank 1 update: 모든 변화가 한 개의 기본 방향을 늘리거나 줄이는 조합이다.
rank 2 update: 서로 독립인 두 기본 방향을 섞을 수 있다.
full-rank update: 훨씬 많은 독립 방향을 각각 따로 바꿀 수 있다.
```

`BA`의 중간 폭이 `r`이므로 `BA`가 만들 수 있는 독립 방향은 최대 `r`개다. 그래서 LoRA가 되는 가정은 “이 downstream task가 원하는 **가중치 변화**가 전체 공간을 다 쓰지 않아도 된다”다. 이는 유용한 empirical hypothesis(경험적 가설)이지 모든 언어·모델·업무에서 성립하는 정리가 아니다.

### 동결은 “저장하지 않는다”가 아니라 “gradient를 받지 않는다”다

동결한 `W₀`는 GPU 메모리에서 사라지지 않는다. forward에서 반드시 읽어야 한다. 다만 optimizer가 `W₀`를 갱신하지 않고, `W₀`에 대한 gradient와 Adam state를 유지하지 않으므로 **학습 중** 비용이 준다. 이에 비해 adapter `A`, `B`는 gradient를 받고 task별 checkpoint에 저장한다.

원 논문은 `A`를 random Gaussian(무작위 가우스)으로, `B`를 0으로 초기화해 처음에는 `BA=0`이라고 한다. 따라서 첫 출력은 정확히 base 출력이다. 더 미세하게 보면 loss의 `BA`에 대한 기울기를 `G`라 할 때 첫 step에서 `∂L/∂A = BᵀG = 0`, `∂L/∂B = GAᵀ`가 된다. 즉 `B`가 먼저 움직이고, 그 다음부터 `A`도 gradient를 받는다. 이것은 “초기 base behavior를 보존”과 “학습을 완전히 멈추지 않음”을 함께 만족시키는 순서다.

## 밑바닥 원리: 왜 BA로 제한해도 되고, 왜 α/r을 곱하며, 왜 병합할 수 있는가

### 1. parameter 수를 숫자로 끝까지 계산한다

가로·세로가 각각 4,096인 square weight 하나를 rank 8 LoRA로 바꾼다고 하자.

\[
\begin{aligned}
\text{full update} &= 4096\times4096 = 16{,}777{,}216\\
\text{LoRA update} &= 8\times(4096+4096)=65{,}536\\
\text{비율} &= 16{,}777{,}216/65{,}536=256
\end{aligned}
\]

이것은 **선형층 하나의 update parameter만** 비교한 계산이다. 실제 model 전체 절감률에는 target module 수, layer 수, bias·embedding·LayerNorm을 학습하는지, optimizer와 activation memory가 모두 들어간다. 그래서 “rank 8은 무조건 256배 적다”는 말도 틀리다. 정확한 말은 “이 4,096×4,096 matrix 한 장의 update는 256배 적다”다.

### 2. `α/r`은 rank를 바꿀 때 update 스케일이 마구 달라지는 것을 완화한다

rank를 4에서 16으로 키우면 경로 수가 네 배가 된다. 아무 보정이 없으면 초기화·learning rate(학습률) 조합에 따라 update 크기가 함께 크게 흔들릴 수 있다. 원 논문은 `BAx`에 `α/r`을 곱하고, Adam에서 적절한 초기화라면 `α` 조정이 learning rate 조정과 대략 같은 역할을 한다고 설명한다. 이는 `α=16`이 모든 model에서 답이라는 뜻이 아니다. rank, target module, data, batch, precision을 바꾸면 validation set으로 다시 확인해야 한다.

### 3. 분리 계산과 병합 계산은 분배법칙으로 같다

학습 때는 보통 다음처럼 base branch와 LoRA branch를 더한다.

\[
y = W_0x + \frac{\alpha}{r}BAx
\]

matrix multiplication의 분배법칙으로 괄호를 묶으면 다음과 같다.

\[
y=\left(W_0+\frac{\alpha}{r}BA\right)x
\]

따라서 평가·배포 전에 `W_merged = W₀ + (α/r)BA`를 한 번 만들어 같은 dense layer로 실행할 수 있다. 이 한 줄이 원 논문의 “adapter처럼 추가 깊이를 남기지 않아 inference latency를 추가하지 않는다”는 근거다. 단, adapter가 여러 개인 요청을 한 batch에 섞어야 할 때는 모든 sample에 서로 다른 `W_merged`가 필요하다. 원 논문 §4.2도 merge를 택하면 서로 다른 `A`,`B`를 가진 task를 한 forward에 batch하기 어렵다고 한계로 적었다.

### 4. Transformer에서 어디에 넣는지는 별도 설계 결정이다

원 논문은 attention 안의 `Wq`, `Wk`, `Wv`, `Wo` 네 projection과 MLP 두 weight가 후보라고 설명하고, 주 실험에서는 간단함과 parameter 효율 때문에 attention weight만 적응했다. GPT-3 175B에서 같은 18M trainable parameter budget으로 비교한 Table 5에서는 `Wq,Wv`를 함께 적응한 설정이 WikiSQL 73.7, MultiNLI 91.3으로 전반적으로 가장 좋았다. 그러나 이것은 그 두 dataset·그 model·그 budget의 결과다.

논문 Table 6은 특히 중요하다. `Wq,Wv`를 함께 바꾼 GPT-3 실험은 WikiSQL에서 `r=1,2,4,8,64`에 각각 73.4, 73.3, 73.7, 73.8, 73.5를, MultiNLI에서 91.3, 91.4, 91.3, 91.6, 91.4를 보고했다. 저자는 작은 rank가 충분할 수 있음을 보였지만, 바로 이어 “pre-training과 다른 언어 같은 downstream task라면 small `r`보다 전체 재학습이 나을 수 있다”고 제한한다. `r=1`을 출발값으로 시험할 수는 있어도 규칙으로 고정할 근거는 아니다.

## 내부 구조와 실제 실행 흐름: `loralib`은 freeze → 두 branch 합산 → eval 병합을 어떻게 구현하는가

### 공식 source에서 확인한 `Linear`의 실제 경로

조사 기준일에 공식 repository를 shallow clone하여 commit `c4593f060e6a368d7bb5af5273b8e42810cdef90`을 확인했다. 이 commit은 2024-12-17 commit이다. 원 논문 자체의 시대와 다르므로, 아래는 **원 논문의 수식을 구현한 현재 공개 library source의 동작**이며 논문 Table의 실행 환경과 동일하다고 주장하지 않는다.

1. [`Linear.__init__`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/layers.py#L90-L125)는 `r>0`이면 `lora_A`를 `(r, in_features)`, `lora_B`를 `(out_features, r)`로 만든다. `scaling = lora_alpha / r`를 저장하고 원래 `self.weight.requires_grad = False`로 동결한다.
2. 같은 함수의 `reset_parameters`는 실제로 `A`를 Kaiming uniform(카이밍 균등분포), `B`를 0으로 초기화한다. 논문의 “A Gaussian, B zero”와 정확히 같은 분포는 아니지만, 시작 `BA=0`이라는 핵심 성질은 같다.
3. [`Linear.forward`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/layers.py#L144-L152)는 아직 merge하지 않았을 때 `F.linear(x, weight, bias)` 결과에 `dropout(x) @ Aᵀ @ Bᵀ * scaling`을 더한다. 행 벡터 표기에서 이는 위 수식의 `W₀x+(α/r)BAx`와 같은 계산이다.
4. [`Linear.train`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/layers.py#L127-L142)은 `eval()` 전환 때 `B @ A * scaling`을 `weight.data`에 더하고 `merged=True`로 표시한다. 다시 `train()`으로 전환하면 같은 값을 빼서 원래 `W₀`를 복구한다. merge가 끝난 `forward`는 base `F.linear`만 호출한다.
5. [`mark_only_lora_as_trainable`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/utils.py#L13-L30)은 이름에 `lora_`가 없는 parameter의 `requires_grad`를 false로 바꾼다. `bias='all'` 또는 `bias='lora_only'`는 추가 선택지다. [`lora_state_dict`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/utils.py#L33-L49)는 기본적으로 `lora_` key만 저장한다.

이를 실행 흐름으로 펼치면 다음과 같다.

```text
1. base checkpoint W₀를 load한다.
2. target Linear에 A, B를 붙인다. W₀는 requires_grad=False다.
3. batch x를 넣는다.
   base = W₀x
   lora = (α/r)B(A(dropout(x)))
   y = base + lora
4. loss.backward()는 A,B(선택한 bias)에만 gradient를 쌓는다.
5. optimizer.step()은 A,B만 갱신한다.
6. 저장할 때는 base 전체가 아니라 A,B state_dict를 저장한다.
7. 배포 정책이 merge라면 W₀ ← W₀+(α/r)BA로 합친다.
   요청은 합쳐진 한 Linear를 지난다.
```

### QKV가 한 Linear로 묶여 있을 때의 실제 처리

Transformer 구현마다 `q_proj`, `k_proj`, `v_proj`가 세 module일 수도 있고, 한 `qkv_proj` Linear일 수도 있다. 이 차이를 무시하고 target module 이름만 복사하면 잘못된 층을 학습하거나 아예 target을 찾지 못한다.

공식 GPT-2 예제의 [`Attention.__init__`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/examples/NLG/src/model.py#L83-L103)는 `c_attn` 하나를 `lora.MergedLinear(nx, 3*n_state, ...)`로 바꾸고 `enable_lora=[True, False, True]`를 준다. 즉 query와 value만 LoRA로 켜고 key는 끈다. [`MergedLinear`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/layers.py#L155-L244)는 선택한 출력 slice에만 `BA`를 zero padding해 더한다.

여기에는 흔히 놓치는 사실이 있다. 이 GPT-2 예제는 `merge_weights=False`를 넘긴다. 반면 `Linear`의 기본값은 `True`다. 따라서 “공식 library는 `model.eval()`이면 언제나 merge한다”도 틀렸다. **`merge_weights` 설정과 layer 종류를 실제로 확인해야 한다.** merge하지 않은 serving은 요청마다 LoRA branch 계산을 더 수행한다. 그 경우 원 논문의 “merge 뒤 추가 latency 없음”을 그대로 주장할 수 없다.

### checkpoint는 adapter만으로 완전한 모델이 아니다

공식 README는 base checkpoint를 먼저 `strict=False`로 load하고, 이어 LoRA checkpoint를 `strict=False`로 load하라고 안내한다. adapter file만 전달받아도 다음 계약이 없으면 같은 model을 복원할 수 없다.

```yaml
base_model_id: 조직/모델명과 정확한 revision 또는 SHA
tokenizer_id: tokenizer revision과 special token 설정
architecture: hidden size, layer count, qkv layout
target_modules: q/v인지 q/k/v/o인지, MLP도 포함하는지
rank_r: 8
lora_alpha: 16
lora_dropout: 0.05
bias_policy: none | lora_only | all
training_data_version: immutable dataset snapshot
training_code_commit: git SHA
evaluation_report: dataset, metric, prompt/template, seed, threshold
adapter_artifact_hash: SHA-256
```

이것은 논문이 제시한 파일 형식이 아니라 **운영을 위한 해석**이다. 데이터 파이프라인 관점에서 `base_model_id + adapter_artifact_hash + tokenizer + prompt template`을 하나의 reproducible release(재현 가능한 릴리스)로 묶어야 한다는 뜻이다. `A,B`의 shape가 우연히 맞아 load되었다고 같은 의미의 model인 것은 아니다.

## 직접 검증과 재현: 작은 행렬에서 분리 계산·병합·초기 상태를 검사했다

### 확인 범위와 환경

**직접 실행 확인:** macOS, Python `3.9.6`, 표준 library만 사용했다. 원 논문 PDF SHA-256은 `e9a0d3128767db616085dc0f4e6e455e672e89af823e8ed1282793682787395a`이고, 공식 repository checkout은 위 `c4593f0`이다. local 환경에 PyTorch가 없어 실제 `loralib.Linear` tensor forward나 GPT-3/GLUE benchmark를 실행하지 않았다. 대신 원 논문 Eq. (3)의 행렬 연산을 순수 Python으로 직접 계산했고, 공개 `layers.py`, `utils.py`는 `python3 -m py_compile`로 문법 compile을 확인했다. compile 성공은 PyTorch runtime 동작 검증이 아니다.

아래 코드는 `W₀`가 3×4, `A`가 2×4, `B`가 3×2인 가장 작은 유의미한 예다. `α=4`, `r=2`라서 scale은 2다. 그대로 복사해 실행할 수 있다.

```python
# verify_lora_math.py — Python 3 표준 library만 사용
def mat_vec(matrix, vector):
    return [sum(value * vector[column] for column, value in enumerate(row))
            for row in matrix]


def mat_mul(left, right):
    columns = list(zip(*right))
    return [[sum(value * column[index] for index, value in enumerate(row))
             for column in columns] for row in left]


def mat_add(left, right):
    return [[a + b for a, b in zip(left_row, right_row)]
            for left_row, right_row in zip(left, right)]


def scale(matrix, factor):
    return [[factor * value for value in row] for row in matrix]


def scale_vec(vector, factor):
    return [factor * value for value in vector]


w0 = [[2, -1, 0, 3], [1, 4, -2, 0], [-3, 1, 5, 2]]
a = [[1, 0, -1, 2], [0, 2, 1, -1]]
b = [[2, -1], [1, 3], [-2, 4]]
x = [1, 2, -1, 3]
alpha, rank = 4, 2
scaling = alpha / rank

base = mat_vec(w0, x)
update = scale_vec(mat_vec(mat_mul(b, a), x), scaling)
separate = [base_value + update_value
            for base_value, update_value in zip(base, update)]
merged_weight = mat_add(w0, scale(mat_mul(b, a), scaling))
merged = mat_vec(merged_weight, x)

assert separate == merged

zero_b = [[0, 0], [0, 0], [0, 0]]
initial = mat_vec(mat_add(w0, scale(mat_mul(zero_b, a), scaling)), x)
assert initial == base

full = 4096 * 4096
lora = 8 * (4096 + 4096)
assert (full, lora, full // lora) == (16_777_216, 65_536, 256)

print(base, separate, merged, initial, full, lora)
```

실행 결과는 다음이었다.

```text
base=[9, 11, 0]
separate=[41.0, 27.0, -32.0]
merged=[41.0, 27.0, -32.0]
B=0_initial=[9.0, 11.0, 0.0]
full=16,777,216; lora_rank8=65,536; ratio=256x
```

관찰은 세 개다.

1. 분리한 `W₀x + scale×BAx`와 합친 `(W₀ + scale×BA)x`가 정확히 일치했다. 이것이 merge의 수학적 근거다.
2. `B=0`이면 update가 0이어서 첫 출력이 base output과 같다.
3. 4,096×4,096 한 층의 rank 8 update는 65,536개다. 위 절의 256배 계산은 실제 정수 산술로도 확인됐다.

### 값을 하나 바꿔 보는 경계 실험

위 코드에서 `rank = 2`를 `rank = 1`로 바꾸면서 `A`와 `B` shape를 각각 1×4, 3×1로 맞추면 모든 `ΔW`의 줄이 한 방향 조합으로만 변한다. parameter 수는 더 작아지지만 표현력도 줄어든다. 반대로 `B`를 모두 0으로 두면 assert가 보이듯 base behavior를 그대로 통과한다. **이 두 결과는 LoRA 수식의 검증이지 “rank 1이면 내 업무가 된다”는 품질 실험이 아니다.** 품질은 held-out evaluation(학습에 쓰지 않은 평가 데이터)으로 별도 측정해야 한다.

### 실제 library 재현 시 최소 점검 순서

```bash
# 1) 공식 source와 현재 실행 모델의 target module 이름을 먼저 확인한다.
python - <<'PY'
for name, module in model.named_modules():
    if "q_proj" in name or "v_proj" in name or "c_attn" in name:
        print(name, type(module), getattr(module, "weight", None).shape)
PY

# 2) 학습 전/후 trainable parameter와 adapter key를 기록한다.
python - <<'PY'
trainable = [(n, tuple(p.shape)) for n, p in model.named_parameters() if p.requires_grad]
print(trainable)
print(list(lora.lora_state_dict(model).keys()))
PY

# 3) merge 전후 같은 fixed input의 logits를 비교한다.
# torch.allclose(logits_before, logits_after, rtol=..., atol=...)
```

위 snippet의 `model`, `lora`, tolerance는 사용하는 framework·dtype에 맞춰 호출자가 채워야 한다. 특히 FP16/BF16/4-bit에서는 부동소수점 반올림 때문에 bitwise equality(비트 단위 동일성)가 아니라 model policy에 맞는 `allclose` 기준과 task metric을 함께 기록해야 한다. merge test가 불일치하면 우선 `alpha/r`, fan-in/fan-out weight orientation, target module slice, dropout/eval mode, base revision을 확인한다.

## 성능, 복잡도와 트레이드오프: 적은 trainable parameter가 무료 추론·자동 품질을 뜻하지 않는다

### 계산 비용을 두 구간으로 나눈다

하나의 `d×k` 선형층에서 다음 정도의 계산을 생각할 수 있다.

| 구간 | base 계산 | LoRA가 추가하는 계산 | 핵심 비용 |
| --- | --- | --- | --- |
| 학습 또는 unmerged inference | `O(dk)` | `O(rk + dr)` | base 읽기는 남고 LoRA branch가 추가됨 |
| merged inference | `O(dk)` | 사전 merge 때만 `O(dkr)` matrix product | 요청당 extra branch 없음 |
| task artifact 저장 | base는 공유 | `O(r(d+k))` | task별 `A,B`만 저장 |
| full fine-tuning task artifact | `O(dk)` | 없음 | 업무별 full update/모델 사본이 큼 |

`BA`를 사전 계산하는 dense matrix product 자체는 비용이 있지만, adapter를 load·switch할 때 또는 deployment artifact를 만들 때 한 번 수행할 수 있다. 요청마다 어느 adapter를 쓸지 바꾸는 multi-tenant serving(다중 임차 서빙)은 선택지가 갈린다.

```text
작업별 worker / 낮은 latency 우선
  → adapter를 merge한 worker를 준비한다.

한 worker에서 adapter를 자주 바꾸고 batch를 크게 묶음
  → unmerged 또는 adapter-aware serving이 나을 수 있다.
     대신 branch 비용·routing·batching 정책을 실제 benchmark한다.
```

`W₀`와 adapter를 요청 중간에 in-place merge/unmerge하면 동시 요청이 다른 업무 weight를 읽을 수 있다. 공식 source는 `weight.data += ...`와 `-=`로 merge state를 바꾼다. 따라서 shared process에서는 mutable shared model 한 개를 요청별로 바꾸지 말고, immutable merged replica, adapter별 worker, 또는 그 동시성을 보장하는 serving runtime을 선택해야 한다. 이것은 source의 locking 기능이 아니라 source 동작에서 도출한 **운영 해석**이다.

### 원 논문 성능을 올바른 단위로 읽는다

원 논문 결과는 LoRA가 가치 있다는 근거이지만, 내 product의 SLA(Service Level Agreement, 서비스 수준 협약)를 보장하는 benchmark는 아니다.

| 논문 표 | 설정과 관찰 | 읽는 법 |
| --- | --- | --- |
| Table 2 | DeBERTa XXL 1.5B GLUE에서 LoRA 4.7M의 평균 91.3, full fine-tuning 1.5B의 평균 91.1 | 서로 다른 GLUE metric을 평균 낸 해당 benchmark 결과다. domain shift·seed·data size가 바뀌면 역전될 수 있다. |
| Table 3 | GPT-2 medium E2E NLG에서 LoRA 0.35M의 BLEU 70.4±0.1, full fine-tuning 354.92M의 68.2 | 한 NLG task에서 적은 parameter로도 더 높은 점수를 낸 사례다. 모든 생성 task의 우월성 증명은 아니다. |
| Table 4 | GPT-3 175B, LoRA 4.7M은 WikiSQL 73.4, MNLI-m 91.7, SAMSum R1/R2/RL 53.8/29.8/45.9 | full fine-tuning의 73.8/89.5/52.0/28.0/44.5와 task별로 다르다. WikiSQL은 full fine-tuning보다 낮다. |
| §4.2 | GPT-3 175B에서 LoRA throughput 43.1 tokens/s/V100, full fine-tuning 32.5 tokens/s/V100 | 같은 weight sharding이라는 논문 조건의 약 25% throughput 증가다. hardware·parallelism을 생략해 일반화하면 안 된다. |

좋은 운영 평가 표는 최소한 다음 열을 같이 가진다.

```text
base model SHA | adapter SHA | dataset snapshot | tokenizer/template
target modules | r/α/dropout | seed | trainable count
quality metric + slice | p50/p95 latency | tokens/s | peak VRAM
merge 여부 | concurrency/batch | rollback candidate | approval date
```

품질만 높고 p95 latency가 SLA를 넘거나, VRAM은 맞지만 long context에서 OOM(Out Of Memory, 메모리 부족)이 나거나, 잘못된 adapter version을 선택해 보안 정책을 무시하면 성공한 fine-tuning이 아니다.

## 실패, 한계, 장애와 운영: LoRA는 학습 데이터·모델 호환성·권한 문제를 없애지 않는다

### 실패 1: `r`을 작게 고정하고 평가를 건너뛴다

**증상:** training loss는 내려가는데 domain QA, SQL exact match, 안전 거절, 긴 문맥 slice에서 품질이 나쁘다.

**원인:** 작은 rank는 변경 공간을 제한한다. 원 논문도 `Wq,Wv`와 WikiSQL/MultiNLI에서 얻은 관찰을 보였을 뿐, 모든 task에 보편적으로 작다고 증명하지 않았다. target module도 `q,v`만, attention 전체, MLP까지 중 무엇이 필요한지 업무마다 다를 수 있다.

**대응:** 먼저 task failure taxonomy(실패 분류)를 만든다. 예를 들어 SQL 생성이면 schema linking, join, aggregation, permission, syntax를 분리한다. 그 뒤 `target_modules × r × α × data amount`의 작은 grid를 정하고 동일한 holdout·seed 정책으로 비교한다. 최고 평균 하나가 아니라 business-critical slice의 최저 성능, latency, 비용을 함께 gate로 둔다.

### 실패 2: base model·tokenizer·adapter가 서로 다른 release다

**증상:** load에서 shape mismatch가 나거나, load는 되지만 answer style·special token 처리·품질이 이상하다.

**원인:** LoRA adapter는 독립 모델이 아니다. `A`, `B`는 특정 base weight 위치와 shape에 붙는다. 같은 “모델 이름”이어도 revision, chat template, tokenizer vocabulary, RoPE(Rotary Position Embedding, 회전 위치 임베딩) 설정, qkv packing이 바뀌면 계약이 바뀔 수 있다.

**대응:** immutable artifact registry(불변 산출물 레지스트리)에 base revision SHA, tokenizer hash, model config, target module mapping, adapter hash를 함께 저장한다. load 직후 shape만 보지 말고 fixed golden prompts의 logits 또는 task output, eval metric을 compare한다. mismatch면 강제로 `strict=False`로 덮어쓰지 말고 release를 거부한다. `strict=False`는 공식 예제가 base와 adapter의 서로 다른 key 집합을 순서대로 load하기 위한 도구이지 호환성 검증 우회 버튼이 아니다.

### 실패 3: merge가 latency·정확도를 자동으로 보장한다고 믿는다

**증상:** adapter별 요청이 섞인 batch에서 다른 업무 답이 섞이거나, merge 전후 logit 차이가 크거나, latency가 예상보다 높다.

**원인:** unmerged mode에는 LoRA branch가 남는다. merged mode는 같은 model instance의 `weight`를 바꾸는 구현도 있다. mixed-adapter batching, quantized base, dtype cast, dropout이 켜진 train mode, fan-in/fan-out orientation은 단순 분배법칙 바깥의 실제 실행 조건이다.

**대응:** 배포 전 `eval` mode에서 adapter별 fixed input merge 전후 비교를 CI(Continuous Integration, 지속적 통합)에 넣고 tolerance·metric threshold를 versioned config로 둔다. request routing과 model instance ownership을 명확히 한다. latency는 p50/p95/p99, batch size, input/output token 길이, concurrent request 수를 기록해 load test한다. rollback은 이전에 검증한 `base SHA + adapter SHA` 조합으로 route를 되돌리는 방식이어야 한다.

### 실패 4: 작은 artifact를 보안상 작은 변경으로 오해한다

**증상:** 허가받지 않은 adapter가 운영 base에 붙거나, 특정 adapter가 safety policy·업무 규칙을 우회한다.

**원인:** adapter file은 작지만 output behavior를 크게 바꿀 수 있다. `A,B`가 작은 것은 권한 범위가 작다는 뜻이 아니다. data poisoning(학습 데이터 오염), license·PII(Personally Identifiable Information, 개인 식별 정보) 유입, malicious adapter 교체는 full fine-tuning과 마찬가지로 위험하다.

**대응:** adapter artifact에 서명·hash allowlist(허용 목록), producer identity, training data lineage(계보), approval record를 둔다. model gateway는 사용자가 adapter ID를 임의 path로 넘기지 못하게 하고, tenant·업무별 allowlist로 해석한다. prompt injection을 LoRA가 막아 주지도 않는다. RAG·tool calling을 같이 쓰면 retrieval source와 tool authorization을 별도로 통제한다.

### 실패 5: LoRA로 최신 사실을 주입하려 한다

**증상:** 이번 달 정책·가격·재고를 고치려고 매번 새 adapter를 train하고, 이전 사실과 충돌하거나 stale(오래된) answer가 나온다.

**원인:** fine-tuning은 model behavior를 바꾸는 학습이다. versioned fact source의 transaction·freshness·citation을 제공하지 않는다. 작은 adapter라서 업데이트가 쉽더라도, factual update가 정확히 한 문장만 바뀌고 다른 일반화에 영향이 없음을 보장하지 않는다.

**대응:** 자주 바뀌고 근거가 필요한 사실은 source-of-truth DB·API와 retrieval을 먼저 설계한다. LoRA는 format, domain language, instruction following, classification 같은 반복 behavior 적응을 검토한다. “지식이 필요하다”를 “모델 weight를 바꿔야 한다”로 바로 번역하지 않는다.

### 장애 대응 runbook: 무엇을 먼저 볼까

```text
1. 어떤 release인가?
   request ID → base SHA, adapter SHA, tokenizer/template, route, merge flag

2. 재현 가능한가?
   같은 fixed prompt, seed policy, eval mode, dtype, device에서 golden output 비교

3. 호환성 문제인가?
   config shape, target module mapping, missing/unexpected key, adapter checksum 확인

4. 품질 문제인가?
   data slice별 metric, training/validation split leakage, prompt/template 변화를 분리

5. 성능 문제인가?
   token 길이·batch·concurrency별 p95, VRAM peak, OOM, queueing을 확인

6. 즉시 완화는 가능한가?
   allowlisted 이전 adapter로 route rollback, canary traffic 축소, write side effect 차단
```

LoRA 자체는 DB transaction이나 model registry를 제공하지 않는다. 하지만 backend·DBA 경험자는 위처럼 release identity, immutable data snapshot, rollback, audit, latency/VRAM observability를 시스템으로 만들 수 있다. 이것이 “adapter 하나를 학습했다”보다 재사용 가능한 경쟁력이다.

## 대안, 비교와 선택 기준: 목표가 adaptation인지, 최신 지식인지, 메모리인지 먼저 분리한다

### 같은 문제층의 선택표

| 먼저 확인할 질문 | 우선 검토 | 이유 | LoRA만으로 부족한 경우 |
| --- | --- | --- | --- |
| 모델이 업무의 말투·출력 형식·분류 기준을 반복해서 틀리는가? | LoRA + SFT | base를 공유하며 behavior를 task data에 맞춤 | data 품질·평가가 없으면 작은 adapter도 나쁜 습관을 학습 |
| best quality가 절대 우선이고 충분한 GPU·저장 공간이 있는가? | full fine-tuning baseline | rank/target 제약이 없음 | 업무별 model 보관·배포·rollback 비용이 커짐 |
| base model을 학습 GPU에 올리기 어려운가? | QLoRA 검토 | 4-bit frozen base와 LoRA를 결합 | quantization precision·kernel·serving compatibility를 별도 검증 |
| 여러 task를 한 base에 작게 붙이고 runtime 추가층을 피하고 싶은가? | merge 가능한 LoRA | `BA`를 base weight로 흡수 가능 | mixed-adapter batch·mutable merge 운영을 설계해야 함 |
| 학습 data가 매우 작고 behavior 변경도 작아야 하는가? | prompt/prefix tuning 또는 in-context example | weight artifact 없이 빠른 실험 가능 | context 길이·prompt 민감도·일관성 한계 |
| 오늘의 정책·가격·문서 근거가 답에 필요해 자주 바뀌는가? | RAG(Retrieval-Augmented Generation, 검색 증강 생성)·DB/API | source와 freshness·citation을 실행 시점에 다룸 | retrieval quality·access control·grounding evaluation이 필요 |
| model 중간에 독립적인 새 변환 함수를 넣어야 하는가? | adapter layer | architecture상 병목 module이 유리할 수 있음 | 추가 layer latency와 배포 경로를 측정 |

선택은 논문 제목이 아니라 **실패하는 업무 사례**에서 시작한다. SQL 생성의 schema 오류라면 schema retrieval과 execution-safe validation이 먼저일 수 있다. 정책 문서의 최신성 문제라면 RAG와 document pipeline이 먼저다. 일정한 JSON output·도메인 용어·분류 행동을 지속적으로 고치고 base model은 공유해야 한다면 LoRA가 강한 후보가 된다.

### 나의 데이터+AI 경로에 적용하는 4단계

나처럼 backend·DB·데이터 파이프라인에서 AI로 넓히는 사람은 “LoRA 수식 암기”와 “파인튜닝 버튼 누르기” 사이를 다음 artifact로 연결하는 편이 효율적이다.

```text
1. 문제 계약
   입력/출력 schema, 금지 결과, 최신성 요구, human approval 경계를 문서화한다.

2. 데이터 계약
   raw source → 정제 → de-identification → train/valid/test split → immutable snapshot
   각 단계의 source, row count, license, PII 처리, hash를 남긴다.

3. 실험 계약
   full fine-tuning 또는 prompt-only baseline을 먼저 만든다.
   LoRA target/rank/alpha를 바꾼 experiment를 같은 dataset·metric·seed로 비교한다.

4. 배포 계약
   base+adapter+tokenizer+template를 release로 묶는다.
   canary, quality/latency/VRAM dashboard, rollback, approval, audit를 구현한다.
```

여기서 data pipeline 설계·구축 역량은 부가 기능이 아니다. training data의 lineage와 evaluation dataset의 누수를 막고, adapter release를 reproducible하게 만들고, feedback을 다음 dataset version으로 흘리는 핵심이다. 석사·박사가 모델 구조를 깊게 연구하는 길과 경쟁할 때도, 문제를 안전한 데이터 계약과 운영 가능한 AI system으로 끝까지 연결하는 능력은 독립적인 무기가 된다.

## 흔한 오해와 최초 질문의 답: LoRA를 어느 깊이까지 이해하고 언제 써야 하는가

### 오해 1. “LoRA는 작은 언어 모델이다.”

아니다. base model parameter 수는 그대로다. LoRA가 작게 만드는 것은 task별로 학습·저장하는 update다. inference에 base model을 올릴 메모리는 여전히 필요하다.

### 오해 2. “LoRA는 4-bit 양자화다.”

아니다. LoRA는 low-rank adaptation이다. QLoRA가 frozen base의 4-bit quantization과 LoRA를 결합한 후속 방법이다. 실험 보고에는 base dtype과 adapter dtype을 명시해야 한다.

### 오해 3. “LoRA는 항상 full fine-tuning보다 품질이 좋다.”

아니다. 원 논문 Table 2–4에서 여러 강한 결과를 보였지만 Table 4 WikiSQL 4.7M LoRA는 full fine-tuning 73.8보다 73.4였다. 논문도 different-language task 같은 경우 큰 rank 또는 full retraining이 더 나을 수 있다고 쓴다. 내 model·data·metric의 baseline이 판정자다.

### 오해 4. “`r=8`, `alpha=16`, target=`q,v`를 복사하면 된다.”

아니다. 그 값들은 자주 쓰이는 시작 후보일 뿐이다. architecture의 module 이름과 qkv layout이 다르고, data 크기·domain shift·metric·VRAM 예산도 다르다. 논문이 찾은 `Wq,Wv`의 장점도 GPT-3의 정해진 budget과 두 dataset에서의 관찰이다.

### 오해 5. “merge하면 모든 serving 문제가 사라진다.”

아니다. 단일 adapter가 합쳐진 dense weight를 쓸 때 추가 branch를 없애는 수학적 이점이 있다. 그러나 adapter 선택, mixed batching, shared instance mutation, quantization, version mismatch, safety는 별도 system problem이다. 공식 GPT-2 예제처럼 merge가 꺼진 layer도 있다.

### 오해 6. “새 사실을 넣으려면 LoRA로 재학습해야 한다.”

대개 먼저 DB·API·RAG를 검토한다. 빈번히 변하는 사실은 source freshness와 access control이 중요하다. LoRA는 행동·형식·도메인 적응의 수단이지 일반적인 최신 지식 저장소가 아니다.

### 처음의 질문: 이 논문을 지금 어떻게 읽을까?

top-down 학습에서 LoRA는 “대형 model을 업무에 맞춘다”는 product 문제에서 시작해, 필요한 만큼 bottom-up으로 내려가기에 좋은 논문이다. 다음 순서로 읽는다.

```text
20분: Abstract → Figure 1 → §4.1 식 (3) → §4.2 장점/한계 → Table 4
40분: §3의 adapter/prefix 문제 → §5의 비교 조건 → §7 Table 5·6
60분: 공식 loralib Linear의 init/forward/train → 작은 BA merge 코드를 직접 계산
프로젝트: 내 업무의 full/prompt baseline → LoRA experiment → versioned deployment
```

여기서 멈추면 “LoRA를 안다”가 아니라 “논문을 읽었다”다. 진짜 체크리스트는 다음이다.

- [ ] `W₀`, `A`, `B`, `r`, `α/r`의 shape와 첫 `B=0` forward를 손으로 설명한다.
- [ ] full update `dk`와 LoRA update `r(d+k)`를 target layer별로 계산한다.
- [ ] 공식 또는 사용할 framework source에서 실제 target module, merge flag, state_dict 경로를 확인한다.
- [ ] 내 업무의 data lineage, split, leakage test, metric·slice·threshold를 만든다.
- [ ] prompt-only/full fine-tuning/LoRA 중 적어도 하나의 baseline과 같은 조건에서 비교한다.
- [ ] merge 전후 fixed input, p95 latency, peak VRAM, concurrency에서 검증한다.
- [ ] base+adapter+tokenizer+template+code+data를 hash로 묶고 canary·rollback을 연습한다.

이 체크리스트를 완성하면 작은 adapter를 만든 사람이 아니라, **비용·품질·최신성·보안·재현성을 함께 판단하는 AI system builder**가 된다. 다음 논문 FlashAttention은 이 글에서 남은 “base model을 여전히 읽어야 하는데, attention memory·IO는 왜 병목인가?”를 하드웨어 알고리즘 층에서 잇는다.

## 출처

### 1차 자료

1. Hu et al., [*LoRA: Low-Rank Adaptation of Large Language Models*](https://arxiv.org/abs/2106.09685), arXiv:2106.09685v2, 2021-10-16, ICLR 2022. 이 글의 정의·Eq. (1)–(3)·초기화·merge 논리·Table 1–7·한계·GPT-3 메모리/throughput 수치의 원문이다. 특히 §4.1–4.2, §5, §7과 Appendix D를 직접 대조했다.
2. Microsoft, [공식 `microsoft/LoRA` repository](https://github.com/microsoft/LoRA/tree/c4593f060e6a368d7bb5af5273b8e42810cdef90), commit `c4593f060e6a368d7bb5af5273b8e42810cdef90`, 확인일 2026-07-24. [`Linear`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/layers.py#L90-L152), [`MergedLinear`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/layers.py#L155-L244), [`utils.py`](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/loralib/utils.py#L13-L49), [GPT-2 QKV 예제](https://github.com/microsoft/LoRA/blob/c4593f060e6a368d7bb5af5273b8e42810cdef90/examples/NLG/src/model.py#L83-L103)를 직접 읽었다.
3. Dettmers et al., [*QLoRA: Efficient Finetuning of Quantized LLMs*](https://arxiv.org/abs/2305.14314), arXiv:2305.14314v1, 2023-05-23. LoRA와 frozen 4-bit quantized base를 결합하는 QLoRA의 경계를 확인했다.

### 직접 검증 기록

1. `/tmp/lora-paper-research/verify_lora_math.py`를 Python 3.9.6으로 직접 실행했다. 순수 행렬 연산으로 Eq. (3)의 분리·병합 동치, `B=0` 초기 출력, 4,096×4,096 rank 8 parameter 수를 assert했다. script SHA-256: `15e6d7f022c823db9cab9605bd93824b7d724de6afff5ad4666e0eb567b1b982`.
2. `python3 -m py_compile /tmp/lora-paper-research/LoRA/loralib/layers.py /tmp/lora-paper-research/LoRA/loralib/utils.py`를 성공시켰다. local PyTorch 미설치 때문에 library tensor runtime·원 논문 benchmark는 실행하지 않았으며, 이 한계를 본문에 명시했다.

### 보조 자료

이 글의 핵심 주장에는 보조 기술 블로그를 사용하지 않았다. 쉬운 비유와 데이터·운영 선택 기준은 원 논문·공식 source를 바탕으로 한 이 글의 해석이며, benchmark 사실과 구분해 표기했다.
