콘텐츠로 이동

LLM 비용과 지연

분류: Layer 12 - AI 시스템 & LLM 애플리케이션 | 선수지식: L12-10 (LLM API)

LLM 비용과 지연 - 비용 모델, Latency, 모델 라우팅, Self-Hosting

섹션 제목: “LLM 비용과 지연 - 비용 모델, Latency, 모델 라우팅, Self-Hosting”

LLM 비용·지연 엔지니어링은 모델 품질만으로는 제품이 성립하지 않는 지점을 다룬다. 같은 답변 품질이라도 input/output token, context window, prefill/decode, streaming, caching, batching, retry, rate limit, queueing, model routing, fallback, self-hosting의 설계에 따라 호출당 단가와 p95/p99 지연이 크게 달라질 수 있다. 절감 배율은 모델, traffic, 계약 단가, 측정 시점에 의존하므로 이 문서에서는 보편 숫자보다 항을 분해해 계산하는 법을 다룬다.

이 문서의 목표는 가격표를 외우는 것이 아니다. 가격과 모델 이름은 빠르게 바뀐다. 대신 다음 질문에 답할 수 있어야 한다.

  1. 한 요청의 비용은 어떤 token과 외부 호출에서 생기는가?
  2. 사용자가 느끼는 지연과 서버가 점유되는 지연은 어떻게 다른가?
  3. 평균 latency가 좋아도 p95/p99가 나쁘면 왜 제품이 실패하는가?
  4. caching, batch, routing, streaming, retry, fallback이 각각 어떤 문제를 줄이고 어떤 새 실패를 만드는가?
  5. API 사용을 계속할지, self-hosting을 검토할지 어떤 산식으로 판단하는가?

2. 등장한 문제 - 똑똑한 모델만으로는 제품이 되지 않는다

섹션 제목: “2. 등장한 문제 - 똑똑한 모델만으로는 제품이 되지 않는다”

L12-10에서 LLM API는 messages -> tokenization -> prefill -> decode -> usage/error 관측 흐름으로 분해했다. 작은 prototype에서는 “좋은 모델을 고르고 prompt를 다듬는다”가 가장 중요해 보인다. production에서는 다른 문제가 먼저 커진다.

  • Unit economics: 한 답변이 좋아도 호출당 비용이 매출·사용자당 예산보다 크면 기능을 켤 수 없다.
  • Tail latency: 평균 응답은 2초여도 p99가 30초면 일부 사용자는 기능이 멈췄다고 느낀다.
  • Retry amplification: HTTP 429 Too Many Requests, timeout, schema validation 실패를 무심코 retry하면 비용과 queue가 함께 커진다.
  • Context growth: 대화 history, RAG(Retrieval-Augmented Generation, 검색한 근거를 생성 입력에 붙이는 방식) context, tool 결과가 누적되면 input token과 prefill 지연이 조용히 늘어난다.
  • Fallback mismatch: provider 장애 때 다른 모델로 넘겨도 schema, tool call, safety filter, streaming chunk 계약이 다르면 downstream이 깨진다.

따라서 이 토픽의 핵심 철학은 품질, 비용, 지연을 같은 요청 trace 안에서 함께 본다는 것이다. 모델 품질은 필요조건이다. 하지만 단가, SLO, retry, rate limit, fallback이 통제되지 않으면 좋은 모델도 제품을 불안정하게 만든다.

선행 기술의 한계 - per-token 단가 인하만 기다릴 수 없다

섹션 제목: “선행 기술의 한계 - per-token 단가 인하만 기다릴 수 없다”

아래 가격 사례는 인용 출처와 문서 작성 시점에 묶인 point-in-time 예시로만 읽는다. 최신 가격 비교가 목적이 아니다.

  • GPT-4 launch(2023.03) 시점 output 가격은 $60/M tokens였고, GPT-4o(2024.05) 및 2024.10 인하 뒤 output $10/M 예시가 있었다. 이 수치는 TokenMix, AI Pricing History에 묶인 역사적 예시다.
  • “good enough” tier 가격이 몇 년 사이 크게 낮아졌다는 추세는 중요하지만, 실제 도입 전에는 provider 공식 가격표와 계약 단가를 다시 확인해야 한다.
  • pretraining·fine-tuning은 일회성 또는 간헐 비용이고, inference는 매 호출 누적된다. 사용 패턴이 single-shot에서 multi-turn, RAG, tool call, reasoning으로 이동하면 호출당 token 수가 늘어 가격 인하분을 상쇄할 수 있다.

단순 모델은 이렇게 생겼다.

월 비용 ~= 호출 수 × 호출당 평균 token × token 단가

운영에서는 이 모델이 세 지점에서 깨진다.

  1. 같은 system prompt와 few-shot prefix를 매번 다시 읽힌다.
  2. 쉬운 query까지 가장 비싼 frontier 모델로 보낸다.
  3. 실시간이 필요 없는 작업도 실시간 API 가격과 SLO로 처리한다.

이 세 지점에서 prompt caching, model routing, Batch API가 등장한다. 단가가 내려가도 이 구조를 모르면 같은 workload에서 절감 여지를 계속 놓친다.

용어첫 정의비용·지연과의 연결
Token모델이 읽고 생성하는 최소 조각. 글자·단어와 1:1이 아니다과금, context window, rate limit의 기본 단위
Input tokensystem, history, user, RAG context, tool result처럼 모델이 읽는 tokenprefill 비용과 TTFT를 키운다
DAGDirected Acyclic Graph(방향성 비순환 그래프). 실행 단계를 node, 선후 의존성을 edge로 표현하며 순환이 없다병렬 branch가 있는 task의 실행 관계를 나타낸다
Critical pathTask DAG의 시작에서 완료까지 이어지는 의존 경로 중 wall-clock이 가장 긴 경로. 다른 branch와 겹친 시간을 중복 합산하지 않는다실측 task E2E를 어떤 실행 단계가 지배했는지 귀속한다
E2EEnd-to-End. 정한 시작 event부터 완료 event까지 실제로 흐른 wall-clock 시간. 이 문서는 model request E2E와 task E2E를 구분한다부분 단계 합이 아니라 사용자가 겪은 전체 지연을 고정한다
Output token모델이 새로 생성하는 assistant 응답 tokendecode 비용과 model request E2E를 키운다
Reasoning token일부 reasoning 모델이 답변 전 내부 사고에 쓰는 별도 token사용자에게 보이지 않아도 비용·한도에 잡힐 수 있다
Context window한 호출에서 input과 예상 output을 합쳐 담을 수 있는 최대 token 창길수록 비용, prefill 지연, lost-in-middle 위험이 커진다
Prefillinput token 전체를 처리해 첫 output을 준비하는 단계병렬화가 가능하지만 input 길이에 비례한다
Decodeoutput token을 한 개씩 순차 생성하는 단계. 이때 KV(Key-Value) cache를 읽고 갱신한다긴 답변의 model request E2E와 output 비용을 지배한다
TTFTTime To First Token, 요청 후 첫 token이 도착하기까지의 시간사용자가 “응답이 시작됐다”고 느끼는 지표
ITL / TPOTITL(Inter-Token Latency)은 인접 output token 사이 시간, TPOT(Time Per Output Token)는 보통 그 간격들의 평균답변이 완성되는 속도를 보여준다. 도구마다 분모 관례가 다를 수 있다
p50 / p95 / p99latency 분포의 50%, 95%, 99% 지점평균이 숨기는 tail latency를 드러낸다
SLO / SLASLO는 내부 목표, SLA는 고객과 약속한 서비스 수준p95 TTFT, p99 task E2E, error rate 같은 목표로 표현한다
Queueingprovider 또는 자체 서버 앞에서 요청이 기다리는 현상평균보다 p95/p99를 먼저 망가뜨린다
Rate limitprovider가 RPM(Requests Per Minute), TPM(Tokens Per Minute), concurrency 등으로 요청량을 제한하는 정책HTTP 429, queue, retry 폭증의 원인이 된다
Retry실패한 요청을 다시 시도하는 동작성공률을 올리지만 비용·중복 실행·queue를 증폭할 수 있다
Fallbackprimary 모델·provider 실패 시 다른 경로로 전환하는 설계availability를 높이지만 계약 불일치와 품질 회귀를 만들 수 있다
Prompt caching / prefix caching반복되는 prompt 앞부분의 처리 비용을 재사용하는 기능input 비용과 TTFT를 줄인다
Batch API실시간 응답을 포기하고 요청 묶음을 비동기로 제출·회수하는 provider API지연 허용 작업의 가격·할당량 정책을 바꾼다
Admission boundaryLoad generator가 시스템에 요청 소유권을 넘기는 측정 경계. 이 문서에서는 gateway/server ingress이며 이 경계에서의 reject도 도착 요청으로 센다Client 내부 대기와 시스템이 받은 offered load를 구분한다
Offered load관측 창에 client가 admission boundary까지 실제로 보낸 전체 요청 수 / 관측 시간. 성공뿐 아니라 reject·error·미완료 요청도 포함한다설정 request rate와 실제 도착 부하를 구분한다
Throughput시스템이 단위 시간에 완료하는 작업량. request/s나 output token/s로 잰다전체 처리 용량을 보여주지만 개별 요청의 latency와는 다른 지표다
Goodput성공 완료했고 미리 정한 latency SLO도 만족한 요청 수 / 관측 시간SLO 밖의 느린 성공을 용량 이득으로 오인하지 않게 한다
Routing labelled setproduction과 닮은 query를 사람이 cheap으로 충분/strong 필요와 위험도로 수동 표기한 routing 전용 evaluation set범용 gold dataset의 동의어가 아니며 route threshold 검증에 쓴다
Serving batchinginference engine이 여러 활성 요청의 prefill·decode 작업을 함께 스케줄링하는 방식GPU throughput을 높이지만 개별 요청의 queue·tail latency가 늘 수 있다
Streaming완성된 응답을 기다리지 않고 token 또는 delta를 순서대로 받는 방식체감 TTFT를 줄이지만 decode 총량과 비용을 없애지는 않는다

Prefill, decode, KV cache는 attention 수식 없이도 다음 실행 흐름으로 이해할 수 있다. Prefill은 요청의 input token 전체를 먼저 읽어 첫 output token을 준비한다. 이때 각 과거 token의 attention 계산에 다시 쓸 K(Key)·V(Value) 상태를 KV cache에 저장한다. Decode는 새 token을 하나 만든 뒤 그 token의 K·V를 cache에 덧붙이는 과정을 순차 반복하므로, output이 길수록 완료 시간이 늘고 동시 요청의 KV cache가 GPU memory를 압박한다. attention 행렬의 수학이나 모델 architecture까지 확인하려는 독자만 L11의 transformer-and-attention을 선택 심화로 돌아보면 된다.

4. 비용 모델 - 무엇에 돈을 내는가

섹션 제목: “4. 비용 모델 - 무엇에 돈을 내는가”
LLM step s의 비용 =
input_tokens_s × effective_input_price_s
+ billed_output_tokens_s × effective_output_price_s
+ tool_cost_s
API 요청 비용 = Σ(LLM step s의 비용)

effective_input_price_seffective_output_price_s는 가격표의 $/token에 해당 step의 cache hit, batch, route, 계약 할인을 반영한 실효 단가다. 가격이 100만 token당 표시되면 먼저 token당 단가로 바꿔 곱한다. Self-host의 GPU·운영 고정비는 이 API step 산식에 token 개수로 더하지 않고, §12의 별도 월간 비용 산식으로 비교한다.

여기서 billed_output_tokens_s는 provider가 과금 대상 output usage로 보고한 token이다. Reasoning token이 output usage의 세부 항목으로 포함되는 provider에서는 output_tokens × price_outreasoning_tokens × price_reasoning을 다시 더하면 중복 계산이다. 반대로 reasoning이 output에서 제외된 별도 과금 항목인 계약이라면 provider usage·invoice의 단위와 단가를 그대로 따라야 한다. 핵심은 token 개수와 달러 비용을 더하지 않고, 각 token에 해당 단가를 곱한 비용 항목만 합산하는 것이다.

