Skip to content

Split View: vLLM 내부 구조 (6) — 컨텍스트 윈도우, max_model_len, max_tokens 완전 정리

✨ Learn with Quiz
|

vLLM 내부 구조 (6) — 컨텍스트 윈도우, max_model_len, max_tokens 완전 정리

왜 이 다섯 개가 헷갈리는가

vLLM을 운영하면서 가장 많이 나오는 질문은 성능이 아니라 길이입니다. 컨텍스트 윈도우가 128k라는 모델을 올렸는데 왜 8192에서 막히는지, max_tokens를 4000으로 줬는데 왜 거부되는지, max_num_batched_tokens는 또 무엇인지.

헷갈리는 이유는 분명합니다. 이름이 전부 비슷하고, 서로 다른 곳에서 정해지며, 넘었을 때 나타나는 증상이 제각각이기 때문입니다. 어떤 것은 요청이 거부되고, 어떤 것은 조용히 응답이 잘리고, 어떤 것은 서버가 아예 안 뜹니다.

이 글은 그 다섯 개를 한 번에 정리합니다. 순서는 이렇습니다. 먼저 비교표로 전체 지도를 잡고, 각각을 설명한 뒤, 실제로 자주 겪는 세 가지 시나리오를 숫자로 따라가 보고, 마지막에 에러 메시지로 원인을 역추적하는 방법을 정리합니다.

내용은 2026-08-12에 공식 문서·소스에서 확인했습니다. vLLM은 변화가 빠르니 설정값과 동작은 사용 중인 버전의 문서로 다시 확인하세요.

한눈에 보는 비교표

이름정하는 곳제한하는 대상넘었을 때 생기는 일
컨텍스트 윈도우모델 자체 (모델 설정 파일)모델이 한 번에 볼 수 있는 토큰 수이보다 큰 엔진 설정을 요구하면 기동 단계에서 문제가 됩니다
max_model_len엔진 기동 인자요청 하나의 입력과 출력을 합친 길이요청이 거부됩니다
max_tokens (또는 max_completion_tokens)요청 본문그 요청이 새로 생성할 토큰 수생성이 그 지점에서 중단됩니다
max_num_batched_tokens엔진 기동 인자한 스텝에 배치 전체가 처리할 토큰 수요청이 여러 스텝으로 나뉘거나 밀립니다
max_num_seqs엔진 기동 인자한 스텝에 동시에 담을 시퀀스 수대기 큐에서 기다립니다

표에서 가장 중요한 줄은 두 번째입니다. max_model_len이 제한하는 것은 입력이 아니라 입력과 출력의 합계입니다. 공식 엔진 인자 문서가 이 값을 프롬프트와 출력을 합친 모델 컨텍스트 길이라고 명시하고 있습니다. 길이 관련 오해의 절반은 여기서 나옵니다.

컨텍스트 윈도우와 max_model_len

두 개는 층이 다릅니다.

컨텍스트 윈도우는 모델의 성질입니다. 학습 방식에서 나온 구조적 한계라서 설정 파일을 고쳐 늘릴 수 있는 성질이 아닙니다. 모델 카드에 128k라고 적힌 그 숫자입니다.

max_model_len은 엔진의 설정입니다. 공식 문서에 따르면 이 값을 지정하지 않으면 모델 설정에서 자동으로 유도됩니다. 즉 아무것도 안 주면 대개 모델의 컨텍스트 윈도우를 따라갑니다. 하지만 명시하면 그 값이 실제 한도가 됩니다.

여기서 첫 번째 오해가 풀립니다. 모델이 128k를 지원한다고 해서 여러분의 서버가 128k를 받는 것이 아닙니다. 받는 것은 max_model_len입니다. 그리고 이 값을 낮춰 두는 것은 실수가 아니라 대개 의도된 선택입니다. 이유는 뒤에서 다룹니다.

기본값을 여기서 숫자로 적지 않는 이유도 밝혀 둡니다. 공식 문서는 이 인자에 고정된 기본값을 제시하지 않고 모델 설정에서 유도된다고만 설명합니다. 그러니 여러분의 배포에서 실제로 몇으로 잡혔는지는 기동 로그로 확인하세요.

