Skip to content

Commit 4ddc021

Browse files
refactor(grammar): move mask dispatch to stream seam
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5f3f67c commit 4ddc021

21 files changed

Lines changed: 477 additions & 393 deletions

docs/backend/xgrammar_design.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ above speaks generic logits-processor vocabulary.
5555
5656
stream / executor / scheduler ── NO
5757
GenerateStream (gates on prepare()), NormalExecutor / MtpExecutor
58-
(call batch_logits_processor::*), FIFOScheduler (generic readiness skip),
58+
(call stream_logits_processor::*), FIFOScheduler (generic readiness skip),
5959
BatchDecodeScheduler (only the generic hasStructuredOutputRequest() predicate)
6060
│ wired once at startup by
6161
@@ -71,11 +71,11 @@ Two seams cross these layers without leaking concrete types:
7171
generic factory, breaking the otherwise-cyclic build dependency
7272
(`models:logits_processor` → xgrammar → `engine_base/stream`
7373
`models:logits_processor`).
74-
2. **Batch-dispatch seam**`batch_logits_processor::*` + `SpecLogitsVerifyRunner`
75-
let executors drive whole-batch masks (verify / draft / decode). The dispatch
76-
is the only code that walks `StreamGroups`; it hands the runner a flat list of
77-
`SpecLogitsProcessor` participants, so the model layer and executors never see
78-
`StreamGroups` or a concrete backend.
74+
2. **Stream-dispatch seam**`stream_logits_processor::*` + `SpecLogitsVerifyRunner`
75+
lets stream/executor preparation code drive whole-batch masks (verify / draft /
76+
decode). The dispatch is the only code that walks `StreamGroups`; it hands the
77+
runner a flat list of `SpecLogitsProcessor` participants, so the model layer and
78+
concrete executors never see a constraint backend.
7979

8080
Data (not behavior) does cross the boundary in two benign forms: the request
8181
config fields on `GenerateConfig` (`json_schema` / `regex` / `ebnf` /
@@ -116,10 +116,10 @@ configuration contracts, not engine-type coupling.
116116
that exposes only per-stream primitives (`tryAcceptAndFillBitmask` for verify,
117117
`fillDecodeBitmask` / `fillDraftBitmask` for decode/draft, plus a generic
118118
`applyBitmask`). For all three batch passes (verify, draft, decode),
119-
`BatchLogitsProcessorDispatch` collects the active processors from
119+
`StreamLogitsProcessorDispatch` collects the active processors from
120120
`StreamGroups` into a flat list and `SpecLogitsVerifyRunner` owns the batch
121121
assembly (allocate, per-stream fill, merge, apply). The model layer never sees
122-
`StreamGroups`; callers name only the generic `batch_logits_processor`
122+
`StreamGroups`; callers name only the generic `stream_logits_processor`
123123
namespace.
124124

125125
5. Fail closed rather than silently generating unconstrained output.
@@ -251,7 +251,7 @@ calls `notifyCommit`.
251251
### Normal Decode Batch Mask
252252

253253
`NormalExecutor` calls
254-
`batch_logits_processor::batchApplyDecodeMaskFromStreams` immediately before
254+
`stream_logits_processor::applyDecodeMaskFromStreams` immediately before
255255
sampling, which collects participants and runs `SpecLogitsVerifyRunner::runDecode`
256256
(one row per stream). This path collapses per-stream mask launches into one batch
257257
apply. If the batch mask ran, `SamplerInputs::is_decode_mask_batched` is set so the
@@ -268,19 +268,21 @@ model layer):
268268

