Compare commits

..
Author SHA1 Message Date
mgoinandClaude Opus 4.8 3ef4bae91c [Kernel] Manual activation+quant fusion via QuantizedActivation (Llama)
Start the ActivationQuantFusionPass manual-fusion migration (RFC #43224)
on the producer side of the QuantizedActivation contract (#44260). Adds
maybe_fused_act_quant: given an activation and the linear it feeds, it
emits a QuantizedActivation via the fused silu_and_mul_quant kernel when
the linear advertises a consumable input_quant_key, and falls back to the
plain activation otherwise. Llama's MLP forward calls it for down_proj;
the change to model code is one line.

When manual fusion fires the silu_and_mul pattern is already consumed, so
the compiler ActivationQuantFusionPass finds nothing and the two never
double-fuse. Only (SiluAndMul, kFp8StaticTensorSym) is registered for now.

Smoke tested on RedHatAI/Llama-3.2-1B-Instruct-FP8 (default compiled +
CUDA-graph path): all down_proj layers take the fused branch and
generations are coherent. AI assistance (Claude) was used; all changed
lines are human-reviewed.

Signed-off-by: mgoin <mgoin64@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:15:14 +00:00
502 changed files with 6092 additions and 37342 deletions
+1 -3
View File
@@ -91,13 +91,11 @@ steps:
- vllm/model_executor/layers/fused_moe/experts/cpu_moe.py
- tests/quantization/test_compressed_tensors.py
- tests/quantization/test_cpu_wna16.py
- tests/quantization/test_cpu_w8a8.py
commands:
- |
bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 45m "
pytest -x -v -s tests/quantization/test_compressed_tensors.py::test_compressed_tensors_w8a8_logprobs
pytest -x -v -s tests/quantization/test_cpu_wna16.py
pytest -x -v -s tests/quantization/test_cpu_w8a8.py"
pytest -x -v -s tests/quantization/test_cpu_wna16.py"
- label: CPU-Distributed Tests (PP+TP)
depends_on: []
@@ -23,5 +23,4 @@ steps:
bash .buildkite/scripts/hardware_ci/run-intel-test.sh
'cd tests &&
export VLLM_WORKER_MULTIPROC_METHOD=spawn &&
pytest -v -s basic_correctness/test_cpu_offload.py &&
pytest -v -s basic_correctness/test_mem.py::test_end_to_end'
+1 -1
View File
@@ -128,10 +128,10 @@ steps:
bash .buildkite/scripts/hardware_ci/run-intel-test.sh
'cd tests &&
export VLLM_WORKER_MULTIPROC_METHOD=spawn &&
(pytest -v -s lora/test_mixtral.py --deselect="tests/lora/test_mixtral.py::test_mixtral_lora[4]" || true) &&
pytest -v -s lora/test_quant_model.py --deselect="tests/lora/test_quant_model.py::test_quant_model_lora[model0]" --deselect="tests/lora/test_quant_model.py::test_quant_model_lora[model1]" --deselect="tests/lora/test_quant_model.py::test_quant_model_tp_equality[model0]" &&
pytest -v -s lora/test_transformers_model.py &&
pytest -v -s lora/test_chatglm3_tp.py &&
pytest -v -s lora/test_llama_tp.py::test_llama_lora &&
pytest -s -v lora/test_minicpmv_tp.py'
- label: LoRA Multimodal
@@ -1,27 +0,0 @@
group: Models - Distributed
depends_on:
- image-build-xpu
steps:
- label: Distributed Model Tests (2 GPUs)
key: distributed-model-tests-2-gpus
timeout_in_minutes: 50
device: intel_gpu
agent_tags:
label: production
gpu: 2+
mem: 24+
no_plugin: true
working_dir: "."
env:
REGISTRY: "public.ecr.aws/q9t5s3a7"
REPO: "vllm-ci-test-repo"
VLLM_TEST_DEVICE: "xpu"
source_file_dependencies:
- vllm/model_executor/model_loader/sharded_state_loader.py
- vllm/model_executor/models/
- tests/model_executor/model_loader/test_sharded_state_loader.py
commands:
- >-
bash .buildkite/scripts/hardware_ci/run-intel-test.sh
'cd tests &&
pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m "not slow_test"'
@@ -125,5 +125,7 @@ steps:
pip install open-clip-torch --no-deps &&
cd tests &&
pytest -v -s models/multimodal/processing/test_tensor_schema.py
--deselect "tests/models/multimodal/processing/test_tensor_schema.py::test_model_tensor_schema[mistralai/Mistral-Large-3-675B-Instruct-2512-NVFP4]"
--deselect "tests/models/multimodal/processing/test_tensor_schema.py::test_model_tensor_schema[Qwen/Qwen2.5-Omni-7B-AWQ]"
--num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB'
parallelism: 4
+16 -180
View File
@@ -15,10 +15,9 @@ set -euo pipefail
DEFAULT_REPO_SLUG="vllm-project/vllm"
DEFAULT_CI_HCL_SOURCE="docker/ci-rocm.hcl"
DEFAULT_CI_BASE_CONTENT_FILES="requirements/common.txt requirements/rocm.txt requirements/test/rocm.txt docker/Dockerfile.rocm_base docker/ci-rocm.hcl docker/docker-bake-rocm.hcl tools/install_torchcodec_rocm.sh tests/vllm_test_utils .buildkite/scripts/ci-bake-rocm.sh"
DEFAULT_CI_BASE_CONTENT_FILES="requirements/common.txt requirements/rocm.txt requirements/test/rocm.txt docker/Dockerfile.rocm_base tools/install_torchcodec_rocm.sh tests/vllm_test_utils"
DEFAULT_CI_BASE_DOCKERFILE="docker/Dockerfile.rocm"
DEFAULT_CI_BASE_DOCKERFILE_STAGES="base build_rixl build_rocshmem build_deepep mori_base ci_base"
DEFAULT_CI_BASE_METADATA_VERSION="1"
IMAGE_EXISTED_BEFORE_BUILD=0
TARGET=""
@@ -526,22 +525,6 @@ get_remote_image_label_with_retry() {
return 0
}
remote_ci_base_metadata_is_current() {
local image_ref="$1"
local metadata_version=""
metadata_version=$(get_remote_image_label "${image_ref}" "vllm.ci_base.metadata_version")
[[ "${metadata_version}" == "${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}" ]]
}
remote_ci_base_metadata_is_current_with_retry() {
local image_ref="$1"
local metadata_version=""
metadata_version=$(get_remote_image_label_with_retry "${image_ref}" "vllm.ci_base.metadata_version")
[[ "${metadata_version}" == "${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}" ]]
}
remote_image_exists() {
local image_ref="$1"
docker manifest inspect "${image_ref}" >/dev/null 2>&1
@@ -598,7 +581,6 @@ init_config() {
CI_BASE_CONTENT_FILES="${CI_BASE_CONTENT_FILES:-${DEFAULT_CI_BASE_CONTENT_FILES}}"
CI_BASE_DOCKERFILE="${CI_BASE_DOCKERFILE:-${DEFAULT_CI_BASE_DOCKERFILE}}"
CI_BASE_DOCKERFILE_STAGES="${CI_BASE_DOCKERFILE_STAGES:-${DEFAULT_CI_BASE_DOCKERFILE_STAGES}}"
CI_BASE_METADATA_VERSION="${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}"
CI_BASE_IMAGE_TAG="${CI_BASE_IMAGE_TAG:-rocm/vllm-dev:ci_base}"
export PYTORCH_ROCM_ARCH
@@ -653,10 +635,6 @@ load_ci_hcl() {
echo "Copied ${CI_HCL_SOURCE} to ${CI_HCL_PATH}"
}
init_bake_files() {
BAKE_FILES=(-f "${VLLM_BAKE_FILE}" -f "${CI_HCL_PATH}")
}
compute_ci_base_hash_if_needed() {
if [[ -z "${CI_BASE_CONTENT_FILES:-}" ]]; then
return 0
@@ -698,14 +676,12 @@ configure_ci_base_image_refs() {
fi
content_tag=$(ci_base_tag_with_suffix "${stable_tag}" "${CI_BASE_CONTENT_HASH}")
CI_BASE_IMAGE_TAG_CONTENT_REF="${content_tag}"
if [[ -n "${BUILDKITE_COMMIT:-}" ]]; then
commit_tag=$(ci_base_tag_with_suffix "${stable_tag}" "${BUILDKITE_COMMIT}")
CI_BASE_IMAGE_TAG_COMMIT="${commit_tag}"
export CI_BASE_IMAGE_TAG_COMMIT
fi
CI_BASE_IMAGE_TAG_COMMIT_REF="${commit_tag}"
# *_REF is the logical tag recorded in metadata. *_EXTRA is only passed to
# bake when that tag is not already the primary tag, avoiding duplicates.
if should_push_stable_ci_base_tag; then
primary_tag="${content_tag}"
CI_BASE_IMAGE_TAG_STABLE="${stable_tag}"
@@ -715,33 +691,19 @@ configure_ci_base_image_refs() {
fi
CI_BASE_IMAGE_TAG="${primary_tag}"
if [[ "${primary_tag}" == "${content_tag}" ]]; then
CI_BASE_IMAGE_TAG_CONTENT_EXTRA=""
CI_BASE_IMAGE_TAG_CONTENT=""
else
CI_BASE_IMAGE_TAG_CONTENT_EXTRA="${content_tag}"
CI_BASE_IMAGE_TAG_CONTENT="${content_tag}"
fi
if [[ -n "${commit_tag}" && "${commit_tag}" != "${primary_tag}" ]]; then
CI_BASE_IMAGE_TAG_COMMIT_EXTRA="${commit_tag}"
else
CI_BASE_IMAGE_TAG_COMMIT_EXTRA=""
fi
export CI_BASE_IMAGE_TAG
export CI_BASE_IMAGE_TAG_COMMIT_EXTRA
export CI_BASE_IMAGE_TAG_CONTENT_EXTRA
export CI_BASE_IMAGE_TAG_CONTENT_REF
export CI_BASE_IMAGE_TAG_COMMIT_REF
export CI_BASE_IMAGE_TAG_STABLE
export CI_BASE_IMAGE_TAG CI_BASE_IMAGE_TAG_CONTENT CI_BASE_IMAGE_TAG_STABLE
if is_ci_base_target; then
IMAGE_TAG="${primary_tag}"
export IMAGE_TAG
echo "ci_base primary image tag: ${CI_BASE_IMAGE_TAG}"
if [[ -n "${commit_tag}" ]]; then
if [[ "${commit_tag}" == "${primary_tag}" ]]; then
echo "ci_base commit image tag: ${commit_tag} (primary)"
else
echo "ci_base commit image tag: ${commit_tag}"
fi
if [[ -n "${CI_BASE_IMAGE_TAG_COMMIT:-}" ]]; then
echo "ci_base commit image tag: ${CI_BASE_IMAGE_TAG_COMMIT}"
fi
echo "ci_base content image tag: ${content_tag}"
if [[ -n "${CI_BASE_IMAGE_TAG_STABLE}" ]]; then
@@ -766,8 +728,8 @@ ci_base_candidate_refs() {
printf '%s\n' \
"${IMAGE_TAG:-}" \
"${CI_BASE_IMAGE_TAG:-}" \
"${CI_BASE_IMAGE_TAG_COMMIT_EXTRA:-}" \
"${CI_BASE_IMAGE_TAG_CONTENT_EXTRA:-}" \
"${CI_BASE_IMAGE_TAG_COMMIT:-}" \
"${CI_BASE_IMAGE_TAG_CONTENT:-}" \
"${CI_BASE_IMAGE_TAG_STABLE:-}" \
| awk 'NF && !seen[$0]++'
}
@@ -781,10 +743,6 @@ find_matching_ci_base_ref() {
remote_image_exists "${candidate}" || continue
candidate_hash=$(get_remote_image_label "${candidate}" "vllm.ci_base.content_hash")
if [[ "${candidate_hash}" == "${CI_BASE_CONTENT_HASH}" ]]; then
if ! remote_ci_base_metadata_is_current "${candidate}"; then
echo "Found matching ci_base content hash but stale metadata: ${candidate}" >&2
continue
fi
printf '%s\n' "${candidate}"
return 0
fi
@@ -859,10 +817,6 @@ maybe_skip_existing_image() {
if [[ -n "${remote_hash}" ]]; then
echo "Remote ci_base content hash: ${remote_hash:0:16}..."
if [[ "${remote_hash}" == "${CI_BASE_CONTENT_HASH}" ]]; then
if ! remote_ci_base_metadata_is_current "${IMAGE_TAG}"; then
echo "Content hashes match but ci_base metadata is stale; rebuilding to refresh metadata"
return 0
fi
if ! refresh_ci_base_tags_from_ref "${IMAGE_TAG}"; then
echo "ci_base tag refresh failed; rebuilding to push expected tags"
return 0
@@ -1044,104 +998,12 @@ prepare_git_cache_metadata() {
fi
}
ci_base_metadata_pairs() {
local dockerfile="${CI_BASE_DOCKERFILE:-${DEFAULT_CI_BASE_DOCKERFILE}}"
local stages="${CI_BASE_DOCKERFILE_STAGES:-${DEFAULT_CI_BASE_DOCKERFILE_STAGES}}"
local content_files="${CI_BASE_CONTENT_FILES:-${DEFAULT_CI_BASE_CONTENT_FILES}}"
local content_files_hash=""
local base_image=""
local base_image_digest=""
local git_branch=""
local -a content_paths=()
local -a content_args=()
read -r -a content_paths <<< "${content_files}"
if [[ ${#content_paths[@]} -gt 0 ]]; then
content_files_hash=$(compute_content_hash "${content_paths[@]}")
fi
mapfile -t content_args < <(
get_content_arg_names "${dockerfile}" "${stages}" "${CI_BASE_CONTENT_ARGS:-}"
)
base_image=$(resolve_dockerfile_arg_value "${dockerfile}" "BASE_IMAGE")
if [[ -n "${base_image}" ]]; then
base_image_digest=$(resolve_image_digest "${base_image}")
fi
git_branch="${BUILDKITE_BRANCH:-${VLLM_BRANCH:-}}"
metadata_pair "vllm.ci_base.metadata_version" "${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}"
metadata_pair "vllm.ci_base.content_hash" "${CI_BASE_CONTENT_HASH:-}"
metadata_pair "vllm.ci_base.content_files_hash" "${content_files_hash}"
metadata_pair "vllm.ci_base.content_files" "${content_files}"
metadata_pair "vllm.ci_base.content_args" "$(join_words "${content_args[@]}")"
metadata_pair "vllm.ci_base.dockerfile" "${dockerfile}"
metadata_pair "vllm.ci_base.dockerfile_stages" "${stages}"
metadata_pair "vllm.ci_base.image.primary" "${CI_BASE_IMAGE_TAG:-}"
metadata_pair "vllm.ci_base.image.content" "${CI_BASE_IMAGE_TAG_CONTENT_REF:-${CI_BASE_IMAGE_TAG_CONTENT_EXTRA:-}}"
metadata_pair "vllm.ci_base.image.commit" "${CI_BASE_IMAGE_TAG_COMMIT_REF:-${CI_BASE_IMAGE_TAG_COMMIT_EXTRA:-}}"
metadata_pair "vllm.ci_base.image.stable" "${CI_BASE_IMAGE_TAG_STABLE:-}"
metadata_pair "vllm.ci_base.git_commit" "${BUILDKITE_COMMIT:-}"
metadata_pair "vllm.ci_base.git_branch" "${git_branch}"
metadata_pair "vllm.ci_base.vllm_branch" "${VLLM_BRANCH:-}"
metadata_pair "vllm.ci_base.stable_branch" "${CI_BASE_STABLE_BRANCH:-main}"
metadata_pair "vllm.rocm.base_image" "${base_image}"
metadata_pair "vllm.rocm.base_image_digest" "${base_image_digest}"
metadata_pair "vllm.rocm.pytorch_rocm_arch" "${PYTORCH_ROCM_ARCH:-}"
metadata_pair "vllm.rocm.nic_backend" "$(resolve_dockerfile_arg_value "${dockerfile}" "NIC_BACKEND")"
metadata_pair "vllm.rocm.ainic_version" "$(resolve_dockerfile_arg_value "${dockerfile}" "AINIC_VERSION")"
metadata_pair "vllm.rocm.ubuntu_codename" "$(resolve_dockerfile_arg_value "${dockerfile}" "UBUNTU_CODENAME")"
metadata_pair "vllm.rocm.rixl_repo" "$(resolve_dockerfile_arg_value "${dockerfile}" "RIXL_REPO")"
metadata_pair "vllm.rocm.rixl_commit" "${RIXL_BRANCH:-$(resolve_dockerfile_arg_value "${dockerfile}" "RIXL_BRANCH")}"
metadata_pair "vllm.rocm.ucx_repo" "$(resolve_dockerfile_arg_value "${dockerfile}" "UCX_REPO")"
metadata_pair "vllm.rocm.ucx_commit" "${UCX_BRANCH:-$(resolve_dockerfile_arg_value "${dockerfile}" "UCX_BRANCH")}"
metadata_pair "vllm.rocm.rocshmem_repo" "$(resolve_dockerfile_arg_value "${dockerfile}" "ROCSHMEM_REPO")"
metadata_pair "vllm.rocm.rocshmem_commit" "${ROCSHMEM_BRANCH:-$(resolve_dockerfile_arg_value "${dockerfile}" "ROCSHMEM_BRANCH")}"
metadata_pair "vllm.rocm.deepep_repo" "$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_REPO")"
metadata_pair "vllm.rocm.deepep_commit" "${DEEPEP_BRANCH:-$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_BRANCH")}"
metadata_pair "vllm.rocm.deepep_nic" "$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_NIC")"
metadata_pair "vllm.rocm.deepep_rocm_arch" "$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_ROCM_ARCH")"
metadata_pair "vllm.rocm.rixl_cache_key" "${RIXL_CACHE_KEY:-}"
metadata_pair "vllm.rocm.rocshmem_cache_key" "${ROCSHMEM_CACHE_KEY:-}"
metadata_pair "vllm.rocm.deepep_cache_key" "${DEEPEP_CACHE_KEY:-}"
metadata_pair "vllm.buildkite.build_number" "${BUILDKITE_BUILD_NUMBER:-}"
metadata_pair "vllm.buildkite.build_id" "${BUILDKITE_BUILD_ID:-}"
}
write_ci_base_metadata_annotations() {
local metadata="$1"
local key=""
local value=""
local annotation=""
[[ -n "${metadata}" ]] || return 0
while IFS=$'\t' read -r key value; do
[[ -n "${key}" && -n "${value}" ]] || continue
annotation="manifest:${key}=${value}"
printf ' "%s",\n' "$(hcl_escape_string "${annotation}")"
done <<< "${metadata}"
}
write_ci_base_metadata_labels() {
local metadata="$1"
local key=""
local value=""
[[ -n "${metadata}" ]] || return 0
while IFS=$'\t' read -r key value; do
[[ -n "${key}" && -n "${value}" ]] || continue
printf ' "%s" = "%s"\n' \
"$(hcl_escape_string "${key}")" \
"$(hcl_escape_string "${value}")"
done <<< "${metadata}"
}
write_ci_base_label_override() {
local target_name=""
local metadata=""
local -a ci_base_targets=()
BAKE_FILES=(-f "${VLLM_BAKE_FILE}" -f "${CI_HCL_PATH}")
if [[ -z "${CI_BASE_CONTENT_HASH:-}" ]]; then
return 0
fi
@@ -1157,23 +1019,16 @@ write_ci_base_label_override() {
return 0
fi
metadata=$(ci_base_metadata_pairs)
: > "${CI_BASE_LABEL_OVERRIDE_PATH}"
for target_name in "${ci_base_targets[@]}"; do
cat >> "${CI_BASE_LABEL_OVERRIDE_PATH}" <<EOF
target "${target_name}" {
annotations = [
"manifest:org.opencontainers.image.revision=",
EOF
write_ci_base_metadata_annotations "${metadata}" >> "${CI_BASE_LABEL_OVERRIDE_PATH}"
cat >> "${CI_BASE_LABEL_OVERRIDE_PATH}" <<EOF
]
labels = {
"org.opencontainers.image.revision" = ""
EOF
write_ci_base_metadata_labels "${metadata}" >> "${CI_BASE_LABEL_OVERRIDE_PATH}"
cat >> "${CI_BASE_LABEL_OVERRIDE_PATH}" <<EOF
"vllm.ci_base.content_hash" = "${CI_BASE_CONTENT_HASH}"
}
}
@@ -1181,7 +1036,7 @@ EOF
done
BAKE_FILES+=(-f "${CI_BASE_LABEL_OVERRIDE_PATH}")
echo "Appended ci_base metadata label override for targets: ${ci_base_targets[*]}"
echo "Appended ci_base content-hash label override for targets: ${ci_base_targets[*]}"
}
uses_rocm_csrc_cache() {
@@ -1264,18 +1119,6 @@ hcl_escape_string() {
printf '%s' "${value}"
}
join_words() {
local IFS=" "
printf '%s' "$*"
}
metadata_pair() {
local key="$1"
local value="${2:-}"
printf '%s\t%s\n' "${key}" "${value}"
}
write_hcl_string_list() {
local indent="$1"
shift
@@ -1698,13 +1541,7 @@ confirm_remote_image_push() {
remote_hash=$(get_remote_image_label_with_retry "${image_ref}" "vllm.ci_base.content_hash")
if [[ -n "${remote_hash}" && "${remote_hash}" == "${CI_BASE_CONTENT_HASH}" ]]; then
if remote_ci_base_metadata_is_current_with_retry "${image_ref}"; then
return 0
fi
echo "Remote image exists with the expected ci_base content hash but stale metadata."
echo " expected metadata version: ${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}"
return 1
return 0
fi
echo "Remote image exists but does not have the expected ci_base content hash."
@@ -1887,16 +1724,15 @@ main() {
print_header
validate_inputs
load_ci_hcl
init_bake_files
compute_ci_base_hash_if_needed
configure_ci_base_image_refs
maybe_skip_existing_image
setup_builder
prepare_git_cache_metadata
write_ci_base_label_override
extract_dependency_pins
write_rocm_build_arg_override
compute_dependency_cache_keys
write_ci_base_label_override
compute_rocm_csrc_content_hash_if_needed
write_rocm_cache_override
resolve_ci_base_dependency_targets
+212 -101
View File
@@ -112,6 +112,33 @@ steps:
# #
#########################################################################################################################################
#----------------------------------------------------- mi250 · basic_correctness -----------------------------------------------------#
- label: Distributed Model Tests (2 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
agent_pool: mi250_2
num_gpus: 2
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/model_executor/model_loader/sharded_state_loader.py
- vllm/model_executor/models/
- vllm/model_executor/layers/
- vllm/v1/attention/backends/
- vllm/v1/attention/selector.py
- tests/basic_correctness/
- tests/model_executor/model_loader/test_sharded_state_loader.py
- tests/models/
- vllm/_aiter_ops.py
- vllm/platforms/rocm.py
commands:
- TARGET_TEST_SUITE=MI250 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)'
- CUDA_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m '(not slow_test)'
- pytest models/test_transformers.py -v -s -m 'distributed(num_gpus=2)'
- pytest models/language -v -s -m 'distributed(num_gpus=2)'
- pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py
- VLLM_WORKER_MULTIPROC_METHOD=spawn pytest models/multimodal/generation/test_whisper.py -v -s -m 'distributed(num_gpus=2)'
#---------------------------------------------------------- mi250 · compile ----------------------------------------------------------#
- label: PyTorch Compilation Unit Tests # TBD
@@ -152,8 +179,48 @@ steps:
commands:
- "find compile/fullgraph/ -name 'test_*.py' -not -name 'test_full_graph.py' -exec pytest -s -v {} \\\\;"
- label: Distributed Compile + RPC Tests (2 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
agent_pool: mi250_2
num_gpus: 2
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/compilation/
- vllm/distributed/
- vllm/engine/
- vllm/executor/
- vllm/worker/worker_base.py
- vllm/v1/engine/
- vllm/v1/worker/
- tests/compile/fullgraph/test_basic_correctness.py
- tests/compile/test_wrapper.py
- tests/entrypoints/llm/test_collective_rpc.py
- vllm/platforms/rocm.py
commands:
- pytest -v -s entrypoints/llm/test_collective_rpc.py
- pytest -v -s ./compile/fullgraph/test_basic_correctness.py
- pytest -v -s ./compile/test_wrapper.py
#-------------------------------------------------------- mi250 · distributed --------------------------------------------------------#
- label: Distributed Comm Ops # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
agent_pool: mi250_2
num_gpus: 2
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/distributed
- tests/distributed
- vllm/platforms/rocm.py
commands:
- pytest -v -s distributed/test_comm_ops.py
- pytest -v -s distributed/test_shm_broadcast.py
- pytest -v -s distributed/test_shm_buffer.py
- pytest -v -s distributed/test_shm_storage.py
- label: Pipeline + Context Parallelism (4 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
@@ -263,6 +330,54 @@ steps:
- pytest -v -s models/multimodal/generation/test_common.py -m core_model -k "not qwen2 and not qwen3 and not gemma"
- pytest -v -s models/multimodal/generation/test_qwen2_vl.py -m core_model
#---------------------------------------------------------- mi250 · plugins ----------------------------------------------------------#
- label: Plugin Tests (2 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
agent_pool: mi250_2
num_gpus: 2
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/plugins/
- tests/plugins/
- vllm/platforms/rocm.py
commands:
# BEGIN: platform plugin and general plugin tests, all the code in-between runs on dummy platform
- pip install -e ./plugins/vllm_add_dummy_platform
- pytest -v -s plugins_tests/test_platform_plugins.py
- pip uninstall vllm_add_dummy_platform -y
# END: platform plugin tests
# BEGIN: `io_processor` plugins test, all the code in between uses the `prithvi_io_processor` plugin
- pip install -e ./plugins/prithvi_io_processor_plugin
- pytest -v -s plugins_tests/test_io_processor_plugins.py
- pytest -v -s plugins_tests/test_terratorch_io_processor_plugins.py
- pip uninstall prithvi_io_processor_plugin -y
# END: `io_processor` plugins test
# BEGIN: `bge_m3_sparse io_processor` test
- pip install -e ./plugins/bge_m3_sparse_plugin
- pytest -v -s plugins_tests/test_bge_m3_sparse_io_processor_plugins.py
- pip uninstall bge_m3_sparse_plugin -y
# END: `bge_m3_sparse io_processor` test
# BEGIN: `colbert_query io_processor` test
- pip install -e ./plugins/colbert_query_plugin
- pytest -v -s plugins_tests/test_colbert_query_io_processor_plugins.py
- pip uninstall colbert_query_plugin -y
# END: `colbert_query io_processor` test
# BEGIN: `stat_logger` plugins test
- pip install -e ./plugins/vllm_add_dummy_stat_logger
- pytest -v -s plugins_tests/test_stats_logger_plugins.py
- pip uninstall dummy_stat_logger -y
# END: `stat_logger` plugins test
# BEGIN: other tests
- pytest -v -s plugins_tests/test_scheduler_plugins.py
- pip install -e ./plugins/vllm_add_dummy_model
- pytest -v -s distributed/test_distributed_oot.py
- pytest -v -s plugins_tests/test_oot_registration_online.py # it needs a clean process
- pytest -v -s plugins_tests/test_oot_registration_offline.py # it needs a clean process
- pytest -v -s plugins_tests/lora_resolvers # unit tests for in-tree lora resolver plugins
#------------------------------------------------------------ mi250 · v1 -------------------------------------------------------------#
- label: Batch Invariance (H100-MI250) # TBD
@@ -390,6 +505,61 @@ steps:
commands:
- pytest -v -s v1/attention
- label: V1 Sample + Logits # TBD
timeout_in_minutes: 60
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
agent_pool: mi250_1
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/
- tests/v1/sample
- tests/v1/logits_processors
- tests/v1/test_oracle.py
- tests/v1/test_request.py
- tests/v1/test_outputs.py
commands:
- pytest -v -s v1/sample
- pytest -v -s v1/logits_processors
- pytest -v -s v1/test_oracle.py
- pytest -v -s v1/test_request.py
- pytest -v -s v1/test_outputs.py
- label: Distributed DP Tests (2 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
agent_pool: mi250_2
num_gpus: 2
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/distributed/
- vllm/engine/
- vllm/executor/
- vllm/worker/worker_base.py
- vllm/v1/engine/
- vllm/v1/worker/
- tests/v1/distributed
- tests/entrypoints/openai/test_multi_api_servers.py
- vllm/platforms/rocm.py
commands:
- TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_async_llm_dp.py
- TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_eagle_dp.py
- TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py
- DP_SIZE=2 pytest -v -s entrypoints/openai/test_multi_api_servers.py
- label: V1 e2e (2 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
agent_pool: mi250_2
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/
- tests/v1/e2e
commands:
- pytest -v -s v1/e2e/spec_decode/test_spec_decode.py -k "tensor_parallelism"
#------------------------------------------------------------- mi250 · misc ------------------------------------------------------------#
- label: Async Engine, Inputs, Utils, Worker, Config (CPU) # TBD
@@ -438,7 +608,7 @@ steps:
#----------------------------------------------------- mi300 · basic_correctness -----------------------------------------------------#
- label: Basic Correctness # TBD
timeout_in_minutes: 50
timeout_in_minutes: 40
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
agent_pool: mi300_1
fast_check: true
@@ -456,7 +626,7 @@ steps:
- pytest -v -s basic_correctness/test_cpu_offload.py
- label: Distributed Model Tests (2 GPUs) # TBD
timeout_in_minutes: 65
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
agent_pool: mi300_2
num_gpus: 2
@@ -632,22 +802,6 @@ steps:
#-------------------------------------------------------- mi300 · distributed --------------------------------------------------------#
- label: Distributed Comm Ops # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
agent_pool: mi300_2
num_gpus: 2
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/distributed
- tests/distributed
- vllm/platforms/rocm.py
commands:
- pytest -v -s distributed/test_comm_ops.py
- pytest -v -s distributed/test_shm_broadcast.py
- pytest -v -s distributed/test_shm_buffer.py
- pytest -v -s distributed/test_shm_storage.py
- label: EPLB Algorithm # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
@@ -678,7 +832,7 @@ steps:
- pytest -v -s distributed/test_eplb_spec_decode.py
- label: Distributed Tests (2xH100-2xMI300) # TBD
timeout_in_minutes: 30
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
agent_pool: mi300_2
num_gpus: 2
@@ -845,12 +999,10 @@ steps:
source_file_dependencies:
- vllm/
- tests/entrypoints/serve
- tests/entrypoints/scale_out
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc
- PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc
- pytest -v -s entrypoints/scale_out
- label: Entrypoints Integration (API Server OpenAI - Part 1) # TBD
timeout_in_minutes: 180
@@ -1198,31 +1350,10 @@ steps:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-mi3xx.txt
- label: ROCm LM Eval Large Models (8 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
agent_pool: mi300_8
optional: true
num_gpus: 8
working_dir: "/vllm-workspace/.buildkite/lm-eval-harness"
source_file_dependencies:
- vllm/model_executor/models/
- vllm/model_executor/model_loader/
- vllm/model_executor/layers/quantization/
- vllm/v1/attention/backends/
- vllm/v1/attention/selector.py
- vllm/model_executor/layers/layernorm.py
- csrc/
- vllm/_aiter_ops.py
- vllm/platforms/rocm.py
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large-rocm.txt --tp-size=8
#--------------------------------------------------------- mi300 · examples ----------------------------------------------------------#
- label: Examples # TBD
timeout_in_minutes: 45
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
agent_pool: mi300_1
optional: true
@@ -1235,7 +1366,7 @@ steps:
commands:
- pip install tensorizer
# Basic
- python3 basic/offline_inference/chat.py
- python3 basic/offline_inference/chat.py --attention-backend TRITON_ATTN
- python3 basic/offline_inference/generate.py --model facebook/opt-125m
- python3 basic/offline_inference/generate.py --model meta-llama/Llama-2-13b-chat-hf --cpu-offload-gb 10
- python3 basic/offline_inference/classify.py
@@ -1243,8 +1374,11 @@ steps:
- python3 basic/offline_inference/score.py
# Multi-modal models
- python3 generate/multimodal/audio_language_offline.py --seed 0
- python3 generate/multimodal/vision_language_offline.py --seed 0
- python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0
# These two examples import transformers before vllm, so on ROCm the HIP context
# is initialized in the parent before vllm sets this guard, poisoning fork. Set it
# inline to keep torch.cuda.is_available() fork-safe (see vllm/env_override.py).
- PYTORCH_NVML_BASED_CUDA_CHECK=1 python3 generate/multimodal/vision_language_offline.py --seed 0
- PYTORCH_NVML_BASED_CUDA_CHECK=1 python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0
- python3 generate/multimodal/encoder_decoder_multimodal_offline.py --model-type whisper --seed 0
# Pooling models
- python3 pooling/embed/vision_embedding_offline.py --seed 0
@@ -1686,57 +1820,12 @@ steps:
- pytest -v -s tests/models/test_transformers.py
- pytest -v -s tests/models/multimodal/test_mapping.py
- python3 examples/basic/offline_inference/chat.py
- python3 examples/generate/multimodal/vision_language_offline.py --model-type qwen2_5_vl
# This example imports transformers before vllm, so on ROCm the HIP context is
# initialized in the parent before vllm sets this guard, poisoning fork. Set it
# inline to keep torch.cuda.is_available() fork-safe (see vllm/env_override.py).
- PYTORCH_NVML_BASED_CUDA_CHECK=1 python3 examples/generate/multimodal/vision_language_offline.py --model-type qwen2_5_vl
- VLLM_WORKER_MULTIPROC_METHOD=spawn python3 examples/generate/multimodal/audio_language_offline.py --model-type whisper
#---------------------------------------------------------- mi300 · plugins ----------------------------------------------------------#
- label: Plugin Tests (2 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
agent_pool: mi300_2
num_gpus: 2
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/plugins/
- tests/plugins/
- vllm/platforms/rocm.py
commands:
# BEGIN: platform plugin and general plugin tests, all the code in-between runs on dummy platform
- pip install -e ./plugins/vllm_add_dummy_platform
- pytest -v -s plugins_tests/test_platform_plugins.py
- pip uninstall vllm_add_dummy_platform -y
# END: platform plugin tests
# BEGIN: `io_processor` plugins test, all the code in between uses the `prithvi_io_processor` plugin
- pip install -e ./plugins/prithvi_io_processor_plugin
- pytest -v -s plugins_tests/test_io_processor_plugins.py
- pytest -v -s plugins_tests/test_terratorch_io_processor_plugins.py
- pip uninstall prithvi_io_processor_plugin -y
# END: `io_processor` plugins test
# BEGIN: `bge_m3_sparse io_processor` test
- pip install -e ./plugins/bge_m3_sparse_plugin
- pytest -v -s plugins_tests/test_bge_m3_sparse_io_processor_plugins.py
- pip uninstall bge_m3_sparse_plugin -y
# END: `bge_m3_sparse io_processor` test
# BEGIN: `colbert_query io_processor` test
- pip install -e ./plugins/colbert_query_plugin
- pytest -v -s plugins_tests/test_colbert_query_io_processor_plugins.py
- pip uninstall colbert_query_plugin -y
# END: `colbert_query io_processor` test
# BEGIN: `stat_logger` plugins test
- pip install -e ./plugins/vllm_add_dummy_stat_logger
- pytest -v -s plugins_tests/test_stats_logger_plugins.py
- pip uninstall dummy_stat_logger -y
# END: `stat_logger` plugins test
# BEGIN: other tests
- pytest -v -s plugins_tests/test_scheduler_plugins.py
- pip install -e ./plugins/vllm_add_dummy_model
- pytest -v -s distributed/test_distributed_oot.py
- pytest -v -s plugins_tests/test_oot_registration_online.py # it needs a clean process
- pytest -v -s plugins_tests/test_oot_registration_offline.py # it needs a clean process
- pytest -v -s plugins_tests/lora_resolvers # unit tests for in-tree lora resolver plugins
#------------------------------------------------------- mi300 · quantization --------------------------------------------------------#
- label: Quantization # TBD
@@ -2413,6 +2502,27 @@ steps:
- export VLLM_USE_DEEP_GEMM=0
- pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large-rocm-fp8.txt --tp-size=4
- label: ROCm LM Eval Large Models (8 GPUs) # TBD
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi325]
agent_pool: mi325_8
optional: true
num_gpus: 8
working_dir: "/vllm-workspace/.buildkite/lm-eval-harness"
source_file_dependencies:
- vllm/model_executor/models/
- vllm/model_executor/model_loader/
- vllm/model_executor/layers/quantization/
- vllm/v1/attention/backends/
- vllm/v1/attention/selector.py
- vllm/model_executor/layers/layernorm.py
- csrc/
- vllm/_aiter_ops.py
- vllm/platforms/rocm.py
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large-rocm.txt --tp-size=8
#----------------------------------------------------- mi325 · models / language -----------------------------------------------------#
- label: Language Models Test (Extended Generation) # TBD
@@ -2561,12 +2671,10 @@ steps:
source_file_dependencies:
- vllm/
- tests/entrypoints/serve
- tests/entrypoints/scale_out
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc
- PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc
- pytest -v -s entrypoints/scale_out
- label: Entrypoints Integration (API Server OpenAI - Part 1) # TBD
timeout_in_minutes: 180
@@ -2771,7 +2879,7 @@ steps:
#--------------------------------------------------------- mi355 · examples ----------------------------------------------------------#
- label: Examples # TBD
timeout_in_minutes: 45
timeout_in_minutes: 180
mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355]
agent_pool: mi355_1
working_dir: "/vllm-workspace/examples"
@@ -2783,7 +2891,7 @@ steps:
commands:
- pip install tensorizer
# Basic
- python3 basic/offline_inference/chat.py
- python3 basic/offline_inference/chat.py --attention-backend TRITON_ATTN
- python3 basic/offline_inference/generate.py --model facebook/opt-125m
- python3 basic/offline_inference/generate.py --model meta-llama/Llama-2-13b-chat-hf --cpu-offload-gb 10
- python3 basic/offline_inference/classify.py
@@ -2791,8 +2899,11 @@ steps:
- python3 basic/offline_inference/score.py
# Multi-modal models
- python3 generate/multimodal/audio_language_offline.py --seed 0
- python3 generate/multimodal/vision_language_offline.py --seed 0
- python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0
# These two examples import transformers before vllm, so on ROCm the HIP context
# is initialized in the parent before vllm sets this guard, poisoning fork. Set it
# inline to keep torch.cuda.is_available() fork-safe (see vllm/env_override.py).
- PYTORCH_NVML_BASED_CUDA_CHECK=1 python3 generate/multimodal/vision_language_offline.py --seed 0
- PYTORCH_NVML_BASED_CUDA_CHECK=1 python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0
- python3 generate/multimodal/encoder_decoder_multimodal_offline.py --model-type whisper --seed 0
# Pooling models
- python3 pooling/embed/vision_embedding_offline.py --seed 0
+1 -1
View File
@@ -19,6 +19,6 @@ steps:
mirror:
amd:
device: mi325_1
timeout_in_minutes: 50
timeout_in_minutes: 40
depends_on:
- image-build-amd
+12
View File
@@ -224,6 +224,18 @@ steps:
- pytest -v -s tests/v1/distributed/test_dbo.py
- VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py
- pytest -v -s tests/distributed/test_packed_tensor.py
mirror:
amd:
device: mi300_2
timeout_in_minutes: 180
depends_on:
- image-build-amd
commands:
- VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_async_new_apis.py
- VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048
- pytest -v -s tests/v1/distributed/test_dbo.py
- VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py
- pytest -v -s tests/distributed/test_packed_tensor.py
- label: Distributed Tests (2xB200)
key: distributed-tests-2xb200
-4
View File
@@ -29,8 +29,6 @@ steps:
mirror:
amd:
device: mi325_1
# TODO(akaratza): Test after Torch >= 2.12 bump
soft_fail: true
depends_on:
- image-build-amd
@@ -42,12 +40,10 @@ steps:
source_file_dependencies:
- vllm/
- tests/entrypoints/serve
- tests/entrypoints/scale_out
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc
- PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc
- pytest -v -s entrypoints/scale_out
mirror:
amd:
device: mi325_1
-12
View File
@@ -65,18 +65,6 @@ steps:
commands:
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-blackwell.txt
- label: LM Eval Small Models (2xL4)
key: lm-eval-small-models-tp
timeout_in_minutes: 10
num_devices: 2
optional: true
source_file_dependencies:
- csrc/
- vllm/model_executor/layers/quantization
autorun_on_main: true
commands:
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-small-tp.txt
- label: LM Eval Large Models EP (2xB200)
key: lm-eval-large-models-ep-2xb200
timeout_in_minutes: 120
-10
View File
@@ -224,16 +224,6 @@ steps:
- python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048
# https://github.com/vllm-project/vllm/pull/26682 uses slightly more memory in PyTorch 2.9+ causing this test to OOM in 1xL4 GPU
- python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536
mirror:
amd:
device: mi325_1
source_file_dependencies:
- vllm/entrypoints
- vllm/multimodal
- examples/
- vllm/platforms/rocm.py
depends_on:
- image-build-amd
- label: Metrics, Tracing (2 GPUs)
key: metrics-tracing-2-gpus
+2 -2
View File
@@ -46,7 +46,7 @@ steps:
- vllm/v1/engine/
- tests/utils.py
# - tests/entrypoints/serve/dev/rpc/test_collective_rpc.py
- tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py
- tests/entrypoints/serve/disagg/test_serving_tokens.py
- tests/entrypoints/serve/instrumentator/test_basic.py
- tests/entrypoints/serve/instrumentator/test_metrics.py
# - tests/entrypoints/serve/dev/test_sleep.py
@@ -55,7 +55,7 @@ steps:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
# - pytest -v -s entrypoints/serve/dev/rpc/test_collective_rpc.py
- pytest -v -s entrypoints/serve/instrumentator/test_basic.py -k "not show_version and not server_load"
- pytest -v -s entrypoints/scale_out/token_in_token_out/test_serving_tokens.py -k "not stream and not lora and not test_generate_logprobs and not stop_string_workflow"
- pytest -v -s entrypoints/serve/disagg/test_serving_tokens.py -k "not stream and not lora and not test_generate_logprobs and not stop_string_workflow"
- pytest -v -s entrypoints/serve/instrumentator/test_metrics.py -k "text and not show and not run_batch and not test_metrics_counts and not test_metrics_exist"
# - pytest -v -s entrypoints/serve/dev/test_sleep.py
-2
View File
@@ -94,8 +94,6 @@ steps:
amd:
device: mi325_1
timeout_in_minutes: 65
# TODO(akaratza): Test after Torch >= 2.12 bump
soft_fail: true
depends_on:
- image-build-amd
source_file_dependencies:
+8 -10
View File
@@ -3,7 +3,7 @@
# This lists cover the "core" components of vLLM that require careful review
/vllm/compilation @zou3519 @youkaichao @ProExpertProg @BoyuanFeng
/vllm/distributed/kv_transfer @NickLucche @ApostaC @orozery @xuechendi @ivanium
/vllm/distributed/kv_transfer @NickLucche @ApostaC @orozery @xuechendi
/vllm/lora @jeejeelee
/vllm/model_executor/layers/attention @LucasWilkinson @MatthewBonanni
/vllm/model_executor/layers/fused_moe @mgoin @pavanimajety @zyongye
@@ -11,7 +11,7 @@
/vllm/model_executor/layers/mamba @tdoublep @tomeras91
/vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py @tdoublep @ZJY0516 @vadiklyutiy
/vllm/model_executor/model_loader @22quinn
/vllm/model_executor/layers/batch_invariant.py @yewentao256
/vllm/model_executor/layers/batch_invariant.py @yewentao256
/vllm/ir @ProExpertProg
/vllm/kernels/ @ProExpertProg @tjtanaa
/vllm/kernels/helion @ProExpertProg @zou3519
@@ -23,7 +23,7 @@
# Any change to the VllmConfig changes can have a large user-facing impact,
# so spam a lot of people
/vllm/config @WoosukKwon @youkaichao @robertgshaw2-redhat @mgoin @tlrmchlsmth @houseroad @yewentao256 @ProExpertProg
/vllm/config/cache.py @heheda12345 @ivanium
/vllm/config/cache.py @heheda12345
# Config utils
/vllm/config/utils.py @hmellor
@@ -67,17 +67,16 @@
/vllm/v1/attention/backends/flashinfer.py @mgoin @pavanimajety @vadiklyutiy
/vllm/v1/attention/backends/triton_attn.py @tdoublep
/vllm/v1/attention/backends/gdn_attn.py @ZJY0516 @vadiklyutiy
/vllm/v1/core @WoosukKwon @robertgshaw2-redhat @njhill @ywang96 @alexm-redhat @heheda12345 @ApostaC @orozery @ivanium
/vllm/v1/core @WoosukKwon @robertgshaw2-redhat @njhill @ywang96 @alexm-redhat @heheda12345 @ApostaC @orozery
/vllm/v1/sample @22quinn @houseroad @njhill
/vllm/v1/spec_decode @benchislett @luccafong @MatthewBonanni
/vllm/v1/structured_output @mgoin @russellb @aarnphm @benchislett
/vllm/v1/kv_cache_interface.py @heheda12345 @ivanium
/vllm/v1/kv_cache_interface.py @heheda12345
/vllm/v1/kv_offload @ApostaC @orozery
/vllm/v1/simple_kv_offload @ivanium
/vllm/v1/engine @njhill
/vllm/v1/executor @njhill
/vllm/v1/worker @njhill
/vllm/v1/worker/kv_connector_model_runner_mixin.py @orozery @NickLucche @ivanium
/vllm/v1/worker/kv_connector_model_runner_mixin.py @orozery @NickLucche
# Model runner V2
/vllm/v1/worker/gpu @WoosukKwon @njhill @yewentao256
@@ -104,14 +103,13 @@
/tests/test_inputs.py @DarkLight1337 @ywang96
/tests/entrypoints/llm/test_struct_output_generate.py @mgoin @russellb @aarnphm
/tests/v1/structured_output @mgoin @russellb @aarnphm
/tests/v1/core @WoosukKwon @robertgshaw2-redhat @njhill @ywang96 @alexm-redhat @heheda12345 @ApostaC @orozery @ivanium
/tests/v1/core @WoosukKwon @robertgshaw2-redhat @njhill @ywang96 @alexm-redhat @heheda12345 @ApostaC @orozery
/tests/weight_loading @mgoin @youkaichao @yewentao256
/tests/lora @jeejeelee
/tests/models/language/generation/test_hybrid.py @tdoublep @tomeras91
/tests/v1/kv_connector/nixl_integration @NickLucche
/tests/v1/kv_connector @ApostaC @orozery @ivanium
/tests/v1/kv_connector @ApostaC @orozery
/tests/v1/kv_offload @ApostaC @orozery
/tests/v1/simple_kv_offload @ivanium
/tests/v1/determinism @yewentao256
/tests/reasoning @aarnphm @chaunceyjiang @sfeng33 @bbrowning
/tests/tool_parsers @aarnphm @chaunceyjiang @sfeng33 @bbrowning
+1 -1
View File
@@ -327,7 +327,7 @@ jobs:
message: 'CC {users} for ROCm-related issue',
},
mistral: {
users: ['patrickvonplaten', 'juliendenize', 'andylolu2', 'NickLucche'],
users: ['patrickvonplaten', 'juliendenize', 'andylolu2'],
message: 'CC {users} for Mistral-related issue',
},
// Add more label -> user mappings here
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
+2 -2
View File
@@ -48,8 +48,8 @@ jobs:
if: always() && (needs.pre-run-check.result == 'success' || needs.pre-run-check.result == 'skipped')
runs-on: [self-hosted, linux, x64, vllm-runners]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
# Provide shellcheck on PATH so tools/pre_commit/shellcheck.sh skips its
-13
View File
@@ -131,19 +131,6 @@ repos:
--python-version, "3.12",
]
files: ^requirements/(common|xpu|test/xpu)\.(in|txt)$
- id: pip-compile
alias: pip-compile-cpu
name: pip-compile-cpu
args: [
requirements/test/cuda.in,
-o, requirements/test/cpu.txt,
--index-strategy, unsafe-best-match,
--torch-backend, cpu,
--python-platform, x86_64-manylinux_2_28,
--python-version, "3.12",
]
files: ^requirements/(common|cpu|test/(cuda|cpu))\.(in|txt)$
exclude: ^requirements/test/cuda\.txt$
- id: pip-compile
alias: pip-compile-docs
name: pip-compile-docs
-25
View File
@@ -140,21 +140,6 @@ if(Python_VERSION VERSION_GREATER_EQUAL "3.11")
WITH_SOABI)
endif()
#
# fs_io extension (pure CXX; must stay above the non-CUDA device branch
# so CPU builds define the target before the early return).
# GIL-releasing filesystem helpers for FileSystemTierManager.
#
if(Python_VERSION VERSION_GREATER_EQUAL "3.11")
define_extension_target(
fs_io_C
DESTINATION vllm
LANGUAGE CXX
SOURCES csrc/fs_io.cpp
USE_SABI 3.11
WITH_SOABI)
endif()
#
# Forward the non-CUDA device extensions to external CMake scripts.
#
@@ -285,16 +270,6 @@ if(VLLM_GPU_LANG STREQUAL "HIP")
#
set(CMAKE_${VLLM_GPU_LANG}_FLAGS "${CMAKE_${VLLM_GPU_LANG}_FLAGS} -Wno-unused-result -Wno-unused-value")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-result -Wno-unused-value")
# When using LTO then *.cpp files must be compiled with same compiler as used linker
# So if HIP uses clang linker we also must use it
# Otherwise symbols will be missing from .so
if (CMAKE_CXX_FLAGS MATCHES "\-flto")
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL CMAKE_HIP_COMPILER_ID)
message(FATAL_ERROR "LTO is enabled for ROCm build, but the C++ compiler (${CMAKE_CXX_COMPILER_ID}) and HIP compiler (${CMAKE_HIP_COMPILER_ID}) are different which is not supported. "
"Please ensure they are same by setting CXX=${CMAKE_HIP_COMPILER} environment variable. Or alternatively disable LTO.")
endif()
endif()
endif()
#
@@ -39,7 +39,7 @@ else()
FetchContent_Declare(
vllm-flash-attn
GIT_REPOSITORY https://github.com/vllm-project/flash-attention.git
GIT_TAG b3964b1d8b95d8e8447435668ab169a2700bab65
GIT_TAG 803020a8fa15407871341d41eba4919ade2ee1ee
GIT_PROGRESS TRUE
# Don't share the vllm-flash-attn build between build types
BINARY_DIR ${CMAKE_BINARY_DIR}/vllm-flash-attn
-69
View File
@@ -1,69 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#include <Python.h>
#include <unistd.h>
#include <vector>
extern "C" {
static void _batch_lookup(const std::vector<const char*>& paths,
std::vector<int>& exists_flags) {
for (size_t i = 0; i < paths.size(); i++) {
exists_flags[i] = (access(paths[i], F_OK) == 0) ? 1 : 0;
}
}
/// @brief Check file existence for a batch of paths.
/// @param paths list[str] absolute paths to check.
/// @return list[bool] True if the corresponding path exists, False otherwise.
/// @note Releases the GIL for the entire batch. File existence via access(2).
static PyObject* batch_lookup(PyObject* /*self*/, PyObject* args) {
PyObject* path_list;
if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &path_list)) {
return nullptr;
}
const Py_ssize_t n = PyList_Size(path_list);
std::vector<const char*> paths(n);
for (Py_ssize_t i = 0; i < n; i++) {
paths[i] = PyUnicode_AsUTF8AndSize(PyList_GetItem(path_list, i), nullptr);
if (paths[i] == nullptr) {
return nullptr;
}
}
std::vector<int> exists_flags(n);
{
Py_BEGIN_ALLOW_THREADS _batch_lookup(paths, exists_flags);
Py_END_ALLOW_THREADS
}
PyObject* result = PyList_New(n);
if (result == nullptr) {
return nullptr;
}
for (Py_ssize_t i = 0; i < n; i++) {
PyList_SetItem(result, i, PyBool_FromLong(exists_flags[i]));
}
return result;
}
static PyMethodDef fs_io_C_methods[] = {
{"batch_lookup", batch_lookup, METH_VARARGS,
"batch_lookup(paths: list[str]) -> list[bool]\n"
"\n"
"Check file existence for a batch of paths."},
{nullptr, nullptr, 0, nullptr},
};
static struct PyModuleDef fs_io_C_module = {
PyModuleDef_HEAD_INIT, "fs_io_C", "Filesystem helpers for KV offload", -1,
fs_io_C_methods,
};
PyMODINIT_FUNC PyInit_fs_io_C(void) { return PyModule_Create(&fs_io_C_module); }
} // extern "C"
+3 -4
View File
@@ -311,10 +311,9 @@ ENV CARGO_BUILD_JOBS=4
# Build the release artifacts. Cache cargo registry/git, but not target/,
# because stale target metadata can outlive source updates across BuildKit
# cache reuse.
RUN --mount=type=cache,target=/root/.cargo/registry,sharing=locked \
--mount=type=cache,target=/root/.cargo/git,sharing=locked \
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/root/.cargo/git \
bash build_rust.sh
#################### RUST BUILD IMAGE ####################
#################### CSRC BUILD IMAGE ####################
@@ -793,7 +792,7 @@ RUN --mount=type=cache,target=/opt/uv/cache \
# Install FlashInfer JIT cache (requires CUDA-version-specific index URL)
# https://docs.flashinfer.ai/installation.html
# From versions.json: .flashinfer.version
ARG FLASHINFER_VERSION=0.6.13
ARG FLASHINFER_VERSION=0.6.12
RUN --mount=type=cache,target=/opt/uv/cache \
uv pip install --system flashinfer-jit-cache==${FLASHINFER_VERSION} \
--index-url https://flashinfer.ai/whl/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.')
+19 -9
View File
@@ -193,16 +193,26 @@ FROM base AS vllm-test-deps
WORKDIR /vllm-workspace
# Test requirements are compiled from requirements/test/cuda.in into
# requirements/test/cpu.txt by the pip-compile-cpu pre-commit hook, which
# resolves CPU wheels via uv's --torch-backend cpu.
COPY requirements/test/cpu.txt requirements/test/cpu.txt
# Copy test requirements
COPY requirements/test/cuda.in requirements/test/cpu.in
# cpu.txt is compiled for x86_64, so platform markers are resolved away. Drop
# packages unavailable on aarch64 (decord, terratorch) for arm builds.
RUN case "$(uname -m)" in \
aarch64|arm64) sed -i '/^decord==/d; /^terratorch==/d' requirements/test/cpu.txt ;; \
esac
RUN \
sed -i '/mamba_ssm/d' requirements/test/cpu.in && \
remove_packages_not_supported_on_aarch64() { \
case "$(uname -m)" in \
aarch64|arm64) \
sed -i '/decord/d' requirements/test/cpu.in; \
sed -i '/terratorch/d' requirements/test/cpu.in; \
;; \
esac; \
}; \
remove_packages_not_supported_on_aarch64 && \
sed -i 's/^torch==.*/torch==2.11.0/g' requirements/test/cpu.in && \
sed -i 's/torchaudio.*/torchaudio/g' requirements/test/cpu.in && \
sed -i 's/torchvision.*/torchvision/g' requirements/test/cpu.in && \
# Related issue: https://github.com/vllm-project/vllm/pull/38800#issuecomment-4228314305
sed -i 's/^sentence-transformers.*/sentence-transformers==5.3.0/g' requirements/test/cpu.in && \
uv pip compile requirements/test/cpu.in -o requirements/test/cpu.txt --index-strategy unsafe-best-match --torch-backend cpu
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install -r requirements/test/cpu.txt
+4 -5
View File
@@ -123,10 +123,9 @@ COPY build_rust.sh build_rust.sh
# (rustc spawns enough concurrent processes to hit RLIMIT_NOFILE otherwise).
ENV CARGO_BUILD_JOBS=4
RUN --mount=type=cache,target=/root/.cargo/registry,sharing=locked \
--mount=type=cache,target=/root/.cargo/git,sharing=locked \
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/root/.cargo/git \
bash build_rust.sh
#################### RUST BUILD IMAGE ####################
#################### WHEEL BUILD IMAGE ####################
@@ -257,13 +256,13 @@ RUN pip install setuptools==75.6.0 packaging==23.2 ninja==1.11.1.3 build==1.2.2.
# build flashinfer for torch nightly from source around 10 mins
# release version: v0.6.13
# release version: v0.6.12
# todo(elainewy): cache flashinfer build result for faster build
ENV CCACHE_DIR=/root/.cache/ccache
RUN --mount=type=cache,target=/root/.cache/ccache \
--mount=type=cache,target=/root/.cache/uv \
echo "git clone flashinfer..." \
&& git clone --depth 1 --branch v0.6.13 --recursive https://github.com/flashinfer-ai/flashinfer.git \
&& git clone --depth 1 --branch v0.6.12 --recursive https://github.com/flashinfer-ai/flashinfer.git \
&& cd flashinfer \
&& git submodule update --init --recursive \
&& echo "finish git clone flashinfer..." \
-6
View File
@@ -575,9 +575,6 @@ RUN --mount=type=cache,target=/root/.cache/uv \
ENV HF_XET_HIGH_PERFORMANCE=1
ENV HF_HUB_DOWNLOAD_TIMEOUT=60
# Keep torch.cuda.is_available() fork-safe (see vllm/env_override.py).
ENV PYTORCH_NVML_BASED_CUDA_CHECK=1
# Pre-install vLLM test dependencies.
COPY requirements/test/rocm.txt /tmp/rocm-test-reqs.txt
RUN --mount=type=cache,target=/root/.cache/uv \
@@ -698,9 +695,6 @@ ENV SAFETENSORS_FAST_GPU=1
# Performance environment variable.
ENV HIP_FORCE_DEV_KERNARG=1
# Keep torch.cuda.is_available() fork-safe (see vllm/env_override.py).
ENV PYTORCH_NVML_BASED_CUDA_CHECK=1
# Workaround for ROCm profiler limits
RUN echo "ROCTRACER_MAX_EVENTS=10000000" > ${COMMON_WORKDIR}/libkineto.conf
ENV KINETO_CONFIG="${COMMON_WORKDIR}/libkineto.conf"
+2 -2
View File
@@ -28,8 +28,8 @@ COPY build_rust.sh build_rust.sh
# (rustc spawns enough concurrent processes to hit RLIMIT_NOFILE otherwise).
ENV CARGO_BUILD_JOBS=4
RUN --mount=type=cache,target=/root/.cargo/registry,sharing=locked \
--mount=type=cache,target=/root/.cargo/git,sharing=locked \
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/root/.cargo/git \
bash build_rust.sh
FROM ubuntu:24.04 AS vllm-base
+6 -15
View File
@@ -302,22 +302,14 @@ group "test-rocm-ci-with-wheel" {
}
# Image tags for the ci_base build. ci-bake-rocm.sh rewrites CI_BASE_IMAGE_TAG
# to the primary tag for this build. Builds always publish a content-scoped tag
# when the ci_base content hash is available. Builds with BUILDKITE_COMMIT also
# publish a commit-scoped tag, either as the primary tag or an additional alias.
# NIGHTLY=1 builds on the stable branch can additionally set
# CI_BASE_IMAGE_TAG_STABLE to refresh rocm/vllm-dev:ci_base.
# to the primary tag for this build. Non-nightly builds use a commit-scoped tag
# and also publish a content tag for reuse. NIGHTLY=1 builds on the stable branch
# can additionally set CI_BASE_IMAGE_TAG_STABLE to refresh rocm/vllm-dev:ci_base.
variable "CI_BASE_IMAGE_TAG" {
default = "rocm/vllm-dev:ci_base"
}
# Supplemental tags only. ci-bake-rocm.sh leaves these empty when the same ref
# is already the primary CI_BASE_IMAGE_TAG.
variable "CI_BASE_IMAGE_TAG_COMMIT_EXTRA" {
default = ""
}
variable "CI_BASE_IMAGE_TAG_CONTENT_EXTRA" {
variable "CI_BASE_IMAGE_TAG_CONTENT" {
default = ""
}
@@ -365,8 +357,7 @@ target "ci-base-rocm-ci" {
cache-from = concat(
compact([
CI_BASE_IMAGE_TAG != "" ? "type=registry,ref=${CI_BASE_IMAGE_TAG}" : "",
CI_BASE_IMAGE_TAG_COMMIT_EXTRA != "" ? "type=registry,ref=${CI_BASE_IMAGE_TAG_COMMIT_EXTRA}" : "",
CI_BASE_IMAGE_TAG_CONTENT_EXTRA != "" ? "type=registry,ref=${CI_BASE_IMAGE_TAG_CONTENT_EXTRA}" : "",
CI_BASE_IMAGE_TAG_CONTENT != "" ? "type=registry,ref=${CI_BASE_IMAGE_TAG_CONTENT}" : "",
CI_BASE_IMAGE_TAG_STABLE != "" ? "type=registry,ref=${CI_BASE_IMAGE_TAG_STABLE}" : "",
]),
# Import upstream dependency caches so RIXL/ROCShmem/DeepEP stages
@@ -374,7 +365,7 @@ target "ci-base-rocm-ci" {
get_cache_from_rocm_deps(),
)
cache-to = ["type=inline"]
tags = compact([CI_BASE_IMAGE_TAG, CI_BASE_IMAGE_TAG_COMMIT_EXTRA, CI_BASE_IMAGE_TAG_CONTENT_EXTRA, CI_BASE_IMAGE_TAG_STABLE])
tags = compact([CI_BASE_IMAGE_TAG, CI_BASE_IMAGE_TAG_CONTENT, CI_BASE_IMAGE_TAG_STABLE])
output = ["type=registry"]
}
+1 -1
View File
@@ -68,7 +68,7 @@
"default": "true"
},
"FLASHINFER_VERSION": {
"default": "0.6.13"
"default": "0.6.12"
},
"GDRCOPY_CUDA_VERSION": {
"default": "12.8"
-6
View File
@@ -80,9 +80,6 @@ vllm chat --url http://{vllm-serve-host}:{vllm-serve-port}/v1
# Quick chat with a single prompt
vllm chat --quick "hi"
# Print TTFT and throughput statistics after each response
vllm chat --stats
```
See [vllm chat](./chat.md) for the full reference of all available arguments.
@@ -100,9 +97,6 @@ vllm complete --url http://{vllm-serve-host}:{vllm-serve-port}/v1
# Quick complete with a single prompt
vllm complete --quick "The future of AI is"
# Print TTFT and throughput statistics after each response
vllm complete --stats
```
See [vllm complete](./complete.md) for the full reference of all available arguments.
+1 -2
View File
@@ -167,7 +167,6 @@ Priority is **1 = highest** (tried first).
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
| `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
| `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder Only | Any |
| `HPC_ATTN` | | fp16, bf16 | `auto`, `fp8_e4m3` | 64 | 128 | ❌ | ❌ | ❌ | ❌ | Decoder | ≥9.0 |
| `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ✅ | ✅ | ❌ | ❌ | Decoder | N/A |
| `ROCM_AITER_UNIFIED_ATTN` | | bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | N/A |
| `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
@@ -221,7 +220,7 @@ MLA decode backends are selected using the standard
| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | | Decoder | 10.x |
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | | Decoder | 10.x |
| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
| `FLASHINFER_MLA_SPARSE_SM120` | bf16 | `auto`, `fp8`, `fp8_e4m3`, `fp8_ds_mla` | 64, 256 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 12.x |
| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
+1 -2
View File
@@ -127,7 +127,6 @@ Models opt-in to encoder CUDA Graphs by implementing the [SupportsEncoderCudaGra
| Architecture | Models | CG for Image | CG for Video | Dual-Path Graph |
| ------------ | ------ | ------------ | ------------ | --------------- |
| `DeepseekOCRForCausalLM` | `DeepSeek-OCR` | ✅︎ | ❌︎ | ✅︎ |
| `Gemma3ForConditionalGeneration` | `Gemma3` | ✅︎ | ❌︎ | ❌︎ |
| `Glm4vForConditionalGeneration` | `GLM-4.1V, GLM-4.6V-Flash` | ✅︎ | ✅︎ | ❌︎ |
| `InternVLChatModel` | `InternVL3.5`, `InternVL3`, `InternVL2.5`, `InternVL2` | ✅︎ | ✅︎ | ❌︎ |
| `KimiVLForConditionalGeneration` | `Kimi-VL` | ✅︎ | ❌︎ | ❌︎ |
@@ -137,7 +136,7 @@ Models opt-in to encoder CUDA Graphs by implementing the [SupportsEncoderCudaGra
| `Qwen3VLForConditionalGeneration` | `Qwen3-VL` | ✅︎ | ✅︎ | ❌︎ |
| `Qwen3_5ForConditionalGeneration` | `Qwen3.5`, `Qwen3.6` | ✅︎ | ✅︎ | ❌︎ |
| `Qwen3_5MoeForConditionalGeneration` | `Qwen3.5-MoE`, `Qwen3.6-MoE` | ✅︎ | ✅︎ | ❌︎ |
| `Step3VLForConditionalGeneration` | `Step3-VL` | ✅︎ | ❌︎ | |
| `Step3VLForConditionalGeneration` | `Step3-VL` | ✅︎ | ❌︎ | |
!!! note
Encoder CUDA Graphs have currently been tested with `--mm-encoder-attn-backend=FLASH_ATTN` and `--mm-encoder-attn-backend=FLASHINFER` on Blackwell GPUs.
-1
View File
@@ -89,7 +89,6 @@ To be used with a particular `FusedMoEPrepareAndFinalizeModular` subclass, MoE k
| gpt oss triton | standard | N/A | N/A | <sup>5</sup> | Y | Y | [`triton_kernel_fused_experts`][vllm.model_executor.layers.fused_moe.experts.gpt_oss_triton_kernels_moe.triton_kernel_fused_experts],</br>[`OAITritonExperts`][vllm.model_executor.layers.fused_moe.experts.gpt_oss_triton_kernels_moe.OAITritonExperts] |
| marlin | standard,</br>batched | <sup>3</sup> / N/A | <sup>3</sup> / N/A | silu,</br>swigluoai | Y | Y | [`fused_marlin_moe`][vllm.model_executor.layers.fused_moe.experts.marlin_moe.fused_marlin_moe],</br>[`MarlinExperts`][vllm.model_executor.layers.fused_moe.experts.marlin_moe.MarlinExperts],</br>[`BatchedMarlinExperts`][vllm.model_executor.layers.fused_moe.experts.marlin_moe.BatchedMarlinExperts] |
| trtllm | standard | mxfp4,</br>nvfp4 | G(16),G(32) | <sup>5</sup> | N | Y | [`TrtLlmMxfp4ExpertsMonolithic`][vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe.TrtLlmMxfp4ExpertsMonolithic],</br>[`TrtLlmMxfp4ExpertsModular`][vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe.TrtLlmMxfp4ExpertsModular],</br>[`TrtLlmNvFp4ExpertsMonolithic`][vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe.TrtLlmNvFp4ExpertsMonolithic],</br>[`TrtLlmNvfp4ExpertsModular`][vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe.TrtLlmNvFp4ExpertsModular] |
| hpc | standard | fp8 | G(128),T | silu | Y | Y | [`HPCExperts`][vllm.model_executor.layers.fused_moe.hpc_moe.HPCExperts] |
| rocm aiter moe | standard | mxfp4,</br>fp8 | G(32),G(128),A,T | silu, gelu,</br>swigluoai | Y | N | `rocm_aiter_fused_experts`,</br>`AiterExperts` |
| cpu_fused_moe | standard | N/A | N/A | silu | N | N | [`CPUFusedMOE`][vllm.model_executor.layers.fused_moe.cpu_fused_moe.CPUFusedMOE] |
| naive batched<sup>4</sup> | batched | int8,</br>fp8 | G,A,T | silu, gelu | <sup>6</sup> | Y | [`NaiveBatchedExperts`][vllm.model_executor.layers.fused_moe.experts.fused_batched_moe.NaiveBatchedExperts] |
+1 -2
View File
@@ -13,6 +13,5 @@ vLLM's examples are organized into the following categories:
- **[`rl/`](../../examples/rl)** Reinforcement learning examples.
- **[`deployment/`](../../examples/deployment)** Examples for deploying vLLM in production.
- **[`ray_serving/`](../../examples/ray_serving)** Scalable serving using Ray.
- **[`disaggregated/`](../../examples/disaggregated)** Examples for Disaggregated P/D (Prefill/Decoding) inference, including various kv cache connectors (LMCache, Mooncake, FlexKV, P2P NCCL) and failure recovery.
- **[`scale_out/`](../../examples/scale_out)** Examples for Token In <> Token Out API Server.
- **[`disaggregated/`](../../examples/disaggregated)** Examples for disaggregated serving (separate prefill and decode), including various kv cache connectors (LMCache, Mooncake, FlexKV, P2P NCCL) and failure recovery.
- **[`observability/`](../../examples/observability)** Metrics, logging, tracing (OpenTelemetry), and dashboards (Grafana, Perses).
-14
View File
@@ -120,20 +120,6 @@ To enable KV cache sharing between multiple vLLM instances using the same `root_
PYTHONHASHSEED=0 vllm serve ...
```
### P2P (Including P/D)
The P2P tier (`type: "p2p"`) shares completed KV blocks between vLLM instances over RDMA via NIXL. Each instance binds a control socket on `host:port` and exchanges blocks directly with peers — no shared filesystem required.
| Key | Required | Default | Notes |
| --- | --- | --- | --- |
| `type` | yes | — | Must be `p2p`. |
| `host` | no | `0.0.0.0` | Address the control socket binds to. |
| `port` | no | `7777` | Port for the control socket. Must be reachable from peers. |
| `backends` | no | `["UCX"]` | NIXL transport backends. See [NixlConnector Usage Guide](nixl_connector_usage.md#selecting-a-nixl-transport-backend-plugin) for available backends and selection guidance. |
| `num_threads` | no | `4` | NIXL agent worker threads. Only used when `backends` is UCX-only; ignored when any non-UCX backend is requested. |
The `backends` and `num_threads` options mirror the conditional logic used by [`NixlConnector`](nixl_connector_usage.md#selecting-a-nixl-transport-backend-plugin): when any non-UCX backend is configured, NIXL is initialised with `backends=...`; otherwise it falls back to a UCX-only agent with the configured `num_threads`. This lets the P2P tier use a different transport (e.g. `MOONCAKE`, `GDS_MT`, `LIBFABRIC`) than the main `NixlConnector` running in the same process.
## Tuning Tips
- `cpu_bytes_to_use`: a bigger CPU tier means fewer trips to slower secondary tiers and a higher hit rate. The value is total across all workers, not per-worker. Leave headroom for the rest of the host workload.
@@ -45,7 +45,6 @@ The BAAI/bge-m3 model leverages token classification for sparse retrieval. For m
| ------------ | ------ | ----------------- | --------------------------- | --------------------------------------- |
| `BertForTokenClassification` | bert-based | `boltuix/NeuroBERT-NER` (see note), etc. | | |
| `ModernBertForTokenClassification` | ModernBERT-based | `disham993/electrical-ner-ModernBERT-base` | | |
| `OpenAIPrivacyFilterForTokenClassification` | gpt-oss-based encoder | `openai/privacy-filter` | | |
| `Qwen3ForTokenClassification`<sup>C</sup> | Qwen3-based | `bd2lcco/Qwen3-0.6B-finetuned` | | |
| `*Model`<sup>C</sup>, `*ForCausalLM`<sup>C</sup>, etc. | Generative models | N/A | \* | \* |
+1 -2
View File
@@ -586,7 +586,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
| `MiDashengLMModel` | MiDashengLM | T + A<sup>+</sup> | `mispeech/midashenglm-7b` | | ✅︎ |
| `MiMoV2OmniForCausalLM` | MiMo-V2.5-Omni | T + I<sup>E+</sup> + V<sup>E+</sup> + A<sup>+</sup> | `XiaomiMiMo/MiMo-V2.5-Omni` | | ✅︎ |
| `MiniCPMO` | MiniCPM-O | T + I<sup>E+</sup> + V<sup>E+</sup> + A<sup>E+</sup> | `openbmb/MiniCPM-o-2_6`, etc. | ✅︎ | ✅︎ |
| `MiniCPMV` | MiniCPM-V | T + I<sup>E+</sup> + V<sup>E+</sup> | `openbmb/MiniCPM-V-2` (see note), `openbmb/MiniCPM-Llama3-V-2_5`, `openbmb/MiniCPM-V-2_6`, `openbmb/MiniCPM-V-4`, `openbmb/MiniCPM-V-4_5`, `openbmb/MiniCPM-V-4_6`, etc. | ✅︎ | |
| `MiniCPMV` | MiniCPM-V | T + I<sup>E+</sup> + V<sup>E+</sup> | `openbmb/MiniCPM-V-2` (see note), `openbmb/MiniCPM-Llama3-V-2_5`, `openbmb/MiniCPM-V-2_6`, `openbmb/MiniCPM-V-4`, `openbmb/MiniCPM-V-4_5`, etc. | ✅︎ | |
| `MiniMaxM3SparseForConditionalGeneration` | MiniMax-M3 | T + I<sup>+</sup> + V<sup>+</sup> | `MiniMaxAI/MiniMax-M3`, `MiniMaxAI/MiniMax-M3-MXFP8`, etc. | | ✅︎ |
| `MiniMaxVL01ForConditionalGeneration` | MiniMax-VL | T + I<sup>E+</sup> | `MiniMaxAI/MiniMax-VL-01`, etc. | | ✅︎ |
| `Mistral3ForConditionalGeneration` | Mistral3 (HF Transformers) | T + I<sup>+</sup> | `mistralai/Mistral-Small-3.1-24B-Instruct-2503`, etc. | ✅︎ | ✅︎ |
@@ -629,7 +629,6 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
| `TarsierForConditionalGeneration` | Tarsier | T + I<sup>E+</sup> | `omni-search/Tarsier-7b`, `omni-search/Tarsier-34b` | | ✅︎ |
| `Tarsier2ForConditionalGeneration`<sup>^</sup> | Tarsier2 | T + I<sup>E+</sup> + V<sup>E+</sup> | `omni-research/Tarsier2-Recap-7b`, `omni-research/Tarsier2-7b-0115` | | ✅︎ |
| `UltravoxModel` | Ultravox | T + A<sup>E+</sup> | `fixie-ai/ultravox-v0_5-llama-3_2-1b` | ✅︎ | ✅︎ |
| `UnlimitedOCRForCausalLM` | Unlimited-OCR | T + I<sup>+</sup> | `baidu/Unlimited-OCR`, etc. | ✅︎ | ✅︎ |
Some models are supported only via the [Transformers modeling backend](#transformers). The purpose of the table below is to acknowledge models which we officially support in this way. The logs will say that the Transformers modeling backend is being used, and you will see no warning that this is fallback behaviour. This means that, if you have issues with any of the models listed below, please [make an issue](https://github.com/vllm-project/vllm/issues/new/choose) and we'll do our best to fix it!
+2 -2
View File
@@ -119,9 +119,9 @@ For further details on profiling vLLM, please refer to [this page](../../contrib
- `/ping` - SageMaker health check
- `/invocations` - SageMaker-compatible endpoint (routes to the same inference functions as `/v1` endpoints)
## Scale-Out APIs
## Disaggregated Everything
### Tokens IN <> Tokens OUT APIs
### Tokens IN <> Tokens OUT
- `/inference/v1/generate` - Generate completions
- `/abort_requests` - Abort in-flight requests (only when `--tokens-only` is also set)
-15
View File
@@ -85,21 +85,6 @@ significantly reduce the attack surface for these types of abuse.
Also, consider setting `VLLM_MEDIA_URL_ALLOW_REDIRECTS=0` to prevent HTTP
redirects from being followed to bypass domain restrictions.
### 5. **Restrict Media Decode Sizes:**
Compressed media files can expand into gigabytes of memory during decoding. vLLM
enforces decode-size limits to prevent out-of-memory denial of service:
| Environment Variable | Default | Description |
| --- | --- | --- |
| `VLLM_MAX_IMAGE_PIXELS` | `178956970` (~179M pixels) | Maximum decoded image size in pixels. Images exceeding this are rejected before raster memory is allocated. Default matches PIL's built-in 2x decompression-bomb threshold (~680 MB for RGB). |
| `VLLM_MAX_AUDIO_CLIP_FILESIZE_MB` | `25` | Maximum filesize in MB for a single audio file. |
| `VLLM_MAX_AUDIO_DECODE_DURATION_S` | `600` | Maximum decoded audio duration in seconds. Prevents compressed audio from expanding into gigabytes of float32 PCM. |
Setting any of these to `0` disables the corresponding limit. This is **not
recommended** for deployments exposed to untrusted users, as it removes the
protection against resource-exhaustion attacks.
## Security and Firewalls: Protecting Exposed vLLM Systems
While vLLM is designed to allow unsafe network services to be isolated to
+4 -3
View File
@@ -190,11 +190,12 @@ class TrainModel:
# Build platform-specific env vars for Ray
ray_env_vars = {}
ray_env_vars = {
# Prevent Ray from setting CUDA_VISIBLE_DEVICES
"RAY_EXPERIMENTAL_NOSET_CUDA_ENV_VAR": "1",
}
if current_platform.is_rocm():
# Workaround for RCCL bug. See https://github.com/ROCm/rocm-systems/issues/5756
ray_env_vars["RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES"] = "1"
# For ROCm, BATCH_INVARIANT vllm is not supported
ray_env_vars["VLLM_ROCM_USE_SKINNY_GEMM"] = "0"
else:
+1 -16
View File
@@ -29,7 +29,6 @@ causes unexpected behavior.
import os
import ray
import torch
from ray.util.placement_group import placement_group
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
from transformers import AutoModelForCausalLM
@@ -40,24 +39,12 @@ from vllm.distributed.weight_transfer.nccl_engine import (
NCCLTrainerSendWeightsArgs,
NCCLWeightTransferEngine,
)
from vllm.platforms import current_platform
from vllm.utils.network_utils import get_ip, get_open_port
MODEL_NAME = "facebook/opt-125m"
# MODEL_NAME = "inference-optimization/Qwen3-0.6B-W4A16-G128"
def get_assigned_gpu():
"""This is a temporary workaround for a runtime bug in RCCL on ROCm."""
if not current_platform.is_rocm():
return 0
assigned_gpu = int(ray.get_gpu_ids()[0])
os.environ.pop("CUDA_VISIBLE_DEVICES", None)
os.environ.pop("HIP_VISIBLE_DEVICES", None)
torch.accelerator.set_device_idx(assigned_gpu)
return assigned_gpu
class MyLLM(LLM):
"""Configure the vLLM worker for Ray placement group execution."""
@@ -71,11 +58,9 @@ class TrainModel:
"""Ray actor that wraps the training model on a dedicated GPU."""
def __init__(self, model_name: str):
assigned_gpu = get_assigned_gpu()
self.model = AutoModelForCausalLM.from_pretrained(
model_name,
).to(f"cuda:{assigned_gpu}")
).to("cuda:0")
self.port = get_open_port()
self.master_address = get_ip()
+2 -3
View File
@@ -8,10 +8,9 @@ torch==2.11.0
torchaudio==2.11.0
# These must be updated alongside torch
torchvision==0.26.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version
PyNvVideoCodec==2.0.4
# FlashInfer should be updated together with the Dockerfile
flashinfer-python==0.6.13
flashinfer-cubin==0.6.13
flashinfer-python==0.6.12
flashinfer-cubin==0.6.12
apache-tvm-ffi==0.1.9
tilelang==0.1.9
nvidia-cudnn-frontend>=1.19.1
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -1,5 +1,3 @@
-r ../common.txt
# testing
pytest
tensorizer==2.10.1
@@ -15,6 +13,7 @@ albumentations # required for Nemotron Parse in test_common.py
av # required for audio_in_video tests
backoff # required for phi4mm test
blobfile # required for kimi-vl test
einops # required for MPT, qwen-vl
httpx
librosa # required for audio tests
vector_quantize_pytorch # required for minicpmo_26 test
@@ -35,6 +34,7 @@ matplotlib # required for qwen-vl test
mistral_common[image,audio] >= 1.11.5 # required for voxtral test
num2words # required for smolvlm test
open_clip_torch==2.32.0 # Required for nemotron_vl test, Nemotron Parse in test_common.py
opencv-python-headless >= 4.13.0 # required for video test
datamodel_code_generator # required for minicpm3 test
lm-eval[api]>=0.4.12 # required for model evaluation test
mteb[bm25s]>=2, <3 # required for mteb test
@@ -55,9 +55,11 @@ grpcio-reflection==1.78.0
arctic-inference == 0.1.1; platform_machine == "x86_64" # Required for suffix decoding test
numba == 0.65.0 # Required for N-gram speculative decoding
numpy
runai-model-streamer[s3,gcs,azure]==0.15.7
fastsafetensors>=0.3.2
instanttensor>=0.1.5; platform_machine == "x86_64"
pydantic>=2.12 # 2.11 leads to error on python 3.13
decord==0.6.0; platform_machine == "x86_64"
# terratorch is temporarily disabled while PyPI has the `lightning` package
# in `quarantined` status (every published terratorch version transitively
@@ -71,7 +73,6 @@ gpt-oss >= 0.0.7; python_version > '3.11'
perceptron # required for isaac test
kaldi-native-fbank >= 1.18.7 # required for fireredasr2 test
cohere_melody>=0.9.0 # required for cohere command reasoning parser test
# Newer versions of datasets require torchcoded, that makes the tests fail in CI because of a missing library.
# Older versions are in conflict with teerratorch requirements.
+18 -290
View File
@@ -9,7 +9,6 @@ aiohappyeyeballs==2.6.1
aiohttp==3.13.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# aiohttp-cors
# datasets
# fsspec
@@ -25,34 +24,17 @@ albumentations==1.4.6
alembic==1.16.4
# via optuna
annotated-doc==0.0.4
# via
# fastapi
# typer
# via fastapi
annotated-types==0.7.0
# via pydantic
anthropic==0.112.0
anyio==4.6.2.post1
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
anyio==4.14.1
# via
# anthropic
# httpx
# mcp
# openai
# sse-starlette
# starlette
# watchfiles
apache-tvm-ffi==0.1.9
# via
# -c requirements/cuda.txt
# xgrammar
arctic-inference==0.1.1
# via -r requirements/test/cuda.in
argcomplete==3.5.1
# via datamodel-code-generator
astor==0.8.1
# via depyf
attrs==24.2.0
# via
# aiohttp
@@ -77,8 +59,6 @@ bitsandbytes==0.49.2
# via -r requirements/test/cuda.in
black==24.10.0
# via datamodel-code-generator
blake3==1.0.9
# via -r requirements/test/../common.txt
blobfile==3.0.0
# via -r requirements/test/cuda.in
bm25s==0.2.13
@@ -96,17 +76,12 @@ bounded-pool-executor==0.0.3
buildkite-test-collector==0.1.9
# via -r requirements/test/cuda.in
cachetools==5.5.2
# via
# -r requirements/test/../common.txt
# google-auth
cbor2==6.1.2
# via -r requirements/test/../common.txt
# via google-auth
certifi==2024.8.30
# via
# httpcore
# httpx
# requests
# sentry-sdk
cffi==2.0.0
# via
# cryptography
@@ -123,13 +98,9 @@ click==8.1.7
# jiwer
# nltk
# ray
# rich-toolkit
# schemathesis
# typer
# uvicorn
cloudpickle==3.1.2
# via -r requirements/test/../common.txt
cohere-melody==0.9.0
# via -r requirements/test/cuda.in
colorama==0.4.6
# via
# perceptron
@@ -138,10 +109,6 @@ colorful==0.5.6
# via ray
colorlog==6.10.1
# via optuna
compressed-tensors==0.17.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
contourpy==1.3.0
# via matplotlib
coverage==7.10.6
@@ -180,49 +147,30 @@ decorator==5.1.1
# via librosa
decord==0.6.0
# via -r requirements/test/cuda.in
depyf==0.20.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
detect-installer==0.1.0
# via fastapi-cloud-cli
dill==0.3.8
# via
# datasets
# depyf
# evaluate
# lm-eval
# multiprocess
diskcache==5.6.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
distlib==0.3.9
# via virtualenv
distro==1.9.0
# via
# anthropic
# openai
dnspython==2.7.0
# via email-validator
docker==7.1.0
# via gpt-oss
docopt==0.6.2
# via num2words
docstring-parser==0.18.0
# via anthropic
einops==0.8.1
# via
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# encodec
# vector-quantize-pytorch
# vocos
einx==0.3.0
# via vector-quantize-pytorch
email-validator==2.2.0
# via
# fastapi
# pydantic
# via pydantic
encodec==0.1.1
# via vocos
et-xmlfile==2.0.0
@@ -232,17 +180,7 @@ evaluate==0.4.3
fastapi==0.136.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# gpt-oss
# model-hosting-container-standards
fastapi-cli==0.0.27
# via fastapi
fastapi-cloud-cli==0.21.0
# via fastapi-cli
fastar==0.11.0
# via
# fastapi
# fastapi-cloud-cli
fastparquet==2024.11.0
# via genai-perf
fastrlock==0.8.2
@@ -254,7 +192,6 @@ fastsafetensors==0.3.2
filelock==3.16.1
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# blobfile
# datasets
# huggingface-hub
@@ -304,10 +241,7 @@ google-crc32c==1.7.1
google-resumable-media==2.7.2
# via google-cloud-storage
googleapis-common-protos==1.70.0
# via
# google-api-core
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# via google-api-core
gpt-oss==0.0.8
# via -r requirements/test/cuda.in
graphql-core==3.2.6
@@ -318,7 +252,6 @@ grpcio==1.78.0
# via
# -r requirements/test/cuda.in
# grpcio-reflection
# opentelemetry-exporter-otlp-proto-grpc
# ray
grpcio-reflection==1.78.0
# via -r requirements/test/cuda.in
@@ -340,22 +273,12 @@ html2text==2025.4.15
# via gpt-oss
httpcore==1.0.6
# via httpx
httptools==0.8.0
# via uvicorn
httpx==0.27.2
# via
# -r requirements/test/cuda.in
# anthropic
# fastapi
# fastapi-cloud-cli
# huggingface-hub
# mcp
# model-hosting-container-standards
# openai
# perceptron
# schemathesis
httpx-sse==0.4.3
# via mcp
huggingface-hub==1.10.2
# via
# accelerate
@@ -389,8 +312,6 @@ idna==3.10
# httpx
# requests
# yarl
ijson==3.5.0
# via -r requirements/test/../common.txt
imagehash==4.3.2
# via -r requirements/test/cuda.in
imageio==2.37.0
@@ -403,8 +324,6 @@ iniconfig==2.0.0
# via pytest
instanttensor==0.1.5
# via -r requirements/test/cuda.in
interegular==0.3.3
# via lm-format-enforcer
isodate==0.7.2
# via azure-storage-blob
isort==5.13.2
@@ -412,21 +331,15 @@ isort==5.13.2
jinja2==3.1.6
# via
# datamodel-code-generator
# fastapi
# genai-perf
# lm-eval
# torch
jiter==0.15.0
# via
# anthropic
# openai
jiwer==3.0.5
# via -r requirements/test/cuda.in
jmespath==1.0.1
# via
# boto3
# botocore
# model-hosting-container-standards
joblib==1.4.2
# via
# librosa
@@ -435,9 +348,7 @@ joblib==1.4.2
jsonschema==4.23.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# hypothesis-jsonschema
# mcp
# mistral-common
# ray
jsonschema-rs==0.46.5
@@ -452,10 +363,6 @@ kaleido==0.2.1
# via genai-perf
kiwisolver==1.4.7
# via matplotlib
lark==1.2.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
lazy-loader==0.4
# via
# librosa
@@ -464,20 +371,10 @@ libnacl==2.1.0
# via tensorizer
librosa==0.10.2.post1
# via -r requirements/test/cuda.in
llguidance==1.7.6
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
llvmlite==0.47.0
# via numba
lm-eval==0.4.12
# via -r requirements/test/cuda.in
lm-format-enforcer==0.11.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
loguru==0.7.3
# via compressed-tensors
lxml==5.3.0
# via
# blobfile
@@ -499,19 +396,12 @@ mbstrdecoder==1.1.3
# dataproperty
# pytablewriter
# typepy
mcp==1.28.1
# via -r requirements/test/../common.txt
mdurl==0.1.2
# via markdown-it-py
mistral-common==1.11.5
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
model-hosting-container-standards==0.1.16
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
more-itertools==10.5.0
# via lm-eval
mpmath==1.3.0
@@ -526,8 +416,6 @@ msgpack==1.1.0
# via
# librosa
# ray
msgspec==0.21.1
# via -r requirements/test/../common.txt
mteb==2.8.3
# via -r requirements/test/cuda.in
multidict==6.1.0
@@ -544,8 +432,6 @@ networkx==3.2.1
# via
# scikit-image
# torch
ninja==1.13.0
# via -r requirements/test/../common.txt
nltk==3.9.1
# via rouge-score
num2words==0.5.14
@@ -557,7 +443,7 @@ numba==0.65.0
# librosa
numpy==2.2.6
# via
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# accelerate
# albumentations
# bitsandbytes
@@ -601,7 +487,6 @@ numpy==2.2.6
# transformers
# tritonclient
# vocos
# xgrammar
nvidia-cublas==13.1.0.3
# via
# cuda-toolkit
@@ -643,14 +528,9 @@ nvidia-nvtx==13.0.85
# via cuda-toolkit
open-clip-torch==2.32.0
# via -r requirements/test/cuda.in
openai==2.44.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
openai-harmony==0.0.4
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# gpt-oss
opencensus==0.11.4
# via ray
@@ -659,7 +539,7 @@ opencensus-context==0.1.3
opencv-python-headless==4.13.0.90
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# albumentations
# mistral-common
openpyxl==3.1.5
@@ -667,54 +547,24 @@ openpyxl==3.1.5
opentelemetry-api==1.35.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-exporter-prometheus
# opentelemetry-sdk
# opentelemetry-semantic-conventions
opentelemetry-exporter-otlp==1.35.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
opentelemetry-exporter-otlp-proto-common==1.35.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-exporter-otlp-proto-grpc==1.35.0
# via opentelemetry-exporter-otlp
opentelemetry-exporter-otlp-proto-http==1.35.0
# via opentelemetry-exporter-otlp
opentelemetry-exporter-prometheus==0.56b0
# via ray
opentelemetry-proto==1.35.0
# via
# opentelemetry-exporter-otlp-proto-common
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# ray
# via ray
opentelemetry-sdk==1.35.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-exporter-prometheus
# ray
opentelemetry-semantic-conventions==0.56b0
# via opentelemetry-sdk
opentelemetry-semantic-conventions-ai==0.4.13
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
optuna==3.6.1
# via genai-perf
orjson==3.11.5
# via genai-perf
outlines-core==0.2.14
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
packaging==24.2
# via
# accelerate
@@ -726,7 +576,6 @@ packaging==24.2
# fastparquet
# huggingface-hub
# lazy-loader
# lm-format-enforcer
# matplotlib
# optuna
# peft
@@ -746,8 +595,6 @@ pandas==2.2.3
# fastparquet
# genai-perf
# statsmodels
partial-json-parser==0.2.1.1.post7
# via -r requirements/test/../common.txt
pathspec==0.12.1
# via black
pathvalidate==3.2.1
@@ -762,7 +609,6 @@ perf-analyzer==0.1.0
# via genai-perf
pillow==10.4.0
# via
# -r requirements/test/../common.txt
# genai-perf
# imagehash
# imageio
@@ -796,14 +642,8 @@ pqdm==0.2.0
prometheus-client==0.22.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# opentelemetry-exporter-prometheus
# prometheus-fastapi-instrumentator
# ray
prometheus-fastapi-instrumentator==8.0.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
propcache==0.2.0
# via
# aiohttp
@@ -813,7 +653,6 @@ proto-plus==1.26.1
protobuf==6.33.6
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# google-api-core
# googleapis-common-protos
# grpcio-reflection
@@ -823,14 +662,11 @@ protobuf==6.33.6
# tensorizer
psutil==6.1.0
# via
# -r requirements/test/../common.txt
# accelerate
# peft
# tensorizer
py==1.11.0
# via pytest-forked
py-cpuinfo==9.0.0
# via -r requirements/test/../common.txt
py-spy==0.4.0
# via ray
pyarrow==23.0.0
@@ -843,8 +679,6 @@ pyasn1==0.6.1
# rsa
pyasn1-modules==0.4.2
# via google-auth
pybase64==1.4.3
# via -r requirements/test/../common.txt
pycountry==24.6.1
# via pydantic-extra-types
pycparser==2.22
@@ -854,43 +688,26 @@ pycryptodomex==3.22.0
pydantic==2.12.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# albumentations
# anthropic
# compressed-tensors
# datamodel-code-generator
# fastapi
# fastapi-cloud-cli
# gpt-oss
# lm-format-enforcer
# mcp
# mistral-common
# model-hosting-container-standards
# mteb
# openai
# openai-harmony
# pydantic-extra-types
# pydantic-settings
# ray
# xgrammar
pydantic-core==2.41.1
# via pydantic
pydantic-extra-types==2.10.5
# via
# fastapi
# mistral-common
pydantic-settings==2.14.2
# via
# fastapi
# mcp
# via mistral-common
pygments==2.18.0
# via
# pytest
# rich
pyjwt==2.11.0
# via
# mcp
# msal
# via msal
pyparsing==3.2.0
# via matplotlib
pyrate-limiter==4.4.0
@@ -932,16 +749,6 @@ python-dateutil==2.9.0.post0
# matplotlib
# pandas
# typepy
python-dotenv==1.2.2
# via
# pydantic-settings
# uvicorn
python-json-logger==4.1.0
# via -r requirements/test/../common.txt
python-multipart==0.0.32
# via
# fastapi
# mcp
python-rapidjson==1.20
# via tritonclient
pytrec-eval-terrier==0.5.7
@@ -954,14 +761,12 @@ pywavelets==1.9.0
# via imagehash
pyyaml==6.0.2
# via
# -r requirements/test/../common.txt
# accelerate
# albumentations
# datamodel-code-generator
# datasets
# genai-perf
# huggingface-hub
# lm-format-enforcer
# optuna
# peft
# ray
@@ -969,12 +774,7 @@ pyyaml==6.0.2
# schemathesis
# timm
# transformers
# uvicorn
# vocos
pyzmq==27.1.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
rapidfuzz==3.12.1
# via jiwer
ray==2.48.0
@@ -987,7 +787,6 @@ referencing==0.35.1
# jsonschema-specifications
regex==2026.2.28
# via
# -r requirements/test/../common.txt
# nltk
# open-clip-torch
# sacrebleu
@@ -996,7 +795,6 @@ regex==2026.2.28
requests==2.32.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# azure-core
# buildkite-test-collector
# datasets
@@ -1009,7 +807,6 @@ requests==2.32.3
# mistral-common
# msal
# mteb
# opentelemetry-exporter-otlp-proto-http
# pooch
# ray
# responses
@@ -1023,15 +820,8 @@ rich==13.9.4
# genai-perf
# mteb
# perceptron
# rich-toolkit
# schemathesis
# typer
rich-toolkit==0.20.1
# via
# fastapi-cli
# fastapi-cloud-cli
rignore==0.7.6
# via fastapi-cloud-cli
rouge-score==0.1.2
# via lm-eval
rpds-py==0.20.1
@@ -1055,7 +845,6 @@ sacrebleu==2.4.3
safetensors==0.7.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# accelerate
# open-clip-torch
# peft
@@ -1091,17 +880,9 @@ sentence-transformers==5.2.0
# via
# -r requirements/test/cuda.in
# mteb
sentencepiece==0.2.1
# via -r requirements/test/../common.txt
sentry-sdk==2.63.0
# via fastapi-cloud-cli
setproctitle==1.3.7
# via -r requirements/test/../common.txt
setuptools==77.0.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# model-hosting-container-standards
# pytablewriter
# torch
shellingham==1.5.4
@@ -1111,7 +892,6 @@ shellingham==1.5.4
six==1.16.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# junit-xml
# opencensus
# python-dateutil
@@ -1120,9 +900,8 @@ smart-open==7.1.0
# via ray
sniffio==1.3.1
# via
# anthropic
# anyio
# httpx
# openai
sortedcontainers==2.4.0
# via hypothesis
soundfile==0.12.1
@@ -1141,17 +920,10 @@ sqlalchemy==2.0.41
# optuna
sqlitedict==2.1.0
# via lm-eval
sse-starlette==3.4.5
# via mcp
starlette==1.3.1
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# fastapi
# mcp
# model-hosting-container-standards
# prometheus-fastapi-instrumentator
# sse-starlette
# starlette-testclient
starlette-testclient==0.4.1
# via schemathesis
@@ -1159,8 +931,6 @@ statsmodels==0.14.4
# via genai-perf
structlog==25.4.0
# via gpt-oss
supervisor==4.3.0
# via model-hosting-container-standards
sympy==1.13.3
# via
# einx
@@ -1190,7 +960,6 @@ tifffile==2025.3.30
tiktoken==0.12.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# gpt-oss
# lm-eval
# mistral-common
@@ -1202,7 +971,6 @@ timm==1.0.17
tokenizers==0.22.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# transformers
torch==2.11.0+cu130
@@ -1211,7 +979,6 @@ torch==2.11.0+cu130
# -r requirements/test/cuda.in
# accelerate
# bitsandbytes
# compressed-tensors
# encodec
# instanttensor
# mteb
@@ -1225,7 +992,6 @@ torch==2.11.0+cu130
# torchvision
# vector-quantize-pytorch
# vocos
# xgrammar
torchaudio==2.11.0+cu130
# via
# -c requirements/cuda.txt
@@ -1241,7 +1007,6 @@ torchvision==0.26.0+cu130
# timm
tqdm==4.67.3
# via
# -r requirements/test/../common.txt
# datasets
# evaluate
# huggingface-hub
@@ -1249,7 +1014,6 @@ tqdm==4.67.3
# mteb
# nltk
# open-clip-torch
# openai
# optuna
# peft
# pqdm
@@ -1259,20 +1023,15 @@ tqdm==4.67.3
transformers==5.5.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# compressed-tensors
# genai-perf
# peft
# sentence-transformers
# transformers-stream-generator
# xgrammar
transformers-stream-generator==0.0.5
# via -r requirements/test/cuda.in
triton==3.6.0
# via
# torch
# xgrammar
# via torch
tritonclient==2.64.0
# via -r requirements/test/cuda.in
typepy==1.3.2
@@ -1280,10 +1039,8 @@ typepy==1.3.2
# dataproperty
# pytablewriter
# tabledata
typer==0.26.8
typer==0.15.2
# via
# fastapi-cli
# fastapi-cloud-cli
# fastsafetensors
# huggingface-hub
# perceptron
@@ -1291,13 +1048,9 @@ typer==0.26.8
typing-extensions==4.15.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# aiosignal
# albumentations
# alembic
# anthropic
# anyio
# apache-tvm-ffi
# azure-core
# azure-identity
# azure-storage-blob
@@ -1307,13 +1060,9 @@ typing-extensions==4.15.0
# huggingface-hub
# librosa
# lm-eval
# mcp
# mistral-common
# mteb
# openai
# opentelemetry-api
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-sdk
# opentelemetry-semantic-conventions
# pqdm
@@ -1321,20 +1070,17 @@ typing-extensions==4.15.0
# pydantic-core
# pydantic-extra-types
# pytest-asyncio
# rich-toolkit
# schemathesis
# sentence-transformers
# sqlalchemy
# starlette
# torch
# typer
# typing-inspection
# xgrammar
typing-inspection==0.4.2
# via
# fastapi
# mcp
# pydantic
# pydantic-settings
tzdata==2024.2
# via pandas
urllib3==2.2.3
@@ -1344,41 +1090,23 @@ urllib3==2.2.3
# docker
# requests
# responses
# sentry-sdk
# tritonclient
uvicorn==0.35.0
# via
# fastapi
# fastapi-cli
# fastapi-cloud-cli
# gpt-oss
# mcp
uvloop==0.22.1
# via uvicorn
# via gpt-oss
vector-quantize-pytorch==1.21.2
# via -r requirements/test/cuda.in
virtualenv==20.31.2
# via ray
vocos==0.1.0
# via -r requirements/test/cuda.in
watchfiles==1.2.0
# via
# -r requirements/test/../common.txt
# uvicorn
wcwidth==0.2.13
# via ftfy
websockets==16.0
# via uvicorn
werkzeug==3.1.3
# via schemathesis
word2number==1.1
# via lm-eval
wrapt==1.17.2
# via smart-open
xgrammar==0.2.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
xxhash==3.5.0
# via
# datasets
+4 -1
View File
@@ -15,6 +15,7 @@ albumentations # required for Nemotron Parse in test_common.py
av # required for audio_in_video tests
backoff # required for phi4mm test
blobfile # required for kimi-vl test
einops # required for MPT, qwen-vl
httpx
librosa # required for audio tests
vector_quantize_pytorch # required for minicpmo_26 test
@@ -32,6 +33,7 @@ matplotlib # required for qwen-vl test
mistral_common[image,audio]>=1.11.5 # required for voxtral test
num2words # required for smolvlm test
open_clip_torch==2.32.0 # Required for nemotron_vl test, Nemotron Parse in test_common.py
opencv-python-headless>=4.13.0 # required for video test
datamodel_code_generator # required for minicpm3 test
lm-eval[api]>=0.4.12 # required for model evaluation test
mteb[bm25s]>=2, <3 # required for mteb test
@@ -52,9 +54,11 @@ grpcio-reflection==1.78.0
arctic-inference==0.1.1 # Required for suffix decoding test
numba==0.65.0 # Required for N-gram speculative decoding
numpy
runai-model-streamer[s3,gcs,azure]==0.15.7
fastsafetensors>=0.3.2
instanttensor>=0.1.5
pydantic>=2.12 # 2.11 leads to error on python 3.13
decord==0.6.0
# Prithvi tests
@@ -70,7 +74,6 @@ gpt-oss>=0.0.7; python_version > '3.11'
perceptron # required for isaac test
kaldi-native-fbank>=1.18.7 # required for fireredasr2 test
cohere_melody>=0.9.0 # required for cohere command reasoning parser test
# Newer versions of datasets require torchcoded, that makes the tests fail in CI because of a missing library.
# Older versions are in conflict with terratorch requirements.
+4 -2
View File
@@ -130,8 +130,6 @@ cloudpickle==3.1.2
# via
# -r requirements/test/../common.txt
# tilelang
cohere-melody==0.9.0
# via -r requirements/test/rocm.in
colorama==0.4.6
# via
# perceptron
@@ -207,6 +205,7 @@ docstring-parser==0.17.0
einops==0.8.2
# via
# -r requirements/test/../common.txt
# -r requirements/test/rocm.in
# encodec
# vector-quantize-pytorch
# vocos
@@ -562,6 +561,7 @@ numba==0.65.0
numpy==2.2.6
# via
# -r requirements/test/../common.txt
# -r requirements/test/rocm.in
# accelerate
# albumentations
# bitsandbytes
@@ -630,6 +630,7 @@ opencv-python-headless==4.13.0.92
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/rocm.in
# albumentations
# mistral-common
openpyxl==3.1.5
@@ -833,6 +834,7 @@ pydantic==2.12.5
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/rocm.in
# albumentations
# anthropic
# compressed-tensors
-2
View File
@@ -1,5 +1,3 @@
-r ../common.txt
# --- Test Infrastructure ---
tblib
pytest
+4 -316
View File
@@ -11,7 +11,6 @@ aiohappyeyeballs==2.6.1
aiohttp==3.13.4
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# fsspec
# gpt-oss
# lm-eval
@@ -25,25 +24,12 @@ annotated-doc==0.0.4
# typer
annotated-types==0.7.0
# via pydantic
anthropic==0.112.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
anyio==4.13.0
# via
# anthropic
# httpx
# mcp
# openai
# sse-starlette
# starlette
# watchfiles
apache-tvm-ffi==0.1.12
# via xgrammar
arctic-inference==0.1.1
# via -r requirements/test/xpu.in
astor==0.8.1
# via depyf
attrs==26.1.0
# via
# aiohttp
@@ -53,8 +39,6 @@ audioread==3.0.1
# via
# -r requirements/test/xpu.in
# librosa
blake3==1.0.9
# via -r requirements/test/../common.txt
blobfile==3.0.0
# via -r requirements/test/xpu.in
bm25s==0.2.13
@@ -63,20 +47,13 @@ bm25s==0.2.13
# mteb
bounded-pool-executor==0.0.3
# via pqdm
cachetools==7.1.4
# via -r requirements/test/../common.txt
cbor2==6.1.2
# via -r requirements/test/../common.txt
certifi==2026.2.25
# via
# httpcore
# httpx
# requests
# sentry-sdk
cffi==2.0.0
# via
# cryptography
# soundfile
# via soundfile
chardet==5.2.0
# via mbstrdecoder
charset-normalizer==3.4.6
@@ -87,22 +64,13 @@ click==8.3.1
# via
# jiwer
# nltk
# rich-toolkit
# schemathesis
# typer
# uvicorn
cloudpickle==3.1.2
# via -r requirements/test/../common.txt
colorama==0.4.6
# via sacrebleu
compressed-tensors==0.17.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
coverage==7.13.5
# via pytest-cov
cryptography==49.0.0
# via pyjwt
dataproperty==1.1.0
# via
# pytablewriter
@@ -114,35 +82,16 @@ datasets==4.8.4
# mteb
decorator==5.2.1
# via librosa
depyf==0.20.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
detect-installer==0.1.0
# via fastapi-cloud-cli
dill==0.4.1
# via
# datasets
# depyf
# evaluate
# lm-eval
# multiprocess
diskcache==5.6.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
distro==1.9.0
# via
# anthropic
# openai
dnspython==2.8.0
# via email-validator
docker==7.1.0
# via gpt-oss
docopt==0.6.2
# via num2words
docstring-parser==0.18.0
# via anthropic
dpcpp-cpp-rt==2025.3.2
# via
# onemkl-sycl-blas
@@ -151,30 +100,15 @@ dpcpp-cpp-rt==2025.3.2
# onemkl-sycl-rng
# onemkl-sycl-sparse
# torch
einops==0.8.2
# via -r requirements/test/../common.txt
email-validator==2.3.0
# via
# fastapi
# pydantic
evaluate==0.4.6
# via lm-eval
fastapi==0.135.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# gpt-oss
# model-hosting-container-standards
fastapi-cli==0.0.27
# via fastapi
fastapi-cloud-cli==0.21.0
# via fastapi-cli
fastar==0.11.0
# via fastapi-cloud-cli
filelock==3.25.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# blobfile
# datasets
# huggingface-hub
@@ -190,16 +124,10 @@ fsspec==2026.2.0
# evaluate
# huggingface-hub
# torch
googleapis-common-protos==1.75.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
gpt-oss==0.0.8
# via -r requirements/test/xpu.in
graphql-core==3.2.8
# via hypothesis-graphql
grpcio==1.81.1
# via opentelemetry-exporter-otlp-proto-grpc
h11==0.16.0
# via
# httpcore
@@ -212,21 +140,11 @@ html2text==2025.4.15
# via gpt-oss
httpcore==1.0.9
# via httpx
httptools==0.8.0
# via uvicorn
httpx==0.28.1
# via
# anthropic
# datasets
# fastapi
# fastapi-cloud-cli
# huggingface-hub
# mcp
# model-hosting-container-standards
# openai
# schemathesis
httpx-sse==0.4.3
# via mcp
huggingface-hub==1.10.2
# via
# accelerate
@@ -248,12 +166,9 @@ hypothesis-jsonschema==0.23.1
idna==3.11
# via
# anyio
# email-validator
# httpx
# requests
# yarl
ijson==3.5.0
# via -r requirements/test/../common.txt
imageio==2.37.3
# via scikit-image
impi-rt==2021.17.2
@@ -297,22 +212,13 @@ intel-sycl-rt==2025.3.2
# dpcpp-cpp-rt
# oneccl
# torch
interegular==0.3.3
# via lm-format-enforcer
jinja2==3.1.6
# via
# -c requirements/xpu.txt
# fastapi
# lm-eval
# torch
jiter==0.15.0
# via
# anthropic
# openai
jiwer==4.0.0
# via -r requirements/test/xpu.in
jmespath==1.1.0
# via model-hosting-container-standards
joblib==1.5.3
# via
# librosa
@@ -321,9 +227,7 @@ joblib==1.5.3
jsonschema==4.26.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# hypothesis-jsonschema
# mcp
# mistral-common
# schemathesis
jsonschema-rs==0.45.0
@@ -332,30 +236,16 @@ jsonschema-specifications==2025.9.1
# via jsonschema
junit-xml==1.9
# via schemathesis
lark==1.2.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
lazy-loader==0.5
# via
# librosa
# scikit-image
librosa==0.10.2.post1
# via -r requirements/test/xpu.in
llguidance==1.7.6
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
llvmlite==0.47.0
# via numba
lm-eval==0.4.12
# via -r requirements/test/xpu.in
lm-format-enforcer==0.11.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
loguru==0.7.3
# via compressed-tensors
lxml==6.0.2
# via
# blobfile
@@ -372,14 +262,11 @@ mbstrdecoder==1.1.4
# dataproperty
# pytablewriter
# typepy
mcp==1.28.1
# via -r requirements/test/../common.txt
mdurl==0.1.2
# via markdown-it-py
mistral-common==1.11.5
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# -r requirements/test/xpu.in
mkl==2025.3.1
# via
@@ -389,10 +276,6 @@ mkl==2025.3.1
# onemkl-sycl-rng
# onemkl-sycl-sparse
# torch
model-hosting-container-standards==0.1.16
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
modelscope==1.35.3
# via -r requirements/test/xpu.in
more-itertools==10.8.0
@@ -401,8 +284,6 @@ mpmath==1.3.0
# via sympy
msgpack==1.1.2
# via librosa
msgspec==0.21.1
# via -r requirements/test/../common.txt
mteb==2.12.7
# via -r requirements/test/xpu.in
multidict==6.7.1
@@ -417,8 +298,6 @@ networkx==3.6.1
# via
# scikit-image
# torch
ninja==1.13.0
# via -r requirements/test/../common.txt
nltk==3.9.4
# via rouge-score
num2words==0.5.14
@@ -429,7 +308,6 @@ numba==0.65.0
# librosa
numpy==2.2.6
# via
# -r requirements/test/../common.txt
# accelerate
# albumentations
# bm25s
@@ -455,7 +333,6 @@ numpy==2.2.6
# tifffile
# torchvision
# transformers
# xgrammar
oneccl==2021.17.2
# via
# oneccl-devel
@@ -479,65 +356,15 @@ onemkl-sycl-rng==2025.3.1
# via torch
onemkl-sycl-sparse==2025.3.1
# via torch
openai==2.44.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
openai-harmony==0.0.8
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# gpt-oss
opencv-python-headless==4.13.0.92
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# albumentations
# mistral-common
opentelemetry-api==1.43.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-sdk
# opentelemetry-semantic-conventions
opentelemetry-exporter-otlp==1.43.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
opentelemetry-exporter-otlp-proto-common==1.43.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-exporter-otlp-proto-grpc==1.43.0
# via opentelemetry-exporter-otlp
opentelemetry-exporter-otlp-proto-http==1.43.0
# via opentelemetry-exporter-otlp
opentelemetry-proto==1.43.0
# via
# opentelemetry-exporter-otlp-proto-common
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-sdk==1.43.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-semantic-conventions-ai
opentelemetry-semantic-conventions==0.64b0
# via
# opentelemetry-sdk
# opentelemetry-semantic-conventions-ai
opentelemetry-semantic-conventions-ai==0.5.1
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
outlines-core==0.2.14
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
packaging==26.0
# via
# -c requirements/xpu.txt
@@ -546,7 +373,6 @@ packaging==26.0
# evaluate
# huggingface-hub
# lazy-loader
# lm-format-enforcer
# modelscope
# pooch
# pytest
@@ -558,13 +384,10 @@ pandas==3.0.1
# via
# datasets
# evaluate
partial-json-parser==0.2.1.1.post7
# via -r requirements/test/../common.txt
pathvalidate==3.3.1
# via pytablewriter
pillow==12.1.1
# via
# -r requirements/test/../common.txt
# imageio
# mistral-common
# scikit-image
@@ -587,37 +410,16 @@ portalocker==3.2.0
# via sacrebleu
pqdm==0.2.0
# via -r requirements/test/xpu.in
prometheus-client==0.25.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# prometheus-fastapi-instrumentator
prometheus-fastapi-instrumentator==8.0.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
propcache==0.4.1
# via
# aiohttp
# yarl
protobuf==7.35.1
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# googleapis-common-protos
# opentelemetry-proto
psutil==7.2.2
# via
# -r requirements/test/../common.txt
# accelerate
# via accelerate
py==1.11.0
# via pytest-forked
py-cpuinfo==9.0.0
# via -r requirements/test/../common.txt
pyarrow==23.0.1
# via datasets
pybase64==1.4.3
# via -r requirements/test/../common.txt
pycountry==26.2.16
# via pydantic-extra-types
pycparser==3.0
@@ -627,41 +429,23 @@ pycryptodomex==3.23.0
pydantic==2.12.5
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# albumentations
# anthropic
# compressed-tensors
# fastapi
# fastapi-cloud-cli
# gpt-oss
# lm-format-enforcer
# mcp
# mistral-common
# model-hosting-container-standards
# mteb
# openai
# openai-harmony
# pydantic-extra-types
# pydantic-settings
# xgrammar
pydantic-core==2.41.5
# via pydantic
pydantic-extra-types==2.11.1
# via
# fastapi
# mistral-common
pydantic-settings==2.14.2
# via
# fastapi
# mcp
# via mistral-common
pyelftools==0.32
# via triton-xpu
pygments==2.20.0
# via
# pytest
# rich
pyjwt==2.13.0
# via mcp
pyrate-limiter==4.1.0
# via schemathesis
pystemmer==3.0.0
@@ -696,36 +480,19 @@ python-dateutil==2.9.0.post0
# via
# pandas
# typepy
python-dotenv==1.2.2
# via
# pydantic-settings
# uvicorn
python-json-logger==4.1.0
# via -r requirements/test/../common.txt
python-multipart==0.0.32
# via
# fastapi
# mcp
pytrec-eval-terrier==0.5.10
# via mteb
pytz==2026.1.post1
# via typepy
pyyaml==6.0.3
# via
# -r requirements/test/../common.txt
# accelerate
# albumentations
# datasets
# huggingface-hub
# lm-format-enforcer
# schemathesis
# timm
# transformers
# uvicorn
pyzmq==27.1.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
rapidfuzz==3.12.1
# via
# -r requirements/test/xpu.in
@@ -736,7 +503,6 @@ referencing==0.37.0
# jsonschema-specifications
regex==2026.3.32
# via
# -r requirements/test/../common.txt
# nltk
# sacrebleu
# tiktoken
@@ -744,7 +510,6 @@ regex==2026.3.32
requests==2.33.1
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# datasets
# docker
# evaluate
@@ -753,7 +518,6 @@ requests==2.33.1
# mistral-common
# modelscope
# mteb
# opentelemetry-exporter-otlp-proto-http
# pooch
# schemathesis
# starlette-testclient
@@ -761,15 +525,8 @@ requests==2.33.1
rich==14.3.3
# via
# mteb
# rich-toolkit
# schemathesis
# typer
rich-toolkit==0.20.1
# via
# fastapi-cli
# fastapi-cloud-cli
rignore==0.7.6
# via fastapi-cloud-cli
rouge-score==0.1.2
# via lm-eval
rpds-py==0.30.0
@@ -781,7 +538,6 @@ sacrebleu==2.6.0
safetensors==0.7.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# accelerate
# timm
# transformers
@@ -808,18 +564,10 @@ scipy==1.17.1
# sentence-transformers
sentence-transformers==5.3.0
# via mteb
sentencepiece==0.2.1
# via -r requirements/test/../common.txt
sentry-sdk==2.63.0
# via fastapi-cloud-cli
setproctitle==1.3.7
# via -r requirements/test/../common.txt
setuptools==80.10.2
# via
# -c requirements/common.txt
# -c requirements/xpu.txt
# -r requirements/test/../common.txt
# model-hosting-container-standards
# modelscope
# pytablewriter
# torch
@@ -828,14 +576,9 @@ shellingham==1.5.4
six==1.17.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# junit-xml
# python-dateutil
# rouge-score
sniffio==1.3.1
# via
# anthropic
# openai
sortedcontainers==2.4.0
# via hypothesis
soundfile==0.13.1
@@ -850,24 +593,15 @@ soxr==0.5.0.post1
# mistral-common
sqlitedict==2.1.0
# via lm-eval
sse-starlette==3.4.5
# via mcp
starlette==1.3.1
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# fastapi
# mcp
# model-hosting-container-standards
# prometheus-fastapi-instrumentator
# sse-starlette
# starlette-testclient
starlette-testclient==0.4.1
# via schemathesis
structlog==25.5.0
# via gpt-oss
supervisor==4.3.0
# via model-hosting-container-standards
sympy==1.14.0
# via torch
tabledata==1.3.4
@@ -902,7 +636,6 @@ tifffile==2026.3.3
tiktoken==0.12.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# gpt-oss
# lm-eval
# mistral-common
@@ -911,23 +644,19 @@ timm==1.0.17
tokenizers==0.22.2
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# transformers
torch==2.12.0+xpu
# via
# -c requirements/xpu.txt
# accelerate
# compressed-tensors
# mteb
# sentence-transformers
# timm
# torchvision
# xgrammar
torchvision==0.27.0+xpu
# via timm
tqdm==4.67.3
# via
# -r requirements/test/../common.txt
# datasets
# evaluate
# huggingface-hub
@@ -935,19 +664,13 @@ tqdm==4.67.3
# modelscope
# mteb
# nltk
# openai
# pqdm
# sentence-transformers
# transformers
transformers==5.5.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# compressed-tensors
# sentence-transformers
# xgrammar
triton==3.7.1
# via xgrammar
triton-xpu==3.7.1
# via torch
typepy==1.3.4
@@ -957,53 +680,36 @@ typepy==1.3.4
# tabledata
typer==0.24.1
# via
# fastapi-cli
# fastapi-cloud-cli
# huggingface-hub
# transformers
typing-extensions==4.15.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
# aiosignal
# albumentations
# anthropic
# anyio
# apache-tvm-ffi
# chz
# fastapi
# grpcio
# huggingface-hub
# librosa
# lm-eval
# mcp
# mistral-common
# mteb
# openai
# opentelemetry-api
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-sdk
# opentelemetry-semantic-conventions
# pqdm
# pydantic
# pydantic-core
# pydantic-extra-types
# pytest-asyncio
# referencing
# rich-toolkit
# schemathesis
# sentence-transformers
# starlette
# torch
# typing-inspection
# xgrammar
typing-inspection==0.4.2
# via
# fastapi
# mcp
# pydantic
# pydantic-settings
umf==1.0.3
# via
# intel-cmplr-lib-ur
@@ -1014,30 +720,12 @@ urllib3==2.6.3
# docker
# modelscope
# requests
# sentry-sdk
uvicorn==0.42.0
# via
# fastapi
# fastapi-cli
# fastapi-cloud-cli
# gpt-oss
# mcp
uvloop==0.22.1
# via uvicorn
watchfiles==1.2.0
# via
# -r requirements/test/../common.txt
# uvicorn
websockets==16.0
# via uvicorn
# via gpt-oss
werkzeug==3.1.7
# via schemathesis
word2number==1.1
# via lm-eval
xgrammar==0.2.3
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
xxhash==3.6.0
# via
# datasets
+20 -70
View File
@@ -272,18 +272,6 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto_enums"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e4487600931c9a89f8db7ffbdf3fbdd45bb7bd85e26861f659a463cd0dff966"
dependencies = [
"derive_utils",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "auto_impl"
version = "1.3.0"
@@ -950,17 +938,6 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "derive_utils"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "362f47930db19fe7735f527e6595e4900316b893ebf6d48ad3d31be928d57dd6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -1501,9 +1478,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.15"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
dependencies = [
"atomic-waker",
"bytes",
@@ -1661,9 +1638,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.10.1"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
@@ -1676,6 +1653,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@@ -2591,14 +2569,15 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.81"
version = "0.10.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
@@ -2631,9 +2610,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.117"
version = "0.9.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
dependencies = [
"cc",
"libc",
@@ -2804,6 +2783,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -3003,7 +2988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
dependencies = [
"heck",
"itertools 0.14.0",
"itertools 0.10.5",
"log",
"multimap",
"petgraph",
@@ -3024,7 +3009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.14.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -3518,9 +3503,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"zeroize",
]
@@ -4400,22 +4385,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "tls-listener"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1461056cc1ef47003f7ee16e4cef3741068d4c7f6b627bfce49b7c00c120a530"
dependencies = [
"axum",
"futures-util",
"openssl",
"pin-project-lite",
"thiserror 2.0.18",
"tokio",
"tokio-openssl",
"tracing",
]
[[package]]
name = "tokenizers"
version = "0.22.2"
@@ -4488,17 +4457,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-openssl"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd"
dependencies = [
"openssl",
"openssl-sys",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@@ -5091,7 +5049,6 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"thiserror-ext",
"time",
"tokio",
"tracing",
"tracing-subscriber",
@@ -5262,7 +5219,6 @@ dependencies = [
"anyhow",
"async-openai",
"asynk-strim-attr",
"auto_enums",
"axum",
"bytes",
"clap",
@@ -5270,13 +5226,10 @@ dependencies = [
"expect-test",
"futures",
"http-body",
"hyper",
"hyper-util",
"indexmap 2.13.0",
"itertools 0.14.0",
"libc",
"llm-multimodal",
"openssl",
"prost",
"prost-types",
"rmp-serde",
@@ -5288,11 +5241,8 @@ dependencies = [
"sha2",
"socket2",
"subtle",
"tempfile",
"thiserror-ext",
"tls-listener",
"tokio",
"tokio-openssl",
"tokio-stream",
"tokio-util",
"tonic",
-10
View File
@@ -26,7 +26,6 @@ arc-swap = "1.9.0"
async-openai = { version = "0.33.1", default-features = false, features = ["native-tls"] }
async-trait = "0.1.89"
asynk-strim-attr = "0.1.0"
auto_enums = { version = "0.8.9", features = ["tokio1"] }
axum = "0.8.8"
base64 = "0.22.1"
bytemuck = { version = "1.25.0", features = ["extern_crate_alloc"] }
@@ -44,12 +43,6 @@ half = { version = "2.7.1", features = ["bytemuck"] }
hex = "0.4.3"
hf-hub = { version = "0.5.0", default-features = false, features = ["tokio"] }
http-body = "1.0.1"
hyper = { version = "1.10.1", features = ["http1", "server"] }
hyper-util = { version = "0.1.20", features = [
"server-graceful",
"service",
"tokio",
] }
indexmap = "2.13.0"
itertools = "0.14.0"
libc = "0.2.177"
@@ -61,7 +54,6 @@ native-tls-vendored = { package = "native-tls", version = "0.2.18", features = [
ndarray = { version = "0.16.1", features = ["serde"] }
openai-harmony = { package = "oss-harmony", git = "https://github.com/oss-harmony/harmony", tag = "v0.0.11", default-features = false }
openai-protocol = "1.6.0"
openssl = "0.10"
parking_lot = "0.12.5"
paste = "1.0.15"
prometheus-client = "0.24.0"
@@ -97,7 +89,6 @@ thiserror = "2.0.16"
thiserror-ext = "0.3.0"
tiktoken-rs = "0.9.1"
time = { version = "0.3.47", features = ["formatting", "local-offset", "macros"] }
tls-listener = { version = "0.11.2", default-features = false, features = ["openssl", "tokio-net", "axum"] }
tokenizers = "0.22.0"
tokio = { version = "1.47.1", features = [
"macros",
@@ -106,7 +97,6 @@ tokio = { version = "1.47.1", features = [
"sync",
"time",
] }
tokio-openssl = "0.6"
tokio-stream = "0.1"
tokio-util = { version = "0.7.18", features = ["rt"] }
tonic = "0.14.5"
-1
View File
@@ -25,7 +25,6 @@ strum.workspace = true
subenum.workspace = true
thiserror.workspace = true
thiserror-ext.workspace = true
time.workspace = true
tokio.workspace = true
tracing.workspace = true
trait-set.workspace = true
+8 -48
View File
@@ -15,9 +15,7 @@ use crate::output::{
DefaultChatOutputProcessor, HarmonyChatOutputProcessor, validate_harmony_parser_overrides,
};
use crate::renderer::hf::{HfChatRenderer, MultimodalRenderInfo};
use crate::renderer::{
DeepSeekV4ChatRenderer, DeepSeekV32ChatRenderer, DynChatRenderer, HarmonyChatRenderer,
};
use crate::renderer::{DeepSeekV4ChatRenderer, DeepSeekV32ChatRenderer, DynChatRenderer};
use crate::request::ChatRequest;
use crate::{DynChatOutputProcessor, RendererSelection};
@@ -63,7 +61,6 @@ impl HfChatBackend {
)?),
RendererSelection::DeepSeekV32 => Arc::new(DeepSeekV32ChatRenderer::new()),
RendererSelection::DeepSeekV4 => Arc::new(DeepSeekV4ChatRenderer::new()),
RendererSelection::Harmony => Arc::new(HarmonyChatRenderer::new()?),
};
info!(
@@ -151,15 +148,13 @@ mod tests {
use std::sync::Arc;
use tempfile::tempdir;
use thiserror_ext::AsReport as _;
use vllm_text::Prompt;
use vllm_text::backend::hf::TokenizerSource;
use vllm_text::tokenizer::{DynTokenizer, Tokenizer};
use super::HfChatBackend;
use crate::backend::{ChatBackend, LoadModelBackendsOptions, NewChatOutputProcessorOptions};
use crate::RendererSelection;
use crate::backend::{ChatBackend, LoadModelBackendsOptions};
use crate::request::{ChatContent, ChatMessage, ChatRequest};
use crate::{ParserSelection, RendererSelection};
fn request_with_user_text(text: &str) -> ChatRequest {
ChatRequest {
@@ -224,12 +219,12 @@ mod tests {
Arc::new(TestTokenizer)
}
fn backend_for_selection(
fn render_prompt(
renderer: RendererSelection,
config_json: &str,
tokenizer_config_json: &str,
) -> HfChatBackend {
HfChatBackend::from_resolved_model_files(
) -> String {
let backend = HfChatBackend::from_resolved_model_files(
resolved_files(config_json, tokenizer_config_json),
"test-model".to_string(),
LoadModelBackendsOptions {
@@ -241,15 +236,9 @@ mod tests {
},
test_tokenizer(),
)
.unwrap()
}
.unwrap();
fn render_prompt(
renderer: RendererSelection,
config_json: &str,
tokenizer_config_json: &str,
) -> String {
backend_for_selection(renderer, config_json, tokenizer_config_json)
backend
.chat_renderer()
.render(&request_with_user_text("hello"))
.unwrap()
@@ -283,35 +272,6 @@ mod tests {
assert_eq!(prompt, "hello");
}
#[test]
fn auto_uses_harmony_renderer_and_output_processor_for_gpt_oss_model_type() {
let backend = backend_for_selection(
RendererSelection::Auto,
r#"{"model_type":"gpt_oss"}"#,
r#"{"chat_template":"{{ messages[0].content }}"}"#,
);
let prompt =
backend.chat_renderer().render(&request_with_user_text("hello")).unwrap().prompt;
assert!(matches!(prompt, Prompt::TokenIds(_)));
let mut request = request_with_user_text("hello");
let error = match backend.new_chat_output_processor(
&mut request,
NewChatOutputProcessorOptions {
tool_call_parser: &ParserSelection::Explicit("json".to_string()),
reasoning_parser: &ParserSelection::Auto,
},
) {
Ok(_) => panic!("gpt_oss should reject generic parser overrides"),
Err(error) => error,
};
assert_eq!(
error.to_report_string(),
"gpt_oss uses native Harmony output parsing; generic tool parser override `json` is not supported"
);
}
#[test]
fn language_model_only_skips_multimodal_preprocessor_config() {
let mut files = resolved_files(
+2 -2
View File
@@ -29,8 +29,8 @@ pub use parser::reasoning::{
pub use parser::tool::{ToolParser, ToolParserError, ToolParserFactory};
pub use renderer::hf::ChatTemplateContentFormatOption;
pub use renderer::{
ChatRenderer, DeepSeekV4ChatRenderer, DeepSeekV32ChatRenderer, DynChatRenderer,
HarmonyChatRenderer, RenderedPrompt, RendererSelection,
ChatRenderer, DeepSeekV4ChatRenderer, DeepSeekV32ChatRenderer, DynChatRenderer, RenderedPrompt,
RendererSelection,
};
pub use request::{
ChatContent, ChatContentPart, ChatMessage, ChatOptions, ChatRequest, ChatRole, ChatTool,
+19 -2
View File
@@ -4,10 +4,16 @@
//! `DecodedTextEvent` token IDs directly and lets the official `openai-harmony`
//! parser recover the structured assistant message shape at token granularity.
use std::sync::LazyLock;
use anyhow::Context;
use asynk_strim_attr::{TryYielder, try_stream};
use futures::StreamExt as _;
use openai_harmony::chat::{Content as HarmonyContent, Message as HarmonyMessage, Role};
use openai_harmony::{HarmonyEncoding, StreamableParser};
use openai_harmony::{
HarmonyEncoding, HarmonyEncodingName, StreamableParser, load_harmony_encoding,
};
use thiserror_ext::AsReport;
use vllm_text::output::DecodedTextEvent;
use crate::Result as ChatResult;
@@ -18,7 +24,6 @@ use crate::output::{
generate_tool_call_id,
};
use crate::parser::ParserSelection;
use crate::renderer::harmony::encoding::harmony_encoding;
use crate::request::ChatRequest;
/// Request-scoped Harmony output processor used for `model_type == "gpt_oss"`.
@@ -379,6 +384,18 @@ async fn harmony_assistant_event_stream(
Ok(())
}
/// Lazily load the shared GPT-OSS Harmony encoding once per process.
fn harmony_encoding() -> Result<&'static HarmonyEncoding> {
static ENCODING: LazyLock<anyhow::Result<HarmonyEncoding>> = LazyLock::new(|| {
load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)
.context("failed to load harmony encoding for gpt-oss")
});
ENCODING.as_ref().map_err(|error| Error::HarmonyOutputParsing {
error: error.to_report_string().into(),
})
}
fn harmony_output_parsing_error(
error: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Error {
@@ -1,21 +0,0 @@
//! Shared Harmony encoding helper for the GPT-OSS renderer and output parser.
use std::sync::LazyLock;
use anyhow::Context as _;
use openai_harmony::{HarmonyEncoding, HarmonyEncodingName, load_harmony_encoding};
use thiserror_ext::AsReport as _;
use crate::error::{Error, Result};
/// Lazily load the shared GPT-OSS Harmony encoding once per process.
pub(crate) fn harmony_encoding() -> Result<&'static HarmonyEncoding> {
static ENCODING: LazyLock<anyhow::Result<HarmonyEncoding>> = LazyLock::new(|| {
load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)
.context("failed to load harmony encoding for gpt-oss")
});
ENCODING.as_ref().map_err(|error| Error::HarmonyOutputParsing {
error: error.to_report_string().into(),
})
}
@@ -1,14 +0,0 @@
{
"add_generation_prompt": false,
"messages": [
{
"role": "user",
"content": "What is 2 + 2?"
},
{
"role": "assistant",
"reasoning_content": "Need simple arithmetic.",
"content": "4"
}
]
}
@@ -1,7 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: medium
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>user<|message|>What is 2 + 2?<|end|><|start|>assistant<|channel|>final<|message|>4<|end|>
@@ -1,27 +0,0 @@
[
{
"role": "developer",
"content": "Use tools when needed.",
"tools": [
{
"function": {
"name": "lookup",
"description": "Lookup a record.",
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": ["id"]
}
}
}
]
},
{
"role": "user",
"content": "Find record abc."
}
]
@@ -1,23 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: medium
# Valid channels: analysis, commentary, final. Channel must be included for every message.
Calls to these tools must go to the commentary channel: 'functions'.<|end|><|start|>developer<|message|># Instructions
Use tools when needed.
# Tools
## functions
namespace functions {
// Lookup a record.
type lookup = (_: {
id: string,
}) => any;
} // namespace functions<|end|><|start|>user<|message|>Find record abc.<|end|><|start|>assistant
@@ -1,15 +0,0 @@
[
{
"role": "user",
"content": "What is 2 + 2?"
},
{
"role": "assistant",
"reasoning_content": "This should be dropped.",
"content": "4"
},
{
"role": "user",
"content": "What is 3 + 5?"
}
]
@@ -1,7 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: medium
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>user<|message|>What is 2 + 2?<|end|><|start|>assistant<|channel|>final<|message|>4<|end|><|start|>user<|message|>What is 3 + 5?<|end|><|start|>assistant
@@ -1,13 +0,0 @@
{
"reasoning_effort": "high",
"messages": [
{
"role": "system",
"content": "Answer tersely."
},
{
"role": "user",
"content": "What is 2 + 2?"
}
]
}
@@ -1,9 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: high
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions
Answer tersely.<|end|><|start|>user<|message|>What is 2 + 2?<|end|><|start|>assistant
@@ -1,26 +0,0 @@
{
"tools": [
{
"function": {
"name": "get_weather",
"description": "Get weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
},
"required": ["city"]
},
"strict": true
}
}
],
"messages": [
{
"role": "user",
"content": "Check Hangzhou weather."
}
]
}
@@ -1,19 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: medium
# Valid channels: analysis, commentary, final. Channel must be included for every message.
Calls to these tools must go to the commentary channel: 'functions'.<|end|><|start|>developer<|message|># Tools
## functions
namespace functions {
// Get weather for a city.
type get_weather = (_: {
city: string,
}) => any;
} // namespace functions<|end|><|start|>user<|message|>Check Hangzhou weather.<|end|><|start|>assistant
@@ -1,6 +0,0 @@
[
{
"role": "user",
"content": "Hello, who are you?"
}
]
@@ -1,7 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: medium
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>user<|message|>Hello, who are you?<|end|><|start|>assistant
@@ -1,8 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Answer tersely.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: high
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>user<|message|>What is 2 + 2?<|end|><|start|>assistant
@@ -1,43 +0,0 @@
{
"tools": [
{
"function": {
"name": "get_weather",
"description": "Get weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
},
"required": ["city"]
}
}
}
],
"messages": [
{
"role": "user",
"content": "Check Hangzhou weather."
},
{
"role": "assistant",
"reasoning_content": "Need current weather.",
"tool_calls": [
{
"id": "call-weather",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Hangzhou\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call-weather",
"content": "{\"temperature\":20}"
}
]
}
@@ -1,19 +0,0 @@
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2024-06
Current date: 2025-06-28
Reasoning: medium
# Valid channels: analysis, commentary, final. Channel must be included for every message.
Calls to these tools must go to the commentary channel: 'functions'.<|end|><|start|>developer<|message|># Tools
## functions
namespace functions {
// Get weather for a city.
type get_weather = (_: {
city: string,
}) => any;
} // namespace functions<|end|><|start|>user<|message|>Check Hangzhou weather.<|end|><|start|>assistant<|channel|>analysis<|message|>Need current weather.<|end|><|start|>assistant<|channel|>commentary to=functions.get_weather <|constrain|>json<|message|>{"city":"Hangzhou"}<|call|><|start|>functions.get_weather<|channel|>commentary to=assistant<|message|>{"temperature":20}<|end|><|start|>assistant
-487
View File
@@ -1,487 +0,0 @@
//! Native Harmony chat renderer for `gpt_oss`.
pub(crate) mod encoding;
use openai_harmony::HarmonyEncoding;
use openai_harmony::chat::{
Author, Conversation, DeveloperContent, Message, ReasoningEffort as HarmonyReasoningEffort,
Role, SystemContent, ToolDescription,
};
use thiserror_ext::AsReport as _;
use time::macros::format_description;
use vllm_text::Prompt;
use self::encoding::harmony_encoding;
use super::{ChatRenderer, RenderedPrompt, request_template_kwargs};
use crate::error::{Error, Result};
use crate::event::AssistantContentBlock;
use crate::request::{ChatContent, ChatMessage, ChatRequest, ChatTool, GenerationPromptMode};
use crate::{AssistantMessageExt as _, ReasoningEffort};
const SYSTEM_START_DATE_ENV: &str = "VLLM_SYSTEM_START_DATE";
const HARMONY_SYSTEM_INSTRUCTIONS_ENV: &str = "VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS";
/// GPT-OSS renderer backed by the official Harmony encoding.
pub struct HarmonyChatRenderer {
encoding: &'static HarmonyEncoding,
options: Options,
}
struct Options {
system_start_date: String,
use_system_instructions: bool,
}
impl HarmonyChatRenderer {
/// Create a Harmony renderer for production use.
///
/// Environment-derived options are resolved once at construction time:
///
/// - `VLLM_SYSTEM_START_DATE` pins the Harmony system start date. When it is
/// unset, the renderer uses the current local date with a UTC fallback.
/// - `VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS` moves leading instructions
/// into the system model identity when set to a non-zero integer.
pub fn new() -> Result<Self> {
Self::with_options(
env_system_start_date(),
env_use_harmony_system_instructions(),
)
}
/// Create a Harmony renderer with explicit preamble options.
///
/// Tests use this constructor to avoid process-global environment mutation.
/// Production code should call [`Self::new`] so the renderer observes the
/// same environment contract as the Python Harmony path.
pub fn with_options(
system_start_date: impl Into<String>,
use_system_instructions: bool,
) -> Result<Self> {
Ok(Self {
encoding: harmony_encoding()?,
options: Options {
system_start_date: system_start_date.into(),
use_system_instructions,
},
})
}
/// Render a chat request directly to Harmony token IDs.
///
/// Harmony owns both prompt formatting and tokenization, so the Rust
/// frontend bypasses the generic HF tokenizer path for GPT-OSS input.
fn render_token_ids(&self, request: &ChatRequest) -> Result<Vec<u32>> {
if request.has_multimodal() {
return Err(Error::UnsupportedMultimodalContent("image_url"));
}
if matches!(
request.chat_options.generation_prompt_mode,
GenerationPromptMode::ContinueFinalAssistant
) {
return Err(Error::ChatTemplate(
"Harmony renderer does not support continue_final_message".to_string(),
));
}
let messages = auto_drop_analysis_messages(to_harmony_messages(request, &self.options)?);
let conversation = Conversation::from_messages(messages);
// Pass `None` so oss-harmony does not apply its narrower built-in
// analysis-drop policy after the Rust-side Python-parity cleanup above.
let token_ids = match request.chat_options.generation_prompt_mode {
GenerationPromptMode::StartNewAssistant => self
.encoding
.render_conversation_for_completion(&conversation, Role::Assistant, None),
GenerationPromptMode::NoGenerationPrompt => {
self.encoding.render_conversation(&conversation, None)
}
GenerationPromptMode::ContinueFinalAssistant => unreachable!("checked above"),
}
.map_err(|error| {
Error::ChatTemplate(format!(
"failed to render Harmony prompt: {}",
error.as_report()
))
})?;
Ok(token_ids)
}
}
impl ChatRenderer for HarmonyChatRenderer {
/// Render a chat request as [`Prompt::TokenIds`] with template kwargs echoed
/// for downstream accounting/debugging.
fn render(&self, request: &ChatRequest) -> Result<RenderedPrompt> {
Ok(RenderedPrompt {
prompt: Prompt::TokenIds(self.render_token_ids(request)?),
effective_template_kwargs: request_template_kwargs(request),
})
}
}
/// Convert a vLLM chat request into a full Harmony conversation.
///
/// This adds the Harmony system/developer preamble, peels at most one leading
/// system/developer instruction message, and then lowers the remaining chat
/// history message-by-message.
fn to_harmony_messages(request: &ChatRequest, options: &Options) -> Result<Vec<Message>> {
let (instructions, leading_developer_tools, remaining_messages) =
peel_leading_instructions(&request.messages)?;
let tool_call_names = tool_call_names(&request.messages);
let mut messages =
build_harmony_preamble(request, instructions, leading_developer_tools, options)?;
for message in remaining_messages {
messages.extend(to_harmony_message(message, &tool_call_names, options)?);
}
Ok(messages)
}
/// Extract the optional leading instruction message used by the Harmony preamble.
///
/// Python only peels the first leading `system` or `developer` message. Later
/// system/developer messages stay in the conversation and are lowered normally.
#[allow(clippy::type_complexity)]
fn peel_leading_instructions(
messages: &[ChatMessage],
) -> Result<(Option<String>, Option<&[ChatTool]>, &[ChatMessage])> {
let Some(first) = messages.first() else {
return Ok((None, None, messages));
};
match first {
ChatMessage::System { content } => Ok((Some(flatten_text(content)?), None, &messages[1..])),
ChatMessage::Developer { content, tools } => Ok((
Some(flatten_text(content)?),
tools.as_deref(),
&messages[1..],
)),
ChatMessage::User { .. }
| ChatMessage::Assistant { .. }
| ChatMessage::ToolResponse { .. } => Ok((None, None, messages)),
}
}
/// Build the Harmony preamble for one request.
///
/// The preamble always contains a system message with date and reasoning-effort
/// metadata. Leading instructions live either in the system model identity or in
/// a developer message depending on `use_system_instructions`; request-level and
/// leading developer tools are attached to the developer message.
fn build_harmony_preamble(
request: &ChatRequest,
instructions: Option<String>,
leading_developer_tools: Option<&[ChatTool]>,
options: &Options,
) -> Result<Vec<Message>> {
let mut messages = vec![Message::from_role_and_content(
Role::System,
system_content(
instructions.as_deref().filter(|_| options.use_system_instructions),
request.chat_options.reasoning_effort,
&options.system_start_date,
)?,
)];
let mut developer = DeveloperContent::new();
let mut has_developer_content = false;
if !options.use_system_instructions
&& let Some(instructions) = instructions.as_deref().filter(|text| !text.is_empty())
{
developer = developer.with_instructions(instructions);
has_developer_content = true;
}
let tool_descriptions = preamble_tool_descriptions(request, leading_developer_tools);
if !tool_descriptions.is_empty() {
developer = developer.with_function_tools(tool_descriptions);
has_developer_content = true;
}
if has_developer_content {
messages.push(Message::from_role_and_content(Role::Developer, developer));
}
Ok(messages)
}
/// Collect request-level and leading developer function tools for the preamble.
fn preamble_tool_descriptions(
request: &ChatRequest,
leading_developer_tools: Option<&[ChatTool]>,
) -> Vec<ToolDescription> {
let mut tools = Vec::new();
if request.tool_parsing_enabled() {
tools.extend(to_tool_descriptions(&request.tools));
}
if let Some(leading_developer_tools) = leading_developer_tools {
tools.extend(to_tool_descriptions(leading_developer_tools));
}
tools
}
/// Construct the Harmony system content for the request preamble.
///
/// Harmony defaults the reasoning effort to `medium` when none is provided, so
/// this only sets an explicit effort after validating vLLM's request value.
fn system_content(
instructions: Option<&str>,
reasoning_effort: Option<ReasoningEffort>,
system_start_date: &str,
) -> Result<SystemContent> {
let mut content =
SystemContent::new().with_conversation_start_date(system_start_date.to_string());
if let Some(reasoning_effort) = reasoning_effort {
content = content.with_reasoning_effort(to_harmony_reasoning_effort(reasoning_effort)?);
}
if let Some(instructions) = instructions.filter(|text| !text.is_empty()) {
let model_identity = match content.model_identity.as_deref() {
Some(identity) if !identity.is_empty() => format!("{identity}\n{instructions}"),
_ => instructions.to_string(),
};
content = content.with_model_identity(model_identity);
}
Ok(content)
}
/// Lower a single vLLM chat message into one or more Harmony messages.
///
/// Assistant messages can split into separate analysis, final, commentary, and
/// tool-call messages. Tool responses require the earlier assistant tool-call ID
/// map so the Harmony tool author can include `functions.{name}`.
fn to_harmony_message(
message: &ChatMessage,
tool_call_names: &std::collections::HashMap<String, String>,
options: &Options,
) -> Result<Vec<Message>> {
Ok(match message {
ChatMessage::System { content } => {
let instructions = flatten_text(content)?;
vec![system_or_developer_message(
"system",
instructions,
None,
options,
)?]
}
ChatMessage::Developer { content, tools } => {
let instructions = flatten_text(content)?;
vec![developer_message(Some(instructions), tools.as_deref())]
}
ChatMessage::User { content } => {
vec![Message::from_role_and_content(
Role::User,
flatten_text(content)?,
)]
}
ChatMessage::Assistant { content } => assistant_messages(content),
ChatMessage::ToolResponse {
content,
tool_call_id,
} => {
let name = tool_call_names.get(tool_call_id).ok_or_else(|| {
Error::ChatTemplate(format!(
"invalid Harmony tool message: unknown tool_call_id `{tool_call_id}`"
))
})?;
vec![
Message::from_author_and_content(
Author::new(Role::Tool, format!("functions.{name}")),
flatten_text(content)?,
)
.with_channel("commentary")
.with_recipient("assistant"),
]
}
})
}
/// Lower a non-leading system/developer message.
///
/// Harmony treats most extra system/developer messages as developer
/// instructions. When system-instructions mode is enabled, system messages are
/// rendered as system model-identity additions to match Python.
fn system_or_developer_message(
role: &str,
instructions: String,
tools: Option<&[ChatTool]>,
options: &Options,
) -> Result<Message> {
if role == "system" && options.use_system_instructions {
return Ok(Message::from_role_and_content(
Role::System,
system_content(Some(&instructions), None, &options.system_start_date)?,
));
}
Ok(developer_message(Some(instructions), tools))
}
/// Build a Harmony developer message with optional instructions and function tools.
fn developer_message(instructions: Option<String>, tools: Option<&[ChatTool]>) -> Message {
let mut content = DeveloperContent::new();
if let Some(instructions) = instructions.filter(|text| !text.is_empty()) {
content = content.with_instructions(instructions);
}
if let Some(tools) = tools {
let tools = to_tool_descriptions(tools);
if !tools.is_empty() {
content = content.with_function_tools(tools);
}
}
Message::from_role_and_content(Role::Developer, content)
}
/// Lower assistant history into Harmony channels.
///
/// Plain assistant text goes to `final`. When the assistant has tool calls,
/// visible text goes to `commentary`, reasoning goes to `analysis`, and each
/// function call becomes a `commentary` message to `functions.{name}` with JSON
/// constrained content.
fn assistant_messages(content: &[AssistantContentBlock]) -> Vec<Message> {
let mut messages = Vec::new();
let has_tool_calls = content.has_tool_calls();
if has_tool_calls {
let text = content.text();
if !text.is_empty() {
messages.push(
Message::from_role_and_content(Role::Assistant, text).with_channel("commentary"),
);
}
}
if let Some(reasoning) = content.reasoning() {
messages.push(
Message::from_role_and_content(Role::Assistant, reasoning).with_channel("analysis"),
);
}
if has_tool_calls {
for tool_call in content.tool_calls() {
messages.push(
Message::from_role_and_content(Role::Assistant, tool_call.arguments.clone())
.with_channel("commentary")
.with_recipient(format!("functions.{}", tool_call.name))
.with_content_type("<|constrain|>json"),
);
}
} else {
let text = content.text();
if !text.is_empty() {
messages
.push(Message::from_role_and_content(Role::Assistant, text).with_channel("final"));
}
}
messages
}
/// Build the tool-call ID to function-name map used by later tool responses.
fn tool_call_names(messages: &[ChatMessage]) -> std::collections::HashMap<String, String> {
let mut names = std::collections::HashMap::new();
for message in messages {
let ChatMessage::Assistant { content } = message else {
continue;
};
for tool_call in content.tool_calls() {
names.insert(tool_call.id.clone(), tool_call.name.clone());
}
}
names
}
/// Drop stale assistant analysis messages using vLLM Python's policy.
///
/// Once an assistant final message exists, earlier analysis messages represent
/// chain-of-thought for completed turns and should not be replayed to the model.
fn auto_drop_analysis_messages(messages: Vec<Message>) -> Vec<Message> {
// Match vLLM Python's Harmony cleanup: once an assistant final message exists,
// previous assistant analysis messages are stale chain-of-thought and should
// be removed. oss-harmony can also drop analysis with `Some(Default::default())`,
// but that built-in path only triggers when the last assistant message is final
// and drops relative to the first final message, which misses longer multi-turn
// histories with later user/tool turns.
let Some(last_assistant_final_index) = messages.iter().rposition(|message| {
message.author.role == Role::Assistant && message.channel.as_deref() == Some("final")
}) else {
return messages;
};
messages
.into_iter()
.enumerate()
.filter_map(|(index, message)| {
(index >= last_assistant_final_index || message.channel.as_deref() != Some("analysis"))
.then_some(message)
})
.collect()
}
/// Flatten vLLM text content and reject unsupported multimodal parts.
fn flatten_text(content: &ChatContent) -> Result<String> {
content.try_flatten_to_text()
}
/// Convert vLLM function tool definitions to Harmony tool descriptions.
fn to_tool_descriptions(tools: &[ChatTool]) -> Vec<ToolDescription> {
tools
.iter()
.map(|tool| {
ToolDescription::new(
tool.name.clone(),
tool.description.clone().unwrap_or_default(),
Some(tool.parameters.clone()),
)
})
.collect()
}
/// Map supported OpenAI reasoning-effort values onto Harmony's enum.
fn to_harmony_reasoning_effort(
reasoning_effort: ReasoningEffort,
) -> Result<HarmonyReasoningEffort> {
match reasoning_effort {
ReasoningEffort::Low => Ok(HarmonyReasoningEffort::Low),
ReasoningEffort::Medium => Ok(HarmonyReasoningEffort::Medium),
ReasoningEffort::High => Ok(HarmonyReasoningEffort::High),
ReasoningEffort::None
| ReasoningEffort::Minimal
| ReasoningEffort::XHigh
| ReasoningEffort::Max => Err(Error::ChatTemplate(format!(
"reasoning_effort={:?} is not supported by Harmony. Supported values are: low, medium, high.",
reasoning_effort.as_str()
))),
}
}
/// Resolve the system start date from the environment or the current date.
fn env_system_start_date() -> String {
std::env::var(SYSTEM_START_DATE_ENV)
.ok()
.filter(|date| !date.is_empty())
.unwrap_or_else(current_date)
}
/// Format today's date as `YYYY-MM-DD`, preferring local time.
fn current_date() -> String {
const DATE_FORMAT: &[time::format_description::FormatItem<'static>] =
format_description!("[year]-[month]-[day]");
let now = time::OffsetDateTime::now_local().unwrap_or_else(|_| time::OffsetDateTime::now_utc());
now.format(DATE_FORMAT).expect("static date format should be valid")
}
/// Resolve the env flag that places leading instructions in system identity.
fn env_use_harmony_system_instructions() -> bool {
std::env::var(HARMONY_SYSTEM_INSTRUCTIONS_ENV)
.ok()
.and_then(|value| value.parse::<i32>().ok())
.is_some_and(|value| value != 0)
}
#[cfg(test)]
mod tests;
-212
View File
@@ -1,212 +0,0 @@
use std::path::PathBuf;
use expect_test::{ExpectFile, expect, expect_file};
use thiserror_ext::AsReport as _;
use super::HarmonyChatRenderer;
use super::encoding::harmony_encoding;
use crate::ChatRenderer;
use crate::error::Error;
use crate::event::{AssistantContentBlock, AssistantToolCall};
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
use crate::request::{
ChatContentPart, ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort,
};
const PINNED_DATE: &str = "2025-06-28";
fn fixture_request(input_name: &str) -> ChatRequest {
fixture_chat_request(
&fixture_path(input_name),
FixtureRequestOptions {
enable_thinking: false,
no_generation_prompt_when_last_assistant: false,
},
)
}
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src/renderer/harmony")
.join("fixtures")
.join(name)
}
fn test_renderer(use_system_instructions: bool) -> HarmonyChatRenderer {
HarmonyChatRenderer::with_options(PINNED_DATE, use_system_instructions).unwrap()
}
fn render_token_ids(request: &ChatRequest) -> Vec<u32> {
render_token_ids_with(&test_renderer(false), request)
}
fn render_token_ids_with(renderer: &HarmonyChatRenderer, request: &ChatRequest) -> Vec<u32> {
renderer
.render(request)
.unwrap()
.prompt
.into_token_ids()
.expect("Harmony renderer returns token IDs")
}
fn render_prompt_text(request: &ChatRequest) -> String {
render_prompt_text_with(&test_renderer(false), request)
}
fn render_prompt_text_with(renderer: &HarmonyChatRenderer, request: &ChatRequest) -> String {
let token_ids = render_token_ids_with(renderer, request);
harmony_encoding().unwrap().tokenizer().decode_utf8(&token_ids).unwrap()
}
fn assert_fixture(input_name: &str, expected: ExpectFile) {
let request = fixture_request(input_name);
let rendered = format!("{}\n", render_prompt_text(&request));
expected.assert_eq(&rendered);
}
#[test]
fn renders_token_ids() {
let request = fixture_request("simple_user.json");
assert!(!render_token_ids(&request).is_empty());
}
#[test]
fn renders_simple_user_fixture() {
assert_fixture("simple_user.json", expect_file!["fixtures/simple_user.txt"]);
}
#[test]
fn renders_leading_system_fixture() {
assert_fixture(
"leading_system.json",
expect_file!["fixtures/leading_system.txt"],
);
}
#[test]
fn renders_system_instructions_env_fixture() {
let renderer = test_renderer(true);
let request = fixture_request("leading_system.json");
let rendered = format!("{}\n", render_prompt_text_with(&renderer, &request));
expect_file!["fixtures/system_instructions_env.txt"].assert_eq(&rendered);
}
#[test]
fn renders_request_tools_fixture() {
assert_fixture(
"request_tools.json",
expect_file!["fixtures/request_tools.txt"],
);
}
#[test]
fn renders_developer_tools_fixture() {
assert_fixture(
"developer_tools.json",
expect_file!["fixtures/developer_tools.txt"],
);
}
#[test]
fn renders_assistant_history_fixture() {
assert_fixture(
"assistant_history.json",
expect_file!["fixtures/assistant_history.txt"],
);
}
#[test]
fn renders_tool_roundtrip_fixture() {
assert_fixture(
"tool_roundtrip.json",
expect_file!["fixtures/tool_roundtrip.txt"],
);
}
#[test]
fn drops_stale_analysis_fixture() {
assert_fixture(
"drop_analysis.json",
expect_file!["fixtures/drop_analysis.txt"],
);
}
#[test]
fn rejects_invalid_reasoning_effort() {
let mut request = ChatRequest::for_test();
request.chat_options.reasoning_effort = Some(ReasoningEffort::None);
let error = test_renderer(false).render(&request).unwrap_err();
expect![[r#"chat template error: reasoning_effort="none" is not supported by Harmony. Supported values are: low, medium, high."#]]
.assert_eq(&error.to_report_string());
}
#[test]
fn rejects_unknown_tool_response_id() {
let request = ChatRequest {
messages: vec![
ChatMessage::assistant_blocks(vec![AssistantContentBlock::ToolCall(
AssistantToolCall {
id: "call-known".to_string(),
name: "lookup".to_string(),
arguments: "{}".to_string(),
},
)]),
ChatMessage::tool_response("{}", "call-unknown"),
],
..ChatRequest::for_test()
};
let error = test_renderer(false).render(&request).unwrap_err();
expect![
"chat template error: invalid Harmony tool message: unknown tool_call_id `call-unknown`"
]
.assert_eq(&error.to_report_string());
}
#[test]
fn rejects_multimodal_input() {
let request = ChatRequest {
messages: vec![ChatMessage::user(vec![ChatContentPart::image_url(
"data:image/png;base64,test",
)])],
..ChatRequest::for_test()
};
let error = test_renderer(false).render(&request).unwrap_err();
assert!(matches!(
error,
Error::UnsupportedMultimodalContent("image_url")
));
}
#[test]
fn rejects_continue_final_assistant() {
let mut request = ChatRequest {
messages: vec![
ChatMessage::user("write"),
ChatMessage::assistant_text("partial"),
],
..ChatRequest::for_test()
};
request.chat_options.generation_prompt_mode = GenerationPromptMode::ContinueFinalAssistant;
let error = test_renderer(false).render(&request).unwrap_err();
expect!["chat template error: Harmony renderer does not support continue_final_message"]
.assert_eq(&error.to_report_string());
}
#[test]
fn no_generation_prompt_omits_trailing_assistant_start() {
let mut request = fixture_request("simple_user.json");
request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
let rendered = render_prompt_text(&request);
assert!(!rendered.ends_with("<|start|>assistant"));
}
-2
View File
@@ -9,7 +9,6 @@ use crate::request::{ChatRequest, ReasoningEffort};
pub mod deepseek_v32;
pub mod deepseek_v4;
pub mod harmony;
pub mod hf;
mod selection;
#[cfg(test)]
@@ -17,7 +16,6 @@ mod test_utils;
pub use deepseek_v4::DeepSeekV4ChatRenderer;
pub use deepseek_v32::DeepSeekV32ChatRenderer;
pub use harmony::HarmonyChatRenderer;
pub use selection::RendererSelection;
/// Rendered chat prompt submitted to the text backend.
+1 -9
View File
@@ -19,16 +19,12 @@ pub enum RendererSelection {
DeepSeekV32,
/// Force the DeepSeek V4 renderer.
DeepSeekV4,
/// Force the GPT-OSS Harmony renderer.
Harmony,
}
impl RendererSelection {
pub const AUTO_LITERAL: &str = "auto";
pub const DEEPSEEK_V32_LITERAL: &str = "deepseek_v32";
pub const DEEPSEEK_V4_LITERAL: &str = "deepseek_v4";
pub const GPT_OSS_MODEL_TYPE: &str = "gpt_oss";
pub const HARMONY_LITERAL: &str = "harmony";
pub const HF_LITERAL: &str = "hf";
/// Resolve the renderer selection using the given model type string, if
@@ -38,7 +34,6 @@ impl RendererSelection {
Self::Auto => match model_type {
Self::DEEPSEEK_V32_LITERAL => Self::DeepSeekV32,
Self::DEEPSEEK_V4_LITERAL => Self::DeepSeekV4,
Self::GPT_OSS_MODEL_TYPE => Self::Harmony,
_ => Self::Hf,
},
selection => selection,
@@ -58,8 +53,6 @@ impl FromStr for RendererSelection {
Ok(Self::DeepSeekV32)
} else if value.eq_ignore_ascii_case(Self::DEEPSEEK_V4_LITERAL) {
Ok(Self::DeepSeekV4)
} else if value.eq_ignore_ascii_case(Self::HARMONY_LITERAL) {
Ok(Self::Harmony)
} else {
Err(format!(
"unknown renderer `{value}` (expected one of: {})",
@@ -76,7 +69,6 @@ impl fmt::Display for RendererSelection {
Self::Hf => f.write_str(Self::HF_LITERAL),
Self::DeepSeekV32 => f.write_str(Self::DEEPSEEK_V32_LITERAL),
Self::DeepSeekV4 => f.write_str(Self::DEEPSEEK_V4_LITERAL),
Self::Harmony => f.write_str(Self::HARMONY_LITERAL),
}
}
}
@@ -103,7 +95,7 @@ mod tests {
fn renderer_selection_expected_error_message() {
let err = RendererSelection::from_str("unknown").unwrap_err();
expect_test::expect![
"unknown renderer `unknown` (expected one of: auto, hf, deepseek_v32, deepseek_v4, harmony)"
"unknown renderer `unknown` (expected one of: auto, hf, deepseek_v32, deepseek_v4)"
]
.assert_eq(&err);
}
+1 -4
View File
@@ -7,7 +7,7 @@ use serde_json::Value;
use crate::event::{AssistantContentBlock, AssistantToolCall};
use crate::request::{
ChatContent, ChatContentPart, ChatMessage, ChatRequest, ChatTool, ChatToolChoice,
GenerationPromptMode, ReasoningEffort,
GenerationPromptMode,
};
/// Options for constructing a [`ChatRequest`] from a fixture file.
@@ -42,7 +42,6 @@ pub(crate) struct FixtureRequest {
tools: Vec<FixtureTool>,
messages: Vec<FixtureMessage>,
add_generation_prompt: Option<bool>,
reasoning_effort: Option<ReasoningEffort>,
}
impl FixtureFile {
@@ -53,7 +52,6 @@ impl FixtureFile {
tools: Vec::new(),
messages,
add_generation_prompt: None,
reasoning_effort: None,
},
}
}
@@ -156,7 +154,6 @@ impl FixtureRequest {
if self.add_generation_prompt == Some(false) {
request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
}
request.chat_options.reasoning_effort = self.reasoning_effort;
if options.enable_thinking {
for key in ["thinking", "enable_thinking"] {
request.chat_options.template_kwargs.insert(key.to_string(), Value::Bool(true));
+1 -66
View File
@@ -25,7 +25,7 @@ use vllm_managed_engine::ManagedEngineConfig;
use vllm_managed_engine::cli::{ManagedEngineArgs, repartition_managed_engine_args};
use vllm_server::{
ApiServerOptions, ChatTemplateContentFormatOption, Config, CoordinatorMode, CorsConfig,
DEFAULT_KEEP_ALIVE_TIMEOUT, HttpListenerMode, ParserSelection, RendererSelection, TlsConfig,
HttpListenerMode, ParserSelection, RendererSelection,
};
use crate::cli::unsupported::UnsupportedArgs;
@@ -154,11 +154,6 @@ pub struct SharedRuntimeArgs {
#[arg(long, default_value_t = 0)]
#[serde(default)]
pub shutdown_timeout: u64,
/// Maximum idle time (seconds) on a keep-alive HTTP connection before the
/// server closes it (default 5).
#[arg(long = "http-timeout-keep-alive", env = "VLLM_HTTP_TIMEOUT_KEEP_ALIVE")]
#[serde(default)]
pub http_timeout_keep_alive: Option<u64>,
/// The file path to the chat template, or the template in single-line form
/// for the specified model.
@@ -262,34 +257,6 @@ pub struct SharedRuntimeArgs {
#[serde(default)]
pub allow_credentials: bool,
/// The file path to the SSL key file. When omitted, the key is read from
/// `--ssl-certfile` (combined PEM).
#[arg(long)]
#[serde(default)]
pub ssl_keyfile: Option<String>,
/// The file path to the SSL cert file. Enables TLS when set.
#[arg(long)]
#[serde(default)]
pub ssl_certfile: Option<String>,
/// The CA certificates file used to verify client certificates (mTLS).
#[arg(long)]
#[serde(default)]
pub ssl_ca_certs: Option<String>,
/// Whether a client certificate is required: 0 = none, 1 = optional,
/// 2 = required (mirrors Python's `ssl.CERT_*`).
#[arg(long, default_value_t = 0, value_parser = clap::value_parser!(i32).range(0..=2))]
#[serde(default)]
pub ssl_cert_reqs: i32,
/// OpenSSL cipher string for HTTPS (TLS 1.2 and below).
/// When unset, the linked OpenSSL's default suites are used.
#[arg(long)]
#[serde(default)]
pub ssl_ciphers: Option<String>,
/// Unsupported Python vLLM frontend arguments recognized but not yet
/// implemented in Rust.
#[educe(Debug(ignore))]
@@ -310,13 +277,6 @@ impl SharedRuntimeArgs {
Duration::from_secs(self.shutdown_timeout)
}
/// Maximum idle time on a keep-alive HTTP connection before the server
/// closes it.
pub fn keep_alive_timeout(&self) -> Duration {
self.http_timeout_keep_alive
.map_or(DEFAULT_KEEP_ALIVE_TIMEOUT, Duration::from_secs)
}
/// Apply fallback logic for API key configuration from env variables.
fn apply_env_api_key_fallback(&mut self) {
if self.api_key.is_empty()
@@ -341,10 +301,8 @@ impl SharedRuntimeArgs {
) -> Config {
let ready_timeout = self.ready_timeout();
let shutdown_timeout = self.shutdown_timeout();
let keep_alive_timeout = self.keep_alive_timeout();
let api_server_options = self.api_server_options();
let cors = self.cors_config();
let tls = self.tls_config();
Config {
transport_mode: TransportMode::Bootstrapped {
@@ -371,12 +329,10 @@ impl SharedRuntimeArgs {
max_logprobs: self.max_logprobs,
api_server_options,
cors,
tls,
api_keys: self.api_key,
disable_log_stats: self.disable_log_stats,
grpc_port: self.grpc_port,
shutdown_timeout,
keep_alive_timeout,
}
}
@@ -393,10 +349,8 @@ impl SharedRuntimeArgs {
) -> Config {
let ready_timeout = self.ready_timeout();
let shutdown_timeout = self.shutdown_timeout();
let keep_alive_timeout = self.keep_alive_timeout();
let api_server_options = self.api_server_options();
let cors = self.cors_config();
let tls = self.tls_config();
Config {
transport_mode: TransportMode::HandshakeOwner {
@@ -421,12 +375,10 @@ impl SharedRuntimeArgs {
max_logprobs: self.max_logprobs,
api_server_options,
cors,
tls,
api_keys: self.api_key,
disable_log_stats: self.disable_log_stats,
grpc_port: self.grpc_port,
shutdown_timeout,
keep_alive_timeout,
}
}
@@ -446,23 +398,6 @@ impl SharedRuntimeArgs {
allow_credentials: self.allow_credentials,
}
}
/// Build the TLS config: `Some` when any `ssl_*` argument is set, else
/// `None` (plaintext). The combination is validated in [`Config::validate`].
fn tls_config(&self) -> Option<TlsConfig> {
let tls_requested = self.ssl_certfile.is_some()
|| self.ssl_keyfile.is_some()
|| self.ssl_ca_certs.is_some()
|| self.ssl_cert_reqs != 0
|| self.ssl_ciphers.is_some();
tls_requested.then(|| TlsConfig {
cert_file: self.ssl_certfile.clone(),
key_file: self.ssl_keyfile.clone(),
ca_certs: self.ssl_ca_certs.clone(),
cert_reqs: self.ssl_cert_reqs,
ciphers: self.ssl_ciphers.clone(),
})
}
}
fn default_engine_ready_timeout_secs() -> u64 {
+10 -168
View File
@@ -41,7 +41,6 @@ fn serve_args_forward_python_flags_with_separator() {
max_logprobs: None,
grpc_port: None,
shutdown_timeout: 0,
http_timeout_keep_alive: None,
chat_template: None,
default_chat_template_kwargs: None,
chat_template_content_format: Auto,
@@ -66,11 +65,6 @@ fn serve_args_forward_python_flags_with_separator() {
],
),
allow_credentials: false,
ssl_keyfile: None,
ssl_certfile: None,
ssl_ca_certs: None,
ssl_cert_reqs: 0,
ssl_ciphers: None,
},
managed_engine: ManagedEngineArgs {
python: "../vllm/.venv/bin/python",
@@ -369,140 +363,6 @@ fn serve_passes_enable_prompt_tokens_details_into_config() {
assert!(config.api_server_options.enable_prompt_tokens_details);
}
#[test]
fn serve_passes_tls_into_config() {
let cli = Cli::try_parse_from([
"vllm-rs",
"serve",
"Qwen/Qwen3-0.6B",
"--ssl-certfile",
"/tmp/cert.pem",
"--ssl-keyfile",
"/tmp/key.pem",
"--ssl-ca-certs",
"/tmp/ca.pem",
"--ssl-cert-reqs",
"2",
])
.unwrap();
let Command::Serve(args) = cli.command else {
panic!("expected serve args");
};
let config = args.to_frontend_config("tcp://127.0.0.1:62100".to_string());
let tls = config.tls.expect("tls configured");
assert_eq!(tls.cert_file.as_deref(), Some("/tmp/cert.pem"));
assert_eq!(tls.key_file.as_deref(), Some("/tmp/key.pem"));
assert_eq!(tls.ca_certs.as_deref(), Some("/tmp/ca.pem"));
assert_eq!(tls.cert_reqs, 2);
}
#[test]
fn serve_without_ssl_flags_has_no_tls() {
let cli = Cli::try_parse_from(["vllm-rs", "serve", "Qwen/Qwen3-0.6B"]).unwrap();
let Command::Serve(args) = cli.command else {
panic!("expected serve args");
};
let config = args.to_frontend_config("tcp://127.0.0.1:62100".to_string());
assert!(config.tls.is_none());
}
#[test]
fn serve_ssl_keyfile_without_certfile_fails_validation() {
let cli = Cli::try_parse_from([
"vllm-rs",
"serve",
"Qwen/Qwen3-0.6B",
"--ssl-keyfile",
"/tmp/key.pem",
])
.unwrap();
let Command::Serve(args) = cli.command else {
panic!("expected serve args");
};
let config = args.to_frontend_config("tcp://127.0.0.1:62100".to_string());
// TLS is requested (a key was given) but there is no certificate, so
// validation fails loud rather than silently serving plaintext.
assert_eq!(config.tls.as_ref().expect("tls requested").cert_file, None);
let err = config.validate().unwrap_err().to_string();
assert!(err.contains("--ssl-certfile is required"), "{err}");
}
#[test]
fn serve_mtls_without_ca_certs_fails_validation() {
let cli = Cli::try_parse_from([
"vllm-rs",
"serve",
"Qwen/Qwen3-0.6B",
"--ssl-certfile",
"/tmp/cert.pem",
"--ssl-cert-reqs",
"2",
])
.unwrap();
let Command::Serve(args) = cli.command else {
panic!("expected serve args");
};
let config = args.to_frontend_config("tcp://127.0.0.1:62100".to_string());
// Client-cert verification without a CA bundle has nothing to verify
// against, so it fails loud at startup.
let err = config.validate().unwrap_err().to_string();
assert!(err.contains("--ssl-ca-certs is required"), "{err}");
}
#[test]
fn frontend_args_json_passes_tls_into_config() {
let cli = Cli::try_parse_from([
"vllm-rs",
"frontend",
"--listen-fd",
"3",
"--input-address",
"ipc:///tmp/input.sock",
"--output-address",
"ipc:///tmp/output.sock",
"--args-json",
r#"{"model_tag":"Qwen/Qwen3-0.6B","ssl_certfile":"/tmp/cert.pem","ssl_keyfile":"/tmp/key.pem"}"#,
])
.unwrap();
let Command::Frontend(args) = cli.command else {
panic!("expected frontend args");
};
let config = args.into_config();
let tls = config.tls.expect("tls configured");
assert_eq!(tls.cert_file.as_deref(), Some("/tmp/cert.pem"));
assert_eq!(tls.key_file.as_deref(), Some("/tmp/key.pem"));
}
#[test]
fn frontend_args_json_rejects_out_of_range_cert_reqs() {
let cli = Cli::try_parse_from([
"vllm-rs",
"frontend",
"--listen-fd",
"3",
"--input-address",
"ipc:///tmp/input.sock",
"--output-address",
"ipc:///tmp/output.sock",
"--args-json",
r#"{"model_tag":"Qwen/Qwen3-0.6B","ssl_certfile":"/tmp/cert.pem","ssl_cert_reqs":5}"#,
])
.unwrap();
let Command::Frontend(args) = cli.command else {
panic!("expected frontend args");
};
// The JSON path bypasses clap's range check, so validate() is the only guard.
let config = args.into_config();
let err = config.validate().unwrap_err().to_string();
assert!(err.contains("--ssl-cert-reqs"), "{err}");
}
#[test]
fn frontend_args_json_passes_enable_request_id_headers_into_config() {
let cli = Cli::try_parse_from([
@@ -608,7 +468,7 @@ fn serve_args_reject_unknown_renderer_value() {
.unwrap_err();
expect![[r#"
error: invalid value 'definitely_missing' for '--tokenizer-mode <RENDERER>': unknown renderer `definitely_missing` (expected one of: auto, hf, deepseek_v32, deepseek_v4, harmony)
error: invalid value 'definitely_missing' for '--tokenizer-mode <RENDERER>': unknown renderer `definitely_missing` (expected one of: auto, hf, deepseek_v32, deepseek_v4)
For more information, try '--help'.
"#]]
@@ -621,13 +481,13 @@ fn serve_args_reject_unsupported_flag_arg() {
"vllm-rs",
"serve",
"Qwen/Qwen3-0.6B",
"--root-path",
"/prefix",
"--ssl-keyfile",
"/tmp/key.pem",
])
.unwrap_err();
expect![[r#"
error: invalid value '/prefix' for '--root-path <ROOT_PATH>': argument is not implemented in Rust frontend yet
error: invalid value '/tmp/key.pem' for '--ssl-keyfile <SSL_KEYFILE>': argument is not implemented in Rust frontend yet
Remove this unsupported argument to continue.
@@ -702,7 +562,6 @@ fn frontend_args_accept_json() {
max_logprobs: None,
grpc_port: None,
shutdown_timeout: 0,
http_timeout_keep_alive: None,
chat_template: None,
default_chat_template_kwargs: None,
chat_template_content_format: Auto,
@@ -727,11 +586,6 @@ fn frontend_args_accept_json() {
],
),
allow_credentials: false,
ssl_keyfile: None,
ssl_certfile: None,
ssl_ca_certs: None,
ssl_cert_reqs: 0,
ssl_ciphers: None,
},
},
),
@@ -944,14 +798,14 @@ fn frontend_args_json_rejects_unsupported_fields() {
"--output-address",
"ipc:///tmp/output.sock",
"--args-json",
r#"{"model_tag":"Qwen/Qwen3-0.6B","root_path":"/prefix"}"#,
r#"{"model_tag":"Qwen/Qwen3-0.6B","ssl_keyfile":"/tmp/key.pem"}"#,
])
.unwrap_err();
expect![[r#"
error: invalid value '{"model_tag":"Qwen/Qwen3-0.6B","root_path":"/prefix"}' for '--args-json <JSON>':
error: invalid value '{"model_tag":"Qwen/Qwen3-0.6B","ssl_keyfile":"/tmp/key.pem"}' for '--args-json <JSON>':
The following arguments are not implemented in Rust frontend yet:
- root_path
- ssl_keyfile
Remove these arguments to continue.
@@ -971,16 +825,16 @@ fn frontend_args_json_aggregates_multiple_unsupported_fields() {
"--output-address",
"ipc:///tmp/output.sock",
"--args-json",
r#"{"model_tag":"Qwen/Qwen3-0.6B","response_role":"assistant","root_path":"/prefix"}"#,
r#"{"model_tag":"Qwen/Qwen3-0.6B","response_role":"assistant","ssl_keyfile":"/tmp/key.pem"}"#,
])
.unwrap_err();
let actual = error.to_string().replace(": \n", ":\n");
expect![[r#"
error: invalid value '{"model_tag":"Qwen/Qwen3-0.6B","response_role":"assistant","root_path":"/prefix"}' for '--args-json <JSON>':
error: invalid value '{"model_tag":"Qwen/Qwen3-0.6B","response_role":"assistant","ssl_keyfile":"/tmp/key.pem"}' for '--args-json <JSON>':
The following arguments are not implemented in Rust frontend yet:
- response_role
- root_path
- ssl_keyfile
Remove these arguments to continue.
@@ -1223,7 +1077,6 @@ fn serve_args_accept_handshake_aliases() {
max_logprobs: None,
grpc_port: None,
shutdown_timeout: 0,
http_timeout_keep_alive: None,
chat_template: None,
default_chat_template_kwargs: None,
chat_template_content_format: Auto,
@@ -1248,11 +1101,6 @@ fn serve_args_accept_handshake_aliases() {
],
),
allow_credentials: false,
ssl_keyfile: None,
ssl_certfile: None,
ssl_ca_certs: None,
ssl_cert_reqs: 0,
ssl_ciphers: None,
},
managed_engine: ManagedEngineArgs {
python: "python3",
@@ -1386,12 +1234,10 @@ fn serve_frontend_config_uses_dp_address_as_advertised_host() {
],
allow_credentials: false,
},
tls: None,
api_keys: [],
disable_log_stats: false,
grpc_port: None,
shutdown_timeout: 0ns,
keep_alive_timeout: 5s,
}
"#]]
.assert_debug_eq(&Config {
@@ -1469,12 +1315,10 @@ fn serve_frontend_config_keeps_tcp_transport_for_non_local_only_topology() {
],
allow_credentials: false,
},
tls: None,
api_keys: [],
disable_log_stats: false,
grpc_port: None,
shutdown_timeout: 0ns,
keep_alive_timeout: 5s,
}
"#]]
.assert_debug_eq(&config);
@@ -1570,12 +1414,10 @@ fn frontend_config_uses_external_coordinator_when_coordinator_address_is_present
],
allow_credentials: false,
},
tls: None,
api_keys: [],
disable_log_stats: false,
grpc_port: None,
shutdown_timeout: 0ns,
keep_alive_timeout: 5s,
}
"#]]
.assert_debug_eq(&config);
+21
View File
@@ -526,6 +526,18 @@ pub struct ServerUnsupportedArgs {
#[arg(long)]
pub disable_access_log_for_endpoints: Option<Noop>,
/// The file path to the SSL key file.
#[arg(long)]
pub ssl_keyfile: Option<Unsupported>,
/// The file path to the SSL cert file.
#[arg(long)]
pub ssl_certfile: Option<Unsupported>,
/// The CA certificates file.
#[arg(long)]
pub ssl_ca_certs: Option<Unsupported>,
/// Refresh SSL Context when SSL certificate files change
#[arg(
long,
@@ -535,6 +547,15 @@ pub struct ServerUnsupportedArgs {
)]
pub enable_ssl_refresh: Option<Unsupported>,
/// Whether client certificate is required (see stdlib ssl module's).
#[arg(long)]
pub ssl_cert_reqs: Option<Unsupported>,
/// SSL cipher suites for HTTPS (TLS 1.2 and below only).
/// Example: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305'
#[arg(long)]
pub ssl_ciphers: Option<Unsupported>,
/// FastAPI root_path when app is behind a path based routing proxy.
#[arg(long)]
pub root_path: Option<Unsupported>,
@@ -100,7 +100,6 @@ impl EngineRoutingState {
pub struct RequestRegistry {
closed: bool,
requests: HashMap<String, TrackedRequest>,
active_lora_requests: usize,
routing_per_engine: BTreeMap<EngineId, EngineRoutingState>,
}
@@ -109,7 +108,6 @@ impl RequestRegistry {
Self {
closed: false,
requests: HashMap::default(),
active_lora_requests: 0,
routing_per_engine: engines
.iter()
.map(|engine| (engine.engine_id.clone(), EngineRoutingState::default()))
@@ -135,19 +133,15 @@ impl RequestRegistry {
let engine_id = self.choose_engine_for_request(data_parallel_rank)?;
let (tx, rx) = mpsc::unbounded_channel();
let lora = lora_name.map(|adapter_name| LoraRequestState {
adapter_name,
phase: LoraPhase::Waiting,
});
if lora.is_some() {
self.active_lora_requests += 1;
}
self.requests.insert(
request_id,
TrackedRequest {
sender: tx,
engine_id: engine_id.clone(),
lora,
lora: lora_name.map(|adapter_name| LoraRequestState {
adapter_name,
phase: LoraPhase::Waiting,
}),
},
);
@@ -236,10 +230,6 @@ impl RequestRegistry {
/// Snapshot the adapter names of tracked LoRA requests as
/// (running, waiting) sets. Feeds the `vllm:lora_requests_info` gauge.
pub fn lora_adapter_states(&self) -> (BTreeSet<String>, BTreeSet<String>) {
if self.active_lora_requests == 0 {
return (BTreeSet::new(), BTreeSet::new());
}
let mut running = BTreeSet::new();
let mut waiting = BTreeSet::new();
for lora in self.requests.values().filter_map(|tracked| tracked.lora.as_ref()) {
@@ -293,7 +283,6 @@ impl RequestRegistry {
}
self.closed = true;
self.active_lora_requests = 0;
std::mem::take(&mut self.requests)
.into_values()
.map(|tracked| tracked.sender)
@@ -333,9 +322,6 @@ impl RequestRegistry {
#[must_use]
pub fn remove(&mut self, request_id: &str) -> Option<(OutputSender, EngineId)> {
let tracked = self.requests.remove(request_id)?;
if tracked.lora.is_some() {
self.active_lora_requests -= 1;
}
self.routing_per_engine
.get_mut(&tracked.engine_id)
.expect("request registry must track all known engines")
@@ -373,11 +359,6 @@ impl RequestRegistry {
pub fn is_closed(&self) -> bool {
self.closed
}
#[cfg(test)]
fn active_lora_requests(&self) -> usize {
self.active_lora_requests
}
}
/// Internal registry for tracking active utility calls and their waiting
@@ -593,63 +574,6 @@ mod tests {
);
}
#[test]
fn registry_counts_only_active_lora_requests() {
let mut registry = RequestRegistry::new(&[connected_engine(EngineId::from(b"engine-0"))]);
registry.register("req-plain".to_string(), None, None).unwrap();
assert_eq!(registry.active_lora_requests(), 0);
assert_eq!(
registry.lora_adapter_states(),
(adapter_names(&[]), adapter_names(&[]))
);
registry
.register(
"req-lora-a".to_string(),
Some("adapter-a".to_string()),
None,
)
.unwrap();
registry
.register(
"req-lora-b".to_string(),
Some("adapter-b".to_string()),
None,
)
.unwrap();
assert_eq!(registry.active_lora_requests(), 2);
drop(registry.remove("req-plain"));
assert_eq!(registry.active_lora_requests(), 2);
drop(registry.finish_many(&["req-lora-a".to_string()]));
assert_eq!(registry.active_lora_requests(), 1);
drop(registry.abort_many(&["req-lora-b".to_string()], 0.0));
assert_eq!(registry.active_lora_requests(), 0);
assert_eq!(
registry.lora_adapter_states(),
(adapter_names(&[]), adapter_names(&[]))
);
}
#[test]
fn registry_clears_lora_count_on_close() {
let mut registry = RequestRegistry::new(&[connected_engine(EngineId::from(b"engine-0"))]);
registry
.register("req-lora".to_string(), Some("adapter-a".to_string()), None)
.unwrap();
assert_eq!(registry.active_lora_requests(), 1);
drop(registry.close());
assert_eq!(registry.active_lora_requests(), 0);
assert_eq!(
registry.lora_adapter_states(),
(adapter_names(&[]), adapter_names(&[]))
);
}
#[test]
fn registry_drops_lora_tracking_on_abort() {
let mut registry = RequestRegistry::new(&[connected_engine(EngineId::from(b"engine-0"))]);
@@ -20,27 +20,6 @@ pub(crate) struct CoordinatorStateSnapshot {
pub engines_running: bool,
}
impl CoordinatorStateSnapshot {
/// Resume the engines for a `FirstRequest` and return the wave to broadcast
/// and the engine to exclude from the wakeup.
///
/// The request may have been stamped with a `request_wave` older than
/// `current_wave` if a `WaveComplete` advanced it after the command was
/// enqueued. Such a request still needs serving, so the current wave is
/// broadcast to every engine (`exclude = None`); the wave is never rewound.
/// A non-stale request excludes the engine that already received it. Mirrors
/// the Python coordinator's front-end path.
pub(crate) fn start_wave_for_first_request(
&mut self,
request_wave: u32,
target_engine_index: u32,
) -> (u32, Option<u32>) {
self.engines_running = true;
let exclude = (request_wave >= self.current_wave).then_some(target_engine_index);
(self.current_wave, exclude)
}
}
/// Shared in-process coordinator state.
pub(crate) type CoordinatorState = Mutex<CoordinatorStateSnapshot>;
@@ -27,10 +27,9 @@ use crate::protocol::{
struct StartDpWaveMessage {
/// DP wave number that all engines should start processing.
wave: u32,
/// Engine index that already received the triggering request and so does not
/// need an extra wakeup. `None` wakes every engine (used when the triggering
/// request was for a stale wave).
exclude_engine_index: Option<u32>,
/// Engine index that already received the triggering request and should not
/// receive an extra wakeup notification.
exclude_engine_index: u32,
}
/// Background half of the in-process coordinator.
@@ -58,11 +57,7 @@ impl InProcCoordinatorRunner {
}
/// Broadcast Python-compatible `START_DP_WAVE` to all connected engines.
async fn broadcast_start_wave(
&mut self,
wave: u32,
exclude_engine_index: Option<u32>,
) -> Result<()> {
async fn broadcast_start_wave(&mut self, wave: u32, exclude_engine_index: u32) -> Result<()> {
let payload = encode_msgpack(&StartDpWaveMessage {
wave,
exclude_engine_index,
@@ -91,17 +86,13 @@ impl InProcCoordinatorRunner {
engine_id: target_engine_id.to_vec(),
}
})?;
let (current_wave, exclude) = {
let mut state = self.state.lock();
state.start_wave_for_first_request(wave, target_engine_index)
};
self.state.lock().current_wave = wave;
debug!(
current_wave,
request_wave = wave,
?exclude,
wave,
exclude_engine_index = target_engine_index,
"starting DP wave after first request while engines were paused"
);
self.broadcast_start_wave(current_wave, exclude).await?;
self.broadcast_start_wave(wave, target_engine_index).await?;
}
}
Ok(())
@@ -159,7 +150,7 @@ impl InProcCoordinatorRunner {
exclude_engine_index = engine_index,
"starting DP wave after stale-wave notification from engine"
);
self.broadcast_start_wave(wave, Some(engine_index)).await?;
self.broadcast_start_wave(wave, engine_index).await?;
}
}
},
@@ -211,48 +202,3 @@ impl InProcCoordinatorRunner {
inner.close_registries(Arc::new(error));
}
}
#[cfg(test)]
mod tests {
use crate::coordinator::handle::CoordinatorStateSnapshot;
/// A `FirstRequest` for the current wave starts that wave and excludes the
/// engine that already received the triggering request.
#[test]
fn first_request_for_current_wave_excludes_target() {
let mut state = CoordinatorStateSnapshot {
current_wave: 3,
engines_running: false,
};
let (wave, exclude) = state.start_wave_for_first_request(3, 2);
assert_eq!(wave, 3);
assert_eq!(exclude, Some(2));
assert!(state.engines_running);
assert_eq!(state.current_wave, 3);
}
/// A `FirstRequest` whose wave was superseded by a racing `WaveComplete`
/// (`request_wave < current_wave`) must still start the request's wave: it
/// broadcasts the current wave and wakes every engine (`exclude = None`)
/// rather than rewinding the wave or dropping the request.
#[test]
fn stale_first_request_starts_current_wave_for_all_engines() {
let mut state = CoordinatorStateSnapshot {
current_wave: 4,
engines_running: false,
};
// Request stamped with wave 3 while the coordinator already advanced to 4.
let (wave, exclude) = state.start_wave_for_first_request(3, 2);
assert_eq!(
wave, 4,
"must broadcast the current wave, not the stale one"
);
assert_eq!(exclude, None, "a stale request must wake every engine");
assert!(state.engines_running);
assert_eq!(state.current_wave, 4, "wave must not be rewound");
}
}
@@ -1285,24 +1285,18 @@ async fn dropping_multiple_live_streams_aborts_all_in_a_burst() {
)
.await;
// Aborts may coalesce into one burst or split across several.
let mut aborted = BTreeSet::new();
while aborted.len() < 3 {
let abort =
timeout(Duration::from_secs(1), recv_engine_message(dealer)).await.unwrap();
assert_eq!(abort[0].as_ref(), &[0x01]);
let ids: Vec<String> = rmp_serde::from_slice(&abort[1]).unwrap();
aborted.extend(ids);
}
let abort =
timeout(Duration::from_secs(1), recv_engine_message(dealer)).await.unwrap();
assert_eq!(abort[0].as_ref(), &[0x01]);
let ids: Vec<String> = rmp_serde::from_slice(&abort[1]).unwrap();
assert_eq!(
aborted,
BTreeSet::from([
ids,
vec![
"req-1".to_string(),
"req-2".to_string(),
"req-3".to_string()
])
]
);
// No spurious extra aborts.
assert!(
timeout(Duration::from_millis(100), recv_engine_message(dealer)).await.is_err()
);
@@ -154,7 +154,7 @@ mod tests {
"flag": true,
"payload": { "nested": true },
"items": [1, 2],
"empty": "null",
"empty": null,
})
);
}
+16 -47
View File
@@ -166,13 +166,7 @@ impl JsonParamType {
// Typically, these types are already handled by checking the "type" field, but
// we can also infer them from their characteristic fields if "type" is missing.
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
// Enum values are treated as strings, except that a `null` member
// makes the parameter nullable (mirrors Python's enum type
// inference), so a literal "null" coerces to JSON null.
if values.iter().any(Value::is_null) {
return Some(Self::one_of(vec![Self::String, Self::Null]));
}
if schema.contains_key("enum") {
return Some(Self::String);
}
if schema.contains_key("items") {
@@ -283,12 +277,9 @@ impl JsonParamType {
/// Convert one parameter input to a normalized JSON value.
fn convert_with_optional_schema(param_type: Option<&JsonParamType>, input: &ParamInput) -> Value {
// Coerce the literal text `null` to JSON null, except for `string`-typed
// params, where it must stay the string "null": a model emitting the literal
// text "null" for a string field means the string, not a missing value.
// For literal `null`, always convert to JSON null value.
if let ParamInput::Text(value) = input
&& value.eq_ignore_ascii_case("null")
&& param_type != Some(&JsonParamType::String)
{
return Value::Null;
}
@@ -694,43 +685,21 @@ mod tests {
}
#[test]
fn string_param_preserves_literal_null_text() {
// A `string`-typed param whose value is the literal text "null"/"NULL"
// must stay a string (the original case is preserved), rather than being
// coerced to JSON null. Non-string types keep coercing "null" to null.
let params = ToolSchema::from_schema(&json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"count": { "type": "integer" },
"anything": {}
}
}));
fn convert_params_preserves_null_for_known_param() {
let schemas = ToolSchemas::from_tools(&[test_tool(
"convert",
json!({
"type": "object",
"properties": {
"value": { "type": "string" }
}
}),
)]);
assert_eq!(params.convert("name", text("null")), json!("null"));
assert_eq!(params.convert("name", text("NULL")), json!("NULL"));
// Non-string and schema-less params are unchanged: "null" -> null.
assert_eq!(params.convert("count", text("null")), json!(null));
assert_eq!(params.convert("anything", text("null")), json!(null));
}
let converted = schemas
.convert_params_with_schema("convert", vec![("value".to_string(), "NULL".to_string())]);
#[test]
fn nullable_enum_param_coerces_literal_null() {
// An enum that includes `null` admits a null value, so a literal "null"
// must coerce to JSON null (matching Python's `extract_types_from_schema`,
// which infers `null` from the enum values), while a non-null enum keeps
// "null" as a string.
let params = ToolSchema::from_schema(&json!({
"type": "object",
"properties": {
"mode": { "enum": [null, "auto"] },
"color": { "enum": ["red", "green"] }
}
}));
assert_eq!(params.convert("mode", text("null")), json!(null));
assert_eq!(params.convert("mode", text("auto")), json!("auto"));
assert_eq!(params.convert("color", text("null")), json!("null"));
assert_eq!(converted.get("value"), Some(&json!(null)));
}
#[test]
@@ -872,7 +841,7 @@ mod tests {
"user_id": 42,
"urgent": true,
"note": "Please leave at front desk.",
"nil": "NULL",
"nil": null,
"shipping": {
"city": "Singapore",
"zip": 18956
-7
View File
@@ -7,18 +7,14 @@ license.workspace = true
[dependencies]
anyhow.workspace = true
asynk-strim-attr.workspace = true
auto_enums.workspace = true
axum.workspace = true
educe.workspace = true
futures.workspace = true
http-body.workspace = true
hyper.workspace = true
hyper-util.workspace = true
indexmap.workspace = true
itertools.workspace = true
libc.workspace = true
llm-multimodal.workspace = true
openssl.workspace = true
prost.workspace = true
prost-types.workspace = true
rmpv.workspace = true
@@ -29,9 +25,7 @@ sha2.workspace = true
socket2.workspace = true
subtle.workspace = true
thiserror-ext.workspace = true
tls-listener.workspace = true
tokio.workspace = true
tokio-openssl.workspace = true
tokio-stream.workspace = true
tokio-util.workspace = true
tonic.workspace = true
@@ -60,7 +54,6 @@ clap.workspace = true
expect-test.workspace = true
rmp-serde.workspace = true
serial_test.workspace = true
tempfile.workspace = true
tower.workspace = true
vllm-engine-core-client = { workspace = true, features = ["test-util"] }
zeromq.workspace = true
@@ -71,12 +71,10 @@ async fn main() -> Result<()> {
max_logprobs: None,
api_server_options: ApiServerOptions::default(),
cors: CorsConfig::default(),
tls: None,
api_keys: Vec::new(),
disable_log_stats: false,
grpc_port: None,
shutdown_timeout: Duration::ZERO,
keep_alive_timeout: Duration::from_secs(5),
};
let bind_address = format!("127.0.0.1:{port}");
-61
View File
@@ -10,10 +10,6 @@ use serde_json::Value;
use vllm_chat::{ChatTemplateContentFormatOption, ParserSelection, RendererSelection};
use vllm_engine_core_client::{CoordinatorMode as EngineCoreCoordinatorMode, TransportMode};
/// Default keep-alive idle timeout (seconds); also the head-read bound
/// when keep-alive is disabled (`0`).
pub const DEFAULT_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5);
/// How the HTTP server obtains its listening socket.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum HttpListenerMode {
@@ -103,54 +99,6 @@ impl CorsConfig {
}
}
/// TLS settings mirroring Python's uvicorn `ssl_*` arguments.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TlsConfig {
/// PEM certificate chain file. Required when TLS is configured; may also
/// hold the private key (combined PEM) when `key_file` is unset.
pub cert_file: Option<String>,
/// PEM private key file. When `None`, the key is read from `cert_file`
/// (combined PEM).
pub key_file: Option<String>,
/// PEM CA bundle used to verify client certificates (mTLS). Required when
/// `cert_reqs` is non-zero.
pub ca_certs: Option<String>,
/// Client-certificate requirement, mirroring Python's `ssl.CERT_*`:
/// 0 = none, 1 = optional, 2 = required.
pub cert_reqs: i32,
/// OpenSSL cipher string for TLS 1.2 and below, mirroring Python's
/// `ssl.set_ciphers`. `None` keeps the forward-secret AEAD default.
pub ciphers: Option<String>,
}
impl TlsConfig {
/// Structurally validate the TLS arguments; the cert/key material is parsed
/// later, when the OpenSSL context is built.
pub fn validate(&self) -> Result<()> {
if self.cert_file.is_none() {
bail!(
"--ssl-certfile is required to enable TLS; \
--ssl-keyfile/--ssl-ca-certs/--ssl-cert-reqs/--ssl-ciphers \
cannot be used without it"
);
}
if !matches!(self.cert_reqs, 0..=2) {
bail!(
"--ssl-cert-reqs must be 0 (none), 1 (optional), or 2 (required), got {}",
self.cert_reqs
);
}
if self.cert_reqs != 0 && self.ca_certs.is_none() {
bail!(
"--ssl-ca-certs is required when --ssl-cert-reqs is {} \
(client certificate verification)",
self.cert_reqs
);
}
Ok(())
}
}
/// Normalized runtime configuration for the minimal OpenAI-compatible server.
#[derive(Educe, Clone, PartialEq, Eq, Serialize)]
#[educe(Debug)]
@@ -190,9 +138,6 @@ pub struct Config {
pub api_server_options: ApiServerOptions,
/// CORS settings applied to every HTTP response.
pub cors: CorsConfig,
/// TLS settings. `None` serves plaintext HTTP; `Some` terminates TLS at the
/// listener.
pub tls: Option<TlsConfig>,
/// API keys accepted as bearer tokens for guarded routes.
#[serde(skip_serializing)]
#[educe(Debug(method(fmt_redacted_api_keys)))]
@@ -205,9 +150,6 @@ pub struct Config {
pub grpc_port: Option<u16>,
/// Maximum time to wait for active HTTP/gRPC requests to drain on shutdown.
pub shutdown_timeout: Duration,
/// Maximum idle time on a keep-alive HTTP connection before the server
/// closes it (`VLLM_HTTP_TIMEOUT_KEEP_ALIVE`, default 5s).
pub keep_alive_timeout: Duration,
}
impl Config {
@@ -216,9 +158,6 @@ impl Config {
pub fn validate(&self) -> Result<()> {
vllm_chat::validate_parser_overrides(&self.tool_call_parser, &self.reasoning_parser)?;
self.cors.validate()?;
if let Some(tls) = &self.tls {
tls.validate()?;
}
if let Some(max_logprobs) = self.max_logprobs
&& max_logprobs < -1
{
+1 -78
View File
@@ -4,21 +4,16 @@ mod convert;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use futures::{Stream, StreamExt as _, stream};
use futures::{Stream, StreamExt as _};
use thiserror_ext::AsReport as _;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::sync::mpsc;
use tokio_openssl::SslStream;
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::server::{Connected, TcpConnectInfo};
use tonic::{Request, Response, Status};
use tracing::info;
use vllm_text::{DecodedTextEvent, TextOutputStreamExt as _};
use self::convert::ResponseOpts;
use crate::listener::{Listener, ListenerIo};
use crate::state::AppState;
/// Generated protobuf/gRPC types for the `vllm` package.
@@ -31,78 +26,6 @@ pub use pb::generate_server::GenerateServer;
#[cfg(test)]
mod tests;
/// Newtype over `tokio-openssl`'s `SslStream` so we can implement tonic's
/// [`Connected`] on it (the orphan rule blocks doing so on the foreign type).
pub(crate) struct GrpcTlsStream {
inner: SslStream<ListenerIo>,
}
impl GrpcTlsStream {
pub(crate) fn new(inner: SslStream<ListenerIo>) -> Self {
Self { inner }
}
}
impl AsyncRead for GrpcTlsStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
}
}
impl AsyncWrite for GrpcTlsStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.get_mut().inner).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
}
}
impl Connected for GrpcTlsStream {
type ConnectInfo = TcpConnectInfo;
fn connect_info(&self) -> TcpConnectInfo {
self.inner.get_ref().connect_info()
}
}
/// Adapt the shared server listener into tonic's incoming stream shape.
pub(crate) fn incoming(listener: Listener) -> impl Stream<Item = std::io::Result<ListenerIo>> {
stream::unfold(listener, |mut listener| async move {
let (io, _) = axum::serve::Listener::accept(&mut listener).await;
Some((Ok(io), listener))
})
}
/// Wrap the gRPC listener so each accepted connection completes a TLS handshake
/// before tonic serves it.
pub(crate) fn tls_incoming(
listener: Listener,
context: openssl::ssl::SslContext,
handshake_timeout: std::time::Duration,
) -> impl Stream<Item = std::io::Result<GrpcTlsStream>> {
tls_listener::builder(context)
.handshake_timeout(handshake_timeout)
.listen(listener)
.map(|res| {
res.map(|(inner, _addr)| GrpcTlsStream::new(inner))
.map_err(std::io::Error::other)
})
}
/// gRPC Generate service implementation backed by the shared application state.
pub struct GenerateServiceImpl {
state: Arc<AppState>,
+15 -357
View File
@@ -1,19 +1,11 @@
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use futures::StreamExt as _;
use hyper_util::rt::TokioIo;
use openssl::ssl::{SslConnector, SslFiletype, SslMethod};
use serial_test::serial;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::net::TcpStream;
use tokio_openssl::SslStream;
use tonic::transport::{Channel, Endpoint, Server as TonicServer, Uri};
use tower::service_fn;
use tonic::transport::Server as TonicServer;
use vllm_chat::{
ChatBackend, ChatLlm, ChatRenderer, ChatRequest, ChatTextBackend, DefaultChatOutputProcessor,
DynChatOutputProcessor, DynChatRenderer, NewChatOutputProcessorOptions, RenderedPrompt,
@@ -30,11 +22,8 @@ use zeromq::prelude::{SocketRecv, SocketSend};
use zeromq::{DealerSocket, PushSocket, ZmqMessage};
use super::pb::generate_client::GenerateClient;
use super::{GenerateServer, GenerateServiceImpl, incoming, pb, tls_incoming};
use crate::listener::Listener;
use super::{GenerateServer, GenerateServiceImpl, pb};
use crate::state::AppState;
use crate::tls;
use crate::tls_tests::{TestCerts, server_tls};
// ========================================================================================
// Helpers (mirrors the patterns in routes/tests.rs)
@@ -222,12 +211,17 @@ impl ChatRenderer for FakeTextBackend {
}
}
/// Build the gRPC service + mock engine that serves a single request with the
/// given output specs. Shared by the plaintext and TLS server fixtures.
async fn setup_grpc_service(
/// Spin up a gRPC server backed by a mock engine that serves a single request
/// with the given output specs. Returns the client, the gRPC server task, and
/// the mock engine task.
async fn grpc_test_server(
engine_id: impl Into<EngineId>,
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
) -> (GenerateServer<GenerateServiceImpl>, MockEngineTask) {
) -> (
GenerateClient<tonic::transport::Channel>,
tokio::task::JoinHandle<()>,
MockEngineTask,
) {
let ipc = IpcNamespace::new().expect("create ipc namespace");
let handshake_address = ipc.handshake_endpoint();
let engine_id = engine_id.into();
@@ -265,29 +259,14 @@ async fn setup_grpc_service(
Arc::new(FakeTextBackend) as Arc<dyn ChatTextBackend>,
);
let state = Arc::new(AppState::new(vec!["test-model".to_string()], chat));
(
GenerateServer::new(GenerateServiceImpl::new(state)),
engine_task,
)
}
/// Spin up a plaintext gRPC server backed by a mock engine. Returns the client,
/// the gRPC server task, and the mock engine task.
async fn grpc_test_server(
engine_id: impl Into<EngineId>,
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
) -> (
GenerateClient<tonic::transport::Channel>,
tokio::task::JoinHandle<()>,
MockEngineTask,
) {
let (svc, engine_task) = setup_grpc_service(engine_id, output_specs).await;
let svc = GenerateServer::new(GenerateServiceImpl::new(state));
// Bind to an OS-assigned port.
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind grpc listener");
let addr = listener.local_addr().expect("local addr");
let server_task = tokio::spawn(async move {
let incoming = incoming(Listener::Tcp(listener));
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
TonicServer::builder()
.add_service(svc)
.serve_with_incoming(incoming)
@@ -295,6 +274,7 @@ async fn grpc_test_server(
.expect("grpc server");
});
// Connect the client.
let grpc_client = GenerateClient::connect(format!("http://{addr}"))
.await
.expect("connect grpc client");
@@ -302,158 +282,6 @@ async fn grpc_test_server(
(grpc_client, server_task, engine_task)
}
/// Spin up a TLS gRPC server (server cert from `certs`, `cert_reqs` mTLS mode).
/// Returns the address, the server task, and the mock engine task.
async fn grpc_tls_test_server(
engine_id: impl Into<EngineId>,
output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
certs: &TestCerts,
cert_reqs: i32,
) -> (String, tokio::task::JoinHandle<()>, MockEngineTask) {
let (svc, engine_task) = setup_grpc_service(engine_id, output_specs).await;
let context = tls::build_grpc_server_config(&server_tls(certs, cert_reqs))
.expect("build grpc tls config");
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind grpc listener");
let addr = listener.local_addr().expect("local addr").to_string();
let server_task = tokio::spawn(async move {
let incoming = tls_incoming(Listener::Tcp(listener), context, tls::TLS_HANDSHAKE_TIMEOUT);
TonicServer::builder()
.add_service(svc)
.serve_with_incoming(incoming)
.await
.expect("grpc tls server");
});
(addr, server_task, engine_task)
}
/// Build a tonic `Generate` client over a tokio-openssl connector, optionally
/// with a client identity for mTLS. Hand-rolled because tonic 0.14 ships no
/// OpenSSL transport.
async fn grpc_tls_client(
certs: &TestCerts,
addr: &str,
identity: Option<&str>,
) -> Result<GenerateClient<Channel>, tonic::transport::Error> {
let ca = certs.path("ca.pem");
let identity = identity.map(|name| {
(
certs.path(&format!("{name}.pem")),
certs.path(&format!("{name}.key")),
)
});
let target = addr.to_string();
let connector = service_fn(move |_: Uri| {
let ca = ca.clone();
let identity = identity.clone();
let target = target.clone();
async move {
let tcp = TcpStream::connect(&target).await?;
let mut builder =
SslConnector::builder(SslMethod::tls_client()).map_err(io::Error::other)?;
builder.set_ca_file(&ca).map_err(io::Error::other)?;
if let Some((cert, key)) = &identity {
builder.set_certificate_chain_file(cert).map_err(io::Error::other)?;
builder.set_private_key_file(key, SslFiletype::PEM).map_err(io::Error::other)?;
}
let mut config = builder.build().configure().map_err(io::Error::other)?;
config.set_verify_hostname(false);
config.set_alpn_protos(b"\x02h2").map_err(io::Error::other)?;
let ssl = config.into_ssl("127.0.0.1").map_err(io::Error::other)?;
let mut stream = SslStream::new(ssl, tcp).map_err(io::Error::other)?;
Pin::new(&mut stream).connect().await.map_err(io::Error::other)?;
Ok::<_, io::Error>(TokioIo::new(stream))
}
});
let channel = Endpoint::from_shared(format!("https://{addr}"))
.expect("grpc endpoint")
.connect_with_connector(connector)
.await?;
Ok(GenerateClient::new(channel))
}
/// Complete a raw TLS handshake against the gRPC port (offering ALPN `h2`) for
/// the ALPN-negotiation assertion.
async fn grpc_tls_handshake(
certs: &TestCerts,
addr: &str,
) -> io::Result<Pin<Box<SslStream<TcpStream>>>> {
let tcp = TcpStream::connect(addr).await?;
let mut builder = SslConnector::builder(SslMethod::tls_client()).map_err(io::Error::other)?;
builder.set_ca_file(certs.path("ca.pem")).map_err(io::Error::other)?;
let mut config = builder.build().configure().map_err(io::Error::other)?;
config.set_verify_hostname(false);
config.set_alpn_protos(b"\x02h2").map_err(io::Error::other)?;
let ssl = config.into_ssl("127.0.0.1").map_err(io::Error::other)?;
let mut stream = Box::pin(SslStream::new(ssl, tcp).map_err(io::Error::other)?);
stream.as_mut().connect().await.map_err(io::Error::other)?;
Ok(stream)
}
/// Spin up a plaintext gRPC server, optionally with HTTP/2 keepalive set to
/// `keepalive` for both the PING interval and the unanswered-PING timeout.
async fn grpc_server_with_keepalive(
engine_id: impl Into<EngineId>,
keepalive: Option<Duration>,
) -> (String, tokio::task::JoinHandle<()>, MockEngineTask) {
let (svc, engine_task) = setup_grpc_service(engine_id, default_stream_output_specs()).await;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind grpc listener");
let addr = listener.local_addr().expect("local addr").to_string();
let mut builder = TonicServer::builder();
if let Some(interval) = keepalive {
builder = builder
.http2_keepalive_interval(Some(interval))
.http2_keepalive_timeout(Some(interval));
}
let server_task = tokio::spawn(async move {
let incoming = incoming(Listener::Tcp(listener));
builder
.add_service(svc)
.serve_with_incoming(incoming)
.await
.expect("grpc server");
});
(addr, server_task, engine_task)
}
/// Establish an HTTP/2 connection (preface + SETTINGS exchange) then go silent,
/// ACKing the server's SETTINGS but never its keepalive PINGs. Returns whether
/// the SERVER closes the connection within `wait`. A minimal hand-rolled h2 peer
/// because a real client auto-ACKs PINGs and so can never be kept-alive-evicted.
async fn h2_unresponsive_peer_closed_within(addr: &str, wait: Duration) -> bool {
let mut tcp = TcpStream::connect(addr).await.expect("connect");
tcp.write_all(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n").await.expect("preface");
tcp.write_all(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0]).await.expect("client settings");
let closed = tokio::time::timeout(wait, async {
let mut header = [0u8; 9];
while tcp.read_exact(&mut header).await.is_ok() {
let len = u32::from_be_bytes([0, header[0], header[1], header[2]]) as usize;
let frame_type = header[3];
let flags = header[4];
let mut payload = vec![0u8; len];
if tcp.read_exact(&mut payload).await.is_err() {
return;
}
// ACK the server's SETTINGS so the only thing left unanswered is PINGs.
if frame_type == 0x4 && flags & 0x1 == 0 {
let _ = tcp.write_all(&[0, 0, 0, 0x4, 0x1, 0, 0, 0, 0]).await;
}
}
})
.await;
closed.is_ok()
}
// ========================================================================================
// Tests
// ========================================================================================
@@ -892,173 +720,3 @@ async fn unary_generate_output_text_defaults_to_true() {
engine_task.await.expect("mock engine task");
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn grpc_generate_succeeds_over_tls() {
let certs = TestCerts::generate();
let (addr, server_task, engine_task) = grpc_tls_test_server(
b"engine-grpc-tls-unary",
default_stream_output_specs(),
&certs,
0,
)
.await;
let mut client = grpc_tls_client(&certs, &addr, None).await.expect("tls client");
let response = client
.generate(pb::GenerateRequest {
request_id: "test-tls-unary".to_string(),
model: "test-model".to_string(),
prompt: Some(pb::generate_request::Prompt::Text("hello".to_string())),
stopping: Some(pb::StoppingCriteria {
max_new_tokens: 10,
..Default::default()
}),
response: Some(pb::ResponseOptions {
output_text: Some(true),
..Default::default()
}),
..Default::default()
})
.await
.expect("unary generate over tls")
.into_inner();
assert_eq!(response.outputs.expect("outputs present").text, "hi");
engine_task.await.expect("mock engine task");
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn grpc_tls_negotiates_h2_alpn() {
let certs = TestCerts::generate();
let (addr, server_task, _engine_task) = grpc_tls_test_server(
b"engine-grpc-tls-alpn",
default_stream_output_specs(),
&certs,
0,
)
.await;
let stream = grpc_tls_handshake(&certs, &addr).await.expect("handshake");
assert_eq!(
stream.ssl().selected_alpn_protocol(),
Some(&b"h2"[..]),
"server must negotiate h2 ALPN"
);
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn grpc_mtls_required_rejects_client_without_certificate() {
let certs = TestCerts::generate();
let (addr, server_task, _engine_task) = grpc_tls_test_server(
b"engine-grpc-tls-mtls-reject",
default_stream_output_specs(),
&certs,
2,
)
.await;
// With TLS 1.3 the missing-client-cert rejection surfaces on first use, not
// at the handshake, so drive an RPC and assert the call fails.
let outcome = match grpc_tls_client(&certs, &addr, None).await {
Err(_) => Err(()),
Ok(mut client) => client
.generate(pb::GenerateRequest {
request_id: "test-tls-mtls-reject".to_string(),
model: "test-model".to_string(),
prompt: Some(pb::generate_request::Prompt::Text("hello".to_string())),
stopping: Some(pb::StoppingCriteria {
max_new_tokens: 10,
..Default::default()
}),
..Default::default()
})
.await
.map(|_| ())
.map_err(|_| ()),
};
assert!(
outcome.is_err(),
"mTLS-required gRPC must reject a client without a certificate"
);
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn grpc_mtls_required_accepts_valid_client_certificate() {
let certs = TestCerts::generate();
let (addr, server_task, engine_task) = grpc_tls_test_server(
b"engine-grpc-tls-mtls-accept",
default_stream_output_specs(),
&certs,
2,
)
.await;
let mut client = grpc_tls_client(&certs, &addr, Some("client")).await.expect("mtls client");
let response = client
.generate(pb::GenerateRequest {
request_id: "test-tls-mtls".to_string(),
model: "test-model".to_string(),
prompt: Some(pb::generate_request::Prompt::Text("hello".to_string())),
stopping: Some(pb::StoppingCriteria {
max_new_tokens: 10,
..Default::default()
}),
response: Some(pb::ResponseOptions {
output_text: Some(true),
..Default::default()
}),
..Default::default()
})
.await
.expect("mtls generate over tls")
.into_inner();
assert_eq!(response.outputs.expect("outputs present").text, "hi");
engine_task.await.expect("mock engine task");
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn grpc_keepalive_closes_unresponsive_connection() {
let (addr, server_task, _engine_task) =
grpc_server_with_keepalive(b"engine-grpc-keepalive", Some(Duration::from_millis(150)))
.await;
let closed = h2_unresponsive_peer_closed_within(&addr, Duration::from_secs(5)).await;
assert!(
closed,
"keepalive must close a peer that stops answering PINGs"
);
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn grpc_without_keepalive_keeps_unresponsive_connection_open() {
// Without keepalive the same unresponsive peer is NOT
// closed, proving the close above is attributable to keepalive.
let (addr, server_task, _engine_task) =
grpc_server_with_keepalive(b"engine-grpc-no-keepalive", None).await;
let closed = h2_unresponsive_peer_closed_within(&addr, Duration::from_secs(1)).await;
assert!(
!closed,
"without keepalive an idle h2 connection must stay open"
);
server_task.abort();
}
+35 -205
View File
@@ -10,34 +10,20 @@ mod routes;
mod runtime;
mod server_info;
mod state;
mod tls;
#[cfg(test)]
mod tls_tests;
mod utils;
use std::future::Future;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use anyhow::{Context as _, Result};
use axum::Router;
use axum::body::Body;
use axum::http::Request;
pub use config::{
ApiServerOptions, Config, CoordinatorMode, CorsConfig, DEFAULT_KEEP_ALIVE_TIMEOUT,
HttpListenerMode, TlsConfig,
};
use futures::FutureExt as _;
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper_util::rt::{TokioIo, TokioTimer};
use hyper_util::server::graceful::GracefulShutdown;
use hyper_util::service::TowerToHyperService;
use axum::serve::ListenerExt as _;
pub use config::{ApiServerOptions, Config, CoordinatorMode, CorsConfig, HttpListenerMode};
use tokio::net::TcpListener;
use tokio::time::{Instant, sleep_until};
use tokio_stream::wrappers::TcpListenerStream;
use tokio_util::either::Either;
use tokio_util::sync::CancellationToken;
use tonic::transport::Server as TonicServer;
use tower::ServiceExt as _;
use tracing::{info, trace, warn};
use vllm_chat::{ChatLlm, LoadModelBackendsOptions, load_model_backends};
pub use vllm_chat::{ChatTemplateContentFormatOption, ParserSelection, RendererSelection};
@@ -50,13 +36,6 @@ use crate::routes::build_router;
use crate::server_info::ServerInfoSnapshot;
use crate::state::AppState;
/// How often the server PINGs an idle gRPC connection to reap a dead peer;
/// tonic enables no keepalive by default. 2h matches the gRPC-core default.
const GRPC_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(7200);
/// How long the server waits for a keepalive PING reply before dropping the gRPC
/// connection. 20s matches the gRPC-core default.
const GRPC_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(20);
/// Resolve the public model names accepted by the frontend.
fn effective_served_model_names(model: &str, served_model_name: &[String]) -> Vec<String> {
if served_model_name.is_empty() {
@@ -66,17 +45,6 @@ fn effective_served_model_names(model: &str, served_model_name: &[String]) -> Ve
}
}
/// Choose the gRPC listener host. It follows the HTTP TCP host when there is
/// one; otherwise (unix socket or inherited fd) it defaults to IPv4 loopback
/// rather than all interfaces, so the side-car is never accidentally
/// network-exposed.
fn grpc_bind_host(listener_mode: &HttpListenerMode) -> &str {
match listener_mode {
HttpListenerMode::BindTcp { host, .. } => host.as_str(),
HttpListenerMode::BindUnix { .. } | HttpListenerMode::InheritedFd { .. } => "127.0.0.1",
}
}
/// Build the shared application state for one configured model and one engine
/// client.
async fn build_state(config: &Config) -> Result<Arc<AppState>> {
@@ -162,15 +130,6 @@ where
{
config.validate().context("invalid OpenAI frontend configuration")?;
// Build the TLS server config once, up front, so a bad cert/key fails fast
// before the (potentially long) engine handshake.
let tls_config = config
.tls
.as_ref()
.map(tls::build_server_config)
.transpose()
.context("invalid TLS configuration")?;
// Also check shutdown during the (potentially long) startup handshake.
let state = tokio::select! {
result = build_state(&config) => result?,
@@ -185,39 +144,40 @@ where
// Optionally bind the gRPC Generate server on a separate port. Bind
// synchronously here so bind errors (port in use, permission denied, ...)
// surface before serving rather than being deferred until shutdown.
// surface before we start serving, rather than being deferred until
// shutdown. The gRPC listener follows the same host as the HTTP listener so
// that enabling --grpc-port does not accidentally expose the service on all
// interfaces when HTTP is intentionally local-only.
let grpc_setup = if let Some(grpc_port) = config.grpc_port {
let grpc_host = grpc_bind_host(&config.listener_mode);
let grpc_host = match &config.listener_mode {
HttpListenerMode::BindTcp { host, .. } => host.as_str(),
HttpListenerMode::BindUnix { .. } | HttpListenerMode::InheritedFd { .. } => "0.0.0.0",
};
let grpc_listener = TcpListener::bind((grpc_host, grpc_port))
.await
.with_context(|| format!("failed to bind gRPC listener on {grpc_host}:{grpc_port}"))?;
let addr = grpc_listener.local_addr()?;
let grpc_listener = Listener::Tcp(grpc_listener);
// gRPC reuses the HTTP TLS config (same SslContext) plus ALPN h2.
let grpc_tls = config
.tls
.as_ref()
.map(tls::build_grpc_server_config)
.transpose()
.context("invalid gRPC TLS configuration")?;
let svc = grpc::GenerateServer::new(grpc::GenerateServiceImpl::new(state.clone()));
let svc = TonicServer::builder()
.http2_keepalive_interval(Some(GRPC_KEEPALIVE_INTERVAL))
.http2_keepalive_timeout(Some(GRPC_KEEPALIVE_TIMEOUT))
.layer(middleware::request_runtime_layer(state.clone()))
.add_service(svc);
info!(%addr, tls = grpc_tls.is_some(), "starting gRPC server");
Some((grpc_listener, svc, grpc_tls))
info!(%addr, "starting gRPC server");
Some((grpc_listener, svc))
} else {
None
};
let scheme = if tls_config.is_some() {
"https"
} else {
"http"
};
info!(%bind_address, %scheme, %model, "starting OpenAI server");
info!(%bind_address, %model, "starting OpenAI server");
// Set TCP_NODELAY on accepted connections to reduce latency.
// By `tap_io` we will do this on every accepted connection.
let listener = listener.tap_io(|io| {
if let Either::Left(tcp_stream) = io
&& let Err(err) = tcp_stream.set_nodelay(true)
{
trace!(error = %err, "failed to enable TCP_NODELAY on accepted HTTP connection");
}
});
// Run HTTP and gRPC concurrently under a child token of the caller's shutdown
// token. Caller cancellation propagates into both protocols; if either
@@ -248,27 +208,17 @@ where
}
});
// 0 disables keep-alive but still bounds the head read (default), so a
// silent client cannot hold the connection open.
let keep_alive_timeout = config.keep_alive_timeout;
let timeouts = ConnectionTimeouts {
handshake: tls::TLS_HANDSHAKE_TIMEOUT,
header_read: if keep_alive_timeout.is_zero() {
DEFAULT_KEEP_ALIVE_TIMEOUT
} else {
keep_alive_timeout
},
keep_alive_enabled: !keep_alive_timeout.is_zero(),
};
let http_fut = {
let shutdown = server_shutdown.child_token();
let server_shutdown = server_shutdown.clone();
let force_shutdown = force_shutdown.clone();
async move {
let server =
axum::serve(listener, app).with_graceful_shutdown(shutdown.cancelled_owned());
let result = tokio::select! {
result = serve_listener(listener, tls_config, app, shutdown.cancelled_owned(), timeouts) => {
result
result = server => {
result.context("HTTP server failed")
}
_ = force_shutdown.cancelled() => {
warn!("HTTP graceful shutdown deadline elapsed; aborting server");
@@ -286,24 +236,16 @@ where
let server_shutdown = server_shutdown.clone();
let force_shutdown = force_shutdown.clone();
async move {
let Some((grpc_listener, svc, grpc_tls)) = grpc_setup else {
let Some((grpc_listener, svc)) = grpc_setup else {
// No gRPC configured: just wait for shutdown so we do not race the
// join! by resolving early and tripping the cancellation token.
shutdown.cancelled().await;
return Ok(());
};
// Box to unify the TLS and plaintext arms' different stream types.
let server = match grpc_tls {
Some(context) => {
let incoming =
grpc::tls_incoming(grpc_listener, context, tls::TLS_HANDSHAKE_TIMEOUT);
svc.serve_with_incoming_shutdown(incoming, shutdown.cancelled_owned()).boxed()
}
None => {
let incoming = grpc::incoming(grpc_listener);
svc.serve_with_incoming_shutdown(incoming, shutdown.cancelled_owned()).boxed()
}
};
let server = svc.serve_with_incoming_shutdown(
TcpListenerStream::new(grpc_listener),
shutdown.cancelled_owned(),
);
let result = tokio::select! {
result = server => {
@@ -330,99 +272,6 @@ where
state.shutdown(shutdown_deadline).await
}
/// Per-connection timeouts applied while serving HTTP/HTTPS.
#[derive(Clone, Copy)]
pub(crate) struct ConnectionTimeouts {
/// Max time for a client to complete the TLS handshake (TLS path only).
pub(crate) handshake: Duration,
/// HTTP/1 header-read timeout (bounds idle keep-alive and the head read).
pub(crate) header_read: Duration,
/// Whether HTTP/1 keep-alive is enabled; `false` closes after each response.
pub(crate) keep_alive_enabled: bool,
}
/// Apply optional TLS termination and per-connection HTTP timeouts, then serve
/// `app`. Shared by [`serve_with_router_extension`] and the TLS tests.
async fn serve_listener(
listener: Listener,
tls: Option<openssl::ssl::SslContext>,
app: Router,
shutdown: impl Future<Output = ()> + Send + 'static,
timeouts: ConnectionTimeouts,
) -> Result<()> {
match tls {
Some(context) => {
// tls-listener terminates TLS (handshake + timeout); serve_connections
// owns the HTTP keep-alive/idle bound that axum::serve cannot express.
// Failed handshakes (incl. timeouts) log at ERROR via tls-listener.
let listener = tls_listener::builder(context)
.handshake_timeout(timeouts.handshake)
.listen(listener);
serve_connections(
listener,
app,
shutdown,
timeouts.header_read,
timeouts.keep_alive_enabled,
)
.await
.context("HTTPS server failed")
}
None => serve_connections(
listener,
app,
shutdown,
timeouts.header_read,
timeouts.keep_alive_enabled,
)
.await
.context("HTTP server failed"),
}
}
/// Serve `app` per connection (HTTP/1) with a keep-alive idle timeout and
/// graceful drain. Hand-rolled on hyper because [`axum::serve()`] takes no config.
async fn serve_connections<L>(
mut listener: L,
app: Router,
shutdown: impl Future<Output = ()> + Send,
header_read: Duration,
keep_alive_enabled: bool,
) -> Result<()>
where
L: axum::serve::Listener,
{
let graceful = GracefulShutdown::new();
let mut shutdown = std::pin::pin!(shutdown);
loop {
let (io, _addr) = tokio::select! {
conn = listener.accept() => conn,
() = &mut shutdown => break,
};
let service = TowerToHyperService::new(
app.clone().map_request(|req: Request<Incoming>| req.map(Body::new)),
);
let mut builder = http1::Builder::new();
builder.timer(TokioTimer::new()).header_read_timeout(header_read);
if !keep_alive_enabled {
builder.keep_alive(false);
}
let connection = builder.serve_connection(TokioIo::new(io), service);
let connection = graceful.watch(connection);
tokio::spawn(async move {
if let Err(err) = connection.await {
trace!(error = %err, "failed to serve connection");
}
});
}
drop(listener);
graceful.shutdown().await;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -444,23 +293,4 @@ mod tests {
served_names
);
}
#[test]
fn grpc_bind_host_follows_http_tcp_host() {
let mode = HttpListenerMode::BindTcp {
host: "0.0.0.0".to_string(),
port: 8000,
};
assert_eq!(grpc_bind_host(&mode), "0.0.0.0");
}
#[test]
fn grpc_bind_host_defaults_to_loopback_without_tcp_host() {
let unix = HttpListenerMode::BindUnix {
path: "/tmp/vllm.sock".to_string(),
};
let inherited = HttpListenerMode::InheritedFd { fd: 3 };
assert_eq!(grpc_bind_host(&unix), "127.0.0.1");
assert_eq!(grpc_bind_host(&inherited), "127.0.0.1");
}
}
+14 -96
View File
@@ -1,49 +1,28 @@
//! Unified listener wrapper for the Rust frontend.
//! Unified HTTP listener wrapper for the Rust frontend.
//!
//! This module hides the difference between TCP and Unix-domain listeners so
//! the rest of the server can bind or inherit one socket and pass it to
//! `axum::serve(...)` through a single type.
use std::io::Result;
use std::net::{SocketAddr, TcpListener as StdTcpListener};
use std::net::TcpListener as StdTcpListener;
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
use std::os::unix::net::UnixListener as StdUnixListener;
use std::pin::Pin;
use std::task::{Context, Poll, ready};
use auto_enums::enum_derive;
use socket2::Socket;
use tls_listener::{AsyncAccept, AsyncListener};
use tokio::net::{TcpListener, TcpStream, UnixListener, UnixStream};
use tonic::transport::server::{Connected, TcpConnectInfo};
use tracing::trace;
use tokio_util::either::Either;
use crate::HttpListenerMode;
/// Runtime listener type used by the OpenAI-compatible HTTP or gRPC server,
/// which is either a TCP listener or a Unix-domain listener.
/// Runtime listener type used by the OpenAI-compatible HTTP server, which is
/// either a TCP listener or a Unix-domain listener.
#[derive(Debug)]
pub enum Listener {
Tcp(TcpListener),
Unix(UnixListener),
}
/// Runtime listener I/O type which is either a TCP stream or a Unix-domain stream.
#[derive(Debug)]
#[enum_derive(tokio1::AsyncRead, tokio1::AsyncWrite)]
pub enum ListenerIo {
Tcp(TcpStream),
Unix(UnixStream),
}
/// Runtime listener address type which is either a TCP address or a Unix-domain address.
#[derive(Debug)]
#[allow(dead_code)]
pub enum ListenerAddr {
Tcp(SocketAddr),
Unix(tokio::net::unix::SocketAddr),
}
impl Listener {
/// Bind or adopt the listener described by the frontend configuration.
///
@@ -91,95 +70,34 @@ impl Listener {
Ok(Self::Tcp(TcpListener::from_std(std_listener)?))
}
}
fn listener_addr(&self) -> Result<ListenerAddr> {
match self {
Self::Tcp(listener) => listener.local_addr().map(ListenerAddr::Tcp),
Self::Unix(listener) => listener.local_addr().map(ListenerAddr::Unix),
}
}
}
impl Connected for ListenerIo {
type ConnectInfo = TcpConnectInfo;
fn connect_info(&self) -> TcpConnectInfo {
match self {
Self::Tcp(stream) => stream.connect_info(),
Self::Unix(_) => TcpConnectInfo {
local_addr: None,
remote_addr: None,
},
}
}
}
/// Attempt to set `TCP_NODELAY` on the accepted TCP stream.
fn enable_tcp_nodelay(stream: TcpStream) -> TcpStream {
if let Err(err) = stream.set_nodelay(true) {
trace!(error = %err, "failed to enable TCP_NODELAY on accepted TCP connection");
}
stream
}
/// Allow the unified listener to plug directly into `axum::serve(...)`.
impl axum::serve::Listener for Listener {
type Addr = ListenerAddr;
type Io = ListenerIo;
type Addr = Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>;
type Io = Either<TcpStream, UnixStream>;
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
match self {
Self::Tcp(listener) => {
let (io, addr) = axum::serve::Listener::accept(listener).await;
(
ListenerIo::Tcp(enable_tcp_nodelay(io)),
ListenerAddr::Tcp(addr),
)
let (io, addr) = listener.accept().await;
(Either::Left(io), Either::Left(addr))
}
Self::Unix(listener) => {
let (io, addr) = axum::serve::Listener::accept(listener).await;
(ListenerIo::Unix(io), ListenerAddr::Unix(addr))
let (io, addr) = listener.accept().await;
(Either::Right(io), Either::Right(addr))
}
}
}
fn local_addr(&self) -> Result<Self::Addr> {
self.listener_addr()
}
}
/// Allow the unified listener to be adaptable to `tls_listener`.
impl AsyncAccept for Listener {
type Connection = ListenerIo;
type Address = ListenerAddr;
type Error = std::io::Error;
fn poll_accept(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(Self::Connection, Self::Address)>> {
match self.get_mut() {
Self::Tcp(listener) => {
let (io, addr) = ready!(listener.poll_accept(cx))?;
Poll::Ready(Ok((
ListenerIo::Tcp(enable_tcp_nodelay(io)),
ListenerAddr::Tcp(addr),
)))
}
Self::Unix(listener) => {
let (io, addr) = ready!(listener.poll_accept(cx))?;
Poll::Ready(Ok((ListenerIo::Unix(io), ListenerAddr::Unix(addr))))
}
match self {
Self::Tcp(listener) => listener.local_addr().map(Either::Left),
Self::Unix(listener) => listener.local_addr().map(Either::Right),
}
}
}
impl AsyncListener for Listener {
fn local_addr(&self) -> Result<Self::Address> {
self.listener_addr()
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, SocketAddrV4};
@@ -34,20 +34,14 @@ pub(super) fn validate_request_compat(
);
}
if let Some(prompt_logprobs) = request.sampling_params.prompt_logprobs {
if prompt_logprobs < 0 && prompt_logprobs != -1 {
bail_invalid_request!(
param = "sampling_params",
"`prompt_logprobs` must be a non-negative value or -1."
);
}
if request.stream {
bail_invalid_request!(
param = "sampling_params",
"`prompt_logprobs` are not available when `stream=true`."
);
}
if let Some(prompt_logprobs) = request.sampling_params.prompt_logprobs
&& prompt_logprobs < 0
&& prompt_logprobs != -1
{
bail_invalid_request!(
param = "sampling_params",
"`prompt_logprobs` must be a non-negative value or -1."
);
}
Ok(())
@@ -103,54 +97,4 @@ mod tests {
};
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
}
#[test]
fn validate_request_compat_rejects_streaming_prompt_logprobs() {
let request: GenerateRequest = serde_json::from_value(json!({
"model": "Qwen/Qwen1.5-0.5B-Chat",
"token_ids": [11, 22],
"stream": true,
"sampling_params": {
"prompt_logprobs": 0
}
}))
.expect("parse request");
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
let request: GenerateRequest = serde_json::from_value(json!({
"model": "Qwen/Qwen1.5-0.5B-Chat",
"token_ids": [11, 22],
"stream": true,
"sampling_params": {
"prompt_logprobs": 1
}
}))
.expect("parse request");
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
let request: GenerateRequest = serde_json::from_value(json!({
"model": "Qwen/Qwen1.5-0.5B-Chat",
"token_ids": [11, 22],
"stream": true,
"sampling_params": {
"prompt_logprobs": -1
}
}))
.expect("parse request");
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
}
#[test]
fn validate_request_compat_accepts_non_stream_prompt_logprobs() {
let request: GenerateRequest = serde_json::from_value(json!({
"model": "Qwen/Qwen1.5-0.5B-Chat",
"token_ids": [11, 22],
"stream": false,
"sampling_params": {
"prompt_logprobs": 1
}
}))
.expect("parse request");
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_ok());
}
}
-39
View File
@@ -4129,45 +4129,6 @@ async fn raw_generate_rejects_empty_token_ids() {
assert_eq!(json["error"]["param"], "token_ids");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn raw_generate_rejects_streaming_prompt_logprobs() {
let mut app = test_app().await;
for prompt_logprobs in [0, 1] {
let response = app
.call(
Request::builder()
.method("POST")
.uri("/inference/v1/generate")
.header("content-type", "application/json")
.body(Body::from(
json!({
"model": "Qwen/Qwen1.5-0.5B-Chat",
"token_ids": [11, 22],
"stream": true,
"sampling_params": {
"prompt_logprobs": prompt_logprobs
}
})
.to_string(),
))
.expect("build request"),
)
.await
.expect("call app");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = to_bytes(response.into_body(), usize::MAX).await.expect("read body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("decode json");
assert_eq!(json["error"]["param"], "sampling_params");
assert_eq!(
json["error"]["message"],
"`prompt_logprobs` are not available when `stream=true`."
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn raw_generate_rejects_wrong_model() {
-119
View File
@@ -1,119 +0,0 @@
//! OpenSSL server-config construction for TLS termination.
//!
//! Builds an OpenSSL [`SslContext`] from the uvicorn-style `ssl_*` arguments
//! (certificate chain, private key, mTLS client verifier, optional cipher list).
//! The `tls-listener` crate drives the handshake on each accepted connection.
//!
//! Crypto runs through whichever OpenSSL the binary links (system by default,
//! vendored when built with that feature).
use std::path::Path;
use std::time::Duration;
use anyhow::{Context as _, Result};
use openssl::ssl::{
AlpnError, SslAcceptor, SslAcceptorBuilder, SslContext, SslContextBuilder, SslFiletype,
SslMethod, SslOptions, SslVerifyMode, select_next_proto,
};
use crate::config::TlsConfig;
/// Time a client has to complete the TLS handshake before the connection is dropped.
pub(crate) const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(60);
/// ALPN wire bytes for HTTP/2 (length-prefixed).
const ALPN_H2: &[u8] = b"\x02h2";
/// Build the shared OpenSSL acceptor from validated [`TlsConfig`]: the full
/// certificate chain, the private key (`key_file`, or the certificate file when
/// unset), the mTLS client verifier, and an optional cipher list.
///
/// Starts from the Mozilla intermediate baseline (forward-secret AEAD suites,
/// TLS 1.2 floor, server cipher preference, no compression), a slightly
/// stricter subset of the Python frontend's default suites; `--ssl-ciphers`
/// overrides it.
fn build_server_builder(tls: &TlsConfig) -> Result<SslAcceptorBuilder> {
let cert_file = tls.cert_file.as_deref().context("--ssl-certfile is required to enable TLS")?;
let mut builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server())
.context("failed to initialize TLS")?;
builder.set_options(SslOptions::CIPHER_SERVER_PREFERENCE);
// Load the whole chain (leaf + intermediates), not just the leaf, so
// deployments behind an intermediate CA serve a complete chain.
ensure_exists(cert_file, "--ssl-certfile")?;
builder.set_certificate_chain_file(cert_file).with_context(|| {
format!("failed to parse certificate chain in --ssl-certfile {cert_file:?}")
})?;
// When `key_file` is unset the key is read from the certificate file
// (combined PEM).
let key_file = tls.key_file.as_deref().unwrap_or(cert_file);
ensure_exists(key_file, "private key file")?;
builder
.set_private_key_file(key_file, SslFiletype::PEM)
.with_context(|| format!("failed to parse private key in {key_file:?}"))?;
builder
.check_private_key()
.context("the certificate and private key do not match")?;
configure_client_auth(&mut builder, tls)?;
if let Some(ciphers) = tls.ciphers.as_deref().filter(|c| !c.is_empty()) {
builder
.set_cipher_list(ciphers)
.with_context(|| format!("invalid --ssl-ciphers {ciphers:?}"))?;
}
Ok(builder)
}
/// Build the HTTP [`SslContext`] (HTTP/1.1; no ALPN, matching uvicorn).
pub(crate) fn build_server_config(tls: &TlsConfig) -> Result<SslContext> {
Ok(build_server_builder(tls)?.build().into_context())
}
/// Build the gRPC [`SslContext`]: identical to [`build_server_config`] but
/// negotiates ALPN `h2`, which HTTP/2 over TLS requires.
pub(crate) fn build_grpc_server_config(tls: &TlsConfig) -> Result<SslContext> {
let mut builder = build_server_builder(tls)?;
builder.set_alpn_select_callback(|_ssl, client| {
select_next_proto(ALPN_H2, client).ok_or(AlpnError::NOACK)
});
Ok(builder.build().into_context())
}
/// Fail loudly with a flag-named message when a configured file is missing,
/// distinguishing it from a malformed-PEM error raised later by OpenSSL (whose
/// `ErrorStack` does not name the offending file).
fn ensure_exists(path: &str, what: &str) -> Result<()> {
std::fs::metadata(Path::new(path))
.map(drop)
.with_context(|| format!("failed to read {what} {path:?}"))
}
/// Apply the `cert_reqs` client-certificate policy: 0 = none, 1 = optional
/// (verify if presented, allow anonymous), 2 = required. `PEER` without a custom
/// verify callback still rejects a presented-but-untrusted certificate.
fn configure_client_auth(builder: &mut SslContextBuilder, tls: &TlsConfig) -> Result<()> {
if tls.cert_reqs == 0 {
builder.set_verify(SslVerifyMode::NONE);
return Ok(());
}
let ca_file = tls
.ca_certs
.as_deref()
.context("--ssl-ca-certs is required for client certificate verification")?;
ensure_exists(ca_file, "--ssl-ca-certs")?;
builder
.set_ca_file(ca_file)
.with_context(|| format!("failed to parse --ssl-ca-certs {ca_file:?}"))?;
let mut mode = SslVerifyMode::PEER;
if tls.cert_reqs == 2 {
mode |= SslVerifyMode::FAIL_IF_NO_PEER_CERT;
}
builder.set_verify(mode);
Ok(())
}
-688
View File
@@ -1,688 +0,0 @@
//! TLS tests: `build_server_config` unit checks plus end-to-end OpenSSL handshakes
//! through the production `serve_listener` path, with a trivial router since TLS
//! terminates below the app.
use std::pin::Pin;
use std::time::Duration;
use axum::Router;
use axum::routing::get;
use openssl::asn1::Asn1Time;
use openssl::bn::{BigNum, MsbOption};
use openssl::ec::{EcGroup, EcKey};
use openssl::hash::MessageDigest;
use openssl::nid::Nid;
use openssl::pkey::{PKey, Private};
use openssl::ssl::{SslConnector, SslFiletype, SslMethod, SslVersion};
use openssl::x509::extension::{BasicConstraints, KeyUsage, SubjectAlternativeName};
use openssl::x509::{X509, X509NameBuilder};
use tempfile::TempDir;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::net::TcpStream;
use tokio_openssl::SslStream;
use tokio_util::sync::CancellationToken;
use crate::config::{HttpListenerMode, TlsConfig};
use crate::listener::Listener;
use crate::{ConnectionTimeouts, serve_listener, tls};
// ============================================================================
// Test infrastructure
// ============================================================================
/// A throwaway CA + server/client/untrusted/chain cert set as PEM files in a
/// temp dir; dropping it deletes them.
pub(crate) struct TestCerts {
dir: TempDir,
}
impl TestCerts {
pub(crate) fn generate() -> Self {
let dir = tempfile::tempdir().expect("tempdir");
let (ca, ca_key) = build_ca();
let (server, server_key) = build_leaf("server", &["127.0.0.1", "localhost"], &ca, &ca_key);
let (client, client_key) = build_leaf("client", &[], &ca, &ca_key);
let (untrusted, untrusted_key) = build_self_signed("untrusted client");
// Leaf signed by an intermediate (itself signed by the root); the cert
// file holds leaf + intermediate, for the chain-serving test.
let (intermediate, intermediate_key) = build_intermediate(&ca, &ca_key);
let (chain_leaf, chain_leaf_key) = build_leaf(
"chain",
&["127.0.0.1", "localhost"],
&intermediate,
&intermediate_key,
);
let server_pem = pem(&server);
let server_key_pem = key_pem(&server_key);
let files = [
("ca.pem", pem(&ca)),
("server.pem", server_pem.clone()),
("server.key", server_key_pem.clone()),
("client.pem", pem(&client)),
("client.key", key_pem(&client_key)),
("untrusted_client.pem", pem(&untrusted)),
("untrusted_client.key", key_pem(&untrusted_key)),
(
"server_combined.pem",
format!("{server_pem}{server_key_pem}"),
),
(
"server_chain.pem",
format!("{}{}", pem(&chain_leaf), pem(&intermediate)),
),
("server_chain.key", key_pem(&chain_leaf_key)),
];
for (name, contents) in files {
std::fs::write(dir.path().join(name), contents).expect("write fixture");
}
Self { dir }
}
/// Absolute path to a fixture by name; the file need not exist.
pub(crate) fn path(&self, name: &str) -> String {
self.dir.path().join(name).to_str().expect("utf-8 path").to_string()
}
}
fn gen_key() -> PKey<Private> {
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).expect("ec group");
let ec = EcKey::generate(&group).expect("ec key");
PKey::from_ec_key(ec).expect("pkey")
}
fn serial() -> openssl::asn1::Asn1Integer {
let mut bn = BigNum::new().expect("bignum");
bn.rand(159, MsbOption::MAYBE_ZERO, false).expect("rand serial");
bn.to_asn1_integer().expect("asn1 serial")
}
fn x509_name(cn: &str) -> openssl::x509::X509Name {
let mut builder = X509NameBuilder::new().expect("name builder");
builder.append_entry_by_text("CN", cn).expect("cn");
builder.build()
}
fn pem(cert: &X509) -> String {
String::from_utf8(cert.to_pem().expect("cert pem")).expect("utf-8 cert")
}
fn key_pem(key: &PKey<Private>) -> String {
String::from_utf8(key.private_key_to_pem_pkcs8().expect("key pem")).expect("utf-8 key")
}
/// A self-signed CA used to sign the server/client leaf certs.
fn build_ca() -> (X509, PKey<Private>) {
let key = gen_key();
let name = x509_name("vLLM Test CA");
let mut builder = X509::builder().expect("x509 builder");
builder.set_version(2).expect("version");
builder.set_serial_number(&serial()).expect("serial");
builder.set_subject_name(&name).expect("subject");
builder.set_issuer_name(&name).expect("issuer");
builder.set_pubkey(&key).expect("pubkey");
builder
.set_not_before(&Asn1Time::days_from_now(0).expect("nb"))
.expect("set nb");
builder
.set_not_after(&Asn1Time::days_from_now(3650).expect("na"))
.expect("set na");
builder
.append_extension(BasicConstraints::new().critical().ca().build().expect("bc"))
.expect("ext bc");
builder
.append_extension(
KeyUsage::new().critical().key_cert_sign().crl_sign().build().expect("ku"),
)
.expect("ext ku");
builder.sign(&key, MessageDigest::sha256()).expect("sign ca");
(builder.build(), key)
}
/// A CA-signed leaf cert with optional subject-alternative names (IP or DNS).
fn build_leaf(cn: &str, sans: &[&str], ca: &X509, ca_key: &PKey<Private>) -> (X509, PKey<Private>) {
let key = gen_key();
let mut builder = X509::builder().expect("x509 builder");
builder.set_version(2).expect("version");
builder.set_serial_number(&serial()).expect("serial");
builder.set_subject_name(&x509_name(cn)).expect("subject");
builder.set_issuer_name(ca.subject_name()).expect("issuer");
builder.set_pubkey(&key).expect("pubkey");
builder
.set_not_before(&Asn1Time::days_from_now(0).expect("nb"))
.expect("set nb");
builder
.set_not_after(&Asn1Time::days_from_now(3650).expect("na"))
.expect("set na");
builder
.append_extension(BasicConstraints::new().build().expect("bc"))
.expect("ext bc");
if !sans.is_empty() {
let mut san = SubjectAlternativeName::new();
for entry in sans {
if entry.parse::<std::net::IpAddr>().is_ok() {
san.ip(entry);
} else {
san.dns(entry);
}
}
let ext = san.build(&builder.x509v3_context(Some(ca), None)).expect("san");
builder.append_extension(ext).expect("ext san");
}
builder.sign(ca_key, MessageDigest::sha256()).expect("sign leaf");
(builder.build(), key)
}
/// A self-signed leaf not chained to the CA, for the untrusted-client test.
fn build_self_signed(cn: &str) -> (X509, PKey<Private>) {
let key = gen_key();
let name = x509_name(cn);
let mut builder = X509::builder().expect("x509 builder");
builder.set_version(2).expect("version");
builder.set_serial_number(&serial()).expect("serial");
builder.set_subject_name(&name).expect("subject");
builder.set_issuer_name(&name).expect("issuer");
builder.set_pubkey(&key).expect("pubkey");
builder
.set_not_before(&Asn1Time::days_from_now(0).expect("nb"))
.expect("set nb");
builder
.set_not_after(&Asn1Time::days_from_now(3650).expect("na"))
.expect("set na");
builder
.append_extension(BasicConstraints::new().build().expect("bc"))
.expect("ext bc");
builder.sign(&key, MessageDigest::sha256()).expect("sign self");
(builder.build(), key)
}
/// A CA-capable intermediate signed by the root, for the full-chain test.
fn build_intermediate(ca: &X509, ca_key: &PKey<Private>) -> (X509, PKey<Private>) {
let key = gen_key();
let mut builder = X509::builder().expect("x509 builder");
builder.set_version(2).expect("version");
builder.set_serial_number(&serial()).expect("serial");
builder
.set_subject_name(&x509_name("vLLM Test Intermediate CA"))
.expect("subject");
builder.set_issuer_name(ca.subject_name()).expect("issuer");
builder.set_pubkey(&key).expect("pubkey");
builder
.set_not_before(&Asn1Time::days_from_now(0).expect("nb"))
.expect("set nb");
builder
.set_not_after(&Asn1Time::days_from_now(3650).expect("na"))
.expect("set na");
builder
.append_extension(BasicConstraints::new().critical().ca().build().expect("bc"))
.expect("ext bc");
builder
.append_extension(
KeyUsage::new().critical().key_cert_sign().crl_sign().build().expect("ku"),
)
.expect("ext ku");
builder.sign(ca_key, MessageDigest::sha256()).expect("sign intermediate");
(builder.build(), key)
}
pub(crate) fn server_tls(certs: &TestCerts, cert_reqs: i32) -> TlsConfig {
TlsConfig {
cert_file: Some(certs.path("server.pem")),
key_file: Some(certs.path("server.key")),
ca_certs: (cert_reqs != 0).then(|| certs.path("ca.pem")),
cert_reqs,
ciphers: None,
}
}
/// A plaintext-listener TLS config for `build_server_config` checks (`cert_reqs`
/// 0, no client auth), with the cert/key files chosen by the caller.
fn build_tls(certs: &TestCerts, cert: &str, key: Option<&str>) -> TlsConfig {
TlsConfig {
cert_file: Some(certs.path(cert)),
key_file: key.map(|k| certs.path(k)),
ca_certs: None,
cert_reqs: 0,
ciphers: None,
}
}
/// Generous per-connection timeouts that never fire during the fast tests.
const TEST_TIMEOUTS: ConnectionTimeouts = ConnectionTimeouts {
handshake: Duration::from_secs(60),
header_read: Duration::from_secs(5),
keep_alive_enabled: true,
};
async fn spawn_server(tls_config: Option<TlsConfig>) -> (String, CancellationToken) {
spawn_server_with_timeouts(tls_config, TEST_TIMEOUTS).await
}
/// Bind an ephemeral listener and serve a trivial router via the production
/// `serve_listener`, optionally with TLS. The listener is bound (and thus
/// accepting into the backlog) before returning, so a client may connect
/// immediately without a sleep.
async fn spawn_server_with_timeouts(
tls_config: Option<TlsConfig>,
timeouts: ConnectionTimeouts,
) -> (String, CancellationToken) {
let listener = Listener::bind(&HttpListenerMode::BindTcp {
host: "127.0.0.1".to_string(),
port: 0,
})
.await
.expect("bind listener");
let addr = listener.local_addr().expect("local addr");
let server_config =
tls_config.map(|cfg| tls::build_server_config(&cfg).expect("build server config"));
let app = Router::new().route("/health", get(|| async { "ok" }));
let shutdown = CancellationToken::new();
let server_shutdown = shutdown.clone();
tokio::spawn(async move {
let _ = serve_listener(
listener,
server_config,
app,
server_shutdown.cancelled_owned(),
timeouts,
)
.await;
});
(addr, shutdown)
}
/// Open a TLS connection trusting the test CA and finish the handshake,
/// optionally presenting a client identity (`<name>.pem` + `<name>.key`) for
/// mTLS. Hostname verification is disabled (the IP-SAN match is not under test);
/// chain verification stays on, so an untrusted server cert is still rejected.
async fn connect_tls(
certs: &TestCerts,
addr: &str,
identity: Option<&str>,
) -> std::io::Result<Pin<Box<SslStream<TcpStream>>>> {
let tcp = TcpStream::connect(addr).await?;
let mut builder = SslConnector::builder(SslMethod::tls_client()).expect("connector builder");
builder.set_ca_file(certs.path("ca.pem")).expect("trust ca");
if let Some(name) = identity {
builder
.set_certificate_chain_file(certs.path(&format!("{name}.pem")))
.expect("client cert");
builder
.set_private_key_file(certs.path(&format!("{name}.key")), SslFiletype::PEM)
.expect("client key");
}
let connector = builder.build();
let mut config = connector.configure().expect("configure");
config.set_verify_hostname(false);
let ssl = config.into_ssl("127.0.0.1").expect("ssl");
let mut stream = Box::pin(SslStream::new(ssl, tcp).expect("client ssl stream"));
stream.as_mut().connect().await.map_err(std::io::Error::other)?;
Ok(stream)
}
/// Issue an HTTPS GET (with `Connection: close`), optionally with an mTLS identity.
async fn https_get(
certs: &TestCerts,
addr: &str,
identity: Option<&str>,
) -> std::io::Result<String> {
let mut stream = connect_tls(certs, addr, identity).await?;
stream
.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
.await?;
let mut response = String::new();
stream.read_to_string(&mut response).await?;
Ok(response)
}
/// Attempt a handshake offering only a legacy CBC+SHA1 suite over TLS 1.2,
/// capping the version so TLS 1.3 cannot rescue the negotiation.
async fn legacy_suite_handshake(certs: &TestCerts, addr: &str) -> std::io::Result<()> {
let tcp = TcpStream::connect(addr).await?;
let mut builder = SslConnector::builder(SslMethod::tls_client()).expect("connector builder");
builder.set_ca_file(certs.path("ca.pem")).expect("trust ca");
builder.set_max_proto_version(Some(SslVersion::TLS1_2)).expect("cap tls1.2");
builder
.set_cipher_list("ECDHE-ECDSA-AES256-SHA:@SECLEVEL=0")
.expect("legacy cipher");
let connector = builder.build();
let mut config = connector.configure().expect("configure");
config.set_verify_hostname(false);
let ssl = config.into_ssl("127.0.0.1").expect("ssl");
let stream = SslStream::new(ssl, tcp).expect("client ssl stream");
tokio::pin!(stream);
stream.as_mut().connect().await.map_err(std::io::Error::other)
}
async fn plain_get(addr: &str) -> std::io::Result<String> {
let mut tcp = TcpStream::connect(addr).await?;
tcp.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
.await?;
let mut response = String::new();
tcp.read_to_string(&mut response).await?;
Ok(response)
}
// ============================================================================
// Tests
// ============================================================================
#[test]
fn builds_from_combined_pem() {
// Key omitted: it is read from the combined cert+key file.
let certs = TestCerts::generate();
assert!(tls::build_server_config(&build_tls(&certs, "server_combined.pem", None)).is_ok());
}
#[test]
fn rejects_missing_cert_file() {
let certs = TestCerts::generate();
assert!(tls::build_server_config(&build_tls(&certs, "does_not_exist.pem", None)).is_err());
}
#[test]
fn accepts_valid_cipher_list() {
let certs = TestCerts::generate();
let mut cfg = build_tls(&certs, "server.pem", Some("server.key"));
cfg.ciphers = Some("ECDHE-ECDSA-AES256-GCM-SHA384".to_string());
assert!(tls::build_server_config(&cfg).is_ok());
}
#[test]
fn rejects_invalid_cipher_list() {
let certs = TestCerts::generate();
let mut cfg = build_tls(&certs, "server.pem", Some("server.key"));
cfg.ciphers = Some("THIS-IS-NOT-A-CIPHER".to_string());
assert!(tls::build_server_config(&cfg).is_err());
}
#[test]
fn rejects_mismatched_cert_and_key() {
// check_private_key must reject a key that does not match the certificate.
let certs = TestCerts::generate();
let tls = build_tls(&certs, "client.pem", Some("server.key"));
assert!(tls::build_server_config(&tls).is_err());
}
#[tokio::test]
async fn https_request_succeeds_over_tls() {
let certs = TestCerts::generate();
let (addr, shutdown) = spawn_server(Some(server_tls(&certs, 0))).await;
let response = https_get(&certs, &addr, None).await.expect("https request");
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
shutdown.cancel();
}
#[tokio::test]
async fn serves_full_certificate_chain() {
// Cert file holds leaf + intermediate; a client trusting only the root can
// verify only if the server sends the intermediate, guarding against a
// leaf-only load.
let certs = TestCerts::generate();
let tls = TlsConfig {
cert_file: Some(certs.path("server_chain.pem")),
key_file: Some(certs.path("server_chain.key")),
ca_certs: None,
cert_reqs: 0,
ciphers: None,
};
let (addr, shutdown) = spawn_server(Some(tls)).await;
let response = https_get(&certs, &addr, None).await.expect("chained https request");
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
shutdown.cancel();
}
#[tokio::test]
async fn rejects_legacy_cipher_only_client() {
let certs = TestCerts::generate();
let (addr, shutdown) = spawn_server(Some(server_tls(&certs, 0))).await;
let result = legacy_suite_handshake(&certs, &addr).await;
assert!(result.is_err(), "legacy-only client must be rejected");
shutdown.cancel();
}
#[tokio::test]
async fn ssl_ciphers_override_widens_past_preset() {
// Counterpart to rejects_legacy_cipher_only_client: --ssl-ciphers set to that
// same legacy suite lets the client through, proving the override beats the preset.
let certs = TestCerts::generate();
let mut tls = server_tls(&certs, 0);
tls.ciphers = Some("ECDHE-ECDSA-AES256-SHA:@SECLEVEL=0".to_string());
let (addr, shutdown) = spawn_server(Some(tls)).await;
let result = legacy_suite_handshake(&certs, &addr).await;
assert!(
result.is_ok(),
"override must allow the legacy suite: {result:?}"
);
shutdown.cancel();
}
#[tokio::test]
async fn mtls_required_rejects_client_without_certificate() {
let certs = TestCerts::generate();
let (addr, shutdown) = spawn_server(Some(server_tls(&certs, 2))).await;
let result = https_get(&certs, &addr, None).await;
assert!(
result.is_err(),
"handshake must fail without a client certificate"
);
shutdown.cancel();
}
#[tokio::test]
async fn mtls_required_accepts_valid_client_certificate() {
let certs = TestCerts::generate();
let (addr, shutdown) = spawn_server(Some(server_tls(&certs, 2))).await;
let response = https_get(&certs, &addr, Some("client")).await.expect("mtls request");
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
shutdown.cancel();
}
#[tokio::test]
async fn mtls_optional_allows_anonymous_and_authenticated() {
let certs = TestCerts::generate();
let (addr, shutdown) = spawn_server(Some(server_tls(&certs, 1))).await;
let anonymous = https_get(&certs, &addr, None).await.expect("anonymous request");
assert!(anonymous.starts_with("HTTP/1.1 200"), "{anonymous}");
let authenticated =
https_get(&certs, &addr, Some("client")).await.expect("authenticated request");
assert!(authenticated.starts_with("HTTP/1.1 200"), "{authenticated}");
shutdown.cancel();
}
#[tokio::test]
async fn mtls_rejects_untrusted_client_certificate() {
// Optional (1) still verifies a presented cert, so a self-signed cert not
// chained to the CA is rejected in both modes, not just required (2).
let certs = TestCerts::generate();
for cert_reqs in [1, 2] {
let (addr, shutdown) = spawn_server(Some(server_tls(&certs, cert_reqs))).await;
let result = https_get(&certs, &addr, Some("untrusted_client")).await;
assert!(
result.is_err(),
"cert_reqs={cert_reqs}: untrusted client cert must be rejected"
);
shutdown.cancel();
}
}
#[tokio::test]
async fn plain_http_serves_when_tls_is_disabled() {
let (addr, shutdown) = spawn_server(None).await;
let response = plain_get(&addr).await.expect("http request");
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
shutdown.cancel();
}
#[tokio::test]
async fn tls_handshake_timeout_drops_silent_client() {
// Silent client (no ClientHello) must be dropped at the handshake deadline.
let certs = TestCerts::generate();
let timeouts = ConnectionTimeouts {
handshake: Duration::from_millis(150),
header_read: Duration::from_secs(5),
keep_alive_enabled: true,
};
let (addr, shutdown) = spawn_server_with_timeouts(Some(server_tls(&certs, 0)), timeouts).await;
let mut tcp = TcpStream::connect(&addr).await.expect("connect");
let mut buf = [0u8; 1];
let read = tokio::time::timeout(Duration::from_secs(5), tcp.read(&mut buf)).await;
assert!(
matches!(read, Ok(Ok(0)) | Ok(Err(_))),
"server must drop a stalled TLS handshake (expected close, got {read:?})"
);
shutdown.cancel();
}
#[tokio::test]
async fn keep_alive_timeout_closes_idle_connection() {
// Idle keep-alive connection must be closed at the deadline.
let timeouts = ConnectionTimeouts {
handshake: Duration::from_secs(60),
header_read: Duration::from_millis(150),
keep_alive_enabled: true,
};
let (addr, shutdown) = spawn_server_with_timeouts(None, timeouts).await;
let mut tcp = TcpStream::connect(&addr).await.expect("connect");
// No `Connection: close`, so it stays alive until the idle deadline.
tcp.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
.await
.expect("write request");
let drained = tokio::time::timeout(Duration::from_secs(5), async {
let mut buf = [0u8; 1024];
loop {
match tcp.read(&mut buf).await {
Ok(0) => return Ok(()),
Ok(_) => continue,
Err(err) => return Err(err),
}
}
})
.await;
assert!(
matches!(drained, Ok(Ok(()))),
"server must close an idle keep-alive connection (got {drained:?})"
);
shutdown.cancel();
}
#[tokio::test]
async fn keep_alive_timeout_closes_idle_tls_connection() {
// The keep-alive idle bound lives in serve_connections, below TLS; assert it
// still fires through tls-listener's post-handshake SslStream, not just plaintext.
let certs = TestCerts::generate();
let timeouts = ConnectionTimeouts {
handshake: Duration::from_secs(60),
header_read: Duration::from_millis(150),
keep_alive_enabled: true,
};
let (addr, shutdown) = spawn_server_with_timeouts(Some(server_tls(&certs, 0)), timeouts).await;
let mut stream = connect_tls(&certs, &addr, None).await.expect("handshake");
// No `Connection: close`, so the connection stays alive until the idle deadline.
stream
.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
.await
.expect("write request");
let closed = tokio::time::timeout(Duration::from_secs(5), async {
let mut buf = [0u8; 1024];
loop {
// A clean close_notify (Ok(0)) or an abrupt TLS EOF both mean the
// server closed; only the outer timeout (still open) is a failure.
match stream.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(_) => continue,
}
}
})
.await;
assert!(
closed.is_ok(),
"server must close an idle keep-alive TLS connection at the deadline"
);
shutdown.cancel();
}
#[tokio::test]
async fn idle_timeout_closes_silent_client() {
// Silent client closed by the header-read timeout (http1-only arms it from byte 0).
let timeouts = ConnectionTimeouts {
handshake: Duration::from_secs(60),
header_read: Duration::from_millis(150),
keep_alive_enabled: true,
};
let (addr, shutdown) = spawn_server_with_timeouts(None, timeouts).await;
let mut tcp = TcpStream::connect(&addr).await.expect("connect");
let mut buf = [0u8; 1];
let read = tokio::time::timeout(Duration::from_secs(5), tcp.read(&mut buf)).await;
assert!(
matches!(read, Ok(Ok(0)) | Ok(Err(_))),
"server must close a silent client (expected close, got {read:?})"
);
shutdown.cancel();
}
#[tokio::test]
async fn keep_alive_zero_disables_keep_alive() {
// 0 disables keep-alive (serve, then close), like uvicorn's timeout_keep_alive=0.
let timeouts = ConnectionTimeouts {
handshake: Duration::from_secs(60),
header_read: Duration::from_secs(5),
keep_alive_enabled: false,
};
let (addr, shutdown) = spawn_server_with_timeouts(None, timeouts).await;
let mut tcp = TcpStream::connect(&addr).await.expect("connect");
tcp.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
.await
.expect("write request");
let mut response = String::new();
let read =
tokio::time::timeout(Duration::from_secs(5), tcp.read_to_string(&mut response)).await;
assert!(
read.is_ok(),
"server must close after one response, not hang"
);
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
// Assert `Connection: close`, not just 200: a 0 header-read timeout would also
// serve an immediate request, so 200 alone wouldn't prove keep-alive is off.
assert!(
response.to_ascii_lowercase().contains("connection: close"),
"keep-alive must be disabled (expected Connection: close): {response}"
);
shutdown.cancel();
}
#[tokio::test]
async fn disabled_keep_alive_still_closes_silent_client() {
// Even with keep-alive off, the head read stays bounded, so a silent client
// is dropped rather than held open.
let timeouts = ConnectionTimeouts {
handshake: Duration::from_secs(60),
header_read: Duration::from_millis(150),
keep_alive_enabled: false,
};
let (addr, shutdown) = spawn_server_with_timeouts(None, timeouts).await;
let mut tcp = TcpStream::connect(&addr).await.expect("connect");
let mut buf = [0u8; 1];
let read = tokio::time::timeout(Duration::from_secs(5), tcp.read(&mut buf)).await;
assert!(
matches!(read, Ok(Ok(0)) | Ok(Err(_))),
"disabled keep-alive must still close a silent client (got {read:?})"
);
shutdown.cancel();
}
-2
View File
@@ -777,7 +777,6 @@ class precompiled_wheel_utils:
"vllm/vllm_flash_attn/_vllm_fa3_C.abi3.so",
"vllm/cumem_allocator.abi3.so",
"vllm/spinloop.abi3.so",
"vllm/fs_io_C.abi3.so",
# ROCm-specific libraries
"vllm/_rocm_C.abi3.so",
}
@@ -1105,7 +1104,6 @@ if _is_cuda() or _is_hip():
if sys.version_info >= (3, 11):
ext_modules.append(CMakeExtension(name="vllm.spinloop"))
ext_modules.append(CMakeExtension(name="vllm.fs_io_C"))
if _is_hip():
ext_modules.append(CMakeExtension(name="vllm._rocm_C"))
+13 -13
View File
@@ -23,7 +23,7 @@ def test_python_error():
error happening from the C++ side.
"""
allocator = get_mem_allocator_instance()
total_bytes = torch.accelerator.get_memory_info()[1]
total_bytes = current_platform.mem_get_info()[1]
alloc_bytes = int(total_bytes * 0.7)
tensors = []
with allocator.use_memory_pool():
@@ -64,9 +64,9 @@ def test_basic_cumem():
output = x + y + z
assert torch.allclose(output, torch.ones_like(output) * 3)
free_bytes = torch.accelerator.get_memory_info()[0]
free_bytes = current_platform.mem_get_info()[0]
allocator.sleep()
free_bytes_after_sleep = torch.accelerator.get_memory_info()[0]
free_bytes_after_sleep = current_platform.mem_get_info()[0]
assert free_bytes_after_sleep > free_bytes
allocator.wake_up()
@@ -99,9 +99,9 @@ def test_cumem_with_cudagraph():
with torch.cuda.graph(model_graph):
y = model(x)
free_bytes = torch.accelerator.get_memory_info()[0]
free_bytes = current_platform.mem_get_info()[0]
allocator.sleep()
free_bytes_after_sleep = torch.accelerator.get_memory_info()[0]
free_bytes_after_sleep = current_platform.mem_get_info()[0]
assert free_bytes_after_sleep > free_bytes
allocator.wake_up()
@@ -132,7 +132,7 @@ def test_cumem_with_cudagraph():
],
)
def test_end_to_end(model: str):
free, total = torch.accelerator.get_memory_info()
free, total = current_platform.mem_get_info()
used_bytes_baseline = total - free # in case other process is running
llm = LLM(model, enable_sleep_mode=True)
prompt = "How are you?"
@@ -144,7 +144,7 @@ def test_end_to_end(model: str):
# test sleep level 1 here.
llm.sleep(level=1)
free_gpu_bytes_after_sleep, total = torch.accelerator.get_memory_info()
free_gpu_bytes_after_sleep, total = current_platform.mem_get_info()
used_bytes = total - free_gpu_bytes_after_sleep - used_bytes_baseline
# now the memory usage is mostly cudagraph memory pool,
# and it should be less than the model weights (1B model, 2GiB weights)
@@ -164,7 +164,7 @@ def test_end_to_end(model: str):
llm.sleep(level=1)
llm.wake_up(tags=["weights"])
free_gpu_bytes_wake_up_w, total = torch.accelerator.get_memory_info()
free_gpu_bytes_wake_up_w, total = current_platform.mem_get_info()
used_bytes = total - free_gpu_bytes_wake_up_w - used_bytes_baseline
# should just reallocate memory for weights (1B model, ~2GiB weights)
@@ -181,7 +181,7 @@ def test_end_to_end(model: str):
@create_new_process_for_each_test()
def test_deep_sleep():
model = "hmellor/tiny-random-LlamaForCausalLM"
free, total = torch.accelerator.get_memory_info()
free, total = current_platform.mem_get_info()
used_bytes_baseline = total - free # in case other process is running
llm = LLM(model, enable_sleep_mode=True)
prompt = "How are you?"
@@ -191,13 +191,13 @@ def test_deep_sleep():
# Put the engine to deep sleep
llm.sleep(level=2)
free_gpu_bytes_after_sleep, total = torch.accelerator.get_memory_info()
free_gpu_bytes_after_sleep, total = current_platform.mem_get_info()
used_bytes = total - free_gpu_bytes_after_sleep - used_bytes_baseline
assert used_bytes < 3 * GiB_bytes
llm.wake_up(tags=["weights"])
llm.collective_rpc("reload_weights")
free_gpu_bytes_wake_up_w, total = torch.accelerator.get_memory_info()
free_gpu_bytes_wake_up_w, total = current_platform.mem_get_info()
used_bytes = total - free_gpu_bytes_wake_up_w - used_bytes_baseline
assert used_bytes < 4 * GiB_bytes
@@ -213,7 +213,7 @@ def test_deep_sleep():
def test_deep_sleep_async():
async def test():
model = "hmellor/tiny-random-LlamaForCausalLM"
free, total = torch.accelerator.get_memory_info()
free, total = current_platform.mem_get_info()
used_bytes_baseline = total - free # in case other process is running
engine_args = AsyncEngineArgs(
model=model,
@@ -232,7 +232,7 @@ def test_deep_sleep_async():
await llm.wake_up(tags=["weights"])
await llm.collective_rpc("reload_weights")
free_gpu_bytes_wake_up_w, total = torch.accelerator.get_memory_info()
free_gpu_bytes_wake_up_w, total = current_platform.mem_get_info()
used_bytes = total - free_gpu_bytes_wake_up_w - used_bytes_baseline
assert used_bytes < 4 * GiB_bytes

Some files were not shown because too many files have changed in this diff Show More