max_tokens와 이름이 바뀐 이야기

max_tokens는 요청 본문에 넣는 값이고, 그 요청이 새로 생성할 토큰 수의 상한입니다. 입력 길이는 여기 안 들어갑니다. 그래서 이 값 자체는 "얼마나 길게 답할지"를 정할 뿐, 얼마나 긴 질문을 받을지와는 무관합니다.

여기서 최신 정보를 하나 짚고 갑니다. main 브랜치의 채팅 완성 요청 정의를 보면 max_tokens 필드에 사용 중단 표시가 붙어 있고, max_completion_tokens를 쓰라는 안내가 달려 있습니다. 그리고 두 값이 모두 들어오면 max_completion_tokens가 우선합니다. OpenAI 호환 API의 흐름을 따라간 변화입니다. 새로 코드를 쓴다면 max_completion_tokens 쪽을 쓰는 편이 낫고, 기존 코드가 max_tokens를 쓰고 있다면 당장 깨지지는 않지만 언젠가 정리해야 할 부채로 두면 됩니다.

이 값을 아예 안 주면 어떻게 될까요. 남은 여유만큼 생성하다가 모델이 끝맺거나 한도에 닿을 때까지 갑니다. 서비스 관점에서는 상한을 명시하는 편이 거의 언제나 낫습니다. 상한이 없으면 사용자 한 명이 KV 캐시를 오래 붙들고 있게 되고, 그것이 4편에서 본 선점으로 이어집니다.

max_num_batched_tokens와 max_num_seqs

이 둘은 이름 때문에 길이 한도처럼 보이지만 성격이 완전히 다릅니다. 요청 하나에 대한 제한이 아니라 한 스텝에 엔진이 처리할 작업량에 대한 제한입니다.

max_num_batched_tokens는 이번 스텝에 배치 전체가 처리할 토큰 수의 상한입니다. 요청 하나가 이 값을 넘는다고 거부되지 않습니다. 청크드 프리필이 켜져 있으면 그냥 여러 스텝에 나뉘어 처리됩니다.

max_num_seqs는 한 스텝에 동시에 담을 시퀀스 수의 상한입니다. 이걸 넘으면 대기할 뿐 실패하지 않습니다.

main 브랜치 SchedulerConfig에는 각각의 클래스 기본값이 2048과 128로 선언되어 있습니다. 다만 실제 적용값은 사용 맥락과 환경에 따라 조정될 수 있으니 기동 로그를 확인하세요.

이 둘이 길이 문제와 만나는 지점이 딱 하나 있습니다. 공식 최적화 문서는 청크드 프리필을 껐을 경우 max_num_batched_tokensmax_model_len보다 커야 한다고 명시합니다. 나눠 처리할 수 없는데 최대 길이 프롬프트를 한 스텝에 담을 수 없다면 방법이 없기 때문입니다.

시나리오 1 — 8k 모델에 6k 입력과 max_tokens 4000

가장 흔한 경우부터 봅니다. 컨텍스트 8192인 모델을 올렸고, 사용자가 6000토큰짜리 문서를 붙여 넣으면서 max_tokens를 4000으로 줬습니다.

max_model_len = 8192  (입력 + 출력이 함께 쓰는 예산)

입력  6000 ██████████████████████████████
출력  4000 ████████████████████
합계 10000 ──────────────────────────────────────────  8192 초과

결과: 생성이 시작되기도 전에 요청이 거부됩니다.

여기서 초심자가 가장 많이 하는 예상은 "입력 6000이 8192보다 작으니 통과하고, 답변이 2192토큰쯤에서 잘리겠지"입니다. 그렇지 않습니다. vLLM은 요청을 받는 시점에 입력과 max_tokens를 더해서 검사하고, 넘으면 거부합니다.

실제로 나오는 메시지는 이런 형태입니다. 공개된 vLLM 이슈에 보고된 원문을 그대로 옮깁니다.

