층 사이에 KV 캐시가 겹치는데도 줄일 게 없었습니다: LoopVQ 소식을 듣고 인접 층 중복을 재 보니
TL;DR 11월 3일 arXiv에 올라온 LoopVQ는 루프 트랜스포머의 KV 캐시를 원소당 0.5비트까지 줄여 SGLang에서 최대 4.3배, 6.1배의 처리량을 냈다고 보고했습니다. 이 소식을 보고 저희는 한 가지가 궁금해졌습니다. 루프가 없는 일반 모델에서도 층과 층 사이에 이만한 중복이 있을까요? 있다면 같은 발상으로 KV 캐시를 더 줄일 수 있을 것입니다. 공개 모델 Qwen
TL;DR
11월 3일 arXiv에 올라온 LoopVQ는 루프 트랜스포머의 KV 캐시를 원소당 0.5비트까지 줄여 SGLang에서 최대 4.3배, 6.1배의 처리량을 냈다고 보고했습니다. 이 소식을 보고 저희는 한 가지가 궁금해졌습니다. 루프가 없는 일반 모델에서도 층과 층 사이에 이만한 중복이 있을까요? 있다면 같은 발상으로 KV 캐시를 더 줄일 수 있을 것입니다.
공개 모델 Qwen3.5-2B로 쟀습니다. 결과는 두 갈래였습니다. 중복은 실제로 있었습니다. 바로 앞 전체주의 층의 K로 다음 전체주의 층의 K를 선형 예측하면, 학습에 쓰지 않은 글에서 R²가 0.34~0.69(깊은 층일수록 큼), V는 0.29~0.60이었고, 토큰 순서를 섞은 대조군은 -0.02~-0.15로 전부 0 아래였습니다. 그러나 줄일 게 거의 없었습니다. 이 모델은 24층 중 6층에만 KV가 있고 층마다 KV 헤드가 2개뿐이라, 32k 문맥의 4비트 KV 캐시가 96MiB입니다. 측정된 중복을 이상적으로 다 써도 더 줄어드는 양은 약 14MiB였습니다.
이 글의 수치는 전부 저희의 자체 측정이고 제3자가 확인한 것이 아닙니다. LoopVQ를 재현한 실험도 아닙니다. 저희는 LoopVQ의 초록만 읽었고, 이 글은 그 논문의 주장을 검증하거나 반박하지 않습니다.
LoopVQ는 무엇을 했고, 저희는 무엇이 궁금했나요?
초록에 따르면 루프 트랜스포머는 같은 층을 여러 번 되풀이해 적용하는 구조입니다. 파라미터 메모리는 아끼지만 루프 한 번마다 자기 키와 값을 따로 저장하기 때문에 KV 캐시가 루프 횟수에 비례해 커지고, 이것이 주된 메모리 병목이 됩니다. 저자들은 루프 축을 따라 KV 캐시가 매우 중복되어 있다는 점에 착안해, 여러 루프 단계의 채널을 함께 부호화하는 벡터 양자화를 제안했습니다. Ouro와 Huginn 모델에서 원소당 0.5비트로 기존 방법보다 낫고, SGLang에 구현했을 때 32K 문맥의 디코드 지연이 거의 반으로 줄며 Ouro 1.4B와 2.6B에서 최대 처리량이 BF16 대비 최대 4.3배, 6.1배라고 합니다(저자 보고, 저희가 재현하지 않았습니다).
여기서 저희가 가져온 질문은 하나입니다. 루프 축의 중복이 같은 층을 되풀이해서 생기는 것이라면, 일반 모델의 층 축에서도 비슷한 중복이 있을까요? 층마다 가중치가 다르니 같을 이유는 없지만, 잔차 연결 덕에 인접 층의 표현이 닮아 있을 수는 있습니다. 닮았다면 인접 층 K와 V를 묶어 부호화하는 길이 열립니다. 결과가 어느 쪽이든 서빙 메모리 예산을 짜는 데 쓸모 있는 숫자라고 보고 측정부터 했습니다. 압축을 구현하지는 않았습니다.
무엇을, 어떻게 쟀나요?
대상은 Qwen3.5-2B(Apache-2.0)입니다. 설정 파일을 확인하면 24층이고, 4번째 층마다 전체 어텐션이며(층 번호 3, 7, 11, 15, 19, 23) 나머지 18층은 선형 어텐션입니다. 전체 어텐션 층의 KV 헤드는 2개, 헤드 차원은 256입니다. 그래서 KV 캐시에 실제로 저장되는 것은 전체 어텐션 6개 층뿐이고, 층당 K와 V가 각각 512차원입니다. 측정은 bf16으로 RTX 5060 노트북 GPU(8GB) 한 장에서 했습니다.
입력은 성격이 다른 글 네 편입니다. 한국어 보고서, 영문 모델 카드(HTML 태그를 제거), 파이썬 코드, 한국어 변경 이력이고, 각 글에서 처음 200토큰을 건너뛰고 1,536토큰씩 잘랐습니다. 재는 값은 RoPE가 적용된 뒤 캐시에 저장되는 K와 V입니다. 지표는 두 가지입니다. 하나는 같은 번호 채널끼리의 피어슨 상관(절댓값의 중앙값), 다른 하나는 앞 층의 K(또는 V) 512차원으로 다음 층의 같은 것을 선형 예측했을 때의 결정계수 R²입니다.
R²를 믿으려면 평가 방식이 중요합니다. 저희는 글 하나를 통째로 빼고 나머지 셋으로 학습한 뒤 뺀 글로 평가했습니다(leave-one-document-out). 릿지 정규화 강도는 학습 글 셋 안에서 다시 한 글을 빼는 방식으로 6개 후보 중에서 골랐습니다. 대조군은 둘입니다. 앞 층의 토큰 순서를 섞어 넣은 입력(기대값은 0 이하), 그리고 인접하지 않은 쌍(3번 층과 23번 층)입니다. 잡음 바닥은 같은 입력을 두 번 돌려 K와 V 12항목의 최대 절대 차이를 봤고, 전부 0.0, 즉 비트 단위로 같았습니다.
첫 시도는 실패였습니다. 글 한 편 안에서 앞 절반으로 학습하고 뒤 절반으로 평가했는데, 글당 1,024토큰으로 512차원 회귀를 맞추려니 과적합이 나서 섞기 대조군의 R²가 -1.3까지 떨어졌습니다. 대조군이 있었기에 이 값이 쓸 수 없는 설정이라는 것을 알았고, 위의 교차 검증 방식으로 다시 쟀습니다.
인접 층 사이에 중복이 정말 있나요?
있습니다. 아래는 글 네 편 평균이고, 괄호는 글별 최소~최대입니다.
| 쌍 | K의 R² | V의 R² | K 섞기 대조 | V 섞기 대조 |
|---|---|---|---|---|
| 3→7 | 0.336 (0.214~0.403) | 0.293 (0.201~0.350) | -0.094 | -0.075 |
| 7→11 | 0.483 (0.407~0.518) | 0.462 (0.411~0.503) | -0.128 | -0.065 |
| 11→15 | 0.653 (0.583~0.684) | 0.412 (0.342~0.466) | -0.129 | -0.042 |
| 15→19 | 0.648 (0.578~0.688) | 0.588 (0.523~0.630) | -0.108 | -0.017 |
| 19→23 | 0.692 (0.617~0.745) | 0.600 (0.485~0.697) | -0.111 | -0.153 |
| 3→23 (비인접) | 0.324 (0.218~0.388) | 0.217 (0.122~0.318) | -0.117 | -0.139 |
세 가지가 눈에 띕니다. 첫째, 깊은 층일수록 중복이 큽니다. K는 3→7에서 0.34이던 것이 11층 이후 0.65~0.69에서 머뭅니다. 둘째, 같은 쌍에서 K가 V보다 대체로 더 잘 예측됩니다. 11→15에서는 0.65 대 0.41로 차이가 컸습니다. 셋째, 섞기 대조군은 48개 행 가운데 단 하나도 0을 넘지 않았습니다(K -0.06~-0.19, V -0.01~-0.22). 예측이 맞은 것이 토큰의 평균적 통계가 아니라 같은 위치의 같은 토큰 정보에서 나왔다는 뜻입니다.
글의 종류에 따라서도 갈립니다. 인접 쌍 평균 R²는 코드에서 K 0.480, V 0.392로 가장 낮았고, 한국어 변경 이력에서 K 0.600, V 0.520, 한국어 보고서에서 K 0.598, V 0.527이었습니다. 영문 모델 카드는 K 0.571, V 0.445였습니다. 코드는 토큰 사이의 예측이 어려운 글이라 층 사이의 중복도 덜 나오는 것으로 보이는데, 글 네 편으로는 이유를 단정할 수 없습니다.
같은 번호 채널을 묶는 방식은 그대로 쓸 수 있나요?
그대로는 어렵다는 것이 이번 측정에서 가장 뜻밖의 결과였습니다. 같은 번호 채널끼리의 상관은 인접 쌍에서 K 0.095, V 0.052였고, 인접하지 않은 쌍에서는 K 0.084, V 0.053이었습니다. 차이가 없습니다. 층마다 키를 만드는 가중치가 다르니 채널 번호가 서로 대응하지 않기 때문입니다. 층 쌍의 정보는 채널 하나하나가 아니라 512차원 전체의 선형 조합에 담겨 있고, 그래서 R²는 0.65까지 나오는데 채널별 상관은 0.1 아래에 머뭅니다.
LoopVQ는 초록에서 여러 루프 단계의 채널을 함께 부호화한다고 설명합니다. 루프는 같은 가중치를 되풀이해 쓰므로 단계 사이에 채널의 의미가 이어질 수 있고, 그래서 같은 자리의 값을 묶는 것이 자연스러울 것입니다(이는 저희의 추정이며 논문 본문을 확인한 것이 아닙니다). 가중치가 다른 층 사이에서는 같은 접근을 쓰려면 층 사이의 변환 행렬이 먼저 있어야 합니다. 저희 측정에서 그 행렬은 쌍마다 512×512였습니다.
또 하나, 비인접 쌍인 3→23도 R²가 K 0.32, V 0.22입니다. 이 값은 3→7의 0.34, 0.29와 거의 같습니다. 모든 층이 공유하는 성분, 이를테면 토큰의 위치나 어휘에 해당하는 몫이 R²의 일부를 차지한다는 신호로 보이지만(추정), 어디까지가 층 고유의 중복인지는 이 실험으로 가를 수 없었습니다.
그래서 얼마나 줄일 수 있나요?
이상적인 상한을 계산해 봤습니다. 가우스 분포 가정에서 R²만큼의 분산을 앞 층이 설명해 준다면, 같은 왜곡에서 원소당 0.5·log₂(1/(1-R²))비트를 아낄 수 있습니다. 숫자를 넣으면 다음과 같습니다.
| 경우 | R² | 아낄 수 있는 비트(원소당) |
|---|---|---|
| K 인접 평균 | 0.562 | 약 0.60 |
| K 최고 쌍(19→23) | 0.692 | 약 0.85 |
| V 인접 평균 | 0.471 | 약 0.46 |
| V 최고 쌍(19→23) | 0.600 | 약 0.66 |
KV 캐시 크기를 같이 봅시다. 이 모델은 토큰당 6개 층에 층마다 (K 512 + V 512)개 값이 있고, f16이면 12,288바이트(12KiB)입니다. 32k 문맥이면 384MiB, 4비트로 양자화하면 96MiB입니다. 4비트에서 0.6비트를 아낀다면 15%이고, 96MiB의 15%는 14.4MiB입니다. 전체 어텐션 층이 10개이고 KV 형태가 같은 모델을 가정해도 f16 640MiB, 4비트 160MiB에서 15%는 24MiB입니다.
이 숫자는 상한에 가깝습니다. 가우스 가정과 선형 예측기를 쓴 이상적 계산이고, 실제 양자화기는 이만큼 얻지 못합니다. 더구나 앞 층의 K로 뒤 층의 K를 예측하려면 앞 층 K가 정확히 저장되어 있어야 하는데, 그 앞 층도 양자화된 값이면 오차가 뒤로 쌓입니다. 반대로 비선형 예측기나 여러 층을 묶는 방식은 선형보다 더 많이 잡을 수도 있습니다. 어느 쪽이든 크기의 자릿수는 바뀌지 않습니다. 줄어드는 양이 수십 MiB 단위이고, 모델 가중치가 수 GB인 환경에서 병목은 KV가 아닙니다.
LoopVQ가 큰 배수를 낸 이유와 저희 결과는 어떻게 이어지나요?
LoopVQ의 배수가 큰 이유는 초록에서 읽을 수 있습니다. 루프 모델은 루프 한 번마다 KV가 통째로 한 벌씩 쌓이기 때문에 캐시가 처음부터 큽니다. 압축할 몫이 크니 줄어드는 양도 큽니다. 반면 저희가 잰 모델은 하이브리드 구조라 24층 가운데 6층에만 KV가 있고 헤드도 2개입니다. 중복률은 같더라도 곱할 모집단이 작습니다.
여기서 얻은 교훈은 "중복이 있다"와 "줄일 만한 값어치가 있다"가 다른 질문이라는 점입니다. 중복의 크기(R²)는 구조가 정하는 비율이고, 줄어드는 양은 그 비율에 KV 캐시의 절대 크기를 곱한 것입니다. 보고된 큰 배수의 기법을 우리 모델로 가져오기 전에, 그 배수가 어디서 나왔는지, 즉 KV가 쌓이는 구조가 같은지를 먼저 보는 편이 측정 비용을 아낍니다. 이번 측정은 노트북 GPU 한 장에서 끝났고, 그 덕에 구현 전에 우선순위를 낮출 수 있었습니다.
직접 재 보려면 어떻게 하나요?
KV 추출은 transformers에서 use_cache=True로 한 번 돌린 뒤 캐시 층에서 키와 값을 꺼내면 됩니다. 아래는 저희 측정에 쓴 추출 코드입니다(전체 어텐션 층만 꺼냅니다).
ATT = [i for i, l in enumerate(model.config.layer_types) if l == "full_attention"]
def kv(ids, T):
with torch.no_grad():
cache = model(ids[None].cuda(), use_cache=True).past_key_values
out = {}
for i in ATT:
layer = cache.layers[i]
out[i] = {"K": layer.keys[0].permute(1, 0, 2).reshape(T, -1).float().cpu().numpy(),
"V": layer.values[0].permute(1, 0, 2).reshape(T, -1).float().cpu().numpy()}
return out
평가는 글 하나를 빼고 학습하는 방식과 섞기 대조군이 핵심입니다. 아래는 그 부분만 뽑은 최소 구현이고, 합성 데이터(512차원이 아닌 64차원, 잡음을 섞은 선형 관계)로 돌려 R² 0.45, 섞기 대조 -0.16이라는 기대한 패턴이 나오는 것까지만 확인했습니다. 실제 모델의 수치가 아니니 오해하지 마세요.
import numpy as np
def fit(X, Y, lam_rel):
mx, my = X.mean(0), Y.mean(0); A, B = X - mx, Y - my
lam = lam_rel * np.trace(A.T @ A) / A.shape[1]
return np.linalg.solve(A.T @ A + lam * np.eye(A.shape[1]), A.T @ B), mx, my
def r2(W, mx, my, X, Y):
P = (X - mx) @ W + my
return 1 - ((Y - P) ** 2).sum() / ((Y - Y.mean(0)) ** 2).sum()
def heldout(docs_x, docs_y, lam=1.0, shuffle=False, seed=0):
rng, out = np.random.default_rng(seed), []
for te in range(len(docs_x)):
tr = [i for i in range(len(docs_x)) if i != te]
Xtr = np.vstack([docs_x[i] for i in tr]); Ytr = np.vstack([docs_y[i] for i in tr])
Xte = docs_x[te][rng.permutation(len(docs_x[te]))] if shuffle else docs_x[te]
out.append(r2(*fit(Xtr, Ytr, lam), Xte, docs_y[te]))
return float(np.mean(out))
heldout(xs, ys)와 heldout(xs, ys, shuffle=True)를 같이 찍어 보세요. 뒤쪽이 0 위로 나오면 평가 설정이 새고 있다는 신호입니다.
자주 묻는 질문
Q. 이 결과는 LoopVQ가 틀렸다는 뜻인가요?
아닙니다. LoopVQ는 루프 모델의 루프 축을 다루고, 저희는 일반 모델의 층 축을 쟀습니다. 대상도 축도 다릅니다. 저희는 초록만 읽었고 실험을 재현하지도 않았습니다.
Q. R²가 0.69면 KV를 거의 다 복원할 수 있다는 건가요?
아닙니다. R²는 분산의 69%를 설명한다는 뜻이고, 나머지 31%는 여전히 저장해야 합니다. 그래서 아낄 수 있는 비트는 0.85비트(상한 계산)에 그치고, 4비트 기준으로 약 21%입니다.
Q. 왜 섞기 대조군이 음수인가요?
R²는 평균으로 찍는 것보다 낫냐를 묻는 값이라, 엉뚱한 토큰으로 예측하면 평균보다 못해 음수가 나옵니다. 중요한 것은 0을 넘지 않았다는 점입니다. 대조군이 0 위로 올라오면 그때는 의심해야 합니다.
Q. 더 큰 모델이나 다른 구조에서도 같은가요?
모릅니다. 이번에는 Qwen3.5-2B 한 모델만 쟀습니다. 비율은 다를 수 있고, 전체 어텐션 층이 훨씬 많은 모델에서는 절대 절감량이 커질 수 있습니다. 같은 코드로 모델만 바꿔 재 보시길 권합니다.
Q. 실제로 양자화해서 정확도를 쟀나요?
아닙니다. 이번 측정은 중복의 크기와 이상적 절감량의 계산까지이고, 압축을 구현해 퍼플렉서티나 과제 정확도, 처리량을 재지는 않았습니다.
한계와 아직 모르는 것
모델은 하나(Qwen3.5-2B)이고 글은 네 편, 글당 1,536토큰입니다. 긴 문맥에서 값이 어떻게 달라지는지는 재지 않았습니다. 예측기는 선형 릿지뿐입니다. 절감량은 가우스 가정으로 계산한 상한에 가까운 추정이고, 실제 양자화를 적용한 정확도와 처리량은 재지 않았습니다. 층 쌍의 변환 행렬 크기와 학습 비용도 절감량에서 빼지 않았습니다. 같은 입력 재실행에서 차이가 0이었다는 점은 측정이 재현된다는 뜻일 뿐, 결과가 일반적이라는 뜻은 아닙니다. 이 글의 모든 수치는 저희 팀의 자체 측정입니다.
Further reading
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.