많은 provider에서 output token은 input token보다 비싸다. 정확한 배율은 모델·시점마다 바뀌지만, 원리는 안정적이다.

  • Input token은 prefill에서 처리된다. 긴 입력을 한 번에 병렬 처리할 수 있어 GPU 활용률을 높이기 쉽다.
  • Output token은 decode에서 만들어진다. 다음 token은 이전 token 뒤에야 결정되므로 순차성이 강하고, KV cache를 계속 읽고 갱신한다.

반례도 있다. output이 더 비싸더라도 input이 훨씬 길면 input 비용이 지배한다.

가정:
price_out = 5 × price_in
요청 A: input 10,000 tokens, output 200 tokens
요청 B: input 500 tokens, output 2,000 tokens
가중 token:
A = 10,000 + 200 × 5 = 11,000
B = 500 + 2,000 × 5 = 10,500

요청 A는 output이 짧아도 긴 context 때문에 비싸다. 요청 B는 짧은 질문이지만 긴 생성 때문에 비싸다. 그래서 비용 최적화는 “output을 줄이자”가 아니라 요청 유형별 input/output 비율을 먼저 측정하는 일에서 시작한다.

4.3 context window는 무료 창이 아니다

섹션 제목: “4.3 context window는 무료 창이 아니다”

Context window는 “많이 넣을 수 있다”는 뜻이지 “많이 넣어도 안전하다”는 뜻이 아니다.

가상 RAG 요청:
system prompt 800 tokens
conversation 1,200 tokens
retrieved chunks 8,000 tokens (800 tokens × top-10)
user question 100 tokens
expected output 600 tokens
총 예산 ~= 10,700 tokens

top-k를 10에서 20으로 올리면 검색 근거가 두 배가 되는 것처럼 보이지만, input token과 prefill 지연도 거의 두 배로 늘 수 있다. 또한 긴 context의 중간에 중요한 근거가 묻히면 품질이 오히려 떨어질 수 있다. RAG에서는 top-k, chunk size, reranker, context packing을 비용·지연 변수로 같이 본다.

4.4 retry는 비용을 조용히 증폭한다

섹션 제목: “4.4 retry는 비용을 조용히 증폭한다”

Retry는 실패 복구 도구지만, LLM에서는 비용 증폭기이기도 하다.

가상 workload 가정:
base_cost_per_request = C
retry_rate = 10%
retry가 평균 1회 추가 호출을 만든다
effective_cost ~= C × (1 + 0.10) = 1.10C

이 단순 계산도 낙관적이다. schema validation 실패로 output을 받은 뒤 retry하면 이미 output 비용을 냈다. timeout이 응답 말미에 발생하면 provider가 내부적으로 거의 전체 decode를 수행했을 수 있다. tool call이 write라면 retry는 비용뿐 아니라 중복 실행 위험도 만든다.

Rate limit과 결합하면 더 위험하다.

가상 rate limit과 요청 분포:
TPM limit = 120,000 tokens/min
평균 요청 = input 2,000 + output 500 = 2,500 tokens
이론 처리량 = 120,000 / 2,500 = 48 requests/min
output이 2,000 tokens로 늘면:
평균 요청 = 4,000 tokens
이론 처리량 = 30 requests/min

이론상 48 RPM이 가능해도 실제 운영에서는 queue, streaming 연결, retry, provider 계산 방식 때문에 한도에 바짝 붙이면 429가 난다. 429가 늘면 retry가 늘고, retry가 늘면 TPM을 더 빨리 소모해 다시 429가 늘어난다. 이것이 retry amplification이다.

반복 prefix가 큰 앱에서는 cache hit가 input 비용을 크게 줄인다. provider별 최소 token, TTL, write/hit 가격은 바뀔 수 있으므로 공식 문서 확인이 필요하다. 아래 첫 계산은 cache write에 별도 가중치가 없다는 가상 workload이고, 그다음 계산에서 write multiplier와 TTL을 변수로 넣는다.

static_prefix = 2,000 tokens
dynamic_suffix = 500 tokens
cache_hit_ratio = 70%
cache_hit_price = 10% of normal input price
static_prefix effective tokens =
2,000 × (miss 30% × 100% + hit 70% × 10%)
= 2,000 × 37%
= 740 token-equivalent
총 input token-equivalent =
static 740 + dynamic 500 = 1,240
캐시 전 input 2,500 대비 약 50% 절감

이 계산은 이 workload에서 “cache hit ratio 70%“라는 가정이 왜 중요한지 보여준다. 다만 user별 권한 문서, 최신 RAG 결과, timestamp, request_id처럼 매번 달라야 하는 값까지 prefix에 넣으면 보안·정확성 문제가 생긴다. 고정 prefix와 동적 suffix를 분리해야 한다.

Cache write multiplier와 TTL 재사용 손익

섹션 제목: “Cache write multiplier와 TTL 재사용 손익”

Cache write가 일반 input보다 비싸다면 hit ratio만 보고 이득을 판단할 수 없다. 같은 prefix가 TTL 안에서 N번 호출된다고 하고, 일반 input 가격을 1, 첫 write multiplier를 W, 이후 hit multiplier를 H라고 두면 prefix 비용은 다음과 같다.

캐시 없음 = N
캐시 사용 = W + (N - 1) × H
break-even 조건:
W + (N - 1) × H < N
N > (W - H) / (1 - H)

예를 들어 W=1.5, H=0.2라는 공급자 가격 주장이 아닌 가상 배수를 넣으면 N > 1.625다. TTL 안에 같은 prefix가 두 번 이상 쓰이면 이득이지만, 한 번 쓰고 TTL이 만료되면 1.5를 내고 재사용하지 못해 손해다. 따라서 TTL은 길수록 좋다가 아니라, prefix의 freshness·권한 경계 안에서 TTL당 실제 재사용 횟수가 break-even을 넘는지 측정해 정한다.

5. Latency 모델 - 사용자는 무엇을 기다리는가

섹션 제목: “5. Latency 모델 - 사용자는 무엇을 기다리는가”
model_request_latency =
TTFT
+ (output_tokens - 1) × mean_ITL
+ provider_response_overhead
TTFT = network + queue + prefill
task_E2E_measured =
task_completed_at - task_started_at
critical_path_attribution =
longest_dependent_path(complete_execution_DAG)
complete causal-span DAG에서:
task_E2E_measured = duration(critical_path_attribution)
complete하고 strictly sequential인 trace에서만:
task_E2E_measured = sum(duration(node_i) for every execution node i)

provider_response_overhead는 마지막 token 생성 뒤 provider 응답이 완료될 때까지의 protocol·buffering 구간이다. Task E2E의 정본은 task_started_at부터 task_completed_at까지 실측한 wall-clock이다. Critical path는 이 값에 더하는 항이 아니라, 그 시간을 complete causal-span execution DAG에 귀속해 설명하는 수단이다. 이 DAG는 task 시작과 완료를 모두 잇고 queue wait, 모든 model attempt, retry backoff, fallback, tool, orchestration, postprocess를 이미 node로 포함한다. 따라서 critical path + queue + retry + fallback처럼 다시 더하지 않는다.

Complete DAG의 longest dependent path는 task E2E와 일치해야 한다. Parent span이 child span을 포함하는 tracing schema라면 critical-path 귀속에는 각 span의 exclusive interval만 쓰며 parent의 inclusive duration과 child duration을 함께 더하지 않는다. Parallel branch가 있으면 겹친 wall-clock을 이중 합산하지 않고 longest dependent path만 귀속한다. 둘이 일치하지 않으면 외부 지연을 별도 항으로 덧붙이지 말고 누락 span, 잘못된 edge, 서로 다른 clock 경계를 먼저 찾는다. Retry, fallback, critic 같은 여러 model request도 각 요청의 output_tokens_imean_ITL_i를 따로 기록하되, task E2E에는 DAG 의존 관계에 따라 한 번만 반영한다.

병렬 branch가 있는 작은 DAG를 숫자로 따라가 보자. 아래 시간은 모두 같은 task clock에서 잰 가상 exclusive interval이다.

0.0s~0.1s 요청 분해 100ms
├─ A: 0.1s~1.0s model request 900ms
└─ B: 0.1s~0.5s tool 400ms -> 0.5s~0.8s model request 300ms
1.0s~1.5s 두 branch join 후 최종 조합 500ms
branch A path = 100 + 900 + 500 = 1,500ms
branch B path = 100 + 400 + 300 + 500 = 1,300ms
critical path = branch A path = task E2E = 1,500ms

100 + 900 + 400 + 300 + 500 = 2,200ms로 더하면 A와 B가 겹쳐 실행된 700ms를 두 번 센다. B가 0.8초에 먼저 끝나도 join은 A가 끝나는 1.0초까지 진행할 수 없으므로 wall-clock을 지배하는 경로는 A다. 반대로 B의 tool이 700ms로 늘어나 B 경로가 더 길어지면 critical path도 B로 바뀐다. 이 예시는 개별 span 합이 비용 계산에는 필요할 수 있어도 task latency 합과는 같지 않다는 경계를 보여준다.

Streaming을 켜면 사용자는 첫 token을 빨리 볼 수 있다. 하지만 모델이 생성해야 할 output token 수는 그대로다. 즉 streaming은 체감 대기를 줄이지, 총 decode 작업을 없애지 않는다.

평균 latency는 안정성을 과대평가한다. 가상 표본 100개 요청 중 94개는 1초, 6개는 30초라면 평균은 약 2.74초다. 평균만 보면 나쁘지 않아 보이지만, 이 표본의 95번째 요청은 30초다. 즉 p95와 p99를 보면 일부 사용자가 긴 대기를 겪는다는 사실이 드러난다.

p50: 절반의 요청이 이 시간 이하로 끝난다.
p95: 95%의 요청이 이 시간 이하로 끝난다. 나머지 5%가 tail이다.
p99: 99%의 요청이 이 시간 이하로 끝난다. 드물지만 사용자 피해가 큰 구간이다.

SLO(Service Level Objective)는 내부 목표다. 예를 들어 “chat p95 TTFT < 1s, p99 task E2E < 15s”처럼 정한다. SLA(Service Level Agreement)는 고객에게 약속한 수준이라 위반 시 보상이나 계약 문제가 붙을 수 있다. 처음 설계할 때는 SLA보다 SLO를 먼저 정하고, 실제 분포를 보고 약속 가능한 값을 좁힌다.

Queueing은 요청이 모델 실행 전에 기다리는 시간이다. provider 내부 queue일 수도 있고, 자체 gateway, rate limiter, vLLM worker 앞 queue일 수도 있다. 평균 부하가 낮아도 순간 burst가 오면 queue가 생긴다.

가상 queueing 예시:
요청 A: prefill 300ms + decode 2s
요청 B: 앞에 대기 5s + prefill 300ms + decode 2s
둘의 모델 실행 시간은 같지만, B의 사용자는 7.3s를 기다린다.

Tail latency를 줄이는 전략은 평균을 줄이는 전략과 다를 수 있다. output을 10% 줄여도 queue가 5초면 p99는 거의 좋아지지 않는다. 각 기법이 queue에 작용하는 방식도 구분해야 한다.

  • Prompt caching은 queue를 직접 지우지 않는다. prefill service time을 줄여 같은 시간에 더 많은 요청을 끝낼 수 있을 때 queue가 간접적으로 짧아진다.
  • Fallback은 요청을 여유 있는 provider·region·worker로 reroute해 primary의 도착 부하를 나눈다. 대체 경로도 포화됐거나 전환 대기가 길면 오히려 tail이 늘 수 있다.
  • Priority queue는 전체 작업량을 줄이지 않고 중요한 요청의 대기를 덜 중요한 요청으로 옮긴다. 전체 p99가 아니라 class별 SLO와 starvation을 함께 봐야 한다.
  • Concurrency cap은 과부하 붕괴를 막지만 초과 요청을 앞단 queue로 옮길 수 있다. Off-peak batch 분리는 실시간 경로에서 지연 허용 작업을 제거해 도착 부하 자체를 낮춘다.