This model's maximum context length is 16384 tokens. However, you requested
122946 tokens (112946 in the messages, 10000 in the completion).
Please reduce the length of the messages or completion.

메시지를 뜯어보면 구조가 그대로 보입니다. 괄호 안이 입력과 출력으로 나뉘어 있고, 둘의 합이 앞의 한도와 비교됩니다. 이 한 줄이 "입력과 출력은 같은 예산을 나눠 쓴다"는 사실의 가장 확실한 증거입니다.

그래서 해법도 셋 중 하나입니다. 입력을 줄이거나, max_tokens를 줄이거나, max_model_len을 늘리는 것입니다. 앞의 둘은 요청 쪽 수정이고 마지막은 서버 재기동이 필요합니다. 참고로 자동으로 잘라 주지 않고 거부하는 이 동작에 대해서는 vLLM 저장소에 개선 요청 이슈가 올라와 있습니다. 즉 버전에 따라 달라질 수 있는 영역이니 사용 중인 버전에서 직접 확인하세요.

실전 팁 하나. 애플리케이션에서 max_tokens를 상수로 박아 두면 긴 입력이 들어올 때마다 이 에러를 맞습니다. 입력 토큰 수를 센 뒤 남은 여유에서 안전 여백을 뺀 값으로 계산해 넣는 편이 안정적입니다.

시나리오 2 — 128k 모델인데 왜 8192에서 막히는가

모델 카드에는 128k라고 적혀 있는데 서버가 훨씬 작은 값에서 요청을 거부합니다. 확인 순서는 이렇습니다.

첫째, 기동 명령에 --max-model-len을 직접 준 적이 있는지 봅니다. 줬다면 그 값이 답입니다. 모델 능력과 무관하게 엔진이 정한 한도가 우선합니다.

둘째, 안 줬다면 엔진이 모델 설정에서 유도한 값을 봅니다. 공식 문서가 밝힌 대로 지정하지 않으면 모델 설정에서 자동 유도되는데, 이 값이 모델 카드의 홍보 문구와 다를 수 있습니다. 확장 기법으로 긴 컨텍스트를 지원하는 모델은 설정 파일에 더 작은 기본 창이 적혀 있기도 합니다.

셋째, 에러 메시지가 말하는 숫자를 믿습니다. 앞의 메시지에서 "maximum context length is" 뒤에 나오는 값이 지금 이 엔진의 실효 한도입니다. 모델 카드보다 이 숫자가 사실입니다.

넷째, 그래도 128k가 필요하다면 KV 캐시를 감당할 수 있는지부터 계산합니다. 그리고 이것이 세 번째 시나리오로 이어집니다.

시나리오 3 — 요청 이전에 기동에서 막히는 경우

--max-model-len을 크게 줬더니 서버가 아예 안 뜨는 경우입니다. vLLM 이슈에 보고된 원문 형태는 이렇습니다.

ValueError: The model's max seq len (4096) is larger than the maximum number
of tokens that can be stored in KV cache (3664). Try increasing
gpu_memory_utilization or decreasing max_model_len when initializing the engine.

이 메시지가 이 글에서 가장 중요할지도 모릅니다. 왜냐하면 여기에 진짜 구조가 드러나기 때문입니다. 엔진은 기동할 때 가중치를 올리고 남은 메모리로 KV 캐시를 잡습니다. 그리고 그 캐시에 토큰 몇 개를 담을 수 있는지 계산합니다. 만약 그 수가 max_model_len보다 작으면, 최대 길이 요청 하나조차 완주시킬 수 없다는 뜻이라 기동을 중단합니다.

즉 길이 한도의 최종 결정권자는 모델도 설정도 아니고 메모리입니다.

진짜 한계를 정하는 것은 KV 캐시 메모리

앞의 시나리오를 일반화하면 이렇게 됩니다.

GPU 전체 메모리
  └─ gpu_memory_utilization 비율만큼을 vLLM이 사용
       ├─ 모델 가중치            (모델 크기와 양자화가 결정)
       ├─ 활성화 및 각종 오버헤드
       └─ 남은 전부 = KV 캐시    ← 여기가 길이와 동시성을 함께 먹는다