269269
- draft-side masking (`runDraft` + `fillDraftBitmask`) keeps proposed tokens in
270270
the legal set when possible: each processor DFS-walks its proposed chain, fills
271-
its single row, then rolls back so observable state is unchanged;
271+
its single row, then rolls back so observable state is unchanged. MTP exposes
272+
this through `MtpDraftMaskState`, so the executor does not manage CPU draft-chain
273+
row alignment itself;
272274
- verify-side masking (`run` + `tryAcceptAndFillBitmask`) fills each stream's
273275
`[propose_step + 1, words]` row block, merges processors for the same stream by
274276
bitwise AND, and applies the merged bitmask through the generic `applyBitmask()`
275-
hook.
277+
hook. This is applied while `MtpBatchStreamProcessor` prepares the target
278+
sampler input.
276279

277280
The actual matcher state remains unchanged during those batch passes. After the
278281
speculative sampler decides the accepted suffix, the normal stream commit path
279282
updates every attached logits processor exactly once.
280283

281-
`MtpExecutor` and `MtpBatchStreamProcessor` therefore know only about
282-
`batch_logits_processor`, `SpecLogitsVerifyRunner`, and `SpecLogitsProcessor`,
283-
not `GrammarLogitsProcessor`.
284+
`MtpExecutor` and `MtpBatchStreamProcessor` therefore know only about the
285+
`stream_logits_processor` seam, not `GrammarLogitsProcessor`.
284286

285287
### PD Separation
286288

@@ -340,7 +342,7 @@ resolve the hook's definition). This keeps the composition root explicit (no
340342

341343
`NormalExecutor`, `MtpExecutor`, and `MtpBatchStreamProcessor` should not know
342344
about grammar. The current design satisfies that by routing all batch mask work
343-
(verify, draft, decode) through `batch_logits_processor`: the stream dispatch
345+
(verify, draft, decode) through `stream_logits_processor`: the stream dispatch
344346
collects `SpecLogitsProcessor` participants into a flat list and delegates the
345347
actual assembly to `SpecLogitsVerifyRunner`, so no concrete processor owns the
346348
whole batch and the model layer never sees `StreamGroups`. The names are

rtp_llm/cpp/engine_base/schedulers/FIFOScheduler.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,10 @@ void FIFOScheduler::evaluateWaitingStreams(list<GenerateStreamPtr>& waiting_stre
247247
}
248248

249249
// Skip streams that are still waiting for async preparation (e.g.
250-
// grammar compile). They stay in waiting_streams_ until prepare()
251-
// completes, which prevents them from being admitted into a batch
252-
// that may contain decode streams (avoiding mixed prefill+decode).
253-
if (stream->isPreparationPending()) {
250+
// grammar compile) AND haven't been externally marked CanRun (e.g.
251+
// by DecodeRpcServer for PD decode streams). Externally-marked streams
252+
// get their preparation polled through moveToNext in Phase 2.
253+
if (stream->isPreparationPending() && !stream->hasEvent(StreamEvents::CanRun)) {
254254
it++;
255255
continue;
256256
}

rtp_llm/cpp/engine_base/stream/BatchLogitsProcessorDispatch.cc

Lines changed: 0 additions & 139 deletions
This file was deleted.

rtp_llm/cpp/engine_base/stream/BatchLogitsProcessorDispatch.h

Lines changed: 0 additions & 71 deletions
This file was deleted.

rtp_llm/cpp/engine_base/stream/GenerateStream.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,17 @@ class GenerateStream: public std::enable_shared_from_this<GenerateStream> {
453453
logits_processor_list_.push_back(std::move(processor));
454454
}
455455

456+
// Post-construction init: set stream back-references on processors that
457+
// need it (e.g. GrammarLogitsProcessor for PD token replay). Must be called
458+
// after make_shared so shared_from_this() is valid.
459+
void initProcessorStreamRefs() {
460+
for (auto& p : logits_processor_list_) {
461+
if (p) {
462+
p->setStream(shared_from_this());
463+
}
464+
}
465+
}
466+
456467
// Generic processor lookup by concrete type. Returns the first attached
457468
// processor whose dynamic type is T (or a subclass), or nullptr.
458469
// Use in lieu of grammar-specific finders so callers depend on the

0 commit comments

Comments
 (0)