컨텍스트 설정값과 채운 양을 따로 재봤다 — VRAM과 속도는 다른 쪽을 따라간다
“컨텍스트를 늘리면 느려지고 VRAM을 더 먹는다”는 말을 자주 봅니다. 그런데 컨텍스트에는 두 가지가 섞여 있습니다.
num_ctx로 설정한 크기- 실제로 채워 넣은 토큰 수
보통은 이 둘을 같이 늘리면서 재기 때문에 무엇이 무엇을 바꿨는지 알 수 없습니다. 처음에 저도 그렇게 쟀다가 데이터를 버렸습니다(맨 아래에 적어뒀습니다).
이번에는 한 번에 하나씩만 바꿨습니다.
엄밀히 말하면 2×2 요인 설계는 아닙니다. 단일변수 스윕 두 개이고 두 스윕은 한 점(num_ctx=16,384, 입력 2,011토큰)에서만 만납니다. 그래서 두 변수의 상호작용은 이 데이터로 추정할 수 없습니다.
설계
RTX 3060 12GB 두 장, Qwen3-Coder 30B-A3B Q4_K_M, 전량 GPU 적재입니다.
- A그룹 —
num_ctx를 16,384로 고정하고 입력 토큰 수만 505에서 12,019까지 변경 - B그룹 — 입력을 약 2,011토큰으로 고정하고
num_ctx만 4,096에서 32,768까지 변경
A: 실제로 채운 양이 속도를 결정한다
num_ctx는 16,384로 내내 같습니다. 입력 길이만 바꿨습니다.
| 입력 토큰 | 프리필 | 생성 속도 | 기준 대비 | VRAM |
|---|---|---|---|---|
| 505 | 1,605 tok/s | 91.0 tok/s | 100% | 19,711 MiB |
| 2,011 | 1,667 tok/s | 86.4 tok/s | 95.0% | 19,713 MiB |
| 4,010 | 1,655 tok/s | 83.1 tok/s | 91.3% | 19,713 MiB |
| 8,016 | 1,546 tok/s | 76.7 tok/s | 84.3% | 19,713 MiB |
| 12,019 | 1,420 tok/s | 70.1 tok/s | 77.1% | 19,715 MiB |
505토큰 기준으로 속도는 23% 떨어졌습니다. 그런데 VRAM은 4 MiB 범위 안에 머물렀습니다. 19,711에서 19,715 MiB입니다.
입력을 24배 늘렸는데 VRAM이 그대로였다는 뜻입니다. 다만 생성 완료 후 1회 샘플이라 nvidia-smi 해상도 아래의 변화나 실행 중 피크는 잡지 못합니다.
B: 설정값이 VRAM을 결정한다
이번엔 입력을 2,011토큰으로 고정하고 num_ctx만 바꿨습니다.
| num_ctx | 입력 토큰 | 생성 속도 | 기준 대비 | VRAM |
|---|---|---|---|---|
| 4,096 | 2,011 | 88.5 tok/s | 100% | 18,533 MiB |
| 8,192 | 2,011 | 88.2 tok/s | 99.6% | 18,929 MiB |
| 16,384 | 2,011 | 87.7 tok/s | 99.1% | 19,713 MiB |
| 32,768 | 2,011 | 89.1 tok/s | 100.6% | 21,279 MiB |
정확히 반대입니다. VRAM은 2,746 MiB(2.68 GiB) 늘었는데 속도는 99.1~100.6% 사이에 머물렀습니다. 32k가 4k보다 오히려 빨랐습니다.
이 폭이 노이즈인지 판단할 근거가 하나 있습니다. 두 스윕이 만나는 지점(num_ctx=16,384, 입력 2,011)이 A에서는 86.42 tok/s, B에서는 87.69 tok/s로 나왔습니다. 같은 조건인데 1.47% 차이입니다. 이것이 제 환경의 실행 간 재현 오차입니다.
B그룹 전체 편차(1.5%p)가 이 재현 오차와 비슷한 크기입니다. 그래서 num_ctx가 속도에 주는 유의한 방향성을 확인하지 못했다까지가 이 데이터로 할 수 있는 말입니다. “영향이 없다”를 증명한 것은 아닙니다.
정리
| 실제 채운 양 ↑ | num_ctx 설정값 ↑ | |
|---|---|---|
| 생성 속도 | 23% 감소 | 방향성 확인 안 됨 (재현 오차 이내) |
| VRAM | 4 MiB 범위 | 2,746 MiB 증가 |
측정한 범위 안에서 두 변수는 서로 다른 쪽을 움직였습니다.
VRAM 쪽 기울기를 계산하면 설정 토큰당 98.1 KiB입니다.
(21,279 − 18,533) MiB ÷ (32,768 − 4,096) 토큰 = 98.1 KiB
이전 글에서 다른 방법으로 측정한 값이 98.2 KiB, 계산기의 이론값이 96.0 KiB였습니다. 세 값이 모입니다.
다만 이 기울기 전부가 KV 캐시라고 말할 수는 없습니다. 잰 것은 프로세스 전체의 VRAM이고, 컨텍스트 설정에 따라 함께 커지는 다른 요소가 섞여 있을 수 있습니다. 정확히는 **“컨텍스트 설정값에 대한 총 VRAM 기울기”**입니다.
이론 KV값보다 2% 큰 이유는 이번 측정으로 가려내지 못했습니다. 런타임 버퍼, 할당 단위, 측정 오차 중 무엇이 얼마나인지 분리하지 않았습니다.
실무에서
num_ctx는 실제로 쓸 만큼만 잡으십시오. 32k로 설정해두고 2k만 쓴 조건에서 속도 이득은 관찰되지 않았고 VRAM은 2.68 GiB 더 잡혔습니다. 그 2.7 GB 때문에 모델이 안 들어가면 CPU 오프로드가 걸리고, 그건 속도가 절반으로 떨어지는 훨씬 큰 손해입니다.
긴 문서를 넣는 작업이면 속도 저하를 예산에 넣으십시오. 이 모델·이 장비에서 12k를 채웠을 때 505토큰 대비 23% 느렸습니다. 기울기는 환경마다 다르겠지만, 짧은 질의로 잰 tok/s를 그대로 기대하면 안 된다는 점은 같습니다.
대화가 길어지면 느려질 것으로 예상됩니다. 설정을 안 바꿔도 누적된 대화가 곧 채운 양이기 때문입니다. 다만 이건 이번 결과에서 유추한 것이고, 대화형 세션은 프롬프트 캐시 재사용 등 조건이 달라 직접 측정하지 않았습니다.
내 모델과 컨텍스트 조합이 VRAM에 들어가는지는 VRAM 계산기로 확인할 수 있습니다.
왜 이렇게 나오는지
속도 쪽은 토큰을 하나 만들 때마다 앞선 모든 토큰의 KV를 읽어야 하기 때문으로, VRAM 쪽은 런타임이 설정값만큼 미리 잡아두기 때문으로 흔히 설명됩니다.
다만 이 설명들은 통용되는 이해이지 이번 측정으로 확인한 것이 아닙니다. 잰 것은 처리량과 VRAM 총량뿐입니다. 메모리 대역폭 사용률, 어텐션 커널 실행 시간, 할당자 동작은 측정하지 않았습니다. 이 글이 보인 것은 **“어느 변수가 무엇을 바꾸는가”**까지이고, 그 아래 기전은 아닙니다.
측정 방법
- RTX 3060 12GB × 2 (합산 24GB), AMD Ryzen 7 5800X, 두 번째 슬롯 PCIe x4
- 각 카드 전력 상한 170W, Ollama 0.20.0, 드라이버 590.48.01
- Qwen3-Coder 30B-A3B Q4_K_M, KV 타입 f16
- CPU 오프로드 없음. 모든 모델 레이어를 GPU에 배치
- Flash Attention 등 나머지 옵션은 Ollama 기본값
temperature=0,seed=42,num_predict=32- 매 조건마다
keep_alive=0으로 완전히 언로드한 뒤 재로드 - 입력 토큰 수는 Ollama가 보고한
prompt_eval_count실측값입니다. 목표치를 문자 수로 환산해 프롬프트를 만든 뒤 실제 보고값을 씁니다 - 모든 조건에서
입력 + 생성 < num_ctx를 assert로 확인해 프롬프트 절단을 배제했습니다 - VRAM은 생성 완료 직후
nvidia-smi로 두 GPU 합산을 1회 샘플링 - 측정 화면은 터미널 녹화로 저장소에 남겼습니다
한계
- 조건당 1회입니다. 변동폭을 모릅니다. B그룹에서 32k가 4k보다 빨랐던 것은 이 변동폭 안이라고 보지만 확인하지 못했습니다.
- A그룹을 먼저, B그룹을 나중에 돌렸습니다. 온도나 클록 같은 시간 경과 효과가 섞였을 수 있습니다. GPU 온도·클록·실소비전력을 기록하지 않았습니다.
- 생성이 32토큰뿐입니다. 생성이 길어지며 KV가 쌓일 때의 추가 감속은 재지 않았습니다.
- VRAM은 프로세스가 잡은 양이지 실제로 접근한 양이 아닙니다. nvidia-smi 기준이고 할당 단위 해상도도 확인하지 않았습니다.
- A그룹의 프리필은 단조롭지 않습니다. 505토큰에서 1,605 tok/s로 2,011토큰(1,667)보다 낮습니다. 짧은 입력에서는 고정 오버헤드 비중이 커서로 보이지만 확인하지 않았습니다.
- 모델 하나, GPU 구성 하나입니다. 어텐션 구현이나 KV 양자화가 다르면 기울기가 달라집니다. Flash Attention 관련 설정은 건드리지 않은 기본값입니다.
- 프롬프트가 반복 텍스트입니다. 실제 문서와 토큰 분포가 다를 수 있습니다.
- 두 스윕의 교차점이 한 곳뿐입니다. 입력 길이와
num_ctx의 상호작용은 추정할 수 없습니다. 각 결론은 해당 절편(A는num_ctx=16k, B는 입력≈2k)에서의 관측입니다. - 조건 순서를 무작위화하지 않았습니다. A그룹을 짧은 것부터, 이어서 B그룹을 작은 것부터 순서대로 돌렸습니다. A의 단조 감소에 시간 경과 효과가 섞였을 가능성을 배제하지 못합니다.
- A그룹은 입력 길이와 함께 생성된 출력 토큰열도 달라집니다. 커널 실행 형태가 출력에 따라 달라질 수 있어 순수한 길이 효과라고 단정할 수 없습니다.
- VRAM은 생성 완료 후 1회 샘플입니다. 프리필·생성 중 피크는 모릅니다. 모델이 실제로 들어가는지 판단하려면 피크를 재야 합니다.
- 두 GPU
memory.used의 단순 합산입니다. GPU별 사용량과 텐서 분할·복제 방식을 기록하지 않아 단일 24GB 풀처럼 다루면 안 됩니다. - 32k 위, 12k 위는 재지 않았습니다.
- 원본 데이터와 녹화는 저장소에 공개해 두었습니다. 다른 환경 결과를 알려주시면 출처를 밝히고 반영하겠습니다.
버린 측정에 대해
처음에는 num_ctx와 입력을 같이 늘리며 쟀습니다. 그런데 모든 조건에서 prompt_eval_count가 num_ctx와 정확히 일치했습니다. 프롬프트가 설정값을 넘겨서 Ollama가 잘라낸 것이었습니다.
그 데이터로도 그래프는 그려졌고 “컨텍스트 늘리니 26% 느려짐”이라는 결론도 나왔습니다. 하지만 두 변수가 완전히 붙어 있어 무엇 때문에 느려졌는지 말할 수 없는 데이터였습니다. 그래서 버리고 다시 쟀습니다. 그 녹화도 저장소에 남겨뒀습니다.