KV 캐시는 두 가지 요구를 동시에 받습니다. 하나는 길이입니다. 요청 하나가 길수록 블록을 많이 씁니다. 다른 하나는 동시성입니다. 요청이 많을수록 블록을 많이 씁니다. 그래서 max_model_len을 두 배로 올리면 같은 메모리로 살릴 수 있는 동시 요청 수가 대략 절반이 됩니다.

이 관계가 실무에서 이렇게 나타납니다. 128k를 열어 두면 실제로 128k를 쓰는 사용자가 없어도 동시 처리 능력이 줄어듭니다. 엔진이 최악의 경우를 감당할 수 있어야 기동하기 때문입니다. 그래서 대부분의 프로덕션 배포는 모델이 지원하는 최대치가 아니라 실제 워크로드의 상위 백분위에 맞춰 max_model_len을 잡습니다. 앞에서 이 값을 낮추는 것이 실수가 아니라고 한 이유입니다.

에러 메시지로 원인 찾기

마지막으로 증상에서 거꾸로 찾아가는 표입니다.

증상가장 유력한 원인먼저 확인할 것
요청이 400 계열로 거부되고 메시지에 messages와 completion 숫자가 보임입력과 max_tokens의 합이 max_model_len 초과요청의 max_tokens 계산 방식
서버 기동 중 KV 캐시에 담을 수 있는 토큰 수가 부족하다는 에러max_model_len이 가용 메모리에 비해 큼--gpu-memory-utilization--max-model-len
답변이 문장 중간에서 갑자기 끊김요청의 생성 상한에 도달응답의 종료 사유 필드
모델 카드보다 작은 값에서 막힘엔진이 유도했거나 지정된 max_model_len기동 로그의 실제 적용값
동시 요청이 늘면 급격히 느려짐길이 한도가 아니라 KV 캐시 부족과 선점선점 경고 로그 (4편 참고)

정리하면 순서는 하나입니다. 먼저 에러 메시지가 요청 단계에서 났는지 기동 단계에서 났는지 구분하고, 요청 단계면 입력과 생성 상한의 합을 보고, 기동 단계면 메모리와 max_model_len의 관계를 봅니다. 이 두 갈래만 구분해도 길이 문제의 대부분은 십 분 안에 끝납니다.

직접 해보기

참고 자료

Inside vLLM (6) — Context Window vs max_model_len vs max_tokens, Fully Explained

Why These Five Get Confused

The most common question when running vLLM in production is not about performance — it is about length. Why does a model with a 128k context window get blocked at 8192? Why does a request with max_tokens set to 4000 get rejected? And what exactly is max_num_batched_tokens?

The reason for the confusion is clear. The names all sound similar, they get set in different places, and the symptom when you exceed each one is different. Exceeding one gets the request rejected, exceeding another quietly truncates the response, and exceeding a third keeps the server from starting at all.

This post sorts out all five at once. Here is the order: first a comparison table to get the whole map, then an explanation of each one, then three scenarios you actually run into, worked through with real numbers, and finally a method for tracing an error message back to its cause.

Everything here was verified against the official documentation and source on 2026-08-12. vLLM moves fast, so re-check settings and behavior against the documentation for the version you are running.

The Comparison Table at a Glance

NameSet whereWhat it limitsWhat happens when exceeded
Context windowThe model itself (the model config file)The number of tokens the model can see at onceAsking the engine to configure something larger than this causes a problem at startup
max_model_lenEngine startup argumentThe combined length of one request's input and outputThe request is rejected
max_tokens (or max_completion_tokens)Request bodyThe number of tokens that request will newly generateGeneration stops at that point
max_num_batched_tokensEngine startup argumentThe number of tokens the whole batch processes in one stepThe request gets split across multiple steps, or queued
max_num_seqsEngine startup argumentThe number of sequences that can be packed in at once, in one stepIt waits in the queue