6.1 Prompt caching - 반복 prefill을 줄인다

섹션 제목: “6.1 Prompt caching - 반복 prefill을 줄인다”

Prompt caching은 반복되는 prompt prefix의 처리 결과 또는 token 비용을 재사용한다. 시스템 프롬프트, policy 문서, few-shot 예시, 변하지 않는 RAG context가 앞에 길게 붙는 앱에서 효과가 크다.

Prompt caching 구현 방식 비교

명시적 cache control

개발자가 어느 prefix를 캐시할지 표시한다. 캐시 단위를 의식적으로 설계할 수 있다.

긴 system prompt, 정책 문서, few-shot block처럼 고정 prefix가 명확할 때

Automatic prefix caching

provider가 일정 길이 이상 반복 prefix를 자동 감지한다. 코드 변경은 적지만 hit 조건을 관측해야 한다.

동일 prefix가 충분히 길고 요청 형식이 안정적일 때

앱 레벨 응답 캐시

동일 질문의 최종 답변을 저장한다. prompt caching과 달리 모델 호출 자체를 건너뛸 수 있다.

질문과 권한, freshness 조건이 같다는 것을 검증할 수 있을 때

실패 신호는 세 가지다.

  • cache_read_tokens 또는 유사 지표가 갑자기 0에 가까워진다.
  • prompt 변경 뒤 TTFT와 input 비용이 동시에 오른다.
  • hit ratio 계산에서 분모가 0이 되어 “캐시가 잘 안 맞는 것”이 아니라 “캐시가 아예 적용되지 않는 것”을 놓친다.

6.2 Batch API와 serving batching - 서로 다른 층의 묶음 처리

섹션 제목: “6.2 Batch API와 serving batching - 서로 다른 층의 묶음 처리”

Batch API는 여러 요청을 파일이나 queue로 모아 비동기 처리하고 나중에 결과를 받는 방식이다. provider마다 할인율과 처리 시간은 다르며 바뀔 수 있다. 중요한 원리는 실시간 요구가 없는 작업은 실시간 SLO로 처리하지 않는다는 것이다.

적합한 작업:

  • eval dataset 채점
  • 대량 문서 요약
  • 로그 분류
  • nightly embedding 또는 metadata 생성
  • 콘텐츠 초안 생성

부적합한 작업:

  • 사용자와 대화 중인 chat 응답
  • tool call 결과를 기다리는 agent step
  • 결제·권한·운영 승인처럼 즉시성이 필요한 flow

Self-host에서는 continuous batching이 다른 층의 문제를 푼다. 여러 사용자의 활성 요청을 실행 중에도 합치고 빼면서 prefill·decode step을 함께 스케줄링해 GPU utilization을 높인다. Batch API처럼 결과를 나중에 받는 상품이 아니므로 대화형 요청에도 쓸 수 있지만, batch가 너무 커지면 개별 요청의 queueing이 늘어 p99가 악화될 수 있다.

6.3 Streaming - 체감 지연을 줄이되 총량은 줄이지 않는다

섹션 제목: “6.3 Streaming - 체감 지연을 줄이되 총량은 줄이지 않는다”

Streaming은 긴 답변에서 특히 UX를 개선한다. 첫 chunk를 보여주면 사용자는 시스템이 살아 있다고 느낀다. 그러나 비용·rate limit·backend 점유 시간은 output token 수를 따라간다.

질문Streaming으로 좋아지는가설명
첫 반응이 빨리 보이는가TTFT 체감이 좋아진다
전체 답변 생성 시간이 줄어드는가보통 아니오decode해야 할 token 수는 같다
output token 비용이 줄어드는가아니오같은 token을 생성하면 비용은 그대로다
JSON/tool argument parsing이 쉬워지는가아니오중간 chunk는 schema 경계와 일치하지 않을 수 있다

Structured output이나 tool calling과 streaming을 함께 쓸 때는 partial JSON을 사용자에게 그대로 보여주지 않는다. backend buffer, incremental parser, 완성 후 validation이 필요하다.

6.4 Model routing - 쉬운 요청을 비싼 모델로 보내지 않는다

섹션 제목: “6.4 Model routing - 쉬운 요청을 비싼 모델로 보내지 않는다”

Model routing은 query, task, user tier, risk, latency budget에 따라 모델을 고르는 층이다. 목표는 “항상 싼 모델”이 아니라 싼 모델이 충분한 요청과 비싼 모델이 필요한 요청을 분리하는 것이다.

Direct routing은 모델 호출 전에 규칙·분류기·semantic router로 요청당 모델 하나를 고른다. 예를 들어 일반 QA는 cheap, 코드 질문은 coding-strong, 위험한 정책 질문은 frontier 모델로 바로 보낸다. 한 요청이 cheap 답변을 받은 뒤 strong을 추가 호출하는 구조가 아니므로 비용은 각 route의 요청 비중과 모델 단가를 가중 평균한다.

cheap model 답변
-> confidence, verifier, rule, eval signal 확인
-> 부족하면 stronger model로 escalation

공개 연구 사례도 point-in-time 결과로 읽는다. RouteLLM은 95% GPT-4 품질 유지라는 자신의 측정 조건에서 MT Bench 비용 85% 절감, MMLU 45% 절감, GSM8K 35% 절감 사례를 보고했다(LMSYS RouteLLM). FrugalGPT는 cascade, prompt adaptation, LLM approximation 결합으로 최대 98% 비용 절감을 보고했다(Chen et al., arXiv:2305.05176). 이 수치는 어느 제품의 보장치가 아니라, routing 정책을 직접 replay로 검증해야 하는 이유를 보여주는 사례다.

이 숫자는 자기 traffic에서 router를 튜닝한 결과로 읽어야 한다. 운영 현장에서는 200~500개 query offline replay로 cheap-vs-frontier 품질 분포를 먼저 보고 threshold를 잡는다. 분포가 쉬운 query와 어려운 query로 선명하게 갈라지면 cascade가 좋고, query 카테고리가 명확하면 classifier routing이 더 단순할 수 있다.

query -> router
├─ 일반 QA -> cheap general model
├─ 코드/디버깅 -> coding-strong model
├─ 복잡 추론 -> reasoning model
└─ 도메인 한국어 -> domain-tuned or selected model

Classifier routing은 작은 모델이나 분류기가 query를 분류한다. Semantic routing은 query embedding을 카테고리 embedding과 비교한다. 둘 다 실패하면 잘못된 모델로 보내 품질이 떨어질 수 있으므로, route별 품질·비용·latency dashboard가 필요하다.

6.5 Fallback - 장애 대응이지만 계약 테스트가 먼저다

섹션 제목: “6.5 Fallback - 장애 대응이지만 계약 테스트가 먼저다”

Fallback은 primary provider, model, region, self-host worker가 실패할 때 다른 경로로 전환하는 설계다. availability에는 도움이 되지만, “OpenAI 호환” 같은 표면 호환만 믿으면 실패한다.

Fallback 전에 확인할 계약:

  • messages, system prompt, tool schema 의미가 같은가?
  • streaming chunk 형식이 adapter와 맞는가?
  • structured output strictness와 validation retry가 같은가?
  • safety refusal 형식과 에러 코드가 downstream에서 처리되는가?
  • rate limit 단위(RPM, TPM, concurrency)가 어떻게 다른가?

좋은 fallback은 에러율을 낮추면서도 schema violation, tool argument failure, refusal rate, cost per request를 같이 본다. 장애 때만 켜지는 경로라면 synthetic traffic으로 정기 테스트해야 한다.

6.6 Self-hosting - per-token을 per-GPU-hour로 바꾼다

섹션 제목: “6.6 Self-hosting - per-token을 per-GPU-hour로 바꾼다”

API는 호출량이 작거나 모델 교체가 잦을 때 유리하다. Self-hosting은 open-weight 모델, 즉 배포 라이선스 범위에서 가중치를 내려받아 직접 실행할 수 있는 모델을 서빙해 per-token 비용을 GPU 시간, 운영 인력, 최적화 비용으로 바꾸는 선택이다. Open-weight는 source code, 학습 데이터, 학습 방법까지 모두 공개됐다는 뜻은 아니다.

API 월비용 =
호출 수 × (평균 input_tokens × price_in
+ 평균 billed_output_tokens × price_out
+ 평균 tool_cost)
Self-host 월비용 =
GPU 시간 × $/h
+ 운영 인력 비용
+ 개발·튜닝 시간
+ 장애 대응·관측성 비용

호출 수만으로 API와 self-host를 가를 수는 없다. 같은 100만 호출도 입력·출력 token, API 계약 단가, GPU utilization, 피크 배수, 필요한 복제 수에 따라 결론이 반대가 된다. 따라서 아래 §12의 가상 산식으로 먼저 계산하고, 다음 경우에만 self-host를 후보로 올린다.

  • 모델과 workload가 충분히 안정적이라 GPU가 지속적으로 일을 한다.
  • 데이터 경계, 지연 요구, open-weight 모델 품질 중 하나가 API보다 명확한 가치를 준다.
  • 용량 계획, eval, 장애 대응을 담당할 사람이 비용표에 포함돼 있다.

Self-host에서 자주 등장하는 엔진:

  • vLLM: PagedAttention(KV cache를 고정 크기 block처럼 관리해 메모리 단편화와 낭비를 줄이는 방식), continuous batching으로 널리 쓰이는 serving engine.
  • TGI: HuggingFace Text Generation Inference, production/k8s 친화.
  • SGLang: structured generation과 serving 최적화에 강점.
  • TensorRT-LLM: NVIDIA GPU 최적화 중심.
  • llama.cpp / Ollama: local, edge, 개발 환경에 적합.

6.7 선택 심화 - inference engine 최적화 기법

섹션 제목: “6.7 선택 심화 - inference engine 최적화 기법”

첫 독서에서는 아래 기법의 내부 kernel보다 어떤 병목을 줄이는가어떤 지표로 실패를 확인하는가만 잡으면 된다. FlashAttention은 attention 계산을 GPU의 빠른 on-chip memory에 맞게 block 단위로 재배치해, 계산 결과를 바꾸지 않으면서 느린 memory와의 읽기·쓰기를 줄이는 구현 기법이다. PagedAttention은 위에서 정의했듯 attention 수식 자체가 아니라 serving 중 KV cache memory 관리 문제를 푼다.

기법줄이는 것잘 맞는 조건깨지는 조건
FlashAttention 2/3attention 메모리·시간긴 sequence, attention 병목짧은 입력에서는 체감 효과가 작을 수 있음
Continuous batchingGPU idle time, throughput동시 요청이 많고 serving queue가 있음단발 요청, p99가 더 중요한 strict SLO
PagedAttentionKV cache 단편화많은 동시 sequence와 긴 context작은 모델·낮은 concurrency에서는 이득 제한
Speculative decodingoutput latency긴 output, draft model acceptance가 높음짧은 output, acceptance가 낮음
Prefix caching반복 prefix prefill고정 system/few-shot/RAG prefix동적 값이 prefix를 계속 깨는 경우
Quantization(INT8/INT4)메모리, sometimes latency품질 손실을 eval로 허용 가능한 작업정확도 critical, 수치·코드처럼 민감한 작업
GQA / MQAGQA(Grouped-Query Attention)와 MQA(Multi-Query Attention)가 줄이는 KV cache 크기모델 architecture가 지원serving에서 임의로 바꾸기 어려움
MoEMoE(Mixture of Experts)의 활성 parameter당 computerouter와 expert 품질이 안정적serving 복잡도와 batch 효율 저하

이 표는 “모두 켜라”가 아니다. 병목이 prefill인지 decode인지 queue인지 GPU memory인지 먼저 측정하고, 해당 병목을 줄이는 기법을 고른다.

