Repository navigation
feat: pass an optional logits_processor through to llama.cpp sampling - #6
Merged
da03 merged 3 commits intoSep 13, 2026
Merged
Conversation
Add an optional `logits_processor` argument to `PawFunction.__call__` and thread it into the per-token `Llama.sample()` call the runtime already makes. It enables token-level constrained decoding (a regex or JSON-schema processor, for example) without reaching into `PawFunction._llm`. Defaults to `None`, which is what `Llama.sample()` already defaults to, so sampling, output parsing, and prefix caching are unchanged for existing callers. `grammar` is deliberately not exposed: `sample()` builds a temporary sampler chain per call, so a grammar passed per token would reset to its root rule on every token.
Passing logits_processor=None unconditionally broke callers whose sample() takes a narrower signature, including the test doubles in tests/test_local_program.py. Build the kwarg conditionally so an unset processor leaves the sampling call byte-identical to previous releases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XfkWkQS5DsDZmx69WBDhH
olliemnicholls
force-pushed
the
logits-processor-passthrough
branch
from
September 11, 2026 15:41
943c2ec to
05228a5
Compare
Capture processor and score-conversion errors before ctypes can swallow them, then re-raise before using the sampled token. Add native-callback regression coverage and clarify the low-level inference hook.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
When the output needs a specific shape (JSON matching a schema, one of exactly two labels) the only lever today is the spec text; nothing enforces the shape at sampling time. llama.cpp already implements constrained decoding:
Llama.sample()takes alogits_processor, andruntime_llamacpp.pyalready callsself._llm.sample(temp=...)once per token. The capability is in the runtime but isn't reachable from the public API, so the only way to use it today is to reachPawFunction._llm— a private attribute with no stability contract.A
logits_processoris stateless from llama.cpp's point of view (it receives the fullinput_idson every call), so a straight per-token passthrough is enough; there is no sampler state to carry across tokens. In my own use the masking costs ~0.40 ms/token against llama.cpp's 151k-token vocabulary. The cost that matters is building the processor's own token tables up front, and that falls on the caller, not on the SDK.What changed
PawFunction.__call__takes a new optionallogits_processor(
llama_cpp.LogitsProcessorList | None, defaultNone), threaded throughPawFunction._generateinto the existingself._llm.sample(...)call, so it applies toevery generated token in both compiled and adapter-free base mode.
programasweights/runtime_llamacpp.py; three tests intests/test_base_interpreter.py; docstring,README.md,docs/api-reference/python-sdk.mdandCHANGELOG.mdupdated.Why no
grammarA GBNF grammar would be more attractive; enforced in C++, no Python callback per token, but it is not a straight passthrough. In llama-cpp-python 0.3.x,
sample()builds a temporary sampler chain when none is attached and discards it after each call, andadd_grammarre-initialises the grammar sampler each time, so a grammar passed per token would reset to its root rule on every token. Supporting it properly means creating one sampler per__call__, asLlama.generate()does. That is a larger change and a separate decision; happy to open it separately if you'd want it.Testing
The hermetic suite CI runs goes from 132 to 135 passing, no failures before or after. The three new tests use the existing fake
llama_cppfixture: that a compiled program passes the processor list on every generated token and the processor is invoked once per token with a token sequence that grows by one each time; that base mode passes it through the same way; and thatsample()still receiveslogits_processor=Nonewhen the argument is unset. Also checked end to end outside the suite against a real cached adapter on CPU (n_gpu_layers=0, llama-cpp-python 0.3.35): a processor masking everything except one token changed the output as expected, and unconstrained output was unchanged.One thing worth a glance from your side — I verified against 0.3.35, but
pyproject.tomlallows>=0.3.0,<=0.3.19, so it would be worth confirming thatlogits_processoris onLlama.sample()at the 0.3.0 floor too.Interest
I maintain a downstream project, paw-kit, whose schema-constrained decoding currently has to reach
PawFunction._llmto get at the sampling loop. That is the use case that prompted this change, and it is why I would rather the hook were public than keep monkeypatching a private attribute.