명시적 cache control
개발자가 어느 prefix를 캐시할지 표시한다. 캐시 단위를 의식적으로 설계할 수 있다.
긴 system prompt, 정책 문서, few-shot block처럼 고정 prefix가 명확할 때분류: Layer 12 - AI 시스템 & LLM 애플리케이션 | 선수지식: L12-10 (LLM API)
LLM 비용·지연 엔지니어링은 모델 품질만으로는 제품이 성립하지 않는 지점을 다룬다. 같은 답변 품질이라도 input/output token, context window, prefill/decode, streaming, caching, batching, retry, rate limit, queueing, model routing, fallback, self-hosting의 설계에 따라 호출당 단가와 p95/p99 지연이 크게 달라질 수 있다. 절감 배율은 모델, traffic, 계약 단가, 측정 시점에 의존하므로 이 문서에서는 보편 숫자보다 항을 분해해 계산하는 법을 다룬다.
이 문서의 목표는 가격표를 외우는 것이 아니다. 가격과 모델 이름은 빠르게 바뀐다. 대신 다음 질문에 답할 수 있어야 한다.
L12-10에서 LLM API는 messages -> tokenization -> prefill -> decode -> usage/error 관측 흐름으로 분해했다. 작은 prototype에서는 “좋은 모델을 고르고 prompt를 다듬는다”가 가장 중요해 보인다. production에서는 다른 문제가 먼저 커진다.
429 Too Many Requests, timeout, schema validation 실패를 무심코 retry하면 비용과 queue가 함께 커진다.따라서 이 토픽의 핵심 철학은 품질, 비용, 지연을 같은 요청 trace 안에서 함께 본다는 것이다. 모델 품질은 필요조건이다. 하지만 단가, SLO, retry, rate limit, fallback이 통제되지 않으면 좋은 모델도 제품을 불안정하게 만든다.
아래 가격 사례는 인용 출처와 문서 작성 시점에 묶인 point-in-time 예시로만 읽는다. 최신 가격 비교가 목적이 아니다.
단순 모델은 이렇게 생겼다.
월 비용 ~= 호출 수 × 호출당 평균 token × token 단가운영에서는 이 모델이 세 지점에서 깨진다.
이 세 지점에서 prompt caching, model routing, Batch API가 등장한다. 단가가 내려가도 이 구조를 모르면 같은 workload에서 절감 여지를 계속 놓친다.
| 용어 | 첫 정의 | 비용·지연과의 연결 |
|---|---|---|
| Token | 모델이 읽고 생성하는 최소 조각. 글자·단어와 1:1이 아니다 | 과금, context window, rate limit의 기본 단위 |
| Input token | system, history, user, RAG context, tool result처럼 모델이 읽는 token | prefill 비용과 TTFT를 키운다 |
| DAG | Directed Acyclic Graph(방향성 비순환 그래프). 실행 단계를 node, 선후 의존성을 edge로 표현하며 순환이 없다 | 병렬 branch가 있는 task의 실행 관계를 나타낸다 |
| Critical path | Task DAG의 시작에서 완료까지 이어지는 의존 경로 중 wall-clock이 가장 긴 경로. 다른 branch와 겹친 시간을 중복 합산하지 않는다 | 실측 task E2E를 어떤 실행 단계가 지배했는지 귀속한다 |
| E2E | End-to-End. 정한 시작 event부터 완료 event까지 실제로 흐른 wall-clock 시간. 이 문서는 model request E2E와 task E2E를 구분한다 | 부분 단계 합이 아니라 사용자가 겪은 전체 지연을 고정한다 |
| Output token | 모델이 새로 생성하는 assistant 응답 token | decode 비용과 model request E2E를 키운다 |
| Reasoning token | 일부 reasoning 모델이 답변 전 내부 사고에 쓰는 별도 token | 사용자에게 보이지 않아도 비용·한도에 잡힐 수 있다 |
| Context window | 한 호출에서 input과 예상 output을 합쳐 담을 수 있는 최대 token 창 | 길수록 비용, prefill 지연, lost-in-middle 위험이 커진다 |
| Prefill | input token 전체를 처리해 첫 output을 준비하는 단계 | 병렬화가 가능하지만 input 길이에 비례한다 |
| Decode | output token을 한 개씩 순차 생성하는 단계. 이때 KV(Key-Value) cache를 읽고 갱신한다 | 긴 답변의 model request E2E와 output 비용을 지배한다 |
| TTFT | Time To First Token, 요청 후 첫 token이 도착하기까지의 시간 | 사용자가 “응답이 시작됐다”고 느끼는 지표 |
| ITL / TPOT | ITL(Inter-Token Latency)은 인접 output token 사이 시간, TPOT(Time Per Output Token)는 보통 그 간격들의 평균 | 답변이 완성되는 속도를 보여준다. 도구마다 분모 관례가 다를 수 있다 |
| p50 / p95 / p99 | latency 분포의 50%, 95%, 99% 지점 | 평균이 숨기는 tail latency를 드러낸다 |
| SLO / SLA | SLO는 내부 목표, SLA는 고객과 약속한 서비스 수준 | p95 TTFT, p99 task E2E, error rate 같은 목표로 표현한다 |
| Queueing | provider 또는 자체 서버 앞에서 요청이 기다리는 현상 | 평균보다 p95/p99를 먼저 망가뜨린다 |
| Rate limit | provider가 RPM(Requests Per Minute), TPM(Tokens Per Minute), concurrency 등으로 요청량을 제한하는 정책 | HTTP 429, queue, retry 폭증의 원인이 된다 |
| Retry | 실패한 요청을 다시 시도하는 동작 | 성공률을 올리지만 비용·중복 실행·queue를 증폭할 수 있다 |
| Fallback | primary 모델·provider 실패 시 다른 경로로 전환하는 설계 | availability를 높이지만 계약 불일치와 품질 회귀를 만들 수 있다 |
| Prompt caching / prefix caching | 반복되는 prompt 앞부분의 처리 비용을 재사용하는 기능 | input 비용과 TTFT를 줄인다 |
| Batch API | 실시간 응답을 포기하고 요청 묶음을 비동기로 제출·회수하는 provider API | 지연 허용 작업의 가격·할당량 정책을 바꾼다 |
| Admission boundary | Load 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 set | production과 닮은 query를 사람이 cheap으로 충분/strong 필요와 위험도로 수동 표기한 routing 전용 evaluation set | 범용 gold dataset의 동의어가 아니며 route threshold 검증에 쓴다 |
| Serving batching | inference 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을 선택 심화로 돌아보면 된다.
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_s와 effective_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_out에 reasoning_tokens × price_reasoning을 다시 더하면 중복 계산이다. 반대로 reasoning이 output에서 제외된 별도 과금 항목인 계약이라면 provider usage·invoice의 단위와 단가를 그대로 따라야 한다. 핵심은 token 개수와 달러 비용을 더하지 않고, 각 token에 해당 단가를 곱한 비용 항목만 합산하는 것이다.
많은 provider에서 output token은 input token보다 비싸다. 정확한 배율은 모델·시점마다 바뀌지만, 원리는 안정적이다.
반례도 있다. 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 비율을 먼저 측정하는 일에서 시작한다.
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 tokenstop-k를 10에서 20으로 올리면 검색 근거가 두 배가 되는 것처럼 보이지만, input token과 prefill 지연도 거의 두 배로 늘 수 있다. 또한 긴 context의 중간에 중요한 근거가 묻히면 품질이 오히려 떨어질 수 있다. RAG에서는 top-k, chunk size, reranker, context packing을 비용·지연 변수로 같이 본다.
Retry는 실패 복구 도구지만, LLM에서는 비용 증폭기이기도 하다.
가상 workload 가정:base_cost_per_request = Cretry_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 tokensdynamic_suffix = 500 tokenscache_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가 일반 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을 넘는지 측정해 정한다.
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_i와 mean_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 300ms1.0s~1.5s 두 branch join 후 최종 조합 500ms
branch A path = 100 + 900 + 500 = 1,500msbranch B path = 100 + 400 + 300 + 500 = 1,300mscritical path = branch A path = task E2E = 1,500ms100 + 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은 반복되는 prompt prefix의 처리 결과 또는 token 비용을 재사용한다. 시스템 프롬프트, policy 문서, few-shot 예시, 변하지 않는 RAG context가 앞에 길게 붙는 앱에서 효과가 크다.
개발자가 어느 prefix를 캐시할지 표시한다. 캐시 단위를 의식적으로 설계할 수 있다.
긴 system prompt, 정책 문서, few-shot block처럼 고정 prefix가 명확할 때provider가 일정 길이 이상 반복 prefix를 자동 감지한다. 코드 변경은 적지만 hit 조건을 관측해야 한다.
동일 prefix가 충분히 길고 요청 형식이 안정적일 때동일 질문의 최종 답변을 저장한다. prompt caching과 달리 모델 호출 자체를 건너뛸 수 있다.
질문과 권한, freshness 조건이 같다는 것을 검증할 수 있을 때실패 신호는 세 가지다.
cache_read_tokens 또는 유사 지표가 갑자기 0에 가까워진다.Batch API는 여러 요청을 파일이나 queue로 모아 비동기 처리하고 나중에 결과를 받는 방식이다. provider마다 할인율과 처리 시간은 다르며 바뀔 수 있다. 중요한 원리는 실시간 요구가 없는 작업은 실시간 SLO로 처리하지 않는다는 것이다.
적합한 작업:
부적합한 작업:
Self-host에서는 continuous batching이 다른 층의 문제를 푼다. 여러 사용자의 활성 요청을 실행 중에도 합치고 빼면서 prefill·decode step을 함께 스케줄링해 GPU utilization을 높인다. Batch API처럼 결과를 나중에 받는 상품이 아니므로 대화형 요청에도 쓸 수 있지만, batch가 너무 커지면 개별 요청의 queueing이 늘어 p99가 악화될 수 있다.
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이 필요하다.
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 modelClassifier routing은 작은 모델이나 분류기가 query를 분류한다. Semantic routing은 query embedding을 카테고리 embedding과 비교한다. 둘 다 실패하면 잘못된 모델로 보내 품질이 떨어질 수 있으므로, route별 품질·비용·latency dashboard가 필요하다.
Fallback은 primary provider, model, region, self-host worker가 실패할 때 다른 경로로 전환하는 설계다. availability에는 도움이 되지만, “OpenAI 호환” 같은 표면 호환만 믿으면 실패한다.
Fallback 전에 확인할 계약:
좋은 fallback은 에러율을 낮추면서도 schema violation, tool argument failure, refusal rate, cost per request를 같이 본다. 장애 때만 켜지는 경로라면 synthetic traffic으로 정기 테스트해야 한다.
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를 후보로 올린다.
Self-host에서 자주 등장하는 엔진:
첫 독서에서는 아래 기법의 내부 kernel보다 어떤 병목을 줄이는가와 어떤 지표로 실패를 확인하는가만 잡으면 된다. FlashAttention은 attention 계산을 GPU의 빠른 on-chip memory에 맞게 block 단위로 재배치해, 계산 결과를 바꾸지 않으면서 느린 memory와의 읽기·쓰기를 줄이는 구현 기법이다. PagedAttention은 위에서 정의했듯 attention 수식 자체가 아니라 serving 중 KV cache memory 관리 문제를 푼다.
| 기법 | 줄이는 것 | 잘 맞는 조건 | 깨지는 조건 |
|---|---|---|---|
| FlashAttention 2/3 | attention 메모리·시간 | 긴 sequence, attention 병목 | 짧은 입력에서는 체감 효과가 작을 수 있음 |
| Continuous batching | GPU idle time, throughput | 동시 요청이 많고 serving queue가 있음 | 단발 요청, p99가 더 중요한 strict SLO |
| PagedAttention | KV cache 단편화 | 많은 동시 sequence와 긴 context | 작은 모델·낮은 concurrency에서는 이득 제한 |
| Speculative decoding | output 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 / MQA | GQA(Grouped-Query Attention)와 MQA(Multi-Query Attention)가 줄이는 KV cache 크기 | 모델 architecture가 지원 | serving에서 임의로 바꾸기 어려움 |
| MoE | MoE(Mixture of Experts)의 활성 parameter당 compute | router와 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_sReasoning 모델 single-step 비용 = C_1이 비교에서도 provider가 reasoning token을 billed_output_tokens_1 안에 포함해 보고하면 별도 reasoning 비용을 더하지 않는다. 어느 쪽이 싼지는 가격표의 예시 단가가 아니라, 같은 workload의 각 step에서 관측한 input·billed output·tool 비용을 넣어 비교한다.
선택 기준:
| 작업 유형 | 우선 후보 | 이유 |
|---|---|---|
| 단순 분류, 짧은 추출 | cheap 일반 모델 + schema | reasoning token이 대부분 낭비 |
| 긴 문서 요약 | 일반 모델 + context/caching 관리 | input과 output 길이가 비용을 지배 |
| 수학, 코드 디버깅, 복잡 계획 | reasoning 모델 또는 verified loop | step 수 감소가 높은 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 routing | cheap 모델이 routing labelled replay에서 충분한 품질을 냄 | 모든 query가 어려운 도메인 | escalation rate, route별 quality |
| Classifier routing | query 카테고리가 명확 | 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 batching | high concurrency | strict per-request latency SLO | throughput, queue wait, p99 |
| Quantization | 메모리 병목, 품질 손실 허용 | 정확도 critical | eval 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 난이도가 바뀌면 다시 보정한다.
상황: 사내 정책 챗봇 월 비용: $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, TTFT | prefix 2,000 tokens의 상당 부분 절감 | user별 권한 문서는 dynamic suffix로 분리 |
| Direct/classifier routing | 모델 단가 | 호출 전에 요청당 모델 하나를 선택 | route별 품질 eval 없으면 품질 회귀 |
| Batch API | 비실시간 token 가격 | eval/요약성 작업을 할인 경로로 이동 | 사용자 대화에는 부적합 |
| Output schema 축소 | output token, parse retry | 짧고 안정적인 답변 | 설명형 답변에는 너무 빡빡할 수 있음 |
| Self-host | per-token -> per-GPU-hour | 대량 고정 workload에서 비용 절감 가능 | 현재 호출량과 운영 부담을 감안하면 보류 |
| Provider fallback | queue, 장애, rate limit | p95/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,740Direct/classifier routing 후 = 3,740 × 0.40 = 1,496Batch 후 = 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가 좋아졌다고 결론 내리면 안 된다.
-캐시 항이다.자원 × 가격 항을 직접 낮춘다.per-token에서 per-GPU-hour로 바꾼다.중요한 것은 결과 숫자보다 어떤 항이 줄었는지 설명 가능해야 한다는 점이다.
“provider prompt caching을 켰고, 같은 prefix 문자열을 Redis에 한 번 더 저장하자”는 제안은 겉보기에는 절감처럼 보인다. 하지만 gateway가 Redis에서 문자열을 읽어 다시 prompt에 붙이는 것만으로는 모델의 prefill을 건너뛰지 못하고, provider에 보내는 input token이 사라지지도 않으므로 token 비용을 줄이지 못한다.
세 경계를 구분한다.
반대로 “비실시간 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 storm | 429 비율, 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별로 나눠 본다.
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 = Ravg_input = Iavg_output = Oprice_in = P_iprice_out = P_oavg_tool_cost = T
api_cost = R × (I × P_i + O × P_o + T)
gpu_hour_price = Gmonthly_gpu_hours = Hops_cost = Opsengineering_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를 나눠 산정한다.아래 숫자는 어떤 provider나 GPU의 최신 가격이 아니라 산식 연습용 가정이다. 계약 단가와 측정한 replica capacity를 넣어 같은 표를 다시 만든다.
월 요청 R = 10,000,000평균 input I = 1,500 tokens평균 output O = 600 tokensAPI input price P_i = $0.60 / M tokensAPI 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.0027API 월비용 = 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) = 12GPU 비용 = 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가 현실적이다.
LLM 비용·지연은 특수해 보이지만, 구조는 일반 분산 시스템과 닮았다.
| LLM 구성요소 | 일반 시스템 매핑 | 공통 질문 |
|---|---|---|
| Input vs Output 비대칭 | read/write 비용 차이, 압축/해제 비용 | 어느 방향이 병목인가? |
| Prefill vs Decode | batch processing vs sequential pipeline | 병렬화 가능한 단계와 순차 단계가 무엇인가? |
| Provider prompt / self-host KV reuse | serving 계층의 prefix prefill 재사용 | 실제 prefill이 줄었는가? 과금 cache usage가 보이는가? |
| 앱 최종 응답 캐시 | HTTP cache, CDN, Redis | 모델 호출을 생략해도 권한·freshness가 맞는가? |
| Batch API | off-peak batch, spot instance | 시간을 내주고 단가를 낮출 수 있는가? |
| Routing | hot/cold tier, query planner | 쉬운 요청과 어려운 요청을 분리할 수 있는가? |
| Fallback | multi-region failover | 대체 경로의 계약이 같은가? |
| Self-host | build vs buy, on-prem vs cloud | 고정비와 운영 책임을 감당할 만큼 큰가? |
| Continuous batching | request coalescing, queue-based worker | throughput 이득이 p99 악화보다 큰가? |
| Quantization | compression, 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이 순차 생성되며, 모델 응답이 비결정적이라는 점이다.
이 절의 목적은 특정 SDK나 serving engine을 설치하는 데 있지 않다. 한 변수만 바꾸고 비용·품질·지연 분포가 어떻게 이동하는지 해석하는 것이 목적이다. 첫 회독에서는 아래 실험 계약과 해석 자산만 읽는다. 실행 코드와 환경 종속 명령은 절 끝의 선택 부록에 있으므로 펼치지 않아도 본문 결론을 이해할 수 있다.
| 실험 | 확인할 질문 | 통제할 입력 변수 | 관측 지표 | 예상 해석 | 반례·중단 기준 |
|---|---|---|---|---|---|
| Prompt cache | 반복 prefix가 실제 비용과 prefill을 줄이는가? | prefix 내용·길이, TTL, 요청률, cache write/hit 단가, 동적 suffix | eligible 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 load | offered 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 버전, 입력/출력 길이 분포, 측정 시각을 함께 기록해야 비교 가능한 데이터가 된다.
“hit ratio가 80%“라는 말만으로는 절감 효과를 계산할 수 없다. 요청 수를 분모로 삼았는지, cache 사용 자격이 있는 요청만 셌는지, token 양을 기준으로 삼았는지에 따라 같은 80%의 의미가 달라진다.
| 지표 | 산식 | 답하는 질문 | 놓치기 쉬운 경계 |
|---|---|---|---|
| Request hit ratio | cache_hit_requests / eligible_requests | 캐시 가능한 요청 중 몇 건이 hit했는가? | 전체 요청을 분모로 쓰면 cache 비대상 route가 비율을 희석한다 |
| Token reuse ratio | cache_read_tokens / (cache_read_tokens + cache_creation_tokens) | 캐시로 관리한 prefix token 중 어느 정도를 재사용했는가? | provider usage가 read·creation을 같은 의미로 보고하는지 확인해야 한다 |
| Workload coverage | eligible_requests / all_requests | 전체 traffic 중 캐시 설계가 적용되는 범위는 얼마인가? | hit ratio가 높아도 coverage가 5%면 전체 비용 효과는 작다 |
| Realized input saving | (uncached_baseline_cost - observed_input_cost) / uncached_baseline_cost | write 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 수에 건다.
Latency 실험은 하나의 latency_ms 열로 끝내지 않는다. TTFT는 request_started_at -> first_token_at, TPOT는 first_token_at -> last_token_at의 첫 token 이후 decode 간격 평균이다. Model request E2E는 request_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 E2E와 task 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 miss | network 구간만 느려졌거나 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 load | Admission boundary에 도착한 전체 요청 수 N / 60s |
| Successful cohort rate | Terminal 관찰 시점까지 성공 완료한 요청 수 S_D / 60s |
| Cohort goodput | 성공 완료했고 model request E2E <= 2s인 요청 수 G_D / 60s |
| Terminal failure rate | Terminal 관찰 시점까지 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 / 60ssuccessful_cohort_rate = S_D / 60scohort_goodput = G_D / 60sterminal_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를 만족한 수다.
| 가상 설정 | N | S_C | F_C | Q_C | I_C | S_D | F_D | G_D |
|---|---|---|---|---|---|---|---|---|
| 8 req/s | 480 | 472 | 0 | 4 | 4 | 480 | 0 | 474 |
| 16 req/s | 960 | 934 | 1 | 10 | 15 | 958 | 2 | 924 |
| 19 req/s | 1140 | 1080 | 8 | 30 | 22 | 1128 | 12 | 846 |
| 22 req/s | 1320 | 1090 | 60 | 110 | 60 | 1208 | 112 | 522 |
다음 rate와 percentage는 모두 위 정수에서 계산했다. TTFT 값은 같은 가상 fixture의 S_D개 성공 요청 timestamp를 정렬한 뒤 nearest-rank 관례 rank_p=ceil(p×S_D)로 읽는다. 따라서 괄호의 #은 정렬된 성공 표본의 1-based index다. 값 자체는 특정 엔진 성능 주장이 아니다.
| 가상 설정 | Offered load | Successful cohort rate | Cohort goodput | TTFT p95 | TTFT p99 | Terminal failure rate | 판단 |
|---|---|---|---|---|---|---|---|
| 8 req/s | 8.00 req/s | 8.00 req/s | 7.90 req/s | 320ms (#456/480) | 480ms (#476/480) | 0 / 480 = 0.00% | 여유가 크고 burst를 흡수할 수 있는 기준선 |
| 16 req/s | 16.00 req/s | 15.97 req/s | 15.40 req/s | 760ms (#911/958) | 1,400ms (#949/958) | 2 / 960 = 0.21% | 평균은 작게 변해도 tail이 먼저 악화 |
| 19 req/s | 19.00 req/s | 18.80 req/s | 14.10 req/s | 2,100ms (#1072/1128) | 6,800ms (#1117/1128) | 12 / 1140 = 1.05% | 성공량은 늘었지만 SLO 내 goodput은 하락한 포화권 |
| 22 req/s | 22.00 req/s | 20.13 req/s | 8.70 req/s | 5,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이다.
평균 요청 비용만 비교하면 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 only | 78% | $0.002 | 기준선 | 기준선 | 기준선 |
| Routed cheap + frontier | 86% | $0.004 | +8%p | +$0.002 | $0.00025 |
| Reasoning 4k | 89% | $0.012 | +3%p | +$0.008 | 약 $0.00267 |
| Reasoning 16k | 90% | $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를 같은 행에 추가해야 한다.
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_routerdirect_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_cheapcascade_saving = strong_only_baseline - cascade_total_costN=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로 단순화하는 편이 낫다.
§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 factor | Replica당 유효 capacity | 필요 replicas | 월 GPU 비용 | 운영비 포함 월비용 | $27,000 API 가상 비용 대비 |
|---|---|---|---|---|---|
| 100% | 400 tok/s | 12 | $21,900 | $25,400 | $1,600 절감 |
| 85% | 340 tok/s | 14 | $25,550 | $29,050 | $2,050 초과 |
| 70% | 280 tok/s | 17 | $31,025 | $34,525 | $7,525 초과 |
| 50% | 200 tok/s | 24 | $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 정수 단위, 운영비를 함께 넣어 민감도 곡선을 만든다.
아래 코드는 특정 provider SDK에 묶이지 않는다. cache_read_tokens와 cache_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 = 300window = 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 Nonerecord_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.5record_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) == 2assert token_reuse_ratio_5min(300) == 0.5assert 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_200assert token_reuse_ratio_5min(300) == 0.5print("cache fixture: PASS")Provider usage 필드명, cache 최소 길이, TTL, write/hit 단가는 provider와 SDK 버전에 의존한다. 실제 adapter에서는 공식 문서의 usage 정의를 먼저 매핑하고, request hit ratio와 workload coverage는 별도 counter로 수집한다.
같은 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_at과 request_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 추정치로 임의 보정하지 않는다.
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 비율로 다시 가중해 비용과 품질을 보고한다.
이 실습의 목적은 특정 엔진의 최고 수치를 재현하는 것이 아니라 offered load와 동시성 설정이 goodput과 tail latency를 함께 바꾼다는 점을 직접 보는 것이다. 아래 vllm serve, vllm bench serve 명령과 flag는 vLLM의 공개 CLI를 사용하는 version-dependent 예시다. 설치한 버전에서 그대로 동작한다는 보편 계약이 아니며, 실행 전 해당 버전의 공식 문서와 vllm serve --help, vllm bench serve --help로 옵션을 확인한다. 이 문서는 실제 설치·실행을 요구하지 않는다.
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는 동시 실행 상한을 제어한다.
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 128Offered load만 비교할 때는 --request-rate를 8, 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에 그쳤다고 가정했다.
| 설정 rate | N | S_C | F_C | Q_C | I_C | S_D | F_D | G_D |
|---|---|---|---|---|---|---|---|---|
| 8 req/s | 480 | 470 | 1 | 3 | 6 | 478 | 2 | 454 |
| 32 req/s | 1440 | 1050 | 60 | 202 | 128 | 1260 | 180 | 1050 |
| 128 req/s | 2400 | 1120 | 300 | 852 | 128 | 1200 | 1200 | 660 |
각 행에서 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 | 동시성 cap | Successful cohort rate | Cohort goodput | Output tok/s | TTFT p95 | TTFT p99 | TPOT p95 | Terminal failure rate | GPU 상태 | 해석 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 8 req/s | 8.00 req/s | 128 | 7.97 req/s | 7.57 req/s | 1,019.73 | 180ms (#455/478) | 420ms (#474/478) | 32ms (#455/478) | 2 / 480 = 0.42% | 여유 | queue가 거의 없지만 GPU 여유가 큰 기준선 |
| 32 req/s | 24.00 req/s | 128 | 21.00 req/s | 17.50 req/s | 2,688.00 | 520ms (#1197/1260) | 1,600ms (#1248/1260) | 40ms (#1197/1260) | 180 / 1440 = 12.50% | 높음 | 처리량은 늘지만 backlog·실패와 tail도 커진다 |
| 128 req/s | 40.00 req/s | 128 | 20.00 req/s | 11.00 req/s | 2,560.00 | 2,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_C와 I_C를 D의 실패로 곧바로 간주하거나, 반대로 successful cohort rate에 넣어서는 안 된다. --max-num-seqs를 낮춘 비교도 유용하지만 한 번에 여러 knob를 바꾸지 않는다. 포화 판정은 cohort goodput 정체·하락, p95/p99 상승, terminal failure rate 증가가 함께 나타나는지를 본다.
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 price | Output price | input은 prefill, output은 decode라 비용 구조가 다르다 |
| Context window | 실제 token budget | 창이 커도 비용·latency·lost-in-middle 때문에 모두 쓰지 않는다 |
| TTFT | Model request E2E | 첫 token 대기 vs 한 모델 요청의 전체 완료 시간 |
| TPOT / ITL | Throughput | 한 요청의 token 간 시간 vs 시스템 전체 처리량 |
| p50 | p95 / p99 | 보통 사용자 경험 vs tail 사용자 경험 |
| SLO | SLA | 내부 목표 vs 고객과의 약속 |
| Streaming | Faster generation | streaming은 체감 시작을 빠르게 할 뿐 생성 총량을 줄이지 않는다 |
| Prompt caching | 응답 캐시 | 반복 prefix 처리 절감 vs 모델 호출 자체 생략 |
| Batch API | Continuous batching | 비동기 가격 절감 API vs serving engine의 throughput 최적화 |
| Retry | Fallback | 같은 경로 재시도 vs 다른 모델·provider·region으로 전환 |
| Cascade routing | Classifier routing | confidence 기반 escalation vs 사전 분류 |
| API | Self-host | per-token 구매 vs per-GPU-hour 운영 |
| Quantization | Distillation | 같은 모델 정밀도 축소 vs 작은 모델 학습 |
| Reasoning model | 일반 모델 multi-step | 내부 사고 token 단발 비용 vs 여러 호출 누적 비용 |
input token × input 단가 + billed output token × output 단가 + tool 비용을 합산하고, reasoning token의 output usage 포함 여부를 확인해야 한다.최종 수정: 2026-07-13