Reasoning 모델은 내부 reasoning token을 더 쓰는 대신, 복잡한 문제를 한 번에 풀 가능성을 높인다. 일반 모델 multi-step과 비교해야 한다.

각 step s의 비용 C_s =
input_tokens_s × input_price_s
+ billed_output_tokens_s × output_price_s
+ tool_cost_s
일반 모델 multi-step 비용 = Σ(s=1..N) C_s
Reasoning 모델 single-step 비용 = C_1

이 비교에서도 provider가 reasoning token을 billed_output_tokens_1 안에 포함해 보고하면 별도 reasoning 비용을 더하지 않는다. 어느 쪽이 싼지는 가격표의 예시 단가가 아니라, 같은 workload의 각 step에서 관측한 input·billed output·tool 비용을 넣어 비교한다.

선택 기준:

작업 유형우선 후보이유
단순 분류, 짧은 추출cheap 일반 모델 + schemareasoning token이 대부분 낭비
긴 문서 요약일반 모델 + context/caching 관리input과 output 길이가 비용을 지배
수학, 코드 디버깅, 복잡 계획reasoning 모델 또는 verified loopstep 수 감소가 높은 token 단가를 상쇄할 수 있음
비용 민감, 정확도 보통일반 모델 + self-consistency 소량같은 입력을 N회 샘플링해 다수결·검증으로 답을 고르되 N을 제한한다
정확도 절대 우선reasoning 모델 + budget cap비용 상한과 timeout이 없으면 폭주 가능

여기서 self-consistency는 같은 prompt를 여러 번 독립 샘플링하고 다수결이나 verifier로 최종 답을 고르는 방식이다. 단일 샘플의 우연한 오류를 줄일 수 있지만 호출 수에 거의 비례해 비용이 늘므로, 작은 N이 실제 합격률을 얼마나 올리는지 먼저 확인한다.

반례는 단순하다. “긴 생각이 좋은 답을 만든다”는 직관 때문에 모든 요청을 reasoning 모델로 보내면 단순 추출과 FAQ의 비용만 늘고 품질 이득이 거의 없을 수 있다. 반대로 일반 모델을 여러 번 호출해 critic, retry, tool loop를 반복하면 단발 reasoning 모델보다 더 비쌀 수 있다. 어느 쪽이 유리한지는 같은 수동 채점 task evaluation set에서 비용과 정확도를 함께 재는 §14.4의 한계비용과 선택 부록 E의 reasoning budget replay로 확인한다.

기법효과 발휘 범위깨지는 조건먼저 볼 지표
Prompt caching같은 prefix 재사용 비율이 높음prompt 구조 변경, 동적 값이 prefix에 섞임cache hit ratio, cached input tokens, TTFT
Batch API비실시간, 지연 허용사용자 대화·agent step처럼 즉시성이 필요batchable token 비율, SLA 영향
Cascade routingcheap 모델이 routing labelled replay에서 충분한 품질을 냄모든 query가 어려운 도메인escalation rate, route별 quality
Classifier routingquery 카테고리가 명확overlap이 많아 오분류가 잦음confusion matrix, route별 cost
Semantic routing의미 기반 카테고리가 안정적embedding이 도메인 intent를 못 잡음route accuracy, fallback rate
Provider fallback장애·rate limit 분산schema/tool/streaming 계약 차이fallback success, schema violation
Self-host높은 호출량, data boundary, 고정 workload낮은 호출량, 빈번한 모델 교체, 운영 인력 부족GPU utilization, p95/p99, on-call cost
Speculative decoding긴 output, acceptance 높음짧은 output, draft quality 낮음TPOT, acceptance rate
Continuous batchinghigh concurrencystrict per-request latency SLOthroughput, queue wait, p99
Quantization메모리 병목, 품질 손실 허용정확도 criticaleval regression, memory, throughput
cheap model answer -> verifier/confidence -> threshold 미달 시 escalate

아래 숫자는 점수가 높을수록 cheap model 답변을 더 신뢰할 수 있다는 가정으로 둔 가상 threshold 후보다. 운영 기본값이 아니며 route별 품질 baseline, 현재 계약 가격, 허용 실패 비용을 넣어 보정한다.

Threshold 감각효과위험
높은 값 (예: 0.8)보수적, 품질 회귀를 줄이기 쉽다escalation이 많아 비용 절감이 작다
중간 값 (예: 0.6)후보를 넓게 탐색하기 좋다점수 의미가 보정되지 않으면 근거가 약하다
낮은 값 (예: 0.4)cheap 처리 비율이 높다품질 폭락과 사용자 피해가 커질 수 있다

새 workload에서는 200~500 query를 초기 replay 규모의 가상 예시로 삼을 수 있다. 각 query에 사람이 정한 합격 기준을 붙이고, threshold 후보마다 비용, 합격률, 위험한 실패 수, escalation 비율을 함께 표로 낸다. 가장 싼 threshold가 아니라, 허용할 수 없는 실패를 막으면서 비용을 가장 낮추는 threshold를 고른다. threshold 점수의 의미와 escalation 비율은 모델·router·도메인·traffic 난이도가 바뀌면 다시 보정한다.

9. Worked example - 사내 챗봇 비용과 지연 줄이기

섹션 제목: “9. Worked example - 사내 챗봇 비용과 지연 줄이기”
상황:
사내 정책 챗봇
월 비용: $15k (문서 작성 시점의 가상 예시)
P95 TTFT: 1.5s
평균 input: 3,000 tokens
평균 output: 400 tokens
반복 system/policy/few-shot prefix: 2,000 tokens
비실시간 eval/요약성 작업: routing 후 비용 지분의 30%

문제는 모델 품질이 아니다. 정책 답변은 충분히 좋지만, 단가와 p95가 내부 예산·UX 목표를 넘는다.

선택지건드리는 축기대 효과비선택 또는 주의 이유
Prompt caching반복 input, prefill, TTFTprefix 2,000 tokens의 상당 부분 절감user별 권한 문서는 dynamic suffix로 분리
Direct/classifier routing모델 단가호출 전에 요청당 모델 하나를 선택route별 품질 eval 없으면 품질 회귀
Batch API비실시간 token 가격eval/요약성 작업을 할인 경로로 이동사용자 대화에는 부적합
Output schema 축소output token, parse retry짧고 안정적인 답변설명형 답변에는 너무 빡빡할 수 있음
Self-hostper-token -> per-GPU-hour대량 고정 workload에서 비용 절감 가능현재 호출량과 운영 부담을 감안하면 보류
Provider fallbackqueue, 장애, rate limitp95/p99와 availability 개선contract test 없으면 downstream 실패
Baseline:
input 3,000 + output 400 × output_weight 5
= 5,000 weighted tokens/request
Caching 적용:
2,000 static prefix의 effective cost가 37%로 감소한다고 가정
input equivalent = 740 static + 1,000 dynamic = 1,740
weighted tokens = 1,740 + 400 × 5 = 3,740
Direct/classifier routing 적용:
각 요청을 cheap 60%, mid 30%, frontier 10% 중 한 모델에 바로 배정한다고 가정
평균 model unit price가 baseline 대비 40%로 감소한다고 가정
Batch 적용:
caching과 routing까지 적용한 비용 중 30%가 비실시간 workload의 post-route cost share이고
이 지분만 50% 할인 경로로 이동한다고 가정

이 가정으로 비용 index를 끝까지 계산하면 다음과 같다.

Baseline cost index = 5,000
Caching 후 = 3,740
Direct/classifier routing 후 = 3,740 × 0.40 = 1,496
Batch 후 = 1,496 × (실시간 70% + 비실시간 30% × 50%)
= 1,496 × 0.85
= 1,272 (반올림)
가상 월비용 = $15,000 × 1,272 / 5,000 = 약 $3,816

즉 이 가정에서는 비용이 약 25%가 된다. 이 계산은 실제 견적이 아니라, 서로 다른 절감률을 무심코 더하지 않고 어느 항에 곱해지는지 보이는 예시다. 여기서 direct/classifier router는 요청마다 모델 하나만 선택한다. 30%도 원래 token 비중이 아니라 모델 선택 뒤 계산된 비용 1,496에서 비실시간 작업이 차지하는 지분이므로, 뒤의 cascade처럼 escalated 요청에 cheap과 strong 비용을 함께 부과하는 산식과 섞이지 않는다. Routing의 40%와 batch 50% 할인은 이 workload에서 직접 측정해야 한다.

지연은 별도 계산이다. cache hit가 실제 prefill을 줄일 때만 TTFT가 좋아질 수 있고, Batch API는 비실시간 작업을 옮긴 것이므로 대화형 요청의 TTFT를 줄이지 않는다. 비용이 줄었다는 사실만으로 p95가 좋아졌다고 결론 내리면 안 된다.

  • Prompt caching은 -캐시 항이다.
  • Routing은 자원 × 가격 항을 직접 낮춘다.
  • Batch는 “시간을 내주는 대신 가격을 낮추는” 항이다.
  • Self-host는 산식 자체를 per-token에서 per-GPU-hour로 바꾼다.

중요한 것은 결과 숫자보다 어떤 항이 줄었는지 설명 가능해야 한다는 점이다.

9.4 반례 - 같은 항을 두 번 줄이려는 제안

섹션 제목: “9.4 반례 - 같은 항을 두 번 줄이려는 제안”

“provider prompt caching을 켰고, 같은 prefix 문자열을 Redis에 한 번 더 저장하자”는 제안은 겉보기에는 절감처럼 보인다. 하지만 gateway가 Redis에서 문자열을 읽어 다시 prompt에 붙이는 것만으로는 모델의 prefill을 건너뛰지 못하고, provider에 보내는 input token이 사라지지도 않으므로 token 비용을 줄이지 못한다.

세 경계를 구분한다.

  • 앱 최종 응답 캐시: 권한·freshness·key가 같은 응답을 Redis 등에서 반환해 모델 호출 자체를 생략한다.
  • Provider prompt caching: provider가 동일 prefix 처리를 재사용해 과금 input이나 prefill 시간을 줄인다. 정확한 hit·TTL·과금 규칙은 계약에 따른다.
  • Self-host prompt/KV reuse: serving engine이 prefix의 KV 상태를 재사용해 실제 prefill 연산을 줄인다. 단순히 Redis에 token ID나 문자열을 저장하는 것과는 다르다.

반대로 “비실시간 eval은 Batch API로 보내고, 실시간 chat은 prompt caching과 routing을 쓴다”는 제안은 서로 다른 항을 건드린다. 이런 조합이 보통 더 강하다.

아래 30%, 50%, 70%, 100k, 2배, 10배, 30초 같은 수치는 모두 가상 workload의 초기 alert 예시이지 보편 임계값이 아니다. route·feature별 정상 분포, 현재 계약 가격, 품질 eval과 사용자 피해 수준으로 다시 보정한다. 특히 escalation 증가는 router가 낡아서가 아니라 어려운 traffic 비중이 늘어서 생길 수도 있으므로, 고정 replay와 실제 traffic mix를 분리해 본다.

