The vLLM engine (GPU concurrent serving)
vLLM is one of Skulk's served engines: instead of loading the model
in-process, the worker launches an external vllm serve subprocess and proxies
its OpenAI HTTP API, the same managed-server-plus-proxy shape as the
llama_server engine. It exists for one reason: vLLM's continuous batching and
paged attention hold latency flat and grow aggregate throughput under
concurrent load, where a single-stream engine collapses. In a benchmark on
an A100 at 64-way concurrency, llama.cpp's time-to-first-token reached ~31
seconds while vLLM's stayed at ~0.5 seconds.
It coexists with the other engines rather than replacing them: MLX owns
Apple Silicon, and the llama.cpp engines remain the GGUF paths. vLLM is
GPU-only in Skulk's scope (vllm-cuda on NVIDIA, vllm-rocm on AMD CDNA).
When a model runs on vLLM
Two things have to line up, the same rule as every engine:
- The model card declares and ranks the engines that can serve it in
compatible_backends. A card that lists a vLLM backend is a vLLM candidate. - The node advertises a vLLM backend, which it does only when
SKULK_VLLM_BINpoints at a usablevllmCLI and a GPU backend resolves (declared viaSKULK_VLLM_BACKENDS, or inferred from the observed GPU vendor). A node without the binary is never a placement candidate for vLLM cards.
When several nodes qualify, placement prefers the card's higher-ranked backend, so the card is where the "this model is better on vLLM than on llama.cpp here" judgment lives.
Current scope
The engine serves single-node, streamed text generation. Its boundaries are enforced loudly rather than degraded silently:
- Tool calling is rejected with a clear error: retry without
toolsor use a model carded forllama_cpp/llama_server. - Per-token logprobs are rejected with a clear error: the OpenAI SSE proxy does not surface them, and Skulk refuses to silently omit what you asked for.
- Multi-node placement is refused. vLLM's own tensor and pipeline parallelism are not wired into Skulk placement.
- Reasoning is best-effort. Thinking controls (
enable_thinking,reasoning_effort) are forwarded so the model behaves as requested, and separated reasoning deltas are parsed into thinking chunks when the server emits them; on models where vLLM needs a family-specific reasoning parser to split thinking from content, the thinking text can arrive inline in the content stream instead.
The served context window is sized to the memory the cluster admitted for the
instance (passed as --max-model-len), never blindly to the model's full
trained context.
Setup
The easiest path is the one-command installer's flag on an NVIDIA Linux node:
curl -fsSL https://raw.githubusercontent.com/Foxlight-Foundation/Skulk/main/install.sh | bash -s -- --with-vllm
This creates a dedicated virtual environment at ~/.skulk/vllm-env with
Skulk's validated dependency matrix (a pinned vLLM release, a compatible
transformers, and the matching CUDA torch backend; several GB of wheels) and
records SKULK_VLLM_BIN=~/.skulk/vllm-env/bin/vllm in ~/.skulk/skulk.env,
which the service wrappers source. The separate venv is not an accident: Skulk's
own environment and vLLM currently require conflicting dependency versions, so
vLLM must never be installed into Skulk's venv. Skulk drives its CLI purely as
an external process.
Already have vLLM installed some other way? Point SKULK_VLLM_BIN at its CLI
before launching Skulk and the node advertises the engine; nothing else is
required.
Concurrency behavior and knobs
Unlike the in-process runners, which serialize one generation at a time, the
vLLM runner dispatches concurrently: it keeps multiple requests in flight
against the one vllm serve process at once, which is what lets the server's
continuous batching actually engage and decode them together.
SKULK_VLLM_MAX_CONCURRENT_REQUESTS(default 32) bounds how many generations the runner keeps in flight; requests beyond it queue in the runner's bounded pool. This is a client-side admission bound, not the server's batch width (vLLM batches up to its own--max-num-seqs).SKULK_VLLM_GPU_MEMORY_UTILIZATION(default 0.90) sets the fraction of GPU VRAM vLLM may use for weights plus KV cache, passed through as--gpu-memory-utilization.
Operationally: server startup on a large model can take a couple of minutes
(weight load, compilation, CUDA-graph capture) and is allowed a generous health
deadline; the server's own log is written to a deterministic per-runner file
under the system temp directory for postmortems. Cancelling a request aborts
its proxied HTTP connection, which stops the server-side generation; if the
runner process itself dies, the kernel reaps the vllm serve child so it never
orphans GPU memory.
Honest performance framing
vLLM's win is concurrency, not single-stream speed. Under concurrent load it holds time-to-first-token flat and grows aggregate throughput where the single-stream engines queue and collapse. For one request at a time, the in-process engines can be as fast or faster depending on the GPU generation (on GPUs without native FP4 support, in particular, a single stream can favor them). Skulk keeps the engines side by side precisely so the choice is made per model and per hardware rather than by ideology; the model card's backend ranking is where that choice is recorded.
vLLM runs card-driven speculative decoding for checkpoints that ship
native multi-token-prediction heads (Qwen3.6 among them): the card's
vllm_spec_method = "mtp" and vllm_spec_num_tokens map to vLLM's
--speculative-config, engaging the model's own prediction heads with no
separate draft model. Measured on an A100-80GB, this roughly doubles
single-stream decode on Qwen3.6-27B-FP8 (2.01x at depth 2, 77%
acceptance). Vendor schemes with a separately published speculator use the
same fields: vllm_spec_method = "dflash" plus vllm_spec_draft_repo
pairs Poolside's Laguna models with their block-parallel DFlash drafter
(vLLM 0.25.1 or later), with the drafter repo resolved through vLLM's own
Hugging Face cache at engine start (measured 1.35x single-stream on an
A100-80GB, which lacks native FP8; newer GPUs should land closer to the
vendor's 1.7-2.6x). Deep speculative depths need more scheduler budget
than vLLM's defaults provide, so for carded depths of 8 or more the runner
raises --max-num-batched-tokens automatically; shallow MTP depths run
with vLLM's defaults untouched. DFlash speculators also JIT their kernels
through NVRTC at engine start and need a CUDA 12.8+ toolchain on the node.
See Speculative Decoding for the other engines'
mechanisms.