[SGLang] LLGuidance: Microsoft의 문법 제약 백엔드
들어가며
LLGuidance는 Microsoft가 개발한 문법 제약 라이브러리로, SGLang의 세 번째 Constrained Decoding 백엔드다. XGrammar와 마찬가지로 JSON Schema, Regex, EBNF, Structural Tag를 모두 지원하며, 비트마스크 기반 마스킹을 사용한다. 핵심 차별점은 LLMatcher를 통한 통합 인터페이스와, 직렬화된 문법 표현(serialized grammar)을 중간 단계로 사용하는 아키텍처다.
소스 파일 경로: python/sglang/srt/constrained/llguidance_backend.py
구조도
┌────────────────────────────────────────────┐
│ GuidanceBackend │
│ ┌───────────────────────┐ │
│ │ LLTokenizer │ (llguidance용) │
│ │ from_tokenizer(hf) │ │
│ └───────────┬───────────┘ │
│ │ │
│ dispatch_json() │
│ └─► LLMatcher.grammar_from_json_schema()│
│ └─► serialized_grammar (str) │
│ │
│ dispatch_regex() │
│ └─► grammar_from("regex", ...) │
│ │
│ dispatch_ebnf() │
│ └─► grammar_from("ebnf", ...) │
│ │
│ dispatch_structural_tag() │
│ └─► StructTag.to_grammar() │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ GuidanceGrammar │ │
│ │ ll_matcher: LLMatcher │
│ │ serialized_grammar │ │
│ └───────────────────────┘ │
└────────────────────────────────────────────┘
핵심 코드 분석
1. 백엔드 초기화
class GuidanceBackend(BaseGrammarBackend):
def __init__(self, tokenizer, any_whitespace=True, whitespace_pattern=None, n_vocab=None):
super().__init__()
self.tokenizer = tokenizer
self.any_whitespace = any_whitespace
self.whitespace_pattern = whitespace_pattern
self.llguidance_tokenizer = from_tokenizer(self.tokenizer, n_vocab)
from_tokenizer()는 HuggingFace 토크나이저를 LLGuidance의 LLTokenizer로 변환한다. XGrammar가 TokenizerInfo를 생성하는 것과 동일한 역할이지만, LLGuidance 고유의 토크나이저 표현을 사용한다.
2. Serialized Grammar 패턴
LLGuidance의 가장 독특한 설계는 모든 제약 조건을 직렬화된 문법 문자열로 변환한 뒤, LLMatcher에 전달하는 것이다.
def dispatch_json(self, key_string: str) -> BaseGrammarObject:
try:
serialized_grammar = LLMatcher.grammar_from_json_schema(
key_string,
defaults={
"whitespace_flexible": self.any_whitespace,
"whitespace_pattern": self.whitespace_pattern,
},
)
except Exception as e:
return InvalidGrammarObject(str(e))
return self._from_serialized(serialized_grammar)
def dispatch_regex(self, key_string: str) -> BaseGrammarObject:
serialized_grammar = grammar_from("regex", key_string)
return self._from_serialized(serialized_grammar)
def dispatch_ebnf(self, key_string: str) -> BaseGrammarObject:
serialized_grammar = grammar_from("ebnf", key_string)
return self._from_serialized(serialized_grammar)
grammar_from() 함수는 제약 타입("regex", "ebnf")과 값을 받아 직렬화된 문법을 반환한다. JSON Schema는 LLMatcher.grammar_from_json_schema()를 통해 whitespace 유연성 등의 설정을 포함하여 변환한다. 이 직렬화된 문법은 텍스트 기반이므로 네트워크 전송이나 캐싱에 유리하다.
3. GuidanceGrammar 객체
class GuidanceGrammar(BaseGrammarObject):
def __init__(self, llguidance_tokenizer, serialized_grammar):
super().__init__()
self.llguidance_tokenizer = llguidance_tokenizer
self.serialized_grammar = serialized_grammar
self.ll_matcher = LLMatcher(
self.llguidance_tokenizer,
self.serialized_grammar,
log_level=int(os.environ.get("LLGUIDANCE_LOG_LEVEL", "1")),
)
self._check_err()
self.eos_token = self.llguidance_tokenizer.eos_token
LLMatcher가 런타임 매칭을 담당한다. XGrammar의 GrammarMatcher와 역할이 동일하다. LLGUIDANCE_LOG_LEVEL 환경변수로 디버그 로깅을 제어할 수 있다.
4. 토큰 수락과 종료 처리
def accept_token(self, token: int):
if self.finished:
return
if self.ll_matcher.is_stopped() and token == self.eos_token:
self.finished = True
return
self.ll_matcher.consume_token(token)
self._check_err()
LLGuidance는 문법 완료(is_stopped())와 EOS 토큰을 분리하여 처리한다. 문법이 완료된 상태에서 EOS 토큰이 들어오면 finished를 설정하되, ll_matcher에는 전달하지 않는다. 이는 XGrammar가 is_terminated() 하나로 처리하는 것과 대조적이다.
5. Rollback 처리
def rollback(self, num_tokens: int) -> None:
if num_tokens <= 0:
return
if self.finished:
self.finished = False
num_tokens -= 1 # EOS 토큰은 ll_matcher에 기록되지 않았으므로
self.ll_matcher.rollback(num_tokens)
self._check_err()
finished 상태에서 rollback 할 때, EOS 토큰은 ll_matcher에 전달되지 않았으므로 rollback 횟수에서 1을 빼야 한다. 이런 세밀한 상태 관리가 LLGuidance의 특징이다.
6. 비트마스크 마스킹
def fill_vocab_mask(self, vocab_mask, idx):
fill_next_token_bitmask(self.ll_matcher, vocab_mask, idx)
self._check_err()
def allocate_vocab_mask(self, vocab_size, batch_size, device):
if self.bitmask is None or self.bitmask.shape[0] < batch_size:
self.bitmask = allocate_token_bitmask(
batch_size, self.llguidance_tokenizer.vocab_size
)
bitmask = self.bitmask
else:
bitmask = self.bitmask[:batch_size]
return bitmask
@staticmethod
def apply_vocab_mask(logits, vocab_mask):
apply_token_bitmask_inplace(logits, vocab_mask)
llguidance.torch 모듈의 allocate_token_bitmask, fill_next_token_bitmask, apply_token_bitmask_inplace를 사용한다. XGrammar와 동일한 비트마스크 방식이지만, 구현 라이브러리가 다르다. 배치 크기가 줄어들 때 비트마스크를 재할당하지 않고 슬라이싱([:batch_size])으로 재사용하는 최적화가 있다.
7. Jump-Forward (Fast-Forward)
def try_jump_forward(self, tokenizer):
ff_tokens = self.ll_matcher.compute_ff_tokens()
if ff_tokens:
return ff_tokens, ""
else:
return None
LLGuidance의 Jump-Forward는 compute_ff_tokens()로 구현된다. Outlines의 FSM 기반 Jump-Forward와 달리, LLMatcher가 내부적으로 확정 토큰 시퀀스를 계산한다. 반환값이 토큰 ID 리스트이므로 별도의 retokenization이 필요 없다.
def jump_and_retokenize(self, old_output_ids, new_output_ids, next_state):
pass # LLGuidance는 토큰 수준으로 처리하므로 retokenize 불필요
jump_and_retokenize()가 no-op인 이유: LLGuidance는 처음부터 토큰 ID를 반환하므로 문자열 → 토큰 재변환 과정이 없다.
8. Structural Tag 처리
def dispatch_structural_tag(self, key_string: str):
structural_tag = json.loads(key_string)
assert is_legacy_structural_tag(structural_tag)
tags = [
StructTag(
begin=structure["begin"],
grammar=structure["schema"],
end=structure["end"],
trigger=structural_tag["triggers"][0],
)
for structure in structural_tag["structures"]
]
g = StructTag.to_grammar(tags)
return self._from_serialized(g)
LLGuidance는 현재 레거시 Structural Tag 포맷만 지원한다(assert is_legacy_structural_tag). XGrammar가 레거시와 새 포맷을 모두 지원하는 것과 대조적이다. StructTag.to_grammar()가 태그 구조를 직렬화된 문법으로 변환한다.
세 백엔드 비교: copy() 구현
캐시에서 Grammar 객체를 복사하는 방식에 차이가 있다.
| 백엔드 | copy() 전략 |
|---|---|
| XGrammar | 새 GrammarMatcher 생성, CompiledGrammar 공유 |
| Outlines | RegexGuide 참조 공유, state=0으로 초기화 |
| LLGuidance | 새 LLMatcher 생성, serialized_grammar 문자열 공유 |
# LLGuidance의 copy()
def copy(self):
return GuidanceGrammar(
llguidance_tokenizer=self.llguidance_tokenizer,
serialized_grammar=self.serialized_grammar,
)
LLGuidance는 serialized_grammar 문자열만 보존하고, LLMatcher를 새로 생성한다. 직렬화된 문법이 불변(immutable) 문자열이므로 안전하게 공유할 수 있다.
관련 포스트
- SGLang Grammar Manager: 구조화된 출력 생성의 통합 관리
- SGLang XGrammar: JSON/Regex 제약 백엔드
- SGLang Outlines: FSM 기반 제약 생성과 Jump-Forward 최적화
- SGLang Reasoner Grammar: 추론 체인 제약 생성
참고
관련 포스트
SGLang 의 다른글
- 이전글 [SGLang] Outlines: FSM 기반 제약 생성과 Jump-Forward 최적화
- 현재글 : [SGLang] LLGuidance: Microsoft의 문법 제약 백엔드
- 다음글 [SGLang] Reasoner Grammar: 추론 체인 제약 생성
댓글