증상정량 신호의미먼저 의심할 것
Cache miss explosion가상 예: hit ratio <30%, cached token 급감prompt prefix가 깨졌다timestamp, request_id, user별 data가 prefix에 섞임
Cache가 아예 미적용read/write cache token 모두 0최소 길이·cache control 조건 미달 가능provider별 threshold, cache 설정
Prompt 누적가상 예: input tokens/hour 2배 증가history가 무한히 붙는다sliding window, summary, context cap
Tool 안의 LLM 호출가상 예: tool당 비용 10배 증가숨은 LLM 호출이 trace 밖에서 돈다tool별 budget, nested trace
Reasoning 폭주가상 예: reasoning token 100k+traffic 난이도 증가 또는 budget cap 부재reasoning_budget, timeout, task routing
Escalation 증가가상 예: escalation 70%+traffic이 어려워졌거나 router/model 기준이 낡았다traffic mix, 고정 replay, router 재학습
429 retry storm429 비율, queue depth 동시 증가rate limit을 retry로 더 악화TPM/RPM/concurrency 분리, backoff+jitter, queue
Latency p99 무시가상 예: 평균 OK, p99 30s+tail user가 심각한 대기를 겪음provider queue, hot path, long output
Fallback 후 schema 실패fallback route schema violation↑provider 계약이 다름adapter contract test, structured output support
Self-host throughput 저하GPU util 낮고 queue 증가batch, KV cache, memory 병목continuous batching, max-num-seqs, model size
TTFT만 느림
-> network, queue, prefill, cache miss를 본다.
첫 token은 빠른데 완료가 느림
-> output token 수, TPOT, decode 병목, streaming UX를 본다.
429가 늘고 retry도 늘어남
-> RPM/TPM/concurrency 중 무엇을 넘는지 나누고 queue를 둔다.
비용만 갑자기 늘어남
-> input/output/reasoning/tool 비용 attribution과 route 변화를 본다.
fallback 때만 실패
-> schema, tool arguments, streaming chunk, safety refusal 계약을 본다.

이 절은 복구 명령어 목록이 아니다. 핵심은 증상을 비용 축과 지연 축으로 분해하는 것이다. “느리다”는 말만으로는 caching을 고칠지, output을 줄일지, provider를 바꿀지, queue를 둘지 결정할 수 없다.

Production LLM 기능은 최소한 다음 지표를 route, model, provider, user tier, feature별로 나눠 본다.

  • input tokens / output tokens / reasoning tokens
  • cost per request, cost per successful task
  • cache read tokens, cache creation tokens, cache hit ratio
  • TTFT·TPOT, model request E2E, task E2E의 p50/p95/p99
  • queue depth, provider 429, timeout, retry count
  • model route, escalation rate, fallback rate
  • tool call latency, tool cost, nested LLM call 여부
  • schema violation rate, validation retry rate

Alert는 평균보다 변화율과 tail을 우선한다. 아래 숫자도 가상 초기값이며 route baseline, 현재 가격, 품질과 실제 사용자 피해를 기준으로 보정한다.

Alert 후보이유
가상 예: 시간당 비용이 baseline 대비 2배prompt 누적, route 변경, retry storm을 빨리 잡음
가상 예: cache hit ratio 50% 미만 지속caching 구조가 깨졌을 가능성
p95 TTFT SLO 초과사용자가 응답 시작을 못 느끼는 상태
p99 task E2E 급등tail queue, long output, provider hotspot
가상 예: escalation 70%+traffic 난이도 상승 또는 stale router를 구분해야 함
429와 retry 동시 증가retry amplification 가능성

정확한 가격은 최신 견적이 필요하므로 먼저 변수로 계산한다.

monthly_requests = R
avg_input = I
avg_output = O
price_in = P_i
price_out = P_o
avg_tool_cost = T
api_cost = R × (I × P_i + O × P_o + T)
gpu_hour_price = G
monthly_gpu_hours = H
ops_cost = Ops
engineering_cost = Eng
self_host_cost = G × H + Ops + Eng

추가로 throughput 관점도 본다.

required_output_tok_per_sec =
R × O / (30 days × 24 hours × 3600 seconds)
required_total_tok_per_sec는 input prefill과 output decode를 나눠 산정한다.

12.1 가상 견적으로 끝까지 계산하기

섹션 제목: “12.1 가상 견적으로 끝까지 계산하기”

아래 숫자는 어떤 provider나 GPU의 최신 가격이 아니라 산식 연습용 가정이다. 계약 단가와 측정한 replica capacity를 넣어 같은 표를 다시 만든다.

월 요청 R = 10,000,000
평균 input I = 1,500 tokens
평균 output O = 600 tokens
API input price P_i = $0.60 / M tokens
API output price P_o = $3.00 / M tokens
평균 tool cost T = $0 (이 가상 workload에는 외부 tool 호출 없음)
API 요청당 비용 = 1,500 × 0.60 / 1,000,000
+ 600 × 3.00 / 1,000,000
+ $0
= $0.0027
API 월비용 = 10,000,000 × $0.0027 = $27,000
필요 평균 output throughput = 10,000,000 × 600 / 2,592,000
= 약 2,315 output tok/s
피크 여유 2배를 포함한 목표 = 약 4,630 output tok/s
측정한 replica capacity = 400 output tok/s (이 workload의 가정)
필요 replica 수 = ceil(4,630 / 400) = 12
GPU 비용 = 12 × 730 h × $2.50/h = $21,900
운영·관측·대기 인력 배부 = $3,500 / 월
self-host 월비용 = $25,400

이 가정에서는 self-host가 월 약 $1,600 저렴하지만, 작은 capacity 측정 오차나 한 번의 장애 대응 비용으로 역전될 수 있다. 호출 수를 절반으로 낮추되 replica 수가 크게 줄지 않으면 API가 더 유리해진다. 따라서 손익분기는 호출 수 하나가 아니라 token mix, 피크 배수, 복제 수, utilization, 운영비가 교차하는 지점이다.

Break-even은 api_cost > self_host_cost 한 줄로 끝나지 않는다. 다음 질문이 모두 “예”에 가까워야 self-host가 현실적이다.

  • workload가 충분히 안정적이라 GPU를 놀리지 않는가?
  • 품질이 open-weight 모델과 domain tuning으로 충분한가?
  • p95/p99 latency를 자체적으로 측정하고 줄일 팀이 있는가?
  • model upgrade, quantization regression, prompt compatibility를 eval할 수 있는가?
  • 장애 대응과 capacity planning을 API provider 대신 감당할 수 있는가?

13. 일반 시스템으로 옮겨 보는 패턴

섹션 제목: “13. 일반 시스템으로 옮겨 보는 패턴”

LLM 비용·지연은 특수해 보이지만, 구조는 일반 분산 시스템과 닮았다.

LLM 구성요소일반 시스템 매핑공통 질문
Input vs Output 비대칭read/write 비용 차이, 압축/해제 비용어느 방향이 병목인가?
Prefill vs Decodebatch processing vs sequential pipeline병렬화 가능한 단계와 순차 단계가 무엇인가?
Provider prompt / self-host KV reuseserving 계층의 prefix prefill 재사용실제 prefill이 줄었는가? 과금 cache usage가 보이는가?
앱 최종 응답 캐시HTTP cache, CDN, Redis모델 호출을 생략해도 권한·freshness가 맞는가?
Batch APIoff-peak batch, spot instance시간을 내주고 단가를 낮출 수 있는가?
Routinghot/cold tier, query planner쉬운 요청과 어려운 요청을 분리할 수 있는가?
Fallbackmulti-region failover대체 경로의 계약이 같은가?
Self-hostbuild vs buy, on-prem vs cloud고정비와 운영 책임을 감당할 만큼 큰가?
Continuous batchingrequest coalescing, queue-based workerthroughput 이득이 p99 악화보다 큰가?
Quantizationcompression, lower precision storage손실을 eval로 허용할 수 있는가?

비용과 지연은 단위가 다르므로 하나의 식으로 합치지 않는다. 각각의 일반식은 다음과 같다.

비용 = 자원 사용량 × 단가 - 캐시/할인 이득 + 운영 고정비
task E2E 실측 = task_completed_at - task_started_at
지연 귀속 = queue·retry·fallback을 이미 포함한 complete execution DAG의 critical path

두 번째 줄은 첫 번째 지연 값에 더하는 산식이 아니다. 실측 wall-clock을 원인별로 설명하는 귀속 규칙이며, queue·retry·fallback을 critical path 밖에서 다시 더하면 이중 계산이다.

LLM의 특수성은 자원이 token이고, output이 순차 생성되며, 모델 응답이 비결정적이라는 점이다.

14. 관찰 실험 - 먼저 질문과 중단 기준을 정한다

섹션 제목: “14. 관찰 실험 - 먼저 질문과 중단 기준을 정한다”

이 절의 목적은 특정 SDK나 serving engine을 설치하는 데 있지 않다. 한 변수만 바꾸고 비용·품질·지연 분포가 어떻게 이동하는지 해석하는 것이 목적이다. 첫 회독에서는 아래 실험 계약과 해석 자산만 읽는다. 실행 코드와 환경 종속 명령은 절 끝의 선택 부록에 있으므로 펼치지 않아도 본문 결론을 이해할 수 있다.

실험확인할 질문통제할 입력 변수관측 지표예상 해석반례·중단 기준
Prompt cache반복 prefix가 실제 비용과 prefill을 줄이는가?prefix 내용·길이, TTL, 요청률, cache write/hit 단가, 동적 suffixeligible request 수, cache read/creation token, 요청·token 기준 hit ratio, input 비용, TTFT 분포hit가 늘며 input 비용과 TTFT tail이 함께 내려가면 재사용 효과가 있다분모가 0이거나 재사용 1회뿐이면 hit 최적화를 중단한다. 권한·freshness 경계를 넘는 prefix는 캐시하지 않는다
Latency distribution느림의 원인이 queue·prefill·decode·외부 단계 중 어디인가?prompt/output 길이, cache 상태, 동시성, route, retry, streaming 여부TTFT·TPOT·model request E2E·task E2E 각각의 p50/p95/p99, queue time, timeout·오류율TTFT tail만 오르면 queue/prefill, TPOT가 오르면 decode, task E2E만 오르면 외부 단계를 의심한다평균만 좋아지고 p95/p99 SLO가 악화되면 실패다. 서로 다른 metric 정의의 결과는 직접 비교하지 않는다
Routing replay어떤 요청을 cheap 모델에 남겨도 품질 계약을 지키는가?고정 routing labelled set, 모델 후보, route threshold, 품질 rubric, task별 위험 가중치confusion matrix, 품질, 요청당 비용, TTFT, model request E2E, task E2E, escalation·fallback 비율쉬움/어려움이 분리되면 cascade, task 범주별 강점이 갈리면 classifier route가 유리하다위험 task의 under-route가 기준을 넘거나 거의 모두 escalate되면 router 복잡도를 정당화하지 못한다
Self-host loadoffered load를 높일 때 goodput 이득이 tail 비용보다 큰가?모델·엔진 버전, GPU, 입력/출력 길이 분포, request rate·동시성, batch·KV cache 설정offered load, successful throughput, goodput, tok/s, queue depth, TTFT·TPOT·model request E2E p50/p95/p99, GPU·오류율낮은 부하에서는 idle, 중간에서는 batching 이득, 포화 뒤에는 goodput 정체와 tail·오류 급등goodput이 정체·하락하면서 p95/p99 또는 오류율이 오르면 더 높은 부하 실험을 멈춘다
Reasoning budget추가 reasoning 비용이 위험 오류를 충분히 줄이는가?동일 수동 채점 evaluation set·출력 계약, 모델, 1k/4k/16k 같은 budget 후보, timeout·retry cap정확도·위험 실패율, billed token, 요청당 비용, p95 task E2E, marginal cost per quality gain작은 budget부터 품질 곡선이 평평해지는 지점을 찾는다. multi-step 누적 비용과도 비교한다품질 증가가 없거나 위험 실패율이 그대로인데 비용·tail만 늘면 다음 budget 단계로 가지 않는다

실험 숫자는 성격을 섞지 않는다. 이 문서의 출처 링크나 논문에서 가져온 값은 인용된 point-in-time 수치, 산식을 이해시키기 위해 만든 값은 가상 planning 값, 독자가 자신의 환경에서 얻은 값은 실측값으로 표시한다. 가상 표의 밀리초나 tok/s를 용량 약속으로 재사용해서는 안 된다. 실측값도 모델 revision, provider, GPU, engine·SDK 버전, 입력/출력 길이 분포, 측정 시각을 함께 기록해야 비교 가능한 데이터가 된다.

14.2 Cache hit의 분모부터 고정한다

섹션 제목: “14.2 Cache hit의 분모부터 고정한다”

“hit ratio가 80%“라는 말만으로는 절감 효과를 계산할 수 없다. 요청 수를 분모로 삼았는지, cache 사용 자격이 있는 요청만 셌는지, token 양을 기준으로 삼았는지에 따라 같은 80%의 의미가 달라진다.

