Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,21 @@ jobs:
run: '"$RUNNER_TEMP/venv/bin/python" -m unittest discover -s serve -p "test_*.py" -t .'
- name: Installer and tools tests
run: '"$RUNNER_TEMP/venv/bin/python" -m unittest discover -s tools -p "test_*.py"'

grammar-windows:
runs-on: windows-latest
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Python packages
run: |
python -m venv "$RUNNER_TEMP/grammar-venv"
"$RUNNER_TEMP/grammar-venv/Scripts/python.exe" -m pip install --quiet numpy jinja2 regex pyyaml tqdm psutil 'jsonschema>=4.18,<5'
- name: Grammar API, native paths and subprocess protocol
run: '"$RUNNER_TEMP/grammar-venv/Scripts/python.exe" -m unittest serve.test_grammar -v'
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1786,3 +1786,5 @@ endif()

# the glm-dsa (GLM-5.3) sources and tests, kept in their own file
include(cmake/glm_dsa.cmake OPTIONAL)

include(cmake/ds4_grammar.cmake)
64 changes: 64 additions & 0 deletions cmake/ds4_grammar.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Tokenizer-aware constrained decoding uses the same pinned llama.cpp as ggml.
# llama is CPU-only, vocab-only at runtime; no second inference engine is loaded.
# CPU-only and unrelated engine builds retain their existing dependency graph.
# An explicit ON also allows building the metadata-only checker without DS4.
option(MAYA_DS4_GRAMMAR "Enable DeepSeek native GBNF constrained decoding" OFF)
if(MAYA_DS4_GRAMMAR)
if(NOT STRATA_NATIVE_EXPERTS)
message(FATAL_ERROR "MAYA_DS4_GRAMMAR requires STRATA_NATIVE_EXPERTS")
endif()
set(_grammar_src "${STRATA_GGML_DIR}")
if(NOT _grammar_src)
set(_grammar_src "${strata_llamacpp_SOURCE_DIR}")
endif()
if(NOT EXISTS "${_grammar_src}/src/llama-grammar.cpp")
message(FATAL_ERROR "DeepSeek grammar requires the full pinned llama.cpp source")
endif()
file(GLOB _grammar_core CONFIGURE_DEPENDS "${_grammar_src}/src/*.cpp")
file(GLOB _grammar_models CONFIGURE_DEPENDS "${_grammar_src}/src/models/*.cpp")
list(REMOVE_ITEM _grammar_core "${_grammar_src}/src/llama-sampler.cpp")
set(_state "${CMAKE_CURRENT_LIST_DIR}/../src/ds4/ds4_grammar_state.cpp")
set_source_files_properties("${_state}" PROPERTIES
COMPILE_DEFINITIONS "MAYA_DS4_LLAMA_SAMPLER_SOURCE=\"${_grammar_src}/src/llama-sampler.cpp\"")
add_library(maya_ds4_grammar STATIC ${_grammar_core} ${_grammar_models} "${_state}")
target_compile_features(maya_ds4_grammar PUBLIC cxx_std_17)
# Preserve the pinned source's version macros rather than borrowing Maya's.
file(STRINGS "${_grammar_src}/CMakeLists.txt" _grammar_version_lines
REGEX "^set\\(LLAMA_VERSION_(MAJOR|MINOR|PATCH) [0-9]+\\)")
set(LLAMA_VERSION "maya-grammar")
set(LLAMA_VERSION_MAJOR 0)
set(LLAMA_VERSION_MINOR 0)
set(LLAMA_VERSION_PATCH 0)
foreach(_line IN LISTS _grammar_version_lines)
if(_line MATCHES "^set\\((LLAMA_VERSION_(MAJOR|MINOR|PATCH)) ([0-9]+)\\)")
set("${CMAKE_MATCH_1}" "${CMAKE_MATCH_3}")
endif()
endforeach()
if(_grammar_version_lines)
set(LLAMA_VERSION "${LLAMA_VERSION_MAJOR}.${LLAMA_VERSION_MINOR}.${LLAMA_VERSION_PATCH}-maya-grammar")
endif()
set(LLAMA_BUILD_NUMBER 0)
set(LLAMA_BUILD_COMMIT "pinned")
configure_file("${_grammar_src}/src/llama-version.h.in" "${CMAKE_CURRENT_BINARY_DIR}/ds4-grammar/llama-version.h" @ONLY)
target_include_directories(maya_ds4_grammar PRIVATE "${_grammar_src}/src" "${CMAKE_CURRENT_BINARY_DIR}/ds4-grammar"
PUBLIC "${_grammar_src}/include" "${_grammar_src}/vendor/nlohmann" "${CMAKE_CURRENT_LIST_DIR}/../src/ds4")
target_compile_definitions(maya_ds4_grammar PUBLIC MAYA_DS4_GRAMMAR=1)
target_link_libraries(maya_ds4_grammar PUBLIC ggml)
if(TARGET maya-ds4)
target_link_libraries(maya-ds4 PRIVATE maya_ds4_grammar)
endif()
add_executable(maya-ds4-grammar-check "${CMAKE_CURRENT_LIST_DIR}/../tools/ds4/grammar_check.cpp")
target_link_libraries(maya-ds4-grammar-check PRIVATE maya_ds4_grammar)
if(MINGW)
target_link_options(maya-ds4-grammar-check PRIVATE -municode)
endif()
endif()
if(MAYA_DS4_GRAMMAR)
add_executable(maya-ds4-grammar-state-test EXCLUDE_FROM_ALL "${CMAKE_CURRENT_LIST_DIR}/../tools/ds4/grammar_state_test.cpp")
target_link_libraries(maya-ds4-grammar-state-test PRIVATE maya_ds4_grammar)
endif()
if(STRATA_BUILD_TESTS)
add_executable(ds4_grammar_path_test "${CMAKE_CURRENT_LIST_DIR}/../tests/ds4_grammar_path_test.cpp")
target_include_directories(ds4_grammar_path_test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../src/ds4")
add_test(NAME ds4_grammar_path_test COMMAND ds4_grammar_path_test)
endif()
34 changes: 34 additions & 0 deletions docs/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2331,3 +2331,37 @@ untested and refused unless this is set when the setup or `--check` runs (in the
| Variable | Default | What it does |
|---|---|---|
| `MAYA_HIP_ALLOW_ROCM6` | unset (ROCm 6 refused) | `1`: the setup goes on with a ROCm 6.x, with a warning (untested: the build or the engine may fail); the build then takes hipBLAS 2's `HIPBLAS_V2` API and leaves out `hipblasSetWorkspace`, which hipBLAS 2 does not have |

## Native GBNF (DeepSeek)

A DeepSeek engine built with native grammar support advertises `INFO grammar=1 grammar_replay=1`.
`POST /v1/chat/completions` accepts a GBNF string in `grammar`, `guided_grammar`, or
`structured_outputs.grammar`. Identical aliases may be sent together; conflicting constraints are a 400.
`structured_outputs.disable_any_whitespace` is accepted as a boolean compatibility field: an explicit
GBNF controls its own whitespace, so this field does not rewrite it.

```json
{"messages":[{"role":"user","content":"Choose A or B"}],
"grammar":"root ::= \"A\" | \"B\"",
"chat_template_kwargs":{"enable_thinking":false},"max_tokens":16}
```

The server compiles the grammar with the native `maya-ds4-grammar-check` helper before sending HTTP
headers. Install that executable beside `maya-ds4`, or set `grammar_checker` in the server's engine
configuration. The helper uses the engine's `--pack` tokenizer; no model weights or GPU are loaded for
validation. Missing native support or an invalid grammar returns 400, including for streaming requests.
The request then constrains native sampling rather than adding an instruction to the prompt. If a grammar
has no valid continuation during generation, the request reports an error; streaming clients can already
have received a partial answer before that error.

Each native request has its own compiled state. Fair scheduling resumes it by replaying its generated token
IDs, including the end-of-thinking trigger when applicable. The loop guard is disabled for constrained
requests, since its inserted steering text would change the grammar's language. Streaming and nonstreaming
chat completions use the same native grammar path. Responses API extensions for GBNF are not provided;
unsupported fields there are rejected. Existing `response_format` behavior is separate.

The native `grammar_hex=1` capability transports temporary grammar paths as UTF-8 hex, so spaces and
Unicode in the platform's temporary directory remain one protocol field. Older engines use `grammar=`
only for paths without whitespace or NUL; an unsafe legacy path is rejected explicitly. A host subprocess
test covers hex paths, parallel routes, replay fields and cancellation ownership with spaces and Unicode.
This is CPU protocol coverage, not Windows GPU qualification.
60 changes: 60 additions & 0 deletions docs/deepseek-grammar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# DeepSeek constrained decoding

Build with `MAYA_DS4_GRAMMAR=ON` (off by default, including native DeepSeek builds).
CPU-only and unrelated engine builds also leave it off by default; explicitly enable
it to build the host checker there. The CPU grammar library uses the
same pinned llama.cpp checkout as ggml; no GPU llama backend or model tensors
are loaded. A full llama.cpp source checkout is required when overriding
`STRATA_GGML_DIR`. `MAYA_DS4_GRAMMAR=OFF` retains the ordinary engine without
the grammar capability.

Only an explicit `grammar`, `guided_grammar` or `structured_outputs.grammar`
selects native GBNF validation. Other structured-output fields keep their existing
server handling, including the ordinary `response_format` JSON response path.

`maya-ds4-grammar-check --pack PACK --grammar FILE` validates a UTF-8 GBNF file
against the pack's exact tokenizer before an HTTP response starts. Success is
exit 0 and `GRAMMAR_OK`; invalid grammar, missing tokenizer metadata or an empty
initial allowed set returns exit 1 with a diagnostic on stderr. The helper
never acquires a GPU. The pack's `tokenizer/{vocab.json,token_type.json,
merges.txt,tokenizer.json}` supply token IDs, token bytes, token types and EOS.
The native engine checks that this vocabulary matches the model's output size.

The engine advertises `INFO grammar=1 grammar_replay=1 grammar_hex=1`. A request may carry:

- `grammar_hex=HEX`: the hex-encoded UTF-8 bytes of a GBNF file path, preserving
spaces and Unicode. The file must have a `root` rule and is read and compiled
before prompt processing. Empty files, NUL bytes and malformed path encodings
are rejected. Clients should use this field when `grammar_hex=1` is advertised.
- `grammar=FILE`: the legacy unencoded path, retained for paths without spaces.
Supplying both path fields is an error.
- `grammar_after=ID`: delay enforcement until this token is emitted. The trigger
itself is not consumed by the grammar.
- `grammar_prefix=ID,...`: replay previously emitted tokens into a fresh state
when a frontend resumes a request. These IDs must satisfy the grammar and
must not contain EOS. The prefix may include a delayed activation trigger.

Grammar state belongs to the request, independently of its conversation slot,
KV/prefix cache or cancelled predecessors. Each emitted token commits once.
DSpark and lookup verification clone this state and mask only the next reachable
target row; a rejected draft never advances the clone. Sampled verification
retains its proposal probabilities and residual correction. The original GPU
sampler still applies bias, repetition penalties, temperature, top-k, top-p,
min-p and the seeded random stream. Requests without grammar skip masking.
Forced thinking-budget closure is disabled for constrained requests so it cannot
inject a token outside the grammar.

Invalid requests report `ERR [rid] grammar: ...` before prompt processing. If a
reachable generation state has no finite permitted token, only that request
ends with `DONE ... grammar`; frontends must expose this as an error, rather
than publish a successful partial answer. Device failures and nonfinite raw
logits remain engine failures.

The optional host test target `maya-ds4-grammar-state-test` takes one `PACK`
argument. It exercises clone isolation, assignment, fresh request reset,
delayed activation, EOS, malformed syntax and file paths with spaces/Unicode without running inference.

A linked pool part or mixed-GPU split layout samples in the other engine. It
reports `grammar=0 grammar_replay=0 grammar_hex=0` and refuses explicit grammar
requests before changing request state. The mixed-GPU helper layout keeps its
sampling head local and supports grammar when built with the option enabled.
Loading