The most important row in the table is the second one. What max_model_len limits is not the input — it is the sum of input and output. The official engine arguments documentation states explicitly that this value is the model context length combining the prompt and the output. Half of all the confusion about length comes from this one point.

Context Window and max_model_len

The two live at different layers.

The context window is a property of the model. It is a structural limit that comes from how the model was trained, so it is not something you can extend by editing a config file. It is the number written as 128k on the model card.

max_model_len is a setting on the engine. According to the official documentation, if you do not specify this value it is derived automatically from the model config. In other words, if you give it nothing, it generally follows the model's context window. But if you specify it, that value becomes the actual limit.

This clears up the first misconception. The fact that a model supports 128k does not mean your server accepts 128k. What your server accepts is max_model_len. And keeping this value lower than the model's maximum is usually not a mistake — it is usually a deliberate choice. The reason is covered later.

Here is also why this post does not write down a default value as a number. The official documentation does not give this argument a fixed default — it only explains that the value is derived from the model config. So check your own startup log to see what number your deployment actually landed on.

max_tokens and the Renaming Story

max_tokens is a value you put in the request body, and it is the upper bound on the number of tokens that request will newly generate. Input length does not count toward it. So this value on its own only sets how long the answer can be — it has nothing to do with how long a question it can accept.

Here is one piece of current information worth noting. In the chat completion request definition on the main branch, the max_tokens field is marked deprecated, with guidance to use max_completion_tokens instead. And if both values are supplied, max_completion_tokens takes priority. This change follows the direction the OpenAI-compatible API has moved in. If you are writing new code, it is better to use max_completion_tokens. If existing code still uses max_tokens, it will not break right away, but you can treat it as debt to clean up eventually.

What happens if you do not supply this value at all? Generation continues using whatever budget remains, until the model ends the response on its own or hits the limit. From a service standpoint, specifying an upper bound is almost always better. Without one, a single user can end up holding the KV cache for a long time, and that leads to the preemption covered in part 4.

max_num_batched_tokens and max_num_seqs

These two look like length limits because of their names, but they are a completely different kind of thing. They are not a limit on any single request — they are a limit on how much work the engine processes in one step.

max_num_batched_tokens is the upper bound on how many tokens the whole batch processes in this step. A single request is not rejected for exceeding this value. If chunked prefill is on, it is simply split across multiple steps.

max_num_seqs is the upper bound on how many sequences can be packed in at once, in one step. Exceeding it means waiting, not failing.

In SchedulerConfig on the main branch, the class defaults are declared as 2048 and 128 respectively. That said, the value actually applied can be adjusted based on usage context and environment, so check your startup log.

There is exactly one point where these two intersect with the length question. The official optimization documentation states that when chunked prefill is off, max_num_batched_tokens must be larger than max_model_len. The reasoning: if the engine cannot split the work across steps, and a maximum-length prompt cannot fit into one step, there is no way forward.

Scenario 1 — An 8k Model With 6k Input and max_tokens 4000

Start with the most common case. You have a model deployed with a context of 8192, and a user pastes in a 6000-token document while setting max_tokens to 4000.

max_model_len = 8192  (the budget input + output share)

input   6000 ██████████████████████████████
output  4000 ████████████████████
total  10000 ──────────────────────────────────────────  exceeds 8192

result: the request is rejected before generation even starts.

The most common assumption beginners make here is "input 6000 is less than 8192, so it will go through, and the answer will get cut off somewhere around 2192 tokens." That is not what happens. vLLM checks the sum of the input and max_tokens at the moment it receives the request, and rejects it if that sum is over the limit.

Here is the actual form the message takes. This is the original text as reported in a public vLLM issue, copied verbatim.

This model's maximum context length is 16384 tokens. However, you requested
122946 tokens (112946 in the messages, 10000 in the completion).
Please reduce the length of the messages or completion.

Pulling the message apart, the structure is right there: the parenthetical splits into input and output, and the sum of the two is what gets compared against the limit stated earlier. This one line is the clearest possible proof that input and output draw from the same budget.