지표산식답하는 질문놓치기 쉬운 경계
Request hit ratiocache_hit_requests / eligible_requests캐시 가능한 요청 중 몇 건이 hit했는가?전체 요청을 분모로 쓰면 cache 비대상 route가 비율을 희석한다
Token reuse ratiocache_read_tokens / (cache_read_tokens + cache_creation_tokens)캐시로 관리한 prefix token 중 어느 정도를 재사용했는가?provider usage가 read·creation을 같은 의미로 보고하는지 확인해야 한다
Workload coverageeligible_requests / all_requests전체 traffic 중 캐시 설계가 적용되는 범위는 얼마인가?hit ratio가 높아도 coverage가 5%면 전체 비용 효과는 작다
Realized input saving(uncached_baseline_cost - observed_input_cost) / uncached_baseline_costwrite multiplier와 hit 단가까지 반영해 실제 얼마를 아꼈나?baseline의 모델·traffic mix가 달라지면 캐시가 아닌 route 변화가 절감처럼 보일 수 있다

작은 반례를 보자. A와 B 모두 eligible 요청 100건 중 80건이 hit해 request hit ratio는 80%다. A의 반복 prefix는 요청당 100 tokens이고 B는 10,000 tokens다. 단가와 write 조건이 같다면 B의 token 재사용량은 A의 100배라 비용·TTFT 영향도 훨씬 크다. 반대로 전체 요청 10,000건 중 eligible 요청이 100건뿐이라면 B의 hit ratio가 높아도 workload coverage는 1%다. 따라서 dashboard에는 최소한 eligible, all, read tokens, creation tokens, uncached_baseline_cost를 함께 둔다.

분모가 0일 때 0%를 반환하면 “캐시는 적용됐지만 모두 miss”와 “캐시 사용량 자체가 없음”을 구분하지 못한다. 선택 부록의 fixture가 None을 반환하는 이유가 이것이다. 또한 고정 개수 maxlen=600으로 5분 창을 흉내 내면 초당 2건을 넘는 traffic에서 아직 유효한 표본을 버린다. 시간 창은 timestamp 또는 1~10초 bucket으로 유지하고, 메모리 상한은 bucket 수에 건다.

14.3 평균과 tail, TTFT와 TPOT와 E2E를 분리한다

섹션 제목: “14.3 평균과 tail, TTFT와 TPOT와 E2E를 분리한다”

Latency 실험은 하나의 latency_ms 열로 끝내지 않는다. TTFT는 request_started_at -> first_token_at, TPOT는 first_token_at -> last_token_at의 첫 token 이후 decode 간격 평균이다. Model request E2Erequest_started_at -> request_completed_at, task E2E는 tool·retry·fallback을 모두 거친 task_started_at -> task_completed_at의 실측 wall-clock이다. Tool 대기, retry backoff, fallback 요청, 최종 후처리 같은 외부 지연을 TPOT 구간에 혼입하지 않는다. Streaming은 첫 token을 보이는 시점을 앞당길 수 있지만 TPOT, 생성 token 수, 두 E2E를 자동으로 줄이지 않는다.

이 문서의 naming boundary는 model request E2Etask E2E다. 다른 SDK나 관측 문서가 end_to_end_latency 또는 total latency라는 단일 이름을 쓰더라도 어느 timestamp 경계인지 확인하기 전에는 둘 중 하나로 간주하지 않는다. 특히 task E2E를 model request E2E + tool + retry + fallback으로 재조립하지 않고 task 양 끝 timestamp에서 직접 잰다.

관측 모양우선 가설함께 확인할 반례
TTFT p95/p99만 상승queue 증가, 긴 prefill, prompt cache missnetwork 구간만 느려졌거나 stream buffer가 첫 chunk를 늦게 flush할 수 있다
TPOT p95/p99와 model request E2E가 함께 상승decode 병목, 긴 output, KV cache memory 압박last_token_at과 protocol 완료 사이 client buffering이 커졌을 수 있다
모델 지표는 정상이고 task E2E만 상승tool, retry backoff, fallback, 후처리 지연외부 단계를 모델 TPOT로 계산하지 않았는지 스키마를 확인한다
TTFT는 개선됐지만 두 E2E와 비용은 그대로streaming 체감만 개선output cap이나 speculative decoding이 함께 바뀌었는지 확인한다
평균은 안정적이지만 p99만 특정 시간대 급등burst queue, provider hotspot, worker saturation소수의 유난히 긴 input/output이 분포를 바꿨을 수 있다
Successful throughput은 오르지만 goodput은 정체serving이 포화점에 접근SLO를 넘긴 성공 요청과 timeout·오류를 분리해 보고하는지 확인한다

Queue 포화는 평균 service time이 그대로여도 tail을 비선형으로 키운다. 여기서 도착 cohort는 같은 arrival window에 admission boundary를 통과한 요청 집합이다. 먼저 이 cohort로 세 비율의 분모를 고정한다.

지표이 절의 60초 도착 cohort 정의
Offered loadAdmission boundary에 도착한 전체 요청 수 N / 60s
Successful cohort rateTerminal 관찰 시점까지 성공 완료한 요청 수 S_D / 60s
Cohort goodput성공 완료했고 model request E2E <= 2s인 요청 수 G_D / 60s
Terminal failure rateTerminal 관찰 시점까지 reject·timeout·error로 끝난 요청 수 F_D / N

재현 가능한 부하 표는 도착 cohort와 관찰 시점을 먼저 고정한다. 아래는 실제 엔진 측정값이 아닌 의도적으로 만든 정수 count fixture다. Arrival window는 W=[t0, C), C=t0+60s이고 goodput SLO는 model request E2E <= 2s다. Cohort는 마지막 순간에 도착한 요청도 SLO를 달성할 시간을 가져야 하므로 D=C+15s까지 추적한다. 이 15초 terminal deadline은 2초 SLO보다 길며, D에도 끝나지 않은 요청은 harness가 deadline timeout으로 종료 실패 처리한다.

Right-censoring은 관찰이 먼저 끝나 요청의 최종 성공·실패를 아직 모르는 상태다. C에서 관찰을 멈추면 queue와 in-flight 요청이 right-censored되어 successful cohort rate와 cohort goodput을 과소 계산한다. 따라서 C에서는 backlog 모양만 snapshot하고, 성공·실패·cohort goodput은 D까지 추적한 terminal ledger에서 계산한다.

C 시점 cutoff snapshot:
N = S_C + F_C + Q_C + I_C
# 성공 완료, 종료 실패, queue 대기, in-flight는 상호 배타적이다.
D 시점 terminal ledger:
N = S_D + F_D
G_D <= S_D
offered_load = N / 60s
successful_cohort_rate = S_D / 60s
cohort_goodput = G_D / 60s
terminal_failure_rate = F_D / N

먼저 C의 네 상태와 D의 terminal 결과를 count로 고정한다. S_C, F_C, Q_C, I_C는 각각 C까지 성공 완료, 종료 실패, queue 대기, 실행 중인 요청 수다. S_D, F_D는 D까지의 최종 성공·실패 수이고 G_D는 그 성공 중 2초 SLO를 만족한 수다.

가상 설정NS_CF_CQ_CI_CS_DF_DG_D
8 req/s4804720444800474
16 req/s960934110159582924
19 req/s1140108083022112812846
22 req/s1320109060110601208112522

다음 rate와 percentage는 모두 위 정수에서 계산했다. TTFT 값은 같은 가상 fixture의 S_D개 성공 요청 timestamp를 정렬한 뒤 nearest-rank 관례 rank_p=ceil(p×S_D)로 읽는다. 따라서 괄호의 #은 정렬된 성공 표본의 1-based index다. 값 자체는 특정 엔진 성능 주장이 아니다.

