상태: 초안 (2026-10-27), 실측. 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.31.0(xgrammar 0.2.7, llguidance 1.7.6), Qwen3-Reranker-0.6B 로 2026-10-27 에 쟀습니다. 공식 문서는 0.32.0 기준으로 확인했고, 구조화 출력 문서는 0.31.0 과 같았습니다. 2026-10-27 추가. 대화용 Qwen3-0.6B·Qwen3-4B(AWQ)와 vLLM 0.32.0(xgrammar 0.2.8)으로 정답이 있는 추출 과제를 잰 결과를 3-1절에 더했습니다. 0.31.0 수치는 그대로 두었습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. 원리: 다음 토큰 후보에서 문법 밖을 지운다
- 2. 쓰는 법과 이름의 변화
- 3. 직접 재 보기
- 3-1. 2026-10-27 재측정: 정답이 있는 추출 과제
- 4. 운영에서 가져갈 것
- 5. 확인하지 못한 것
- 참고
1. 원리: 다음 토큰 후보에서 문법 밖을 지운다
모델은 걸음마다 어휘 전체(이 모델은 151,669개)에 대한 점수를 내고, 그중 하나를 고릅니다. 구조화 출력은 고르기 직전에 한 단계를 끼웁니다. 지금까지 쓴 글을 문법 기계(유한 상태 기계나 푸시다운 오토마타)에 넣어 「지금 올 수 있는 토큰」의 집합을 구하고, 나머지 토큰의 점수를 음의 무한대로 덮습니다. 이 집합을 비트마스크로 만들어 GPU 의 점수 텐서에 한 번에 적용합니다.
지금까지: {"priority": "
문법상 다음에 올 수 있는 것: l(ow), m(edium), h(igh) 로 시작하는 토큰만
나머지 151,000여 토큰 → 점수 -inf
그래서 모델이 아무리 엉뚱해도 결과는 문법을 지킵니다. 대신 두 가지 비용이 생깁니다.
- 문법을 만드는 비용: JSON 스키마나 정규식을 토큰 단위의 기계로 바꾸는 일입니다. 어휘가 15만 개라 토큰마다 「이 토큰이 이 상태에서 허용되는가」를 미리 계산해 두는 것이 무겁습니다. 같은 스키마는 캐시합니다.
- 걸음마다 마스크를 만드는 비용: 요청마다 상태를 한 칸 옮기고 마스크를 만들어 GPU 로 보냅니다.
XGrammar 논문(Dong 외, MLSys 2025)은 문맥과 무관하게 판정되는 토큰을 미리 계산해 두고, 문법 계산을 GPU 실행과 겹쳐 돌려 기존 방식보다 최대 100배 빠르고, 서빙 엔진과 함께 쓰면 오버헤드가 거의 없다고 보고합니다(초록 기준, 조건은 초록에 없음). vLLM 은 xgrammar 와 llguidance(guidance 백엔드) 둘을 쓰고, 기본값 auto 는 요청을 보고 고릅니다.
2. 쓰는 법과 이름의 변화
OpenAI 형식의 response_format 과 vLLM 전용 structured_outputs 두 길이 있습니다.
| 원하는 것 | 요청에 넣는 것 |
|---|---|
| JSON 스키마 | response_format: {"type": "json_schema", "json_schema": {...}} 또는 structured_outputs: {"json": 스키마} |
| 아무 JSON 객체 | response_format: {"type": "json_object"} |
| 보기 중 하나 | structured_outputs: {"choice": ["low", "medium", "high"]} |
| 정규식 | structured_outputs: {"regex": "[A-Z]{3}-[0-9]{4}"} |
| 문맥 자유 문법(EBNF) | structured_outputs: {"grammar": "..."} |
| 태그 안만 JSON | structured_outputs: {"structural_tag": ...}(5편의 strict 도구 호출이 이 방식) |
판이 바뀌며 이름이 바뀌었습니다. 예전 guided_json, guided_regex, guided_choice, guided_grammar 와 요청별 guided_decoding_backend 는 0.12.0 에서 지워졌고, 지금은 structured_outputs 하나로 모였습니다. 백엔드는 서버에서 --structured-outputs-config.backend 로 정하며, 요청마다 백엔드를 고르는 기능은 V1 에서 없어졌습니다. 정규식 문법은 백엔드마다 달라 xgrammar·guidance 는 Rust 식, lm-format-enforcer 는 파이썬 re 식입니다. 0.32.0 부터는 64단계보다 깊게 중첩된 JSON 스키마를 400 으로 거절합니다(릴리스 노트 기준, 미실측).
3. 직접 재 보기
방법
- 환경: 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.31.0, Qwen3-Reranker-0.6B(bf16). 이 모델은 대화로 물으면 거의 늘 「No.」라고 답합니다. 그래서 강제가 없을 때와 있을 때의 차이가 아주 뚜렷하게 보입니다.
- 요청: 채팅 API 로 「작업 하나를 name, priority(low/medium/high), tokens(정수), tags(문자열 목록) 키를 가진 JSON 으로만 답하라」고 묻고,
temperature 0.7,max_tokens 160, 요청마다 다른 시드를 줬습니다. - 변형: 강제 없음,
json_schema(네 키 필수, 추가 키 금지, 문자열 길이 상한),json_object,choice,regex. - 서버: 백엔드를
auto(기본),guidance,xgrammar로 바꿔 세 번 띄웠습니다. - 반복: 동시 1 은 다섯 묶음(첫 묶음 예열), 동시 16 은 네 묶음(첫 묶음 예열). 「올바름」은 응답을 JSON 으로 읽어 네 키가 정확히 있고 값의 형이 맞는지로 판정했습니다.
결과 1. 강제하면 틀린 것은 잘린 것뿐이었다
동시 16, 측정 세 묶음(48요청)의 합계입니다.
| 변형 | auto | guidance | xgrammar | 응답 예 |
|---|---|---|---|---|
| 강제 없음 | 0/48 | 0/48 | 0/48 | No. |
| json_object | 0/48 | 0/48 | 0/48 | {"status": "no", "data": []} |
| json_schema | 47/48 | 46/48 | 46/48 | {"name": "", "priority": "high", "tokens": 0, "tags": []} |
| choice | 48/48 | 48/48 | 48/48 | low |
| regex | 48/48 | 48/48 | 48/48 | IOW-1139 |
- json_schema 에서 틀린 응답은 모두
finish_reason이length였습니다. guidance 와 xgrammar 에서 두 번씩 160토큰에 걸려 JSON 이 닫히기 전에 끝났습니다(auto 서버는 이 항목을 기록하기 전에 돌려 원인을 적지 못했습니다). 문법은 「다음 토큰」만 묶을 뿐 「제때 끝나기」를 보장하지 않습니다.max_tokens를 스키마의 최대 길이보다 넉넉하게 주고, 클라이언트는finish_reason을 꼭 확인해야 합니다. - json_object 는 JSON 문법만 지킵니다. 표본은 문법상 올바른 JSON 이었지만 요구한 키가 하나도 없었습니다. 키가 중요하면 json_schema 를 씁니다.
- 올바른 것과 쓸모 있는 것은 다릅니다. 스키마를 지킨 응답의 대부분은 빈 이름, 0, 빈 목록이었습니다. 모델이 할 줄 모르는 일을 문법이 대신해 주지는 않습니다.
- auto 서버의 json_schema 응답은 출력 토큰 수가 xgrammar 고정 서버와 묶음마다 같았습니다(동시 1 에서 28, 35, 32, 27). 이 요청에서는 auto 가 xgrammar 를 골랐다고 추정합니다(로그에 선택 기록이 없어 미확인).
결과 2. 비용: 첫 요청, 그리고 토큰마다 조금
| 변형 | 백엔드 | 새 스키마 첫 요청 TTFT(동시 1, 예열 묶음) | 그 뒤 TTFT 중앙 | 토큰 간격 중앙(동시 1) | 토큰 간격 중앙(동시 16) |
|---|---|---|---|---|---|
| 강제 없음 | xgrammar 서버 | 43.6ms | 14.1ms | 5.9ms | 6.3ms |
| json_schema | xgrammar | 445.7ms | 14.2ms | 6.3ms | 7.4ms |
| json_schema | guidance | 974.6ms | 14.3ms | 6.0ms | 7.3ms |
| json_schema | auto | 508.0ms | 14.3ms | 6.2ms | 7.5ms |
| regex | xgrammar | 15.0ms | 13.7ms | 6.5ms | 7.8ms |
| choice | xgrammar | 22.3ms | 14.2ms | 6.4ms | 7.7ms |
- 새 스키마의 첫 요청이 가장 비쌉니다. 같은 스키마의 두 번째 요청부터 TTFT 는 강제 없음과 같아졌습니다(약 14ms). 첫 요청의 값에는 문법 만들기뿐 아니라 서버에서 처음 도는 커널의 준비 시간도 섞여 있어 둘을 가르지 못했습니다.
- 토큰 간격은 혼자일 때 약 7%(5.9 → 6.3ms), 동시 16 에서 약 17%(6.3 → 7.4ms) 늘었습니다. 다만 강제 없는 응답은 「No.」 세 토큰뿐이라 비교의 기준이 짧습니다. 동시 16 의 증가가 더 큰 것은 열여섯 요청의 마스크를 걸음마다 만들기 때문으로 보입니다.
- regex 와 choice 는 문법이 작아 첫 요청 비용이 거의 없었습니다.
겪은 일: 첫 표본 추출 묶음의 몇 초
측정 도중 서버 로그에 이런 경고가 찍혔습니다.
WARNING [jit_monitor.py:140] Triton kernel JIT compilation during inference: _topk_topp_kernel.
This causes a latency spike; consider extending warmup to cover this shape/config.
새 서버에서 처음으로 온도 표본 추출(top-k·top-p) 요청 묶음이 들어오면 그 커널을 그 자리에서 컴파일했고, 그 묶음의 TTFT 가 3.9초(guidance 서버, 동시 1)에서 11초(guidance 서버, 동시 16)까지 늘었습니다. 탐욕 디코딩만 하던 동안에는 이 경고가 없었습니다. 컴파일은 한 번으로 끝나지도 않았습니다. 다른 서버에서 온도 1.0 요청을 동시 1, 8, 32 로 차례로 보냈더니 동시성을 처음 바꿀 때마다 그 묶음이 다시 느려졌습니다(256토큰 생성에 동시 1 은 5.4초 대 이후 1.5초, 동시 8 은 7.4초 대 1.9초, 동시 32 는 8.4초 대 3.2초). 묶음 크기마다 커널을 새로 만드는 것으로 보입니다. 모델의 generation_config.json 이 기본 표본 추출 값(temperature 0.6, top_k 20, top_p 0.95)을 덮어쓴다는 경고도 함께 찍혔습니다. 운영에서는 서버를 띄운 뒤 실제와 같은 표본 추출 설정의 요청으로 한 번 데우고 나서 트래픽을 받아야 합니다. 0.32.0 릴리스 노트에는 표본 추출기 예열 컴파일을 줄였다는 항목이 있습니다(미실측).
3-1. 2026-10-27 재측정: 정답이 있는 추출 과제
앞의 시험은 「올바른 JSON 인가」만 봤습니다. 이번에는 값이 맞는지도 봤습니다.
- 환경: vLLM 0.32.0 공식 컨테이너. 모델은 대화용 Qwen3-0.6B(bf16), 그 FP8 온라인 양자화, GPTQ 8비트판, Qwen3-4B AWQ 4비트판. 사고 과정은 껐습니다.
- 과제: 「The job called backup-db has high priority, needs 1200 tokens and should be tagged nightly and storage. Reply with JSON only.」 같은 문장 여섯 개(작업 이름·우선순위·토큰 수·태그가 다름)를 동시에 보내고, 세 묶음(18요청)을 셌습니다. 첫 묶음은 예열로 뺐습니다.
- 셈: 「바로 읽힘」은 응답 그대로
json.loads가 되는가, 「값이 맞음」은 코드 울타리를 벗긴 뒤 네 값이 모두 맞는가입니다.
| 모델 | 변형 | 바로 읽힘 | 스키마 준수 | 값이 맞음 | 응답 시간 중앙 |
|---|---|---|---|---|---|
| Qwen3-0.6B | 강제 없음 | 0/18 | 0/18 | 18/18 | 254.9ms |
| Qwen3-0.6B | json_object | 18/18 | 18/18 | 18/18 | 248.0ms |
| Qwen3-0.6B | json_schema | 18/18 | 18/18 | 18/18 | 273.5ms |
| Qwen3-0.6B FP8 | 강제 없음 | 6/18 | 6/18 | 18/18 | 187.0ms |
| Qwen3-0.6B GPTQ 8비트 | 강제 없음 | 0/18 | 0/18 | 18/18 | 185.6ms |
| 위 세 모델 | json_object·json_schema | 54/54 | 54/54 | 54/54 | |
| Qwen3-4B AWQ 4비트 | 강제 없음 | 18/18 | 18/18 | 18/18 | 476.0ms |
| Qwen3-4B AWQ 4비트 | json_schema | 18/18 | 18/18 | 18/18 | 507.5ms |
- 작은 대화 모델은 값을 맞히고 모양을 틀렸습니다. Qwen3-0.6B 는 강제가 없으면 늘
json …처럼 마크다운 울타리로 감쌌습니다. 사람이 읽기에는 맞는 답이지만 프로그램은 바로 읽지 못합니다. json_object 하나로 이 문제가 사라졌습니다. - 같은 모델도 양자화에 따라 버릇이 바뀌었습니다. FP8 판은 18번 중 6번 울타리 없이 답했습니다. 버릇에 기대는 파서는 모델이나 양자화를 바꿀 때 깨질 수 있다는 뜻입니다.
- 큰 모델은 울타리 없이 답했습니다. 4B 는 강제 없이도 18번 모두 바로 읽혔습니다.
- 구조화 출력은 값을 바꾸지 않았습니다. 세 변형 모두 값은 18/18 이었습니다. 이 과제가 쉬워서이기도 합니다.
- 비용은 0.31.0 때와 같은 방향이었습니다. 0.6B 에서 json_schema 는 강제 없음보다 응답이 약 19ms(7%) 길었고, 새 스키마의 첫 요청은 예열 묶음에서 TTFT 195ms 였습니다.
- 재순위 모델로 앞의 시험을 0.32.0 에서 다시 돌린 결과(동시 16, json_schema 47/48, 틀린 하나는 길이 초과)도 0.31.0 과 같았습니다. 첫 표본 추출 묶음의 커널 컴파일 지연도 0.32.0 에 그대로 있었습니다(재순위 모델 5.3초, Qwen3-0.6B 6.2초).
4. 운영에서 가져갈 것
- 스키마는 미리 데웁니다. 서비스가 쓰는 스키마의 수가 정해져 있다면 서버 기동 직후 한 번씩 보내 두면 첫 사용자가 수백 ms 를 더 기다리지 않습니다. 요청마다 스키마가 달라지는 서비스(사용자가 스키마를 보내는 API)는 이 비용을 계속 냅니다.
- 사용자가 보낸 스키마는 신뢰할 수 없는 입력입니다. 문법을 만드는 비용은 스키마의 크기와 모양에 따라 커지므로, 외부에서 스키마를 받는다면 크기와 깊이에 상한을 둡니다. 0.32.0 이 깊이 64 를 넘는 스키마를 거절하게 된 것도 같은 이유로 보입니다.
finish_reason을 확인합니다. 구조화 출력의 「항상 올바른 JSON」은 끝까지 썼을 때의 이야기입니다.- 키가 필요하면 json_object 가 아니라 json_schema 를 씁니다.
- 백엔드는 기본 auto 로 두되, 첫 요청 지연이 문제라면 이번 측정처럼 두 백엔드를 같은 스키마로 재 보고 고릅니다. 이번 스키마에서는 xgrammar 의 첫 요청이 guidance 의 절반이었고, 그 뒤의 값은 비슷했습니다.
5. 확인하지 못한 것
- 큰 스키마(중첩 깊은 객체, 긴 enum), 문맥 자유 문법(EBNF), 구조 태그의 비용은 미실측입니다.
- 첫 요청의 비용 가운데 문법 만들기와 커널 준비를 가르지 못했습니다. auto 와 guidance 서버에서 동시 16 의 첫 json_schema 묶음이 1.5초 넘게 걸리고 xgrammar 서버에서는 30ms 였던 까닭도 미확인입니다.
- 추론 모델(사고 과정을 먼저 쓰는 모델)과 함께 쓸 때의 동작(
--structured-outputs-config.enable_in_reasoning)은 미실측입니다.
참고
- vLLM 문서, Structured Outputs(0.31.0·0.32.0 동일): https://docs.vllm.ai/en/latest/features/structured_outputs.html
- Dong 외, 「XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models」, MLSys 2025. https://arxiv.org/abs/2411.15100
- llguidance: https://github.com/guidance-ai/llguidance
- vLLM 0.32.0 릴리스 노트(스키마 깊이 제한, 표본 추출기 예열): https://github.com/vllm-project/vllm/releases/tag/v0.32.0
- 이 시리즈 5편 도구 호출