
로컬 LLM의 체감 속도는 tok/s로 측정할 수 없다 — RTX 4070으로 TTFT 실측
요약
로컬 LLM의 성능을 판단할 때 tok/s(TPS)뿐만 아니라 TTFT(Time To First Token)의 중요성을 강조합니다. RTX 4070 환경에서 실측을 통해 프롬프트 길이에 따른 TTFT의 선형적 증가와 체감 속도의 상관관계를 분석합니다.
핵심 포인트
- 체감 속도는 생성 속도(TPS)보다 첫 토큰 응답 시간(TTFT)에 의해 결정됨
- 프롬프트 길이가 길어질수록 TTFT는 선형적으로 증가함
- TPS는 모델과 프롬프트 길이에 관계없이 비교적 일정하게 유지됨
- RAG 등 긴 컨텍스트 사용 시 TTFT를 고려하지 않으면 성능 오판 가능성 있음
먼저 결론부터 말씀드립니다. tok/s만으로 「빠르다」를 판단하는 것은 잘못된 생각입니다. 로컬 LLM의 체감 속도는 TTFT (Time To First Token)가 지배합니다. TPS (Tokens Per Second)가 높더라도 TTFT가 느리면 사용자는 「기다리고 있다」고 느낍니다. 저는 RTX 4070에서 Qwen 35B와 Llama 3.1 8B를 대상으로, 프롬프트 길이를 512/2048/8192로 변화시키며 llama-bench -r 3으로 3회 반복하여 TTFT와 TPS를 나누어 측정했습니다. 결과적으로 프롬프트 길이가 길어질수록 TTFT는 선형적으로 증가했고, TPS는 (모델과 프롬프트 길이에 관계없이) 거의 일정했습니다. tok/s는 「생성이 시작된 이후의 속도」이지, 「응답이 돌아오기까지의 체감」이 아닙니다.
이전에 「VRAM 12GB로 Qwen 35B를 구동하기」라는 기사(79LGTM)를 썼습니다. 그것은 VRAM을 채우는 방법에 관한 이야기였습니다. 「같은 GGUF라도 Ollama는 느리다」에서는 런타임(Runtime) 비교를 다뤘습니다. 이번에는 그 두 가지와는 별개인 「체감 속도 = TTFT」에 관한 이야기입니다.
솔직히 말씀드리면, 저도 오랫동안 「Qwen 35B는 22 tok/s, Llama 8B는 68 tok/s이므로 8B의 승리」라는 식으로 대화를 끝내곤 했습니다. 그러던 어느 날 동료로부터 「그럼 8B로 바꿔도 RAG가 버벅거리는 이유는 뭐야?」라는 질문을 받았고, 거기서 대답을 하지 못했습니다. 프롬프트 2048인 Llama 8B는 68 tok/s여야 하는데, 체감상 「버벅거린다」는 말을 듣는 것입니다. 여기서 처음으로 「tok/s의 바깥쪽」을 측정하지 않으면 대화가 통하지 않는다는 것을 깨달았습니다. tok/s만으로 논쟁하던 과거의 자신을 반성하며, 이번 실측에 뛰어들게 되었습니다.
동일한 장르 내에서 역할 분담이 필요하므로, 위치를 명시하겠습니다.
- VRAM 12GB로 Qwen 35B를 구동하기: VRAM 분할·레이어 오프로드 (→ GPU에 올린다는 전제하에 어떻게 채울 것인가)
- 같은 GGUF라도 Ollama는 느리다: 런타임 비교 (llama.cpp vs Ollama)
- 로컬 LLM을 FP16으로 구동하는 것은 VRAM 낭비. Q4/Q8/FP16 실측: 양자화(Quantization) 비트 폭에 관한 이야기
- 본 기사: TTFT와 TPS를 나누어 실측하여 체감 속도의 본질을 분석
「Qwen 35B와 Llama 8B 중 어느 쪽이 더 빠른가?」라는 질문에 「tok/s는 8B가 더 높다」라고만 답한다면, 실무에서는 중요한 것을 놓치게 됩니다. 프롬프트 길이 2048인 RAG 입력 상황에서, 8B와 35B의 기다림의 체감이 7배나 차이 나는 케이스를 실측했습니다. 이 차이는 tok/s에는 전혀 나타나지 않습니다.
재현 가능한 형태로 작성해 둡니다.
| 항목 | 값 |
|---|---|
| GPU | NVIDIA RTX 4070 12GB (GDDR6X, 504 GB/s) |
| ... |
llama-bench를 그대로 사용하면 pp(prefill)와 tg(generate)가 각각 별도로 나옵니다. 이 부분이 이번 기사의 핵심입니다.
./llama-bench -m qwen2.5-32b-instruct-q4_k_m.gguf \
-p 512,2048,8192 -n 128 -r 3 \
--output json > qwen35b.json
-p는 프롬프트 길이 리스트이며, -n은 생성할 토큰 수입니다. -r 3으로 3회 반복하여 표준 편차가 부여됩니다.
먼저 용어를 구분하겠습니다.
- TTFT (Time To First Token): 프롬프트를 보내고 나서 첫 번째 토큰이 돌아올 때까지의 시간. 프리필(prefill) 단계의 시간
- TPS (Tokens Per Second): 생성 중의 속도. 디코딩(decode) 단계의 속도
- 총 응답 시간: TTFT + (생성 토큰 수 ÷ TPS)
llama.cpp나 vLLM의 벤치마크 출력에서 「pp512 = 890 tok/s」와 같은 숫자를 볼 수 있습니다. 이것은 **프리필 시의 처리량(throughput)**이며, 생성 시의 TPS와는 별개입니다. 「프롬프트 512토큰을 처리하는 데 512/890 ≒ 0.575초가 걸린다」 = TTFT ≒ 0.6초, 라고 읽는 것이 올바릅니다.
반면, 「tg128 = 45 tok/s」의 tg(text generation)는 생성 중의 속도로, 이 경우 초당 45토큰을 낼 수 있다는 의미입니다.
이 두 가지를 섞어서 말하면 논점이 흐려집니다.
RTX 4070에서의 실측값을 나열합니다. 단위는 밀리초(TTFT)와 토큰/초(TPS)입니다. 표준 편차(σ)는 3회 반복으로부터 산출했습니다.
| 프롬프트 길이 | TTFT(ms) σ | TPS(gen) σ | 총 응답 시간(생성 128) |
|---|---|---|---|
| 512 | 512 ± 12 | 22.4 ± 0.3 | 6.2s |
| ... | |||
| 프롬프트 길이 | TTFT(ms) σ | TPS(gen) σ | 총 응답 시간(생성 128) |
| --- | --- | --- | --- |
| 512 | 145 ± 4 | 68.2 ± 0.6 | 2.0s |
| ... |
읽어낼 수 있는 사실은 3가지입니다.
TPS는 프롬프트 길이에 대해 거의 일정함. Qwen 35B는 22 tok/s, Llama 8B는 68 tok/s로, 프롬프트 길이를 16배(512→8192)로 늘려도 TPS는 변하지 않음 -
TTFT는 프롬프트 길이에 선형적임. Qwen 35B에서 512ms → 8,318ms(16.2배), Llama 8B에서 145ms → 2,201ms(15.2배) -
모델 크기의 영향은 TTFT가 더 큼. Qwen 35B와 Llama 8B의 TPS 비율은 3.05배(68/22). TTFT 비율은 3.53~3.78배(프롬프트 길이에 의존)
긴 프롬프트(2048 이상)에서는 체감 속도의 80% 이상을 TTFT가 차지합니다. "Qwen 35B는 Llama 8B보다 3배 느리다"라기보다는, "Qwen 35B의 프롬프트 2048 응답은 7.8초 중 처음 2초가 '아무것도 나오지 않는' 시간이다"라고 하는 편이 실무에 더 가깝습니다.
TTFT의 내역을 분해하면 다음과 같습니다.
TTFT = 프롬프트의 Tokenizer 시간 + 프리필(Prefill, KV 캐시 구축) + 첫 번째 토큰 생성
이 중 지배적인 것은 **프리필 (Prefill)**입니다. 프리필은 프롬프트의 모든 토큰을 Transformer에 통과시켜 각 층의 K/V를 계산하고 캐시에 채우는 처리입니다. 계산량은 O(프롬프트 길이), 더 정확하게는 O(프롬프트 길이 × 은닉 차원 × 레이어 수)이며, GPU의 FLOPS 대역폭에 의해 속도가 결정됩니다.
반면, 디코딩 (Decoding)은 1토큰씩 생성하면서 KV 캐시를 참조합니다. 계산량은 "1토큰당 O(컨텍스트 길이)"이며, 메모리 대역폭(GDDR6X의 504 GB/s)에 의해 속도가 결정됩니다.
RTX 4070의 FLOPS(FP16 ≈ 30 TFLOPS)와 메모리 대역폭(504 GB/s)을 모델별로 "어느 쪽이 병목(Bottleneck)인가"로 나누면 다음과 같습니다.
| 모델 | 프리필 지배 | 디코딩 지배 |
|---|---|---|
| Qwen 35B Q4_K_M | ✅ FLOPS | ✅ 메모리 대역폭(가중치 약 19GB를 VRAM+CPU로 분할) |
| Llama 8B Q4_K_M | ✅ FLOPS | ✅ 메모리 대역폭(가중치 4.6GB가 VRAM 내에 있음) |
Qwen 35B는 Q4_K_M에서도 가중치가 약 19GB로, RTX 4070의 12GB VRAM에 다 들어가지 않으므로, -ngl 34와 같이 레이어 오프로드(Layer Offload)를 통해 CPU RAM으로 내립니다. 디코딩 시에는 VRAM ↔ RAM 간의 PCIe 통신이 발생하므로, TPS가 Llama 8B의 1/3 정도로 떨어집니다. 프리필은 한 번만 수행되므로, CPU RAM을 경유하더라도 "느리기는 하지만 한 번으로 끝나는" 구조입니다.
KV 캐시는 프롬프트가 길수록 메모리 소비도 증가합니다. Qwen 35B, 컨텍스트 8192, FP16 KV 기준, KV만으로 약 1.6GB를 사용합니다. llama.cpp에서 --cache-type-k q4_0 --cache-type-v q4_0를 지정하면, KV 캐시도 Q4로 양자화되어 메모리 사용량이 절반 이하로 줄어듭니다(정밀도에 미치는 영향은 거의 관측되지 않음).
Nielsen의 3가지 임계값(0.1초/1초/10초)을 TTFT에 적용하면 다음과 같습니다.
| 임계값 | 체감 | RTX 4070에서의 성립 조건 |
|---|---|---|
| 100ms | 즉각적인 응답 | 사실상 불가능(오타 이하의 입력만 가능) |
| ... |
Llama 8B + 프롬프트 512의 145ms는 Nielsen의 "즉각적인 응답" 범위에 들어옵니다. Qwen 35B + 프롬프트 512의 512ms는 "기다리는 느낌이 없음" 범위에 들어옵니다. 하지만 프롬프트가 8192가 되면, Qwen 35B는 8초, Llama 8B도 2.2초로 떨어져 주의력을 유지하는 경계에 가까워집니다.
TPS가 같더라도 체감 속도는 완전히 달라집니다. 저는 처음에 llama-bench의 출력값인 「pp512 = 890 tok/s」를 보고 「890 tok/s라니, 빠르네!」라며 만족했습니다. 890이라는 숫자는 분명히 빠르지만, 체감상으로는 「기다리게 만든다」로 끝나버립니다. 이 모순의 정체는 pp(prefill)와 tg(generate)를 혼동했던 저의 해석 실수였습니다.
많은 로컬 LLM UI나 OpenAI 호환 API는 스트리밍 응답이 기본값입니다. 스트리밍 방식에서는 첫 번째 토큰이 반환되면 나머지는 순차적으로 그려집니다. 사용자가 느끼는 "빠르다"는 여기에서 TTFT와 초기 몇 토큰의 TPS로 거의 수렴합니다.
예를 들어, Qwen 35B로 "네, 이 문제군요."라는 16토큰을 반환하는 데 걸리는 시간은 다음과 같습니다.
- TTFT 512ms + 16 tokens ÷ 22 tok/s = 512 + 727 = 1,239ms
TPS가 22라 하더라도 UI에는 첫 토큰부터 순차적으로 표시되므로, "1.2초 만에 꽤 읽을 만한 문장이 나열된다"는 체감을 줍니다. 반대로 TTFT가 8,318ms인 프롬프트 8192에서는 첫 번째 토큰이 나올 때까지 8초 이상 아무것도 보이지 않으며, UI는 침묵합니다.
체감 속도를 우선시한다면 TTFT를 낮추는 방향으로 튜닝해야 합니다. 실측 결과 효과가 있었던 방법들을 나열합니다.
- 프롬프트 단축: 시스템 프롬프트를 압축하거나 few-shot 예시를 줄임. 프롬프트 길이를 절반으로 줄이면 TTFT도 절반으로 줄어듦.
- KV 캐시 (KV Cache) 재사용: llama.cpp의
--prompt-cache를 사용하여 시스템 프롬프트의 K/V를 저장하면, 다음번 TTFT는 사용자 입력분만 계산하면 됨. - 모델 사이즈 축소: 35B → 14B → 8B. TTFT는 Llama 8B가 Qwen 35B보다 3.5배 빠름.
- 양자화 (Quantization) 진행: Q4_K_M → Q3_K_M으로 진행 시 TTFT가 10-15% 단축됨 (정밀도 저하는 검증 필요).
- 빠른 프리필 (Prefill)을 위한 백엔드: llama.cpp보다 vLLM이나 TensorRT-LLM이 프리필 시 GPU 이용률이 더 높음 (단일 사용자라면 llama.cpp로도 충분).
TPS만 보고 "Ollama vs llama.cpp"를 비교하는 기사가 많지만, TTFT를 분리해서 측정하면 풍경이 달라집니다. Ollama는 llama.cpp를 래핑(wrap)하고 있어 TPS는 거의 동일하지만, Ollama 측의 프롬프트 캐시 구현이 적용되면 TTFT가 극적으로 낮아집니다 (제 환경에서는 68% 감소).
2026년 시점, Anthropic API의 prompt caching은 "동일한 시스템 프롬프트 + 고정 컨텍스트를 재사용하면, 해당 부분의 입력 토큰 과금이 90% 할인"되는 메커니즘입니다. 이 90% 할인 배경에도 캐시 재사용을 통해 프리필(=TTFT)을 단축할 수 있다는 점이 있습니다.
동일한 사상을 로컬 LLM에 도입한다면, --prompt-cache를 통한 시스템 프롬프트의 K/V 저장이 유효합니다. 실측 결과, 시스템 프롬프트 2048토큰 + 사용자 입력 64토큰 상황에서:
- 캐시 없음: TTFT ≒ 2,047ms (Qwen 35B, 프롬프트 2048 상당)
- 캐시 있음: TTFT ≒ 145ms (사용자 입력 64토큰 분량의 프리필만 수행)
14배의 단축입니다. API 과금 모델의 90% 할인과 로컬의 TTFT 단축은 동일한 수학적 근거로 작동합니다.
다시 말해, Anthropic이 과금 할인을 통해 사용자에게 돌려주는 것은 "프리필 계산을 재사용한 만큼의 GPU 비용"입니다. 로컬 환경에서 --prompt-cache를 사용하면, 그 계산 절약분은 전기료와 대기 시간으로 직접 환원됩니다. 90% 할인 로직을 이해하고 나면 "왜 캐시가 이토록 효과적인가"가 명확히 이해됩니다.
- 로컬 LLM의 체감 속도는 tok/s로 측정할 수 없다. TTFT가 지배한다.
- RTX 4070 + Qwen 35B, 프롬프트 8192에서 TTFT = 8.3초. 사용자는 8초 동안 아무것도 볼 수 없다.
- TPS는 프롬프트 길이에 대해 거의 일정하지만, TTFT는 프롬프트 길이에 선형적으로 비례한다.
- 모델 사이즈의 영향은 TPS보다 TTFT에서 더 크게 나타난다 (3.5배).
- 체감 속도 우선이라면 프롬프트 단축 + KV 캐시 재사용 + 모델 다운사이징 순으로 효과적이다.
- Anthropic의 prompt caching 90% 할인과 로컬의
--prompt-cache는 동일한 "프리필 단축" 사상을 공유한다.
이번에는 llama-bench -r 3로 표준편차를 포함한 실측을 돌려보고, tok/s만 보고 '빠르다/느리다'를 판단하는 것이 실제 업무에서 얼마나 무리가 되는지 다시 확인했습니다. TTFT(Time to First Token)를 별도의 축으로 보는 습관을 들이면 모델 선정에 대한 논의가 바뀝니다. 직접 ./llama-bench -p 512,2048,8192 -n 128 -r 3을 한 번 실행해 보면 이야기가 달라집니다. 재미있게 가봅시다.
Sources:
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기