Skip to content

Commit 74eec60

Browse files
mudlerclaude
andcommitted
feat(structured-output): native GBNF/regex/choice grammar engine (M3.4 Task 4)
Original vllm.cpp component (§9) behind the 1:1-ported StructuredOutputBackend/ StructuredOutputGrammar ABCs: a from-scratch, correctness-grade grammar engine covering GRAMMAR (GBNF/EBNF), REGEX (regex->GBNF lowering) and CHOICE (choice->GBNF, mirrors utils.py::choice_as_grammar). JSON/json_object stay deferred to Task 5 (throw). Engine: - GBNF/EBNF parser -> a byte-level rule table (every terminal matches exactly one byte; multi-byte codepoints lowered to byte sequences; char classes, alternation, grouping, and * + ? {m,n} repetition via synthetic sub-rules). - Stack-based push-down FSM over grammar positions (accept a token by decoding it to raw bytes via the tokenizer's inverse GPT-2 bytes_to_unicode map and advancing byte-by-byte across token boundaries). - THE BYTE-ALIGNMENT CORE: a token-byte trie built ONCE at construction over all regular vocab tokens; fill_bitmask is a single DFS over (trie x FSM state), so its cost is ~ reachable trie nodes, roughly independent of vocab size (not the naive O(vocab x token_len x grammar) per step). EOS/stop tokens allowed only at an accepting state; added/special tokens and vocab holes never matchable. - MakeNativeBackendFactory(tokenizer, vocab_size, stop_ids) wires the engine to the manager's BackendFactory. Tests (real byte-level BPE fixture): yes/no GBNF, cat/dog/bird choice, [0-9]+ char class, byte-alignment across a token boundary + a multi-byte UTF-8 codepoint, EOS-only-at-accept, sub-O(vocab) fill (trie-node visit counter), rollback/reset, validate_tokens, regex lowering, factory wiring. 15 cases / 100 assertions green; full suite 69/69 passing; warnings-as-errors clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9b640ee commit 74eec60

5 files changed

Lines changed: 1743 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ add_library(vllm STATIC
9595
src/vllm/v1/attention/backend.cpp
9696
src/vllm/v1/attention/backends/gdn_attn.cpp
9797
src/vllm/v1/structured_output/backend_types.cpp
98+
src/vllm/v1/structured_output/backend_native.cpp
9899
src/vllm/v1/structured_output/request.cpp
99100
src/vllm/v1/structured_output/manager.cpp
100101
src/vllm/v1/engine/types.cpp
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// vllm.cpp ORIGINAL component (§9 deviation) — NOT a 1:1 upstream port.
2+
//
3+
// The NATIVE grammar engine that plugs into the 1:1-ported structured-output
4+
// seam (StructuredOutputBackend / StructuredOutputGrammar ABCs, Task 1). Upstream
5+
// vLLM delegates to xgrammar/guidance/outlines/lm-format-enforcer; we ship a
6+
// from-scratch, correctness-grade GBNF/EBNF + regex + choice engine at T0 and
7+
// vendor xgrammar in a LATER milestone (a second backend behind this same proven
8+
// seam). This mirrors upstream's own multi-backend design (see the plan's
9+
// ARCHITECTURE DECISION) rather than deviating from it.
10+
//
11+
// The engine:
12+
// - parses an EBNF/GBNF grammar (llama.cpp GBNF-style) into a byte-level rule
13+
// table (every terminal matches exactly ONE byte; multi-byte codepoints are
14+
// lowered to byte sequences),
15+
// - runs a stack-based push-down FSM over grammar positions per request,
16+
// - decodes each vocab token to its RAW bytes via the tokenizer's inverse
17+
// GPT-2 bytes_to_unicode map and advances the FSM byte-by-byte,
18+
// - fills the per-step token bitmask WITHOUT re-running every token: a
19+
// TOKEN-BYTE TRIE is built ONCE at construction, and fill_bitmask is a single
20+
// DFS over (trie x grammar-state) so its cost is ~ (reachable trie nodes),
21+
// roughly independent of vocab size (THE BYTE-ALIGNMENT CORE).
22+
//
23+
// Coverage at T0: GRAMMAR (GBNF/EBNF), REGEX (regex->GBNF lowering,
24+
// correctness-grade for common constructs), CHOICE (choice->GBNF, mirrors
25+
// utils.py::choice_as_grammar). JSON / JSON_OBJECT are M3.4 Task 5 (JSON-schema
26+
// -> GBNF) and throw here until then.
27+
#pragma once
28+
29+
#include <cstdint>
30+
#include <functional>
31+
#include <memory>
32+
#include <string>
33+
#include <vector>
34+
35+
#include "vllm/v1/structured_output/backend_types.h"
36+
37+
namespace vllm::tok {
38+
class Tokenizer;
39+
}
40+
41+
namespace vllm::v1 {
42+
43+
// Opaque engine internals (defined in backend_native.cpp):
44+
// NativeCompiledGrammar — the parsed byte-level rule table.
45+
// NativeBackendShared — the once-built token-byte trie + stop/EOS token ids
46+
// + vocab size, shared by every grammar the backend
47+
// compiles.
48+
struct NativeCompiledGrammar;
49+
struct NativeBackendShared;
50+
51+
// The per-request grammar + FSM state (a concrete StructuredOutputGrammar).
52+
// Declared here (not just in the .cpp) so tests can down-cast to inspect the
53+
// byte-trie perf counter.
54+
class NativeGrammar : public StructuredOutputGrammar {
55+
public:
56+
NativeGrammar(std::shared_ptr<const NativeBackendShared> shared,
57+
std::shared_ptr<const NativeCompiledGrammar> grammar);
58+
~NativeGrammar() override;
59+
60+
bool accept_tokens(const std::string& request_id,
61+
const std::vector<int32_t>& tokens) override;
62+
std::vector<int32_t> validate_tokens(
63+
const std::vector<int32_t>& tokens) override;
64+
void rollback(int num_tokens) override;
65+
void fill_bitmask(TokenBitmask& bitmask, int batch_index) override;
66+
bool is_terminated() override;
67+
void reset() override;
68+
69+
// The number of (trie node) visits the LAST fill_bitmask performed. Used by
70+
// the perf test to assert the fill is sub-O(vocab): a restrictive grammar
71+
// visits far fewer nodes than there are vocab tokens.
72+
int64_t last_fill_visited_nodes() const { return last_fill_visited_nodes_; }
73+
74+
private:
75+
// One FSM snapshot per accepted token (the front is the initial state); the
76+
// back is the current state. `done` marks a state reached by consuming the
77+
// EOS/stop token (nothing may follow).
78+
struct Snapshot;
79+
std::shared_ptr<const NativeBackendShared> shared_;
80+
std::shared_ptr<const NativeCompiledGrammar> grammar_;
81+
std::vector<Snapshot> history_;
82+
int64_t last_fill_visited_nodes_ = 0;
83+
};
84+
85+
// The engine-level native backend. Constructed with the tokenizer + vocab size;
86+
// builds the token-byte trie ONCE (over all regular vocab tokens, decoded to raw
87+
// bytes). `stop_token_ids` are the tokens allowed only at an accepting state
88+
// (typically just EOS); if empty, the tokenizer's EosId() is used when >= 0.
89+
class NativeStructuredOutputBackend : public StructuredOutputBackend {
90+
public:
91+
NativeStructuredOutputBackend(const tok::Tokenizer& tokenizer, int vocab_size,
92+
std::vector<int32_t> stop_token_ids = {});
93+
~NativeStructuredOutputBackend() override;
94+
95+
std::unique_ptr<StructuredOutputGrammar> compile_grammar(
96+
StructuredOutputOptions request_type,
97+
const std::string& grammar_spec) override;
98+
TokenBitmask allocate_token_bitmask(int max_num_seqs) override;
99+
void destroy() override;
100+
101+
int vocab_size() const;
102+
103+
private:
104+
std::shared_ptr<const NativeBackendShared> shared_;
105+
};
106+
107+
// Factory helper for wiring the StructuredOutputManager's BackendFactory to the
108+
// native engine. The manager builds its single backend lazily on the first
109+
// grammar; this returns a std::function that does so with the given tokenizer +
110+
// vocab size. The tokenizer must outlive the manager (the backend keeps a
111+
// reference only during construction — the trie is a value copy of the bytes).
112+
std::function<std::unique_ptr<StructuredOutputBackend>()>
113+
MakeNativeBackendFactory(const tok::Tokenizer& tokenizer, int vocab_size,
114+
std::vector<int32_t> stop_token_ids = {});
115+
116+
} // namespace vllm::v1

0 commit comments

Comments
 (0)