Repository navigation
feat: bring /v1/decision onto perf #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2da866a
server : add /v1/decision for parallel constrained decisions
thecodacus e65d203
parallel-decision : add README
thecodacus a46f6bc
perf(parallel-decision): score hybrid-model branches in one pass
thecodacus 62e33fb
common : do not propagate n_seq_decision to speculative (MTP) context…
akudo7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # shared engine: used by llama-parallel-decision and by llama-server's /decision endpoint | ||
| add_library(llama-decision STATIC decision-engine.cpp decision-engine.h) | ||
| target_include_directories(llama-decision PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) | ||
| target_link_libraries(llama-decision PUBLIC llama-common llama) | ||
| target_compile_features(llama-decision PUBLIC cxx_std_17) | ||
| # llama-server links it into a shared library (libllama-server-impl) | ||
| set_target_properties(llama-decision PROPERTIES POSITION_INDEPENDENT_CODE ON) | ||
|
|
||
| set(TARGET llama-parallel-decision) | ||
| add_executable(${TARGET} parallel-decision.cpp) | ||
| if(LLAMA_TOOLS_INSTALL) | ||
| install(TARGETS ${TARGET} RUNTIME) | ||
| endif() | ||
| target_link_libraries(${TARGET} PRIVATE llama-decision llama-common llama ${CMAKE_THREAD_LIBS_INIT}) | ||
| target_compile_features(${TARGET} PRIVATE cxx_std_17) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| # parallel-decision | ||
|
|
||
| Answer a finite JSON schema in one batched forward pass, instead of generating the JSON token by token. | ||
|
|
||
| Every field of a schema has a fixed set of allowed values (enums, booleans, bounded integers, numbers on a grid). | ||
| After the context, each field's allowed values are scored as token paths that fork from the same KV cache, so all | ||
| fields are answered in one `llama_decode` and cannot see each other. Each answer comes back with a probability, and | ||
| the JSON object is assembled by code, so it always matches the schema. | ||
|
|
||
| This directory holds the engine (`decision-engine.*`), a CLI (`llama-parallel-decision`), and the engine is also | ||
| served by `llama-server` as `POST /v1/decision`. | ||
|
|
||
| ## Build | ||
|
|
||
| Same as llama.cpp: | ||
|
|
||
| ```bash | ||
| cmake -B build -DGGML_CUDA=ON # or plain `cmake -B build` for CPU / Metal | ||
| cmake --build build --config Release -j | ||
| ``` | ||
|
|
||
| ## Run the server | ||
|
|
||
| `--decision-seqs N` reserves the sequence slots the decisions need: one holds the cached instructions, one per context | ||
| in flight, the rest are the parallel questions. It also switches the KV cache to unified, which is what lets the | ||
| branches share the context's cells. | ||
|
|
||
| ```bash | ||
| ./build/bin/llama-server -m model.gguf -ngl 99 -fa on -c 32768 --decision-seqs 24 --port 8096 | ||
| ``` | ||
|
|
||
| With a presets file, one loaded model serves chat and decisions: | ||
|
|
||
| ```ini | ||
| [*] | ||
| ngl = 99 | ||
| fa = on | ||
| jinja = 1 | ||
| parallel = 1 | ||
| cache-type-k = q8_0 | ||
| cache-type-v = q8_0 | ||
| decision-seqs = 24 | ||
|
|
||
| [gemma-4-12b] | ||
| model = ./models/gemma-4-12b-it-UD-Q4_K_XL.gguf | ||
| ctx-size = 32768 | ||
| ubatch-size = 512 | ||
| decision-seqs = 12 | ||
| ``` | ||
|
|
||
| ```bash | ||
| ./build/bin/llama-server --models-preset models.ini --models-max 1 --port 8096 | ||
| ``` | ||
|
|
||
| How many sequences a model affords depends on its attention. A plain-attention model shares the context's cells, so | ||
| 128 sequences cost almost nothing. A sliding-window model (Gemma) allocates its window per sequence, so keep it low | ||
| (12 on a 12 GB card). Hybrid models with recurrent layers (Qwen3.5, Nemotron-H) keep a recurrent state per | ||
| sequence (about 50 MB each for Qwen3.5 4B and 9B), and llama.cpp only batches their sequences together when they hold | ||
| the same number of tokens. The engine right-pads each group of branches to its longest one, so they still score in a | ||
| single pass; the padding comes after the token that is read, so it doesn't change the result. | ||
|
|
||
| ## POST /v1/decision | ||
|
|
||
| `contexts` is a list of 1-256 strings. They share one schema, one set of instructions, and one cached prefix; results | ||
| come back in the same order. | ||
|
|
||
| ```bash | ||
| curl http://localhost:8096/v1/decision -H "Content-Type: application/json" -d '{ | ||
| "model": "gemma-4-12b", | ||
| "instructions": "Answer each question about this support request from its state.", | ||
| "schema": { | ||
| "category": {"type": "enum", "choices": ["billing","technical","cancellation","other"], | ||
| "description": "What type of support request is this?"}, | ||
| "urgent": {"type": "boolean", "description": "Does this need urgent handling?"}, | ||
| "priority": {"type": "enum", "choices": ["low","medium","high","critical"], | ||
| "description": "Rate support priority."} | ||
| }, | ||
| "contexts": ["I was charged twice and need this fixed today."] | ||
| }' | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "object": "decision", | ||
| "results": [ | ||
| { | ||
| "decision": {"category": "billing", "urgent": true, "priority": "high"}, | ||
| "fields": { | ||
| "category": {"value": "billing", "probability": 1.0, "scored_nodes": 1, "tree": true}, | ||
| "urgent": {"value": true, "probability": 1.0, "scored_nodes": 1, "tree": true}, | ||
| "priority": {"value": "high", "probability": 0.74, "scored_nodes": 1, "tree": true} | ||
| }, | ||
| "usage": {"context_tokens": 21, "scored_rows": 14} | ||
| } | ||
| ], | ||
| "usage": {"prompt_tokens": 137, "cached_tokens": 116, "context_tokens": 21, "scored_rows": 14}, | ||
| "timings": {"prefill_ms": 50.7, "scoring_ms": 50.0, "total_ms": 100.7, "rounds": 1, "per_decision_ms": 100.7} | ||
| } | ||
| ``` | ||
|
|
||
| (That response is a real one: Gemma 4 12B on an RTX 3060, warm cache.) | ||
|
|
||
| ### Schema | ||
|
|
||
| Compact fields, or a JSON Schema object with `properties`: | ||
|
|
||
| | type | keys | notes | | ||
| |---|---|---| | ||
| | `enum` | `choices` (or `enum`) | 1-255 values | | ||
| | `boolean` | - | true / false | | ||
| | `integer` | `minimum`, `maximum` | 1-255 values | | ||
| | `number` | `minimum`, `maximum`, `step` (`multipleOf` in JSON Schema) | fixed-width decimals | | ||
|
|
||
| Numeric fields take `aggregate`: `mode` (default), `median` or `mean`. | ||
|
|
||
| ### Options | ||
|
|
||
| | field | default | meaning | | ||
| |---|---|---| | ||
| | `instructions` | `""` | prepended to the generated field catalogue; cached with it | | ||
| | `mode` | `auto` | `tree` scores every divergence node and returns exact probabilities; `greedy` walks the trie; `auto` picks tree up to `tree_max` values | | ||
| | `tree_max` | 128 | per-field switch between tree and greedy | | ||
| | `cache_prompt` | true | reuse the cached instructions + schema prefix | | ||
|
|
||
| ## CLI | ||
|
|
||
| `llama-parallel-decision` runs the same engine from a worker process (stdin/stdout protocol, one JSON request per | ||
| line). Environment: `DECIDE_TREE`, `DECIDE_TREE_MAX`, `DECIDE_NSEQ`, `DECIDE_SPLIT_BOUNDARY`. | ||
|
|
||
| ## A UI for it | ||
|
|
||
| [decision-playground](https://github.com/thecodacus/decision-playground) is a browser-only playground: it talks | ||
| straight to your llama-server, runs a decision and the same question as a chat completion side by side with live | ||
| timers, and has a small game whose agents decide through the endpoint. | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the CLI protocol description.
The README says the worker reads "one JSON request per line". The worker in
parallel-decision.cpp(Lines 141-150) reads something else. Each line is tab-separated:@shared|@fresh STATIC CONTEXT FIELD..., where each item is a file path. Each field file holds a count, the allowed values and a suffix. A user who follows the README will send JSON, and the worker exits withERROR request: ....📝 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents