Skip to content
Merged
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file.
### Added

- Checked-in raw Rust declarations for the 19-symbol stable vllm.cpp C API at ABI version 10, with header, symbol, layout, and runtime conformance checks.
- A safe API for model loading, blocking completion and streaming, raw-JSON and optional serde chat, structured output, owned sampling parameters, and concurrent request submission, cancellation, waiting, and diagnostics.
- A safe API for model loading, blocking completion and streaming, raw-JSON and optional serde chat, structured output, owned sampling parameters, panic-contained custom logits processors, native version diagnostics, and concurrent request submission, cancellation, waiting, and diagnostics.
- An always-available synchronous `hf-hub` resolver for standalone GGUF files and runtime-complete sparse Safetensors snapshots, defaulting to the Hub's mutable `main` revision with an explicit branch/tag/commit override, cache/token/progress/offline controls, and no async runtime.
- Consistent local, Hugging Face GGUF, and Hugging Face Safetensors model-source arguments across every runnable example, with cache reuse and optional revisions; plus a weather extraction example and model-backed test using JSON-Schema structured output.
- A Clap-based interactive `chat` example with prompt/file startup input, retained system/user/assistant history, supported sampling controls, default streaming or blocking output, and shared local/Hugging Face resolution.
Expand All @@ -24,6 +24,8 @@ All notable changes to this project will be documented in this file.

### Known limitations

- The priority scheduler is selectable, and raw and serde chat request JSON can carry a `priority` field that the native OpenAI-compatible path parses and submits. Direct completion, completion streaming, and `Request` submissions currently default to priority zero and tie by arrival; caller-selected priorities for those direct APIs remain deferred until a future C ABI/API change.

- The supported runtime tier is native Linux x86_64 CPU. Accelerator features are experimental build/configuration surfaces, not runtime-support claims.
- Known native blockers include a CUDA teardown SIGSEGV after otherwise successful tests, a CUDA bf16 numerical tolerance failure, CUTLASS concurrent-output differences, incomplete Vulkan attention/model runtime, and external MLX deployment plus unvalidated release-lane model/runtime behavior.
- The hosted Metal lane checks compile/link only, the software Vulkan lane checks backend/ops only, and accelerator builds do not establish runtime correctness.
Expand Down
86 changes: 16 additions & 70 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ set shell := ["bash", "-euo", "pipefail", "-c"]

root := justfile_directory()
bindings_file := root + "/vllm-cpp-sys/src/bindings.rs"
model_revision := "c1899de289a04d12100db370d81485cdf75e47ca"

# Maintainer workflows require Just 1.40 or newer.

Expand Down Expand Up @@ -491,6 +490,7 @@ package-test:
examples/common/mod.rs \
examples/complete.rs \
examples/concurrent.rs \
examples/setup_test_model.rs \
examples/stream.rs \
examples/structured.rs \
src/callback.rs \
Expand Down Expand Up @@ -757,13 +757,16 @@ package-test:
EOF
cat > "$safe_consumer/src/main.rs" <<'EOF'
use vllm_cpp::{
abi_version, expected_abi_version, Engine, Error, HuggingFaceModel, SamplingParams,
abi_version, expected_abi_version, version, Engine, Error, HuggingFaceModel, SamplingParams,
};