가상 설정Offered loadSuccessful cohort rateCohort goodputTTFT p95TTFT p99Terminal failure rate판단
8 req/s8.00 req/s8.00 req/s7.90 req/s320ms (#456/480)480ms (#476/480)0 / 480 = 0.00%여유가 크고 burst를 흡수할 수 있는 기준선
16 req/s16.00 req/s15.97 req/s15.40 req/s760ms (#911/958)1,400ms (#949/958)2 / 960 = 0.21%평균은 작게 변해도 tail이 먼저 악화
19 req/s19.00 req/s18.80 req/s14.10 req/s2,100ms (#1072/1128)6,800ms (#1117/1128)12 / 1140 = 1.05%성공량은 늘었지만 SLO 내 goodput은 하락한 포화권
22 req/s22.00 req/s20.13 req/s8.70 req/s5,400ms (#1148/1208)12,000ms (#1196/1208)112 / 1320 = 8.48%backlog와 오류가 늘어 안정 상태가 아니므로 실험을 중단한다

Timeout·error는 offered load와 terminal failure rate의 분모에 남긴다. 성공 latency p50/p95/p99는 S_D개 성공 요청만으로 계산하되 count ledger를 반드시 같이 보고한다. Timeout 기준값을 실제 latency처럼 percentile 표본에 삽입하면 실패가 늘수록 성공 분포가 빨라 보이는 survivorship bias를 숨길 수 있다. 실패 요청의 event history는 별도로 보존하고, 강제 terminal deadline을 두지 않는 분석이라면 D에도 미완료인 수를 right-censored count로 따로 보고한다.

마지막 행은 “GPU utilization이 높으니 효율적”이라는 직관의 반례다. 장기 offered load가 처리용량보다 크면 queue는 언젠가 반드시 넘친다. 이때 필요한 것은 더 긴 timeout이 아니라 admission control, queue cap, backpressure, capacity 증설, 낮은 비용 route로의 안전한 분산 중 하나다. 이 cohort 계약에서는 cohort goodput이 정체·하락하는지, p95/p99가 오르는지, terminal failure rate가 느는지를 함께 보고 포화를 판정한다. 목표는 최대 successful cohort rate가 아니라 SLO를 지키며 지속 가능한 cohort goodput이다.

14.4 품질 1단위를 더 얻는 한계비용

섹션 제목: “14.4 품질 1단위를 더 얻는 한계비용”

평균 요청 비용만 비교하면 expensive route가 비싸다는 사실만 보인다. 의사결정에는 marginal cost per quality gain, 즉 현재 후보에서 다음 후보로 갈 때 품질 1단위를 더 얻기 위해 추가로 내는 비용이 필요하다.

marginal_cost_per_quality_point(A -> B) =
(cost_per_request_B - cost_per_request_A)
/ (quality_percent_B - quality_percent_A)

아래 값은 산식 연습용 가상 planning 값이다. quality는 동일한 수동 채점 task evaluation set과 rubric에서 얻은 합격률이며 서로 다른 benchmark 점수를 섞지 않는다.

후보품질 합격률요청당 비용직전 후보 대비 품질 증가직전 후보 대비 추가 비용품질 1%p당 한계비용
Cheap only78%$0.002기준선기준선기준선
Routed cheap + frontier86%$0.004+8%p+$0.002$0.00025
Reasoning 4k89%$0.012+3%p+$0.008약 $0.00267
Reasoning 16k90%$0.036+1%p+$0.024$0.02400

이 표에서는 마지막 1%p가 첫 8%p보다 96배 비싸다. 그렇다고 reasoning 16k가 항상 틀린 선택은 아니다. 마지막 1%p가 결제 승인, 보안 변경, 의료·법률 검토처럼 기대 손실이 큰 오류를 제거한다면 지불할 가치가 있다. 반대로 낮은 위험의 초안 작성이라면 plateau 이후 budget은 중단한다. 품질 평균뿐 아니라 위험 실패율, abstention(불확실할 때 답을 강행하지 않고 escalation·human review로 넘기는 선택), retry·critic loop 누적 비용, p95 task E2E를 같은 행에 추가해야 한다.

14.5 Routing은 confusion matrix와 cost matrix로 본다

섹션 제목: “14.5 Routing은 confusion matrix와 cost matrix로 본다”

Routing의 정답은 모델 이름이 아니라 각 요청에 필요한 최소 capability다. Offline replay에서는 사람이 cheap으로 충분strong 필요를 label하고, router 결정을 교차시킨다.

Direct routing과 cascade는 비용 경로가 다르다. Direct routing은 모델 호출 전에 cheap 또는 strong 하나만 선택한다. Cascade는 모든 요청에서 cheap을 먼저 호출하고 verifier가 미통과 요청만 strong을 추가 호출한다. 따라서 cascade의 escalated 요청은 cheap과 strong 비용을 모두 낸다.

strong_only_baseline = N × C_strong
direct_cost = N × (p_cheap × C_cheap + p_strong × C_strong) + N × C_router
direct_saving = strong_only_baseline - direct_cost
cascade_total_cost = N × C_cheap + (N × escalation_rate) × C_strong
+ N × (C_verifier + C_router)
cascade_incremental_over_cheap = cascade_total_cost - N × C_cheap
cascade_saving = strong_only_baseline - cascade_total_cost

N=100, C_cheap=$0.002, C_strong=$0.020, direct의 router overhead 또는 cascade의 router·verifier overhead 합이 요청당 $0.001이고 30%가 strong을 쓴다고 가정하자. Direct는 $0.14 + $0.60 + $0.10 = $0.84로 strong-only $2.00 대비 $1.16을 절감한다. Cascade total은 모든 cheap 비용 $0.20 + 30건의 추가 strong 비용 $0.60 + overhead $0.10 = $0.90이고, cheap-only 대비 증분은 $0.90 - $0.20 = $0.70이다. 따라서 strong-only $2.00 대비 절감액은 $1.10이다. Naive한 $0.60 계산을 total로 해석하면 cheap 비용 $0.20과 overhead $0.10을 모두 누락하고, cheap-only 대비 incremental로 해석해도 overhead $0.10을 누락한다.

실제 필요 / Router 결정Cheap으로 보냄Strong으로 보냄
Cheap으로 충분올바른 절감불필요한 escalation: 추가 비용·지연
Strong 필요위험한 under-route: 품질·안전 계약 위반올바른 escalation

두 오류의 가격은 같지 않다. 불필요한 escalation 한 건의 손실은 대체로 strong 비용 - cheap 비용과 추가 latency다. Under-route 한 건의 손실은 재문의·human review·환불 같은 복구 비용일 수도 있고, 위험 작업에서는 사고의 기대 손실일 수도 있다. 따라서 threshold는 단순 accuracy가 아니라 다음과 같은 가중 손실을 줄이는 방향으로 잡는다.

routing_loss =
under_route_count × expected_under_route_loss
+ unnecessary_escalation_count × extra_strong_route_cost
+ contract_failure_count × fallback_or_recovery_cost

예를 들어 1,000건 중 strong이 필요한 요청이 20건뿐인 traffic에서 모든 요청을 cheap으로 보내면 router accuracy는 98%다. 하지만 중요한 20건을 전부 놓쳤으므로 제품에는 실패다. 반대로 strong 필요 20건을 모두 잡기 위해 600건을 불필요하게 escalate하면 품질은 지켜도 절감 목표는 실패할 수 있다. task 위험도별 confusion matrix, escalation rate, route별 p95 latency와 cost matrix를 함께 봐야 하는 이유다.

Routing replay의 중단 기준은 세 가지다. 첫째, 위험 task의 under-route가 사전 기준을 넘는다. 둘째, threshold를 올려도 cheap과 strong의 품질 분포가 겹쳐 개선되지 않는다. 셋째, escalation이 거의 100%라 router 호출과 관측 복잡도만 추가된다. 이 경우 prompt/RAG를 고치거나, 단일 strong/reasoning 모델을 쓰거나, 명시적 task rule로 단순화하는 편이 낫다.

14.6 Self-host 손익분기는 utilization에 민감하다

섹션 제목: “14.6 Self-host 손익분기는 utilization에 민감하다”

§12의 가상 견적은 replica당 400 output tok/s를 목표 SLO에서 지속 가능한 실측 capacity라고 가정해 12 replicas와 월 $25,400을 계산했다. 실제 fleet에서는 burst, 긴 context, 모델별 memory footprint, 장애 여유, traffic 시간대 때문에 명목 capacity를 모두 매출성 workload에 쓰지 못한다. 아래 표는 같은 가격과 목표 4,630 output tok/s에서 effective capacity factor만 바꾼 가상 민감도 분석이다. 이 factor는 GPU utilization metric 하나가 아니라 SLO를 지키며 판매 가능한 capacity의 비율이다.

필요 replicas = ceil(4,630 / (400 × effective_capacity_factor))
월 GPU 비용 = replicas × 730h × $2.50/h
월 self-host 비용 = 월 GPU 비용 + $3,500 운영비
Effective capacity factorReplica당 유효 capacity필요 replicas월 GPU 비용운영비 포함 월비용$27,000 API 가상 비용 대비
100%400 tok/s12$21,900$25,400$1,600 절감
85%340 tok/s14$25,550$29,050$2,050 초과
70%280 tok/s17$31,025$34,525$7,525 초과
50%200 tok/s24$43,800$47,300$20,300 초과

기준선에서 겨우 $1,600 저렴했던 결론은 effective capacity가 85%만 되어도 뒤집힌다. 반대로 traffic이 시간대별로 평평하고 여러 workload가 GPU idle을 채우며, spot·예약 계약이나 더 높은 실측 capacity가 가능하면 self-host가 다시 유리할 수 있다. 그래서 GPU 평균 utilization만 높이는 것이 목표가 아니다. queue와 p99를 숨기지 않는 sustainable tok/s, 장애 headroom, replica 정수 단위, 운영비를 함께 넣어 민감도 곡선을 만든다.

14.7 선택 부록 - 재현 fixture와 환경 종속 실행 예시

섹션 제목: “14.7 선택 부록 - 재현 fixture와 환경 종속 실행 예시”
A. Cache token reuse ratio의 deterministic 최소 fixture

아래 코드는 특정 provider SDK에 묶이지 않는다. cache_read_tokenscache_creation_tokens를 timestamp 단조 증가 순서로 같은 5분 시간 창에 넣고, 분모가 없을 때 None을 반환한다. 포함 경계는 now - WINDOW_SECONDS <= timestamp <= now다. Out-of-window event는 제거하고 future event와 역순 입력은 오류로 거부한다. 마지막 assert 묶음은 경계와 오류 규칙, 고처리량에서 maxlen=600을 사용하지 않아 1,200개 표본이 보존되는지까지 확인한다. 실제 gateway 군집에서는 이 구조를 1~10초 bucket 집계로 바꾼다.

from collections import deque
WINDOW_SECONDS = 300
window = deque()
def reset():
window.clear()
def evict_expired(now):
if window and window[-1][0] > now:
raise ValueError("future events are not allowed")
cutoff = now - WINDOW_SECONDS
while window and window[0][0] < cutoff:
window.popleft()
assert all(cutoff <= timestamp <= now for timestamp, _, _ in window)
def record_usage(timestamp, cache_read_tokens, cache_creation_tokens):
if window and timestamp < window[-1][0]:
raise ValueError("events must be ordered by timestamp")
window.append((timestamp, cache_read_tokens, cache_creation_tokens))
evict_expired(timestamp)
def token_reuse_ratio_5min(now):
evict_expired(now)
reads = sum(reads for _, reads, _ in window)
writes = sum(writes for _, _, writes in window)
if reads + writes == 0:
return None
return reads / (reads + writes)
reset()
assert token_reuse_ratio_5min(0) is None
record_usage(0, cache_read_tokens=0, cache_creation_tokens=100)
record_usage(100, cache_read_tokens=100, cache_creation_tokens=0)
assert token_reuse_ratio_5min(100) == 0.5
record_usage(301, cache_read_tokens=100, cache_creation_tokens=0)
assert token_reuse_ratio_5min(301) == 1.0
reset()
record_usage(0, cache_read_tokens=0, cache_creation_tokens=100)
record_usage(300, cache_read_tokens=100, cache_creation_tokens=0)
assert len(window) == 2
assert token_reuse_ratio_5min(300) == 0.5
assert token_reuse_ratio_5min(300.001) == 1.0
try:
token_reuse_ratio_5min(299)
except ValueError as error:
assert str(error) == "future events are not allowed"
else:
raise AssertionError("future event must be rejected")
reset()
for index in range(1_200):
record_usage(
timestamp=index / 4,
cache_read_tokens=index % 2,
cache_creation_tokens=(index + 1) % 2,
)
assert len(window) == 1_200
assert token_reuse_ratio_5min(300) == 0.5
print("cache fixture: PASS")

Provider usage 필드명, cache 최소 길이, TTL, write/hit 단가는 provider와 SDK 버전에 의존한다. 실제 adapter에서는 공식 문서의 usage 정의를 먼저 매핑하고, request hit ratio와 workload coverage는 별도 counter로 수집한다.

B. Latency 분포 기록 스키마와 계산 관례

같은 prompt만 반복하면 길이 분포가 지나치게 좁아질 수 있다. 먼저 고정 fixture로 회귀를 확인하고, 그다음 production과 같은 input/output 길이 bucket을 가진 표본으로 확장한다. 100회는 빠른 smoke 비교, 1,000회 이상은 드문 tail을 보기 위한 출발점일 뿐이며 p99 신뢰도는 독립 표본 수와 traffic 변동성에 좌우된다.

같은 조건을 100~1,000회 이상 실행해 기록:
task_started_at
request_started_at
admitted_at
first_token_at
last_token_at
request_completed_at
task_completed_at
model_request_id
input_tokens
output_tokens
model_and_revision
provider_or_engine_version
cache_hit
retry_count
route
execution_nodes[]:
node_id
parent_node_ids[]
node_type # queue_wait | model_attempt | retry_backoff | fallback | tool | orchestration | postprocess
node_started_at
node_completed_at
계산:
queue_time = admitted_at - request_started_at
TTFT = first_token_at - request_started_at
TPOT ~= (last_token_at - first_token_at) / (output_tokens - 1)
# output_tokens > 1이고 첫 token 뒤 간격 평균을 쓰는 관례
provider_response_overhead = request_completed_at - last_token_at
model_request_latency = TTFT + (output_tokens - 1) × TPOT
+ provider_response_overhead
= request_completed_at - request_started_at
task_E2E_measured = task_completed_at - task_started_at
critical_path_attribution = longest_dependent_path(complete_execution_DAG)
complete causal-span DAG에서:
task_E2E_measured = duration(critical_path_attribution)
complete하고 strictly sequential인 trace에서만:
task_E2E_measured = sum(duration(node_i) for every execution node i)

일부 SDK·benchmark는 TPOT 분모를 output_tokens로 두거나 TTFT를 포함한 값을 TPOT라는 이름으로 내보낸다. 어떤 도구는 ITL의 개별 분포를 제공하고, 다른 도구는 요청별 평균만 제공한다. 절대값을 비교하기 전에 첫 token 포함 여부, last_token_atrequest_completed_at의 차이, timeout·error 표본 처리, client clock과 server clock의 경계를 맞춘다.

Tool·retry·fallback 시간은 task E2E causal span에 속하며 TPOT에는 넣지 않는다. Queue wait와 retry backoff도 독립 가산 항이 아니라 complete execution DAG의 node다. 모든 node가 직렬이고 node duration이 서로 겹치지 않는 exclusive interval인 trace에서만 각 duration을 단순 합산한다. Parallel branch나 nested span이 있으면 겹친 wall-clock을 이중 합산하지 않고 longest dependent path의 exclusive interval로 귀속한다. 실측 task E2E와 critical path duration의 차이는 누락 span 또는 clock 경계 문제로 기록하며, 그 차이를 queue·retry·fallback 추정치로 임의 보정하지 않는다.

C. Routing offline replay 절차
1. production query 200~500개를 개인정보 제거 후 sample한다.
2. 사람이 task 위험도와 cheap으로 충분한지 label한다.
3. cheap, mid, frontier 모델 결과를 같은 rubric과 출력 계약으로 비교한다.
4. threshold 후보별 confusion matrix, 비용, 품질, latency, escalation 비율을 계산한다.
5. task별 under-route 허용 기준과 전체 비용 상한을 동시에 만족하는 후보만 남긴다.
6. online에서는 route별 품질·비용·latency와 traffic mix drift를 계속 모니터링한다.

200~500개는 threshold 탐색을 시작하기 위한 작은 표본이지 production 안전성을 증명하는 보편 표본 수가 아니다. 희귀하지만 위험한 task가 1%라면 단순 무작위 200개에 해당 사례가 거의 없을 수 있다. 위험 class를 의도적으로 더 뽑고, 실제 traffic 비율로 다시 가중해 비용과 품질을 보고한다.

D. vLLM self-host 부하 실험의 version-dependent 명령 예시

이 실습의 목적은 특정 엔진의 최고 수치를 재현하는 것이 아니라 offered load와 동시성 설정이 goodput과 tail latency를 함께 바꾼다는 점을 직접 보는 것이다. 아래 vllm serve, vllm bench serve 명령과 flag는 vLLM의 공개 CLI를 사용하는 version-dependent 예시다. 설치한 버전에서 그대로 동작한다는 보편 계약이 아니며, 실행 전 해당 버전의 공식 문서와 vllm serve --help, vllm bench serve --help로 옵션을 확인한다. 이 문서는 실제 설치·실행을 요구하지 않는다.

Terminal window
vllm serve <model-id> \
--tensor-parallel-size 1 \
--max-num-seqs 256 \
--gpu-memory-utilization 0.90 \
--port 8000

서버를 띄운 뒤 외부 dataset 파일 없이 실험할 수 있도록 synthetic random dataset을 쓴다. --request-rate는 offered load를, --max-concurrency는 동시 실행 상한을 제어한다.

Terminal window
vllm bench serve \
--backend vllm \
--model <model-id> \
--base-url http://localhost:8000 \
--dataset-name random \
--random-input-len 1024 \
--random-output-len 128 \
--num-prompts 200 \
--request-rate 8 \
--max-concurrency 128

Offered load만 비교할 때는 --request-rate8, 32, 128로 바꾸고 --max-concurrency 128을 포함한 다른 변수는 고정한다. 동시성 cap 영향을 비교하는 별도 실험에서만 --max-concurrency를 바꾼다. 설정한 request rate와 서버에 실제로 도달한 offered load는 client concurrency cap이나 benchmark scheduler의 pacing 때문에 다를 수 있으므로 둘 다 기록한다. ShareGPT로 바꾼다면 --dataset-name sharegpt --dataset-path /absolute/path/to/sharegpt.json처럼 dataset path를 반드시 제공한다.

아래 fixture도 §14.3과 같은 관측 계약, 즉 W=[t0, C), C=t0+60s, D=C+15s, model request E2E <= 2s goodput SLO를 사용한다. 값은 위 명령의 실제 출력이 아니라 일관성을 손으로 검산하도록 의도적으로 만든 정수 count다. --num-prompts 200 명령만으로는 이 60초 cohort를 만들 수 없으므로 표를 재현했다고 주장할 수 없다. 실제 실험에서는 generator가 60초 이상 load를 제공하고 D까지 event log를 수집해야 한다. 설정 request rate가 32와 128 req/s여도 client concurrency cap과 pacing 때문에 admission boundary에서 측정한 offered load는 각각 24와 40 req/s에 그쳤다고 가정했다.

설정 rateNS_CF_CQ_CI_CS_DF_DG_D
8 req/s4804701364782454
32 req/s144010506020212812601801050
128 req/s2400112030085212812001200660

각 행에서 cutoff 동일식 N=S_C+F_C+Q_C+I_C, terminal 동일식 N=S_D+F_D, goodput 경계 G_D<=S_D가 성립한다. 다음 rate와 percentage는 이 count에서만 계산했다. 성공 요청은 모두 정확히 128 output token을 생성했다고 가정했으므로 output throughput도 S_D × 128 / 60s로 계산한다. TTFT·TPOT은 같은 가상 event fixture의 성공 timestamp를 nearest-rank로 읽었으며 괄호에 표본 index를 표시한다. GPU 관측은 request accounting에서 유도할 수 없으므로 숫자 비율 대신 별도의 정성 상태로만 남긴다.

설정 rate실측 offered load동시성 capSuccessful cohort rateCohort goodputOutput tok/sTTFT p95TTFT p99TPOT p95Terminal failure rateGPU 상태해석
8 req/s8.00 req/s1287.97 req/s7.57 req/s1,019.73180ms (#455/478)420ms (#474/478)32ms (#455/478)2 / 480 = 0.42%여유queue가 거의 없지만 GPU 여유가 큰 기준선
32 req/s24.00 req/s12821.00 req/s17.50 req/s2,688.00520ms (#1197/1260)1,600ms (#1248/1260)40ms (#1197/1260)180 / 1440 = 12.50%높음처리량은 늘지만 backlog·실패와 tail도 커진다
128 req/s40.00 req/s12820.00 req/s11.00 req/s2,560.002,800ms (#1140/1200)9,200ms (#1188/1200)67ms (#1140/1200)1200 / 2400 = 50.00%포화권성공량 이득은 없고 goodput·tail·실패는 악화한다

부하 측정에서는 설정 request rate, admission boundary의 실측 offered load, C의 네 상태 count, D의 terminal count, successful cohort rate, cohort goodput, input/output token throughput, queue depth, TTFT·TPOT·model request E2E p50/p95/p99, GPU utilization·memory, timeout·오류율을 함께 기록한다. C의 Q_CI_C를 D의 실패로 곧바로 간주하거나, 반대로 successful cohort rate에 넣어서는 안 된다. --max-num-seqs를 낮춘 비교도 유용하지만 한 번에 여러 knob를 바꾸지 않는다. 포화 판정은 cohort goodput 정체·하락, p95/p99 상승, terminal failure rate 증가가 함께 나타나는지를 본다.

E. Reasoning budget replay 절차와 기록표

Reasoning 모델이 실제로 이득인지 확인하려면 여러 모델 이름을 한 번씩 호출하는 대신 작은 수동 채점 task evaluation set에서 예산과 정답률의 곡선을 본다.

1. 사람이 채점할 수 있는 작업 20~50개를 고르고 위험 class를 표시한다.
2. 일반 모델 multi-step과 reasoning 모델을 같은 출력 계약으로 실행한다.
3. reasoning 예산 후보를 1k / 4k / 16k token처럼 세 단계로 둔다.
4. 정확도, 위험 실패, input/output/reasoning token, retry, 비용, task E2E를 기록한다.
5. 각 단계의 marginal cost per quality gain을 계산한다.
6. 비용이 가장 낮으면서 합격률과 위험 실패 기준을 만족하는 예산을 고른다.
후보정확도위험 실패율요청당 billed token요청당 비용p95 task E2E품질 1%p당 한계비용선택 판단
일반 모델 multi-step측정값측정값측정값측정값측정값기준선반복 호출의 누적 비용 확인
reasoning 1k측정값측정값측정값측정값측정값계산값작은 예산으로 충분한지 확인
reasoning 4k측정값측정값측정값측정값측정값계산값품질 향상 폭과 비용을 비교
reasoning 16k측정값측정값측정값측정값측정값계산값추가 token이 실제 오류를 줄이는지 확인

20~50개 역시 메커니즘을 익히는 최소 fixture다. 실제 배포 판단에서는 class별 신뢰구간과 회귀 위험을 볼 만큼 표본을 늘린다. 예산이 커져도 정확도와 위험 실패율이 더 좋아지지 않으면 긴 reasoning은 낭비다. 반대로 일반 모델의 retry와 critic loop가 누적돼 더 비싸고 느리다면 reasoning 단발이 더 단순한 경로가 될 수 있다.

개념 A개념 B차이점
Input priceOutput priceinput은 prefill, output은 decode라 비용 구조가 다르다
Context window실제 token budget창이 커도 비용·latency·lost-in-middle 때문에 모두 쓰지 않는다
TTFTModel request E2E첫 token 대기 vs 한 모델 요청의 전체 완료 시간
TPOT / ITLThroughput한 요청의 token 간 시간 vs 시스템 전체 처리량
p50p95 / p99보통 사용자 경험 vs tail 사용자 경험
SLOSLA내부 목표 vs 고객과의 약속
StreamingFaster generationstreaming은 체감 시작을 빠르게 할 뿐 생성 총량을 줄이지 않는다
Prompt caching응답 캐시반복 prefix 처리 절감 vs 모델 호출 자체 생략
Batch APIContinuous batching비동기 가격 절감 API vs serving engine의 throughput 최적화
RetryFallback같은 경로 재시도 vs 다른 모델·provider·region으로 전환
Cascade routingClassifier routingconfidence 기반 escalation vs 사전 분류
APISelf-hostper-token 구매 vs per-GPU-hour 운영
QuantizationDistillation같은 모델 정밀도 축소 vs 작은 모델 학습
Reasoning model일반 모델 multi-step내부 사고 token 단발 비용 vs 여러 호출 누적 비용

16. 체크리스트

  • input token, output token, reasoning token, context window를 비용 단위로 설명할 수 있다
  • prefill과 decode가 TTFT, TPOT, model request E2E에 어떻게 연결되는지 말할 수 있다
  • p50, p95, p99, SLO, SLA를 구분하고 평균 latency만 보면 안 되는 이유를 설명할 수 있다
  • rate limit을 RPM, TPM, concurrency로 나누고 retry amplification을 설명할 수 있다
  • prompt caching이 줄이는 항과 응답 캐시가 줄이는 항을 구분할 수 있다
  • Batch API, continuous batching, streaming이 각각 다른 문제를 푼다는 점을 설명할 수 있다
  • cascade, classifier, semantic routing의 선택 기준과 실패 신호를 말할 수 있다
  • fallback 도입 전에 schema, tool, streaming, safety 계약 테스트가 필요함을 안다
  • API와 self-hosting을 per-token 산식과 per-GPU-hour 산식으로 비교할 수 있다
  • cache miss explosion, stale routing, 429 retry storm, p99 latency 악화를 진단할 수 있다
  • 비용: input/output token, reasoning token, prompt caching, batch discount, tool cost, cost attribution
  • Latency: TTFT, TPOT, ITL, end-to-end latency, p50/p95/p99, queueing, tail latency
  • Rate limit: RPM, TPM, concurrency, Retry-After, backoff, jitter, circuit breaker
  • Routing: cascade, classifier router, semantic router, verifier, threshold, escalation rate
  • Fallback: provider failover, model fallback, region fallback, contract test, adapter
  • Serving: vLLM, TGI, SGLang, TensorRT-LLM, continuous batching, PagedAttention
  • Optimization: FlashAttention, speculative decoding, prefix caching, quantization, GQA/MQA, MoE
  • Observability: cost dashboard, cache hit ratio, token trace, route trace, schema violation rate
  1. LLM API 비용은 각 step의 input token × input 단가 + billed output token × output 단가 + tool 비용을 합산하고, reasoning token의 output usage 포함 여부를 확인해야 한다.
  2. Latency는 TTFT, TPOT, model request E2E, task E2E와 각각의 p50/p95/p99로 나눠야 병목이 prefill인지 decode인지 queue·외부 단계인지 보인다.
  3. Prompt caching, Batch API, model routing은 서로 다른 비용 항을 줄이므로 조합할 때 효과가 크다.
  4. Retry, rate limit, fallback은 안정성 도구지만 잘못 설계하면 비용·queue·schema 실패를 증폭한다.
  5. Self-hosting은 싸게 호출하는 기술이 아니라 per-token 구매를 per-GPU-hour 운영 책임으로 바꾸는 결정이다.

최종 수정: 2026-07-13