Skip to content

[EtsyML] Native TRIE guided decoding (per-beam, host-staging)#1

Open
orsonadams wants to merge 8 commits into
base-1.3.0rc21from
oadams/native-trie-guided-decoding
Open

[EtsyML] Native TRIE guided decoding (per-beam, host-staging)#1
orsonadams wants to merge 8 commits into
base-1.3.0rc21from
oadams/native-trie-guided-decoding

Conversation

@orsonadams

Copy link
Copy Markdown
Owner

Native trie-based constrained beam search in the C++ GuidedDecoder, for gen_search on the Triton tensorrt_llm backend. Base is clean upstream 1.3.0rc21 (d567f924b7) so this diff is only the fork's trie work.

Contents

  • Flat-CSR trie loader (v2) + GuidedDecoder kTRIE path.
  • Per-beam masking, beam-correct via the decoder's gathered histories (host-staging path).
  • C++ Triton backend wiring: guided_decoding_backend + guided_decoding_trie_path config params.
  • Redundant-sync cleanup in execute().

Beam-correct at beam=128 (640/640 valid, 128 unique). The device-resident kernel that removes the per-step host work stacks on top of this in a follow-up PR.

Why a fork (and why not the Python LogitsPostProcessor): https://docs.google.com/document/d/1OsciNfXKMkCgfpI-8jkN7qo_yeLnidknRbGQPvCIBlw/edit

dblincoe and others added 8 commits July 1, 2026 13:26
- Add GuidedTrie and GuidedTrieNode support in guidedDecoder for trie-based
  token constraint handling with bitmask caching
- Extend GuidedDecodingConfig with trie_path parameter in executor configs
- Add Python bindings for trie_path in nanobind and pybind executor configs
- Add trie_decoding module with tools for trie generation, conversion,
  load testing, and documentation
- Add trie_loader, trie_mask, and trie_mask_xgr libraries for Triton backend
- Update Triton model.py to integrate trie-based decoding masks
- Update Triton config.pbtxt files with trie_path parameters
Replace the per-node unordered_map<int32,shared_ptr<GuidedTrieNode>> trie with a
flat CSR representation (child offsets + token-sorted child arrays + isEnd), and
allow the trie EOS token at terminal nodes so explicit '...,EOS' leaf nodes are no
longer materialized. Together these cut runtime memory for the gen_search 73M-SID
trie from ~20GB to ~1GB (no per-node heap/hashmap; ~half the node count).

Binary format gains an eos_token_id header field; convert_trie_to_binary.py writes
it and no longer needs EOS appended to sequences. Also demote per-step TRIE debug
logging from INFO to DEBUG.

Verified: gen_search BART enc-dec produces the identical constrained SID as the
pointer-based trie.

Signed-off-by: Orson Adams <oadams@etsy.com>
Teach the tensorrtllm C++ Triton backend (libtriton_tensorrtllm.so) to construct a
GuidedDecodingConfig(kTRIE) from config.pbtxt when guided_decoding_backend=trie,
reading the binary trie path from the trie_path parameter. Previously TRIE was only
wired in the Python-backend model.py; prod serves via the C++ backend for throughput
(beam search), so the C++ path needs it. Unlike xgrammar, TRIE needs only the trie
path (no tokenizer info / encoded vocab).

Signed-off-by: Orson Adams <oadams@etsy.com>
Replace the per-node v1 parse loop with a v2 format that stores the CSR
arrays contiguously (magic, version, numNodes, numEdges, eos, childOffsets,
childTokens, childNodes, isEnd). Loading becomes four bulk reads (O(file
size), ~6s for the 73M-SID gen_search trie) and the loader validates the
format version and truncation. The builder writes children token-sorted,
so the runtime-side child sort is removed. Python writer updated to v2;
v1 files are rejected with a clear error.

Signed-off-by: Orson Adams <oadams@etsy.com>
…eam search)

The TRIE backend previously kept one trie state per request (advanced by
beam 0's last token) and masked only beam 0's logits row, so at beam>1
only the first token was constrained and outputs were invalid. Also,
request token buffers are raw slot buffers whose history goes stale when
beam search switches hypothesis parents, so any re-walk from them is
unsound.

Redesign:
- GuidedDecoder::build re-walks every beam's generated tokens from the
  trie root each step (stateless, bounded by trie depth) and stages one
  bitmask row per (seqSlot, beam); off-trie or finished beams get an
  EOS-only mask so they terminate instead of sampling all -inf logits.
- GuidedDecoder::execute registers beamWidth (logits row, bitmask row)
  entries per request; the logitsBitmask kernel is unchanged.
- Before build(), the model refreshes each beam-search request's token
  histories with the same non-destructive per-step gatherTree used by
  streaming+beam (postProcessRequest gains a streamingGather flag),
  skipped until the first token exists.
- Bitmask buffers grow to [maxNumSequences * maxBeamWidth, bitmaskSize];
  the xgrammar backend keeps its per-request design on beam 0's row.

Validated on the gen_search BART enc-dec engine with the full 73M-SID
trie: beam=128 yields 128/128 valid unique SIDs per query (was 0/128);
beam=1 unchanged. Per-query latency at beam=128: 27.9ms vs 12.8ms
unconstrained.

Signed-off-by: Orson Adams <oadams@etsy.com>
…ild context

cpp/build, .git, *.whl and trie_decoding data are not needed by the image
build and inflate the build context by tens of GB.

Signed-off-by: Orson Adams <oadams@etsy.com>
The per-beam TRIE mask path issued two hard stream synchronizations every
decode step: one after the pointer-vector H2D copies and one after the
logits-bitmask kernel. The copies, the kernel, and the downstream decoder
all run on the runtime stream, so stream ordering already guarantees the
masks land before the logits are consumed -- the syncs only stalled the
host. Remove both (plus the now-misleading "kernel completed" debug lines).

Correctness unchanged (beam=128 640/640 valid/128 unique, beam=1 5/5,
beam_threshold 17/128 OK). Trie overhead at beam=128 drops ~1.7 ms/query
(27.9 -> 26.2), ~0.45 ms/decode-step.

Signed-off-by: Orson Adams <oadams@etsy.com>
Rename the native-TRIE config.pbtxt parameter the C++ backend reads from
"trie_path" to "guided_decoding_trie_path", pairing consistently with the
existing "guided_decoding_backend" param. The etsyml model manifest and the
kserve handler emit this key; keeping the name aligned end to end.

Signed-off-by: Orson Adams <oadams@etsy.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants