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
19 changes: 16 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
steps:
- name: Check out sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
Expand All @@ -42,6 +44,7 @@ jobs:
- name: Check out sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand All @@ -68,6 +71,7 @@ jobs:
- name: Check out sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand All @@ -92,6 +96,7 @@ jobs:
- name: Check out sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand All @@ -112,6 +117,7 @@ jobs:
- name: Check out sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand All @@ -135,11 +141,12 @@ jobs:
package:
needs: native
runs-on: ubuntu-24.04
timeout-minutes: 45
timeout-minutes: 60
steps:
- name: Check out sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand All @@ -148,6 +155,12 @@ jobs:
with:
save-if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
- name: Install native dependencies
run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends build-essential cmake ninja-build
run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends build-essential cmake ninja-build jq
- name: Install Just
uses: extractions/setup-just@v3.1.0
with:
just-version: "1.40.0"
- name: Test crate archives and downstream consumers
run: just package-test
- name: Dry-run workspace publish
run: cargo publish --workspace --locked --dry-run
run: just publish-dry-run
5 changes: 5 additions & 0 deletions .github/workflows/platforms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- name: Check out exact sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Verify checkout and native pin
shell: bash
Expand Down Expand Up @@ -52,6 +53,7 @@ jobs:
- name: Check out exact sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Verify checkout, native pin, and host
shell: bash
Expand All @@ -76,6 +78,7 @@ jobs:
- name: Check out exact sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Verify checkout, native pin, and host
shell: bash
Expand All @@ -102,6 +105,7 @@ jobs:
- name: Check out exact sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Verify checkout, native pin, and Metal toolchain
shell: bash
Expand Down Expand Up @@ -131,6 +135,7 @@ jobs:
- name: Check out exact sources
uses: actions/checkout@v4.4.0
with:
persist-credentials: false
submodules: recursive
- name: Verify checkout and native pin
shell: bash
Expand Down
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ All notable changes to this project will be documented in this file.

## Unreleased

## [0.0.1] - 2026-08-22

### 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, 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.
- 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 with request-scoped callback state, 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, explicit/`HF_TOKEN`/cached authentication precedence, cache/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.
- RAII ownership for native engines, requests, completions, and strings, including callback panic containment and callback-thread-safe deferred request cleanup.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ members = ["vllm-cpp", "vllm-cpp-sys"]
resolver = "2"

[workspace.package]
version = "0.1.0"
version = "0.0.1"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/querymt/vllm-cpp-rs"
rust-version = "1.85"

[workspace.dependencies]
vllm-cpp-sys = { version = "=0.1.0", path = "vllm-cpp-sys", default-features = false }
vllm-cpp-sys = { version = "=0.0.1", path = "vllm-cpp-sys", default-features = false }
11 changes: 11 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,17 @@ package-test:
echo "crate versions differ: sys=$version safe=$safe_version" >&2
exit 1
}
native_version=$(sed -nE \
's/^project\(vllm_cpp VERSION ([0-9]+\.[0-9]+\.[0-9]+) LANGUAGES CXX\)$/\1/p' \
"$repo_root/vllm-cpp-sys/vllm.cpp/CMakeLists.txt")
[[ -n $native_version ]] || {
echo 'could not read the native project version from vllm.cpp/CMakeLists.txt' >&2
exit 1
}
[[ $native_version == "$version" ]] || {
echo "native and crate versions differ: native=$native_version crates=$version" >&2
exit 1
}
jq -e --arg version "$version" '
[.packages[] | select(.name == "vllm-cpp" or .name == "vllm-cpp-sys")]
| length == 2
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The packaged [`vllm-cpp` guide](vllm-cpp/README.md) covers local and Hugging Fac

`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, 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.
`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. Processor state remains registered only through the blocking call or asynchronous request lifetime; stale native invocations after cleanup become no-ops. `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.

Expand Down
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Releases are prepared and published manually. The repository does not tag, publi
test -z "$(git -C vllm-cpp-sys/vllm.cpp status --short --untracked-files=all)"
```