So there are three possible fixes. Reduce the input, reduce max_tokens, or increase max_model_len. The first two are changes on the request side; the last one requires restarting the server. As a side note, there is an enhancement request issue open in the vLLM repository about this behavior of rejecting instead of automatically truncating. In other words, this is an area that can change between versions, so check it directly on the version you are running.

One practical tip. If your application hardcodes max_tokens as a constant, you will hit this error every time a long input comes in. It is more robust to count the input tokens first, then compute that value as whatever budget remains, minus a safety margin.

Scenario 2 — Why a 128k Model Gets Blocked at 8192

The model card says 128k, but the server rejects requests at a much smaller value. Here is the order to check things in.

First, check whether --max-model-len was ever passed explicitly in the startup command. If it was, that value is the answer. Regardless of what the model is capable of, the limit the engine was given takes priority.

Second, if it was not, look at the value the engine derived from the model config. As the official documentation states, if you do not specify it, the value is automatically derived from the model config, and this value can differ from the marketing language on the model card. Models that support long context through some extension technique sometimes have a smaller default window written into their config file.

Third, trust the number the error message states. In the message shown earlier, the value that follows "maximum context length is" is the effective limit of this engine right now. That number is the truth, more than the model card is.

Fourth, if you genuinely need 128k anyway, start by calculating whether your KV cache can support it. And that leads into the third scenario.

Scenario 3 — Blocked at Startup, Before Any Request

This is the case where you set --max-model-len to a large value and the server does not start at all. Here is the form reported in a vLLM issue.

ValueError: The model's max seq len (4096) is larger than the maximum number
of tokens that can be stored in KV cache (3664). Try increasing
gpu_memory_utilization or decreasing max_model_len when initializing the engine.

This message might be the single most important thing in this post, because it exposes the real mechanism underneath. At startup, the engine loads the weights and allocates whatever memory remains to the KV cache. Then it calculates how many tokens that cache can hold. If that number is smaller than max_model_len, it means the engine could not even complete a single maximum-length request, so it aborts startup.

In other words, the final authority on the length limit is neither the model nor the configuration — it is memory.

What Actually Sets the Limit Is KV Cache Memory

Generalizing the scenarios above gives this picture.

Total GPU memory
  └─ vLLM uses the fraction set by gpu_memory_utilization
       ├─ model weights          (determined by model size and quantization)
       ├─ activations and other overhead
       └─ everything left = KV cache    ← this is what eats length and concurrency together

The KV cache faces two demands at the same time. One is length: the longer a single request is, the more blocks it uses. The other is concurrency: the more requests there are, the more blocks get used. So if you double max_model_len, the number of concurrent requests the same memory can sustain roughly gets cut in half.

In practice, this relationship shows up like this: leaving 128k open reduces concurrent capacity even if no user is actually using 128k, because the engine only starts if it can handle the worst case. That is why most production deployments set max_model_len to the upper percentile of the actual workload, not to the maximum the model supports. This is the reason stated earlier for why keeping this value low is not a mistake.

Tracing the Cause From the Error Message

Last, a table for working backward from the symptom.

SymptomMost likely causeCheck first
Request rejected with a 400-class error, message shows messages and completion numbersSum of input and max_tokens exceeds max_model_lenHow the request computes max_tokens
Error during server startup that the KV cache cannot hold enough tokensmax_model_len is large relative to available memory--gpu-memory-utilization and --max-model-len
Answer suddenly cuts off mid-sentenceHit the generation cap for the requestThe finish-reason field of the response
Blocked at a value smaller than the model card statesmax_model_len was derived or was explicitly setThe actual applied value in the startup log
Sharply slows down as concurrent requests increaseNot a length limit — KV cache shortage and preemptionPreemption warning logs (see part 4)

To sum up, there is one procedure. First, work out whether the error happened at the request stage or the startup stage. If it is the request stage, look at the sum of input and the generation cap. If it is the startup stage, look at the relationship between memory and max_model_len. Just separating these two branches resolves most length problems within ten minutes.

Try It Yourself

References