fn main() {
assert_eq!(expected_abi_version(), 10);
assert_eq!(abi_version(), 10);
let _params = SamplingParams::greedy().max_tokens(1);
assert!(!version().expect("native version").is_empty());
let _params = SamplingParams::greedy()
.max_tokens(1)
.logits_processor(|_, logits| logits.fill(0.0));
let resolver = HuggingFaceModel::gguf("owner/model", "model.gguf")
.revision("revision")
.cache_dir("/nonexistent/vllm-cpp-rs-safe-package-hf-cache")
Expand Down Expand Up @@ -804,69 +807,24 @@ publish-dry-run:
# dry-run preserves Cargo's sys-first order without requiring sys on crates.io.
cargo publish --workspace --locked --dry-run --allow-dirty --no-verify

# Download and verify the pinned Qwen3-0.6B model fixture.
setup-test-model destination=env_var_or_default("VLLM_CPP_TEST_MODEL", env_var_or_default("XDG_CACHE_HOME", env_var("HOME") + "/.cache") + "/vllm-cpp-rs/Qwen3-0.6B-" + model_revision):
# Resolve the pinned Qwen3-0.6B test fixture into the standard Hugging Face cache.
setup-test-model:
#!/usr/bin/env bash
set -euo pipefail
revision={{ quote(model_revision) }}
base="https://huggingface.co/Qwen/Qwen3-0.6B/resolve/$revision"
destination={{ quote(destination) }}
mkdir -p "$destination"

files=(
LICENSE
config.json
generation_config.json
merges.txt
model.safetensors
tokenizer.json
tokenizer_config.json
vocab.json
)
for file in "${files[@]}"; do
if [[ ! -f $destination/$file ]]; then
echo "downloading $file" >&2
curl --fail --location --retry 3 --continue-at - \
"$base/$file" --output "$destination/$file"
fi
done

cat > "$destination/SHA256SUMS.expected" <<'EOF'
832dd9e00a68dd83b3c3fb9f5588dad7dcf337a0db50f7d9483f310cd292e92e LICENSE
660db3b73d788119c04535e48cf9be5f55bc3100841a718637ae695b442f27dd config.json
2325da0f15bb848e018c5ae071b7943332e9f871d6b60e2ed22ca97d4cb993d2 generation_config.json
8831e4f1a044471340f7c0a83d7bd71306a5b867e95fd870f74d0c5308a904d5 merges.txt
f47f71177f32bcd101b7573ec9171e6a57f4f4d31148d38e382306f42996874b model.safetensors
aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4 tokenizer.json
d5d09f07b48c3086c508b30d1c9114bd1189145b74e982a265350c923acd8101 tokenizer_config.json
ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910 vocab.json
EOF
(cd "$destination" && sha256sum --check SHA256SUMS.expected) >&2
printf '%s\n' "$destination"
cd {{ quote(root) }}
cargo run --quiet --locked -p vllm-cpp --example setup_test_model

# Run the full safe/request/model suites with ASan, UBSan, and leak detection.
sanitizers model=env_var_or_default("VLLM_CPP_TEST_MODEL", ""):
#!/usr/bin/env bash
set -euo pipefail
model={{ quote(model) }}
if [[ -z $model ]]; then
echo 'set VLLM_CPP_TEST_MODEL or pass model=<verified-model-directory>' >&2
echo 'set VLLM_CPP_TEST_MODEL or pass model=<prepared-model-directory>' >&2
exit 1
fi
required_model_files=(
model.safetensors
config.json
tokenizer.json
tokenizer_config.json
)
missing=()
for file in "${required_model_files[@]}"; do
[[ -f $model/$file ]] || missing+=("$file")
done
if ((${#missing[@]})); then
printf 'model fixture is incomplete at %s; missing:' "$model" >&2
printf ' %s' "${missing[@]}" >&2
printf '\n' >&2
if [[ ! -d $model ]]; then
echo "model fixture is not a directory: $model" >&2
exit 1
fi
cd {{ quote(root) }}
Expand Down Expand Up @@ -908,23 +866,11 @@ tsan model=env_var_or_default("VLLM_CPP_TEST_MODEL", ""):
fi
model={{ quote(model) }}
if [[ -z $model ]]; then
echo 'set VLLM_CPP_TEST_MODEL or pass model=<verified-model-directory>' >&2
echo 'set VLLM_CPP_TEST_MODEL or pass model=<prepared-model-directory>' >&2
exit 1
fi
required_model_files=(
model.safetensors
config.json
tokenizer.json
tokenizer_config.json
)
missing=()
for file in "${required_model_files[@]}"; do
[[ -f $model/$file ]] || missing+=("$file")
done
if ((${#missing[@]})); then
printf 'model fixture is incomplete at %s; missing:' "$model" >&2
printf ' %s' "${missing[@]}" >&2
printf '\n' >&2
if [[ ! -d $model ]]; then
echo "model fixture is not a directory: $model" >&2
exit 1
fi
cd {{ quote(root) }}
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Native builds require:
- Ninja or another CMake build tool.
- A C11 and C++20 compiler.
- A system linker and C++ standard library.
- Just 1.40 or newer for maintainer workflows, plus Git, `jq`, GNU tar, and `curl` for the model fixture recipe.
- Just 1.40 or newer for maintainer workflows, plus Git, `jq`, and GNU tar.

This repository provides a Nix development shell with the pinned development tools. Linux also has minimal CUDA and Vulkan shells:

Expand All @@ -43,13 +43,13 @@ git submodule update --init --recursive

The packaged [`vllm-cpp` guide](vllm-cpp/README.md) covers local and Hugging Face model resolution, safe ownership, callbacks, concurrency, features, link modes, and deployment. The [`vllm-cpp-sys` guide](vllm-cpp-sys/README.md) documents the raw ABI and native build boundary.

`Engine::load` accepts a native-compatible model directory or standalone GGUF. `HuggingFaceModel` synchronously resolves into the normal Hugging Face cache before engine construction, defaulting to the Hub's mutable `main` revision; `.revision(...)` can pin a branch, tag, or commit. GGUF mode selects one safe root file. Safetensors mode pins downloads to repository metadata's commit SHA and retrieves only native runtime requirements: root configuration/tokenizer files and either unsharded weights or an index plus all root shards. Every runnable example accepts a bare or explicit local path and both Hub artifact forms with optional `--revision`. Cached downloads are reused. Retrieval does not prove model/backend compatibility.
`Engine::load` accepts a native-compatible model directory or standalone GGUF. `HuggingFaceModel` synchronously resolves into the normal Hugging Face cache before engine construction, defaulting to the Hub's mutable `main` revision; `.revision(...)` can pin a branch, tag, or commit. GGUF mode selects one safe root file. Safetensors mode pins downloads to repository metadata's commit SHA and retrieves only native runtime requirements: root configuration/tokenizer files and either unsharded weights or an index plus all root shards. Every inference example accepts a bare or explicit local path and both Hub artifact forms with optional `--revision`. Cached downloads are reused. Retrieval does not prove model/backend compatibility.

`EngineBuilder` owns model settings and converts them to temporary C strings only for the load call. `SamplingParams` owns stop strings and structured constraints. Completion and chat strings are copied into Rust values before the matching native free function runs.
`EngineBuilder` owns model settings and converts them to temporary C strings only for the load call. `SamplingParams` owns stop strings, structured constraints, and optional `Send + Sync` custom logits processors. Processor panics are contained before the C boundary and reported through Rust errors; processor-backed generation must be bounded because ABI v10 has no callback abort channel. Each processor invocation retains its state until the engine is dropped because ABI v10 has no sampler-quiescence primitive. `version()` copies the linked native diagnostic version string. Completion and chat strings are copied into Rust values before the matching native free function runs.

`Engine` is `Clone + Send + Sync`; each `Request` retains the shared engine until native callback delivery has joined. A request is `Send` but deliberately not `Sync`. `submit` returns before generation finishes, and `Request` provides `is_done`, idempotent `cancel`, `wait`, and copied `native_error` diagnostics. `wait` classifies completion as `Completed`, `StoppedByCallback`, or `Cancelled`; an explicit asynchronous `Stop` is classified as `StoppedByCallback` even when returned for the terminal event.

All streaming callbacks receive copied UTF-8 deltas. Blocking callbacks may borrow stack data; their panics are caught before the C boundary and resumed only after the native call returns. Asynchronous callbacks must be `Send + 'static`, run on a native delivery thread, and report panic as `Error::CallbackPanicked` from `wait`. Waiting for or freeing a request from its own callback thread is prohibited by ABI v10: `wait` returns `Error::RequestCallbackThread`, while drop transfers cleanup to a prestarted reaper that owns the request, callback, and engine until native free/cancel/join completes. Chat methods accept raw OpenAI-compatible request JSON; enable `serde` for `serde_json::Value` request and response helpers.
All streaming callbacks receive copied UTF-8 deltas. Blocking callbacks may borrow stack data; their panics are caught before the C boundary and resumed only after the native call returns. Asynchronous callbacks must be `Send + 'static`, run on a native delivery thread, and report panic as `Error::CallbackPanicked` from `wait`. Waiting for or freeing a request from its own callback thread is prohibited by ABI v10: `wait` returns `Error::RequestCallbackThread`, while drop transfers cleanup to a prestarted reaper that owns the request, callback, and engine until native free/cancel/join completes. Chat methods accept raw OpenAI-compatible request JSON; enable `serde` for `serde_json::Value` request and response helpers. `SchedulerPolicy::Priority` selects the native queue. Raw and serde chat request JSON can carry a `priority` field that the native OpenAI-compatible path parses and submits. Direct completion, completion streaming, and `Request` submissions currently default to priority zero and tie by arrival; caller-selected priorities for those direct APIs require a future C ABI/API change.

See [the examples guide](vllm-cpp/examples/README.md) for ordinary Linux and optional Nix setup, commands for every example, and the interactive chat CLI's local/Hub model forms and generation options. Release-facing changes are recorded in the [changelog](CHANGELOG.md), and maintainers use the manual [release process](RELEASING.md).

Expand Down Expand Up @@ -105,15 +105,15 @@ Compilation does not establish runtime correctness. Known native evidence blocke

## Test Model and Sanitizers

Model-backed tests use Apache-2.0 `Qwen/Qwen3-0.6B` at pinned revision `c1899de289a04d12100db370d81485cdf75e47ca`. Download or reuse the cache and verify every file, then run exactly 15 blocking and request-lifecycle model tests serially, including choice and JSON-Schema structured-output enforcement:
Model-backed tests use Apache-2.0 `Qwen/Qwen3-0.6B` at pinned revision `c1899de289a04d12100db370d81485cdf75e47ca`. Explicitly resolve its complete Safetensors snapshot into the standard Hugging Face cache, then run exactly 18 blocking and request-lifecycle model tests serially, including choice and JSON-Schema structured-output enforcement:

```console
model=$(just setup-test-model)
VLLM_CPP_TEST_MODEL="$model" \
cargo test --locked -p vllm-cpp --release --test qwen3 -- --test-threads=1
```

The approximately 1.5 GB model stays in the user cache and is not included in repository or crate packages. Model-backed tests skip with an explanatory message when `VLLM_CPP_TEST_MODEL` is unset. When it is set, the test helper and sanitizer gate require `model.safetensors`, `config.json`, `tokenizer.json`, and `tokenizer_config.json` and report every missing file.
`just setup-test-model` is the only explicit test-fixture acquisition step. It uses `HuggingFaceModel` with the immutable revision above, honors normal `HF_HOME` and Hugging Face authentication, reuses the standard cache, and prints the resolved directory. The approximately 1.5 GB model is not included in repository or crate packages. Ordinary tests, sanitizers, and TSan never resolve or download models: `VLLM_CPP_TEST_MODEL` must name an externally prepared model directory. Model-backed tests skip with an explanatory message when it is unset; when set, tests and instrumentation recipes require it to be a directory.

AddressSanitizer, UndefinedBehaviorSanitizer, and leak detection run the full safe/request/model suites with native instrumentation. The Linux x86_64 GCC ThreadSanitizer lane runs selected request lifecycle tests individually and instruments native C++ only; it does not claim race coverage for Rust or the Rust standard library. Callback-thread self-drop remains in the normal and ASan/leak suites because its handoff uses uninstrumented Rust synchronization.

Expand Down
Loading
Loading