3. Confirm the release version in the workspace manifest, both normalized package manifests, and `Cargo.lock`. Both crates must use the same version, and `vllm-cpp` must depend on exactly that `vllm-cpp-sys` version.
3. Confirm the release version in the workspace manifest, both normalized package manifests, `Cargo.lock`, and the pinned native `project(vllm_cpp VERSION ...)` declaration. Both crates and the native CMake project must use the same version, and `vllm-cpp` must depend on exactly that `vllm-cpp-sys` version. The CMake project declaration is the native release version authority; do not derive the crate version from `git describe` or the nearest native tag.
4. Confirm the native gitlink is `34aedfbe8ed9779697905541a62e2160ccfd9c05`, `VLLM_ABI_VERSION` is 10 in the pinned public C header and checked-in bindings, and generated bindings have no drift.
5. Move the relevant entries from `Unreleased` to a dated version section. Describe only validated support; preserve known backend/runtime blockers.
6. Audit dual-license metadata, crate license files, `NOTICE`, `THIRD_PARTY.md`, imported license texts, and the package inventory. Do not publish models, fixtures, build output, caches, SDKs, external CUTLASS trees, or repository-local paths.
Expand Down
4 changes: 4 additions & 0 deletions vllm-cpp-sys/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ ports, and/or vendors code from third-party projects, including:
- Marlin / GPTQ-Marlin mixed-precision kernels (Apache-2.0), as vendored
by vLLM under src/vt/cuda/marlin/.
- NVIDIA CUTLASS (BSD-3-Clause) — used for FP4/FP8 GEMM kernels.
- FlashAttention-2 (BSD-3-Clause) — vendored CUDA attention sources under
src/vt/cuda/flash_attn/.
- Flash Linear Attention (MIT) — vendored Triton kernel sources under
triton_kernels/.
- xgrammar (Apache-2.0) — C++ grammar core, vendored for structured output.
- cpp-httplib (MIT) and nlohmann/json (MIT) — header-only dependencies.
- google/minja (MIT) - header-only Jinja engine vendored under
Expand Down
2 changes: 1 addition & 1 deletion vllm-cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ In the repository checkout, `just setup-test-model` explicitly resolves `Qwen/Qw
## API and ownership

- `EngineBuilder` configures and loads a model. `Engine` is `Clone + Send + Sync`; clones share one reference-counted native engine.
- `SamplingParams` owns stop strings, structured constraints, and an optional `Send + Sync` custom logits processor. The processor receives generated token IDs and a mutable logits row each decode step; panics are contained and returned as `Error::LogitsProcessorPanicked`. Processor-backed generation must have a finite `max_tokens` bound because ABI v10 cannot abort from that callback. Each processor invocation retains its state until the engine is dropped because ABI v10 has no sampler-quiescence primitive.
- `SamplingParams` owns stop strings, structured constraints, and an optional `Send + Sync` custom logits processor. The processor receives generated token IDs and a mutable logits row each decode step; panics are contained and returned as `Error::LogitsProcessorPanicked`. Processor-backed generation must have a finite `max_tokens` bound because ABI v10 cannot abort from that callback. Processor state remains registered only through the blocking call or asynchronous request lifetime; stale native invocations after cleanup become no-ops.
- Completion, chat, error, and stream text is copied into Rust-owned values before native storage is released or reused.
- Blocking `complete`, `complete_stream`, `chat_json`, and `chat_stream_json` calls keep borrowed callbacks alive only for the call. Callback panics are caught before crossing C and resumed after the native call returns.
- `Engine::submit` returns a `Request` before generation finishes. A request retains its engine and callback until native free/join completes, is `Send`, and is deliberately not `Sync`.
Expand Down
27 changes: 4 additions & 23 deletions vllm-cpp/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ use std::mem::MaybeUninit;
use std::os::raw::c_char;
use std::path::{Path, PathBuf};
use std::ptr::{self, NonNull};
use std::sync::{Arc, Mutex};
use std::sync::Arc;

use vllm_cpp_sys as ffi;

use crate::callback::{
callback_trampoline, CallbackState, StreamControl, StreamEvent, StreamOutcome,
};
use crate::error::{invalid_configuration, status_result, Error};
use crate::params::{LogitsProcessorState, SamplingParams, SchedulerPolicy, Toggle};
use crate::params::{SamplingParams, SchedulerPolicy, Toggle};

/// A cloneable vllm.cpp serving engine.
#[derive(Clone)]
Expand All @@ -21,7 +21,6 @@ pub struct Engine {

pub(crate) struct EngineInner {
pub(crate) raw: NonNull<ffi::vllm_engine>,
logits_processors: Mutex<Vec<Arc<LogitsProcessorState>>>,
}

impl std::fmt::Debug for Engine {
Expand Down Expand Up @@ -75,14 +74,6 @@ pub struct Completion {
}

impl Engine {
pub(crate) fn retain_logits_processor(&self, state: Arc<LogitsProcessorState>) {
self.inner
.logits_processors
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(state);
}

/// Starts configuring an engine for a model directory or GGUF file.
pub fn builder(model_path: impl Into<PathBuf>) -> EngineBuilder {
EngineBuilder::new(model_path)
Expand All @@ -97,9 +88,6 @@ impl Engine {
pub fn complete(&self, prompt: &str, params: &SamplingParams) -> Result<Completion, Error> {
let prompt = to_cstring(prompt, "prompt")?;
let params = params.marshal()?;
if let Some(logits_processor) = params.logits_processor() {
self.retain_logits_processor(logits_processor);
}
let mut raw = MaybeUninit::<ffi::vllm_completion>::uninit();
// SAFETY: the engine is owned and live, all pointers remain valid for the
// call, and out storage is initialized by native code on success.
Expand Down Expand Up @@ -142,9 +130,6 @@ impl Engine {
{
let prompt = to_cstring(prompt, "prompt")?;
let params = params.marshal()?;
if let Some(logits_processor) = params.logits_processor() {
self.retain_logits_processor(logits_processor);
}
let mut state = CallbackState::new(&mut callback);
// SAFETY: state has a stable stack address for this blocking call; the C
// API does not retain user_data after returning.
Expand Down Expand Up @@ -238,8 +223,7 @@ impl Engine {
impl Drop for EngineInner {
fn drop(&mut self) {
// SAFETY: EngineInner exclusively owns this live handle. Native teardown
// joins engine workers before retained logits states drop with the other
// fields, so no callback can outlive its user_data.
// joins engine workers before returning.
unsafe { ffi::vllm_engine_free(self.raw.as_ptr()) };
}
}
Expand Down Expand Up @@ -405,10 +389,7 @@ impl EngineBuilder {
message: "vllm_engine_load succeeded without a handle".to_owned(),
})?;
Ok(Engine {
inner: Arc::new(EngineInner {
raw,
logits_processors: Mutex::new(Vec::new()),
}),
inner: Arc::new(EngineInner { raw }),
})
}
}
Expand Down
31 changes: 28 additions & 3 deletions vllm-cpp/src/hf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TOKENIZER_CONFIG: &str = "tokenizer_config.json";
const SAFETENSORS: &str = "model.safetensors";
const SAFETENSORS_INDEX: &str = "model.safetensors.index.json";
const DEFAULT_REVISION: &str = "main";
const HF_TOKEN: &str = "HF_TOKEN";

/// A synchronous Hugging Face model resolver.
///
Expand Down Expand Up @@ -101,7 +102,9 @@ impl HuggingFaceModel {
self
}

/// Overrides the cached Hugging Face token for this resolver.
/// Sets the Hugging Face token for this resolver.
///
/// An explicit token takes precedence over `HF_TOKEN` and the cached token.
#[must_use]
pub fn token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
Expand Down Expand Up @@ -166,12 +169,18 @@ impl HuggingFaceModel {

fn api_builder(&self, cache: Cache) -> ApiBuilder {
let builder = ApiBuilder::from_cache(cache).with_progress(self.progress);
match &self.token {
Some(token) => builder.with_token(Some(token.clone())),
match self.selected_token(std::env::var(HF_TOKEN).ok()) {
Some(token) => builder.with_token(Some(token)),
None => builder,
}
}

fn selected_token(&self, environment_token: Option<String>) -> Option<String> {
self.token
.clone()
.or_else(|| environment_token.filter(|token| !token.trim().is_empty()))
}

fn requested_repo(&self) -> Repo {
Repo::with_revision(self.repo_id.clone(), RepoType::Model, self.revision.clone())
}
Expand Down Expand Up @@ -762,6 +771,22 @@ mod tests {
assert_eq!(pinned.requested_repo().revision(), REVISION);
}

#[test]
fn explicit_token_precedes_environment_token() {
let explicit = HuggingFaceModel::safetensors(REPO).token("explicit");
assert_eq!(
explicit.selected_token(Some("environment".to_owned())),
Some("explicit".to_owned())
);

let environment = HuggingFaceModel::safetensors(REPO);
assert_eq!(
environment.selected_token(Some("environment".to_owned())),
Some("environment".to_owned())
);
assert_eq!(environment.selected_token(Some(" ".to_owned())), None);
}

#[test]
fn debug_redacts_explicit_token() {
let model = HuggingFaceModel::safetensors(REPO)
Expand Down
Loading
Loading