llguidance

repository·main·Indexed 21 days ago

https://github.com/guidance-ai/llguidance

A high-performance library for constrained decoding that enables LLMs to produce structured outputs following JSON schemas, regular expressions, or context-free grammars. It provides bindings for Rust, C, C++, and Python, and is integrated into projects such as llama.cpp, vLLM, SGLang, and Chromium. The library works by computing token masks to ensure generated output adheres to a specified grammar with minimal CPU overhead.

Tokens
40.3K
Snippets
133
Records
167
Agent score
74%

What's inside llguidance

  1. Supported languages and integrations for llguidance

    main

    llguidance provides bindings and integrations across several environments:

    Language Bindings

    Major Integrations

    • OpenAI: Powers Structured Output (JSON Schema only).
    • llama.cpp: Available via -DLLAMA_LLGUIDANCE=ON in cmake or via the Guidance Python package.
    • vLLM: Integrated in recent versions.
    • SGLang: Use --grammar-backend llguidance. When passing Lark grammars, prefix them with %llguidance {} (similar to llama.cpp).
    • Chromium: Used for JSON Schema enforcement in window.ai.
    • Other: Guidance (Python), LLGTRT (TensorRT-LLM), mistral.rs, and onnxruntime-genai.
  2. What is llguidance and how does it work?

    main

    llguidance is a library for constrained decoding (structured outputs) for Large Language Models (LLMs). It enforces arbitrary context-free grammars on LLM outputs with high performance (approximately 50μs of CPU time per token for a 128k tokenizer) and negligible startup costs.

    Supported Grammar Formats

    • JSON schemas: A large subset is supported.
    • Regular expressions.
    • Context-free grammars: Uses a variation of the Lark format, which can include embedded JSON schemas and regular expressions.
    • llguidance format: An internal JSON-based format (being deprecated in favor of the Lark-like format).

    Core Mechanism

    Given a context-free grammar, a tokenizer, and a prefix of tokens, llguidance computes a token mask—a set of valid tokens from the tokenizer that can follow the current prefix while remaining compliant with the grammar. It achieves this by traversing a prefix tree (trie) using an Earley-based parser and a lexer based on regular expression derivatives.

  3. LLGuidance Grammar Syntax Overview

    main

    LLGuidance grammars use a syntax variant based on the Python Lark parsing toolkit. It uses an Earley parser, which is functionally equivalent to setting lexer='dynamic' in Lark.

    While the syntax is inspired by Lark and GBNF, it is not a direct drop-in replacement for either. If you are transitioning from llama.cpp, you can use the provided conversion script to translate GBNF grammars into LLGuidance-compatible Lark syntax.

    # Use the provided script to convert GBNF to Lark syntax
    python ../python/llguidance/gbnf_to_lark.py <input_gbnf_file> <output_lark_file>
  4. Use Greedy Grammars for Programming Languages

    main

    Greedy Grammars are designed to handle programming language syntax by using standard lexer rules. They are defined using the greedy_grammar(grammar, skip=whitespace_regex) function and can be invoked from within a lazy grammar using gen(grammar=...).

    Key characteristics of Greedy Grammars:

    • Longest Match: Unlike lazy grammars, the greedy lexer takes the longest match for every lexeme (standard lexer behavior).
    • Skipping: It automatically skips over any content that matches the provided skip regex (e.g., whitespace).
    • Isolation: From the perspective of the outer (lazy) parser, a greedy_grammar() construct is treated as a single token, meaning the lexers do not interact.
    • Lexeme Construction: Lexemes in greedy grammars are constructed from regex() nodes and string literals (note that gen() nodes are not allowed inside the greedy grammar definition).
    def identifier():
      return regex(r'[a-zA-Z_][a-zA-Z0-9_]*')
    
    def sql_program():
      return select([
        "SELECT" + identifier() + "FROM" + identifier() + 
        optional("WHERE" + identifier() + "=" + ...),
        ...
      ])
    
    # Define the greedy grammar with whitespace skipping
    def sql():
      return greedy_grammar(sql_program(), skip=r'\s*')
    
    # Use the greedy grammar inside a lazy grammar
    def my_program():
      return f"""
    Query to list all animals
    ```sql
    {gen(grammar=sql(), suffix="```")}
    """
  5. Use parametric grammars for combinatorial structures

    main

    In llguidance, grammar rules can be parameterized by a 64-bit integer. This allows you to express complex combinatorial concepts like permutations, unique selections, or bounded counts concisely without writing massive context-free grammars.

    Technically, these are context-free grammars that are materialized lazily during Earley parsing. Each rule can have a single 64-bit integer parameter. You can pass an initial value to a rule using the rule_name::value syntax (e.g., perm::0x0).

    Example: Permutation of 3 elements

    This grammar ensures a, b, and c are used exactly once in any order:

    start    :  perm::0x0
    perm::_  :  ""                       %if is_ones([0:3])
             |  "a" perm::set_bit(0)     %if bit_clear(0)
             |  "b" perm::set_bit(1)     %if bit_clear(1)
             |  "c" perm::set_bit(2)     %if bit_clear(2)
  6. Understand Lazy Grammars and Lexemes

    main

    In llguidance, Lazy Grammars are produced when using gen() without a stop= parameter. These grammars use a lexer (or scanner) that treats literal strings and regular expressions inside gen() calls as lexemes (terminals).

    Key characteristics of Lazy Grammars:

    • Shortest Match: The lazy lexer takes the shortest match for every lexeme available in a given parser state.
    • Contextual Lexemes: Lexemes are only enabled in specific rows where the Earley parser allows them, making them inherently contextual.
    • Regex Requirement: All regexes used for lexemes must never match an empty string.
    • Performance Tip: To ensure high performance, use gen(regex=...) instead of using single-character strings or functions like zero_or_more(" ").
    # Example of a lazy grammar structure
    "Name: " + gen(regex=r'[A-Z][a-z]+', stop='\n') + "\n" + 
    select([
      "Married? " + gen(regex=r'Yes|No'),
      "Age: " + gen(regex=r'\d+', stop='\n')
    ]) + "\n"
  7. What are Fast-forward tokens?

    main
    Fast-forward (FF) tokens—also known as zero-entropy, forced, fixed, or jump tokens—are tokens added to a sequence in a single step due to grammar constraints. Instead of sampling tokens one by one, the system 'jumps' forward to the next valid state defined by the grammar. This process is similar to speculative decoding but is 100% correct because the tokens are forced by the grammar. This is highly efficient for generating structured data like JSON, where large portions of the sequence (e.g., keys and structural characters) are deterministic.
  8. How regex containment is checked for Slicers

    main

    To determine if a slice $S$ can be used to skip trie walking, LLGuidance checks if the slice is a subset of the prefix of the allowed lexeme $L$: $S \subseteq P(L)$.

    Because a full containment check is expensive, LLGuidance uses an under-approximation (it may return false even if containment is actually true).

    The Logic

    1. The lexeme $L$ is modeled as $(X^{{m,n}} & \sim E) T$, where $X$ is a character class, $E$ is a set of excluded patterns, and $T$ is a suffix.
    2. The engine checks if $S$ is of the form $Y^{{m',n'}}$.
    3. It verifies if $Y$ is contained in $X$ (using cached symbolic derivatives).
    4. It verifies if $n' \le n$.

    This approach ensures that if the check passes, the slice is guaranteed to be contained, allowing for a safe and massive speedup in mask computation.

  9. Use And/Not Operators in Terminal Definitions

    main

    LLGuidance extends regular expression capabilities by allowing & (AND) and ~ (NOT) operators in Lark terminal definitions (outside of /.../ syntax).

    • & (AND) binds tighter than | (alternation).
    • ~ (NOT) binds tighter than + or *.

    Important: Negation ~ may match invalid UTF-8 sequences. To ensure valid UTF-8, intersect your negated regex with /(?s:.*)/.

    Example: Matching ASCII lines that do not contain double newlines: ASCII_LINES: /[a-zA-Z \n]*/ & ~/(?s:.*)\n\n(?s:.*)/

    # Valid UTF-8 safe negation
    SAFE_NON_AAA: /(?s:.*)/ & ~/(?s:.*)AAA(?s:.*)/
  10. How to handle special tokens in llguidance

    main

    By default, llguidance treats sequences that look like special tokens (e.g., <|eot_id|>) as regular bytes. If you want these sequences to be treated as actual special tokens during tokenization, you must prefix the byte sequence with the marker byte 0xFF (255), defined as TokTrie::SPECIAL_TOKEN_MARKER.

    Implementation Details

    • Why 0xFF?: The byte 0xFF is not a valid UTF-8 byte, ensuring it does not occur in standard text inputs.
    • Rust Users: You cannot include 0xFF in a &str; you must work with &[u8].
    • Python Users: Be careful with encoding. Use b"\xFF" to represent the marker byte. Note that "\xFF".encode("utf-8") results in the two-byte sequence b"\xC3\xBF", which is not the correct marker.
    • When to use: You generally only need to manage these marker bytes when manually constructing the token array for a TokTrie constructor.
    # Correct way to represent the marker byte in Python
    marker = b"\xFF"
    
    # Incorrect way (this encodes the character, resulting in two bytes)
    # incorrect = "\xFF".encode("utf-8") # b'\xc3\xbf'
  11. Design strategies for LLM tool call grammars

    main

    When designing grammars for tool calls, you must decide between two primary strategies to prevent 'Out-of-Distribution' (OOD) behavior, where a model produces incoherent or garbled text because it is forced into an unfamiliar format.

    1. Model-specific grammar (Recommended for production): Create a grammar that matches the exact native output format the model was trained to produce (e.g., specific delimiters like <|tool_call|> or specific JSON structures). This maximizes reliability and performance.
    2. Prompt-injection for a unified format: Use the system prompt to instruct the model to use a standardized, universal format (e.g., prefixing calls with functools[). This allows you to use a single grammar across multiple different models, but requires thorough testing to ensure models follow the injected instructions consistently.
  12. How the Token Trie traversal algorithm works

    main

    The traversal algorithm computes the set of allowed tokens by traversing the trie in DFS order. The results are stored in a logits array where entries with a value of 0.0 indicate an allowed token.

    Traversal Logic

    1. DFS Order: The tree is laid out in memory in DFS order to allow for efficient iteration without explicit recursion.
    2. Byte Validation: As the algorithm traverses, it checks if a byte is allowed (byte_allowed(n.byte)). If allowed, it pushes the byte to a stack and marks the corresponding token_id in the logits array as 0.0.
    3. Stack Management: The algorithm uses pop_bytes() to manage the stack of bytes. This is used to backtrack to parent nodes when a branch is completed or when a node is skipped.
    4. Performance Optimization: The implementation avoids recursion by using a loop and uses bit operations for pop_bytes to ensure the branch is branchless, minimizing mispredictions.
    // Example of the logic used to mark allowed tokens
    let mut logits = vec![-100.0; VOCAB_SIZE + 1];
    
    // Simplified traversal logic
    fn traverse(mut p: usize, nodes: &[TrieNode], logits: &mut [f32]) {
        // ... traversal logic that sets logits[n.token_id] = 0.0;
    }