forked from Karylab-cklius/vllm
Merge branch 'main' into wentao-fix-dcp-IMA-for-v2
This commit is contained in:
@@ -1,25 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script runs test inside the corresponding ROCm docker container.
|
||||
# This script runs tests inside the corresponding ROCm docker container.
|
||||
# It handles both single-node and multi-node test configurations.
|
||||
#
|
||||
# Multi-node detection: Instead of matching on fragile group names, we detect
|
||||
# multi-node jobs structurally by looking for the bracket command syntax
|
||||
# "[node0_cmds] && [node1_cmds]" or via the NUM_NODES environment variable.
|
||||
set -o pipefail
|
||||
|
||||
# Export Python path
|
||||
export PYTHONPATH=".."
|
||||
|
||||
# Print ROCm version
|
||||
echo "--- Confirming Clean Initial State"
|
||||
while true; do
|
||||
sleep 3
|
||||
if grep -q clean /opt/amdgpu/etc/gpu_state; then
|
||||
echo "GPUs state is \"clean\""
|
||||
break
|
||||
fi
|
||||
done
|
||||
###############################################################################
|
||||
# Helper Functions
|
||||
###############################################################################
|
||||
|
||||
echo "--- ROCm info"
|
||||
rocminfo
|
||||
wait_for_clean_gpus() {
|
||||
local timeout=${1:-300}
|
||||
local start=$SECONDS
|
||||
echo "--- Waiting for clean GPU state (timeout: ${timeout}s)"
|
||||
while true; do
|
||||
if grep -q clean /opt/amdgpu/etc/gpu_state; then
|
||||
echo "GPUs state is \"clean\""
|
||||
return
|
||||
fi
|
||||
if (( SECONDS - start >= timeout )); then
|
||||
echo "Error: GPUs did not reach clean state within ${timeout}s" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
# cleanup older docker images
|
||||
cleanup_docker() {
|
||||
# Get Docker's root directory
|
||||
docker_root=$(docker info -f '{{.DockerRootDir}}')
|
||||
@@ -28,15 +40,12 @@ cleanup_docker() {
|
||||
exit 1
|
||||
fi
|
||||
echo "Docker root directory: $docker_root"
|
||||
# Check disk usage of the filesystem where Docker's root directory is located
|
||||
|
||||
disk_usage=$(df "$docker_root" | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||
# Define the threshold
|
||||
threshold=70
|
||||
if [ "$disk_usage" -gt "$threshold" ]; then
|
||||
echo "Disk usage is above $threshold%. Cleaning up Docker images and volumes..."
|
||||
# Remove dangling images (those that are not tagged and not used by any container)
|
||||
docker image prune -f
|
||||
# Remove unused volumes / force the system prune for old images as well.
|
||||
docker volume prune -f && docker system prune --force --filter "until=72h" --all
|
||||
echo "Docker images and volumes cleanup completed."
|
||||
else
|
||||
@@ -45,193 +54,258 @@ cleanup_docker() {
|
||||
}
|
||||
|
||||
cleanup_network() {
|
||||
for node in $(seq 0 $((NUM_NODES-1))); do
|
||||
if docker pr -a -q -f name="node${node}" | grep -q .; then
|
||||
docker stop "node${node}"
|
||||
local max_nodes=${NUM_NODES:-2}
|
||||
for node in $(seq 0 $((max_nodes - 1))); do
|
||||
if docker ps -a -q -f name="node${node}" | grep -q .; then
|
||||
docker stop "node${node}" || true
|
||||
fi
|
||||
done
|
||||
if docker network ls | grep docker-net; then
|
||||
docker network rm docker-net
|
||||
if docker network ls | grep -q docker-net; then
|
||||
docker network rm docker-net || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Call the cleanup docker function
|
||||
is_multi_node() {
|
||||
local cmds="$1"
|
||||
# Primary signal: NUM_NODES environment variable set by the pipeline
|
||||
if [[ "${NUM_NODES:-1}" -gt 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
# Fallback: detect the bracket syntax structurally
|
||||
# Pattern: [...] && [...] (per-node command arrays)
|
||||
if [[ "$cmds" =~ \[.*\].*\&\&.*\[.*\] ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Pytest marker re-quoting
|
||||
#
|
||||
# When commands are passed through Buildkite -> shell -> $* -> bash -c,
|
||||
# quotes around pytest -m marker expressions get stripped:
|
||||
# pytest -v -s -m 'not cpu_test' v1/core
|
||||
# becomes:
|
||||
# pytest -v -s -m not cpu_test v1/core
|
||||
#
|
||||
# pytest then interprets "cpu_test" as a file path, not part of the marker.
|
||||
# This function detects unquoted multi-word marker expressions and re-quotes
|
||||
# them so they survive the final bash -c expansion.
|
||||
###############################################################################
|
||||
|
||||
re_quote_pytest_markers() {
|
||||
local cmds="$1"
|
||||
# Pattern: -m not <identifier> -> -m 'not <identifier>'
|
||||
# Handles the common cases: 'not cpu_test', 'not slow_test', etc.
|
||||
cmds=$(echo "$cmds" | sed -E "s/-m not ([a-zA-Z_][a-zA-Z0-9_]*)/-m 'not \1'/g")
|
||||
echo "$cmds"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# ROCm-specific pytest command rewrites
|
||||
#
|
||||
# These apply ignore flags and environment overrides for tests that are not
|
||||
# yet supported or behave differently on ROCm hardware. Kept as a single
|
||||
# function so new exclusions are easy to add in one place.
|
||||
###############################################################################
|
||||
|
||||
apply_rocm_test_overrides() {
|
||||
local cmds="$1"
|
||||
|
||||
# --- Model registry filter ---
|
||||
if [[ $cmds == *"pytest -v -s models/test_registry.py"* ]]; then
|
||||
cmds=${cmds//"pytest -v -s models/test_registry.py"/"pytest -v -s models/test_registry.py -k 'not BambaForCausalLM and not GritLM and not Mamba2ForCausalLM and not Zamba2ForCausalLM'"}
|
||||
fi
|
||||
|
||||
# --- LoRA: disable custom paged attention ---
|
||||
if [[ $cmds == *"pytest -v -s lora"* ]]; then
|
||||
cmds=${cmds//"pytest -v -s lora"/"VLLM_ROCM_CUSTOM_PAGED_ATTN=0 pytest -v -s lora"}
|
||||
fi
|
||||
|
||||
# --- Kernel ignores ---
|
||||
if [[ $cmds == *" kernels/core"* ]]; then
|
||||
cmds="${cmds} \
|
||||
--ignore=kernels/core/test_fused_quant_layernorm.py \
|
||||
--ignore=kernels/core/test_permute_cols.py"
|
||||
fi
|
||||
|
||||
if [[ $cmds == *" kernels/attention"* ]]; then
|
||||
cmds="${cmds} \
|
||||
--ignore=kernels/attention/test_attention_selector.py \
|
||||
--ignore=kernels/attention/test_encoder_decoder_attn.py \
|
||||
--ignore=kernels/attention/test_flash_attn.py \
|
||||
--ignore=kernels/attention/test_flashinfer.py \
|
||||
--ignore=kernels/attention/test_prefix_prefill.py \
|
||||
--ignore=kernels/attention/test_cascade_flash_attn.py \
|
||||
--ignore=kernels/attention/test_mha_attn.py \
|
||||
--ignore=kernels/attention/test_lightning_attn.py \
|
||||
--ignore=kernels/attention/test_attention.py"
|
||||
fi
|
||||
|
||||
if [[ $cmds == *" kernels/quantization"* ]]; then
|
||||
cmds="${cmds} \
|
||||
--ignore=kernels/quantization/test_int8_quant.py \
|
||||
--ignore=kernels/quantization/test_machete_mm.py \
|
||||
--ignore=kernels/quantization/test_block_fp8.py \
|
||||
--ignore=kernels/quantization/test_block_int8.py \
|
||||
--ignore=kernels/quantization/test_marlin_gemm.py \
|
||||
--ignore=kernels/quantization/test_cutlass_scaled_mm.py \
|
||||
--ignore=kernels/quantization/test_int8_kernel.py"
|
||||
fi
|
||||
|
||||
if [[ $cmds == *" kernels/mamba"* ]]; then
|
||||
cmds="${cmds} \
|
||||
--ignore=kernels/mamba/test_mamba_mixer2.py \
|
||||
--ignore=kernels/mamba/test_causal_conv1d.py \
|
||||
--ignore=kernels/mamba/test_mamba_ssm_ssd.py"
|
||||
fi
|
||||
|
||||
if [[ $cmds == *" kernels/moe"* ]]; then
|
||||
cmds="${cmds} \
|
||||
--ignore=kernels/moe/test_moe.py \
|
||||
--ignore=kernels/moe/test_cutlass_moe.py \
|
||||
--ignore=kernels/moe/test_triton_moe_ptpc_fp8.py"
|
||||
fi
|
||||
|
||||
# --- Entrypoint ignores ---
|
||||
if [[ $cmds == *" entrypoints/openai "* ]]; then
|
||||
cmds=${cmds//" entrypoints/openai "/" entrypoints/openai \
|
||||
--ignore=entrypoints/openai/test_audio.py \
|
||||
--ignore=entrypoints/openai/test_shutdown.py \
|
||||
--ignore=entrypoints/openai/test_completion.py \
|
||||
--ignore=entrypoints/openai/test_models.py \
|
||||
--ignore=entrypoints/openai/test_lora_adapters.py \
|
||||
--ignore=entrypoints/openai/test_return_tokens_as_ids.py \
|
||||
--ignore=entrypoints/openai/test_root_path.py \
|
||||
--ignore=entrypoints/openai/test_tokenization.py \
|
||||
--ignore=entrypoints/openai/test_prompt_validation.py "}
|
||||
fi
|
||||
|
||||
if [[ $cmds == *" entrypoints/llm "* ]]; then
|
||||
cmds=${cmds//" entrypoints/llm "/" entrypoints/llm \
|
||||
--ignore=entrypoints/llm/test_chat.py \
|
||||
--ignore=entrypoints/llm/test_accuracy.py \
|
||||
--ignore=entrypoints/llm/test_init.py \
|
||||
--ignore=entrypoints/llm/test_prompt_validation.py "}
|
||||
fi
|
||||
|
||||
# Clean up escaped newlines from --ignore appends
|
||||
cmds=$(echo "$cmds" | sed 's/ \\ / /g')
|
||||
|
||||
echo "$cmds"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main
|
||||
###############################################################################
|
||||
|
||||
# --- GPU initialization ---
|
||||
echo "--- Confirming Clean Initial State"
|
||||
wait_for_clean_gpus
|
||||
|
||||
echo "--- ROCm info"
|
||||
rocminfo
|
||||
|
||||
# --- Docker housekeeping ---
|
||||
cleanup_docker
|
||||
|
||||
echo "--- Resetting GPUs"
|
||||
|
||||
echo "reset" > /opt/amdgpu/etc/gpu_state
|
||||
wait_for_clean_gpus
|
||||
|
||||
while true; do
|
||||
sleep 3
|
||||
if grep -q clean /opt/amdgpu/etc/gpu_state; then
|
||||
echo "GPUs state is \"clean\""
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Pull test image ---
|
||||
echo "--- Pulling container"
|
||||
image_name="rocm/vllm-ci:${BUILDKITE_COMMIT}"
|
||||
container_name="rocm_${BUILDKITE_COMMIT}_$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 10; echo)"
|
||||
docker pull "${image_name}"
|
||||
|
||||
remove_docker_container() {
|
||||
docker rm -f "${container_name}" || docker image rm -f "${image_name}" || true
|
||||
docker rm -f "${container_name}" || docker image rm -f "${image_name}" || true
|
||||
}
|
||||
trap remove_docker_container EXIT
|
||||
|
||||
# --- Prepare commands ---
|
||||
echo "--- Running container"
|
||||
|
||||
HF_CACHE="$(realpath ~)/huggingface"
|
||||
mkdir -p "${HF_CACHE}"
|
||||
HF_MOUNT="/root/.cache/huggingface"
|
||||
|
||||
commands=$@
|
||||
commands="$*"
|
||||
echo "Raw commands: $commands"
|
||||
|
||||
commands=${commands//"pytest -v -s basic_correctness/test_basic_correctness.py"/"pytest -v -s basic_correctness/test_basic_correctness.py"}
|
||||
|
||||
if [[ $commands == *"pytest -v -s models/test_registry.py"* ]]; then
|
||||
commands=${commands//"pytest -v -s models/test_registry.py"/"pytest -v -s models/test_registry.py -k 'not BambaForCausalLM and not GritLM and not Mamba2ForCausalLM and not Zamba2ForCausalLM'"}
|
||||
fi
|
||||
|
||||
commands=${commands//"pytest -v -s compile/test_basic_correctness.py"/"pytest -v -s compile/test_basic_correctness.py"}
|
||||
|
||||
if [[ $commands == *"pytest -v -s lora"* ]]; then
|
||||
commands=${commands//"pytest -v -s lora"/"VLLM_ROCM_CUSTOM_PAGED_ATTN=0 pytest -v -s lora"}
|
||||
fi
|
||||
|
||||
#ignore certain kernels tests
|
||||
if [[ $commands == *" kernels/core"* ]]; then
|
||||
commands="${commands} \
|
||||
--ignore=kernels/core/test_fused_quant_layernorm.py \
|
||||
--ignore=kernels/core/test_permute_cols.py"
|
||||
fi
|
||||
|
||||
if [[ $commands == *" kernels/attention"* ]]; then
|
||||
commands="${commands} \
|
||||
--ignore=kernels/attention/test_attention_selector.py \
|
||||
--ignore=kernels/attention/test_encoder_decoder_attn.py \
|
||||
--ignore=kernels/attention/test_flash_attn.py \
|
||||
--ignore=kernels/attention/test_flashinfer.py \
|
||||
--ignore=kernels/attention/test_prefix_prefill.py \
|
||||
--ignore=kernels/attention/test_cascade_flash_attn.py \
|
||||
--ignore=kernels/attention/test_mha_attn.py \
|
||||
--ignore=kernels/attention/test_lightning_attn.py \
|
||||
--ignore=kernels/attention/test_attention.py"
|
||||
fi
|
||||
|
||||
if [[ $commands == *" kernels/quantization"* ]]; then
|
||||
commands="${commands} \
|
||||
--ignore=kernels/quantization/test_int8_quant.py \
|
||||
--ignore=kernels/quantization/test_machete_mm.py \
|
||||
--ignore=kernels/quantization/test_block_fp8.py \
|
||||
--ignore=kernels/quantization/test_block_int8.py \
|
||||
--ignore=kernels/quantization/test_marlin_gemm.py \
|
||||
--ignore=kernels/quantization/test_cutlass_scaled_mm.py \
|
||||
--ignore=kernels/quantization/test_int8_kernel.py"
|
||||
fi
|
||||
|
||||
if [[ $commands == *" kernels/mamba"* ]]; then
|
||||
commands="${commands} \
|
||||
--ignore=kernels/mamba/test_mamba_mixer2.py \
|
||||
--ignore=kernels/mamba/test_causal_conv1d.py \
|
||||
--ignore=kernels/mamba/test_mamba_ssm_ssd.py"
|
||||
fi
|
||||
|
||||
if [[ $commands == *" kernels/moe"* ]]; then
|
||||
commands="${commands} \
|
||||
--ignore=kernels/moe/test_moe.py \
|
||||
--ignore=kernels/moe/test_cutlass_moe.py \
|
||||
--ignore=kernels/moe/test_triton_moe_ptpc_fp8.py"
|
||||
fi
|
||||
|
||||
#ignore certain Entrypoints/openai tests
|
||||
if [[ $commands == *" entrypoints/openai "* ]]; then
|
||||
commands=${commands//" entrypoints/openai "/" entrypoints/openai \
|
||||
--ignore=entrypoints/openai/test_audio.py \
|
||||
--ignore=entrypoints/openai/test_shutdown.py \
|
||||
--ignore=entrypoints/openai/test_completion.py \
|
||||
--ignore=entrypoints/openai/test_models.py \
|
||||
--ignore=entrypoints/openai/test_lora_adapters.py \
|
||||
--ignore=entrypoints/openai/test_return_tokens_as_ids.py \
|
||||
--ignore=entrypoints/openai/test_root_path.py \
|
||||
--ignore=entrypoints/openai/test_tokenization.py \
|
||||
--ignore=entrypoints/openai/test_prompt_validation.py "}
|
||||
fi
|
||||
|
||||
#ignore certain Entrypoints/llm tests
|
||||
if [[ $commands == *" entrypoints/llm "* ]]; then
|
||||
commands=${commands//" entrypoints/llm "/" entrypoints/llm \
|
||||
--ignore=entrypoints/llm/test_chat.py \
|
||||
--ignore=entrypoints/llm/test_accuracy.py \
|
||||
--ignore=entrypoints/llm/test_init.py \
|
||||
--ignore=entrypoints/llm/test_prompt_validation.py "}
|
||||
fi
|
||||
|
||||
commands=$(echo "$commands" | sed 's/ \\ / /g')
|
||||
# Fix quoting before ROCm overrides (so overrides see correct structure)
|
||||
commands=$(re_quote_pytest_markers "$commands")
|
||||
commands=$(apply_rocm_test_overrides "$commands")
|
||||
echo "Final commands: $commands"
|
||||
|
||||
# --ignore=entrypoints/openai/test_encoder_decoder.py \
|
||||
# --ignore=entrypoints/openai/test_embedding.py \
|
||||
# --ignore=entrypoints/openai/test_oot_registration.py
|
||||
# --ignore=entrypoints/openai/test_accuracy.py \
|
||||
# --ignore=entrypoints/openai/test_models.py <= Fails on MI250 but passes on MI300 as of 2025-03-13
|
||||
|
||||
|
||||
MYPYTHONPATH=".."
|
||||
|
||||
# Test that we're launching on the machine that has
|
||||
# proper access to GPUs
|
||||
# Verify GPU access
|
||||
render_gid=$(getent group render | cut -d: -f3)
|
||||
if [[ -z "$render_gid" ]]; then
|
||||
echo "Error: 'render' group not found. This is required for GPU access." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $commands == *"VLLM_TEST_GROUP_NAME=mi325_4-2-node-tests-4-gpus-in-total"* ]]; then
|
||||
|
||||
# --- Route: multi-node vs single-node ---
|
||||
if is_multi_node "$commands"; then
|
||||
echo "--- Multi-node job detected"
|
||||
export DCKR_VER=$(docker --version | sed 's/Docker version \(.*\), build .*/\1/')
|
||||
|
||||
if [[ "$commands" =~ ^(.*)"["(.*)"] && ["(.*)"]"$ ]]; then
|
||||
prefix=$( echo "${BASH_REMATCH[1]}" | sed 's/;//g')
|
||||
echo "PREFIX: ${prefix}"
|
||||
export composite_command="(command rocm-smi || true)"
|
||||
myIFS=$IFS
|
||||
IFS=','
|
||||
read -ra node0 <<< ${BASH_REMATCH[2]}
|
||||
read -ra node1 <<< ${BASH_REMATCH[3]}
|
||||
IFS=$myIFS
|
||||
for i in "${!node0[@]}";do
|
||||
command_node_0=$(echo ${node0[i]} | sed 's/\"//g')
|
||||
command_node_1=$(echo ${node1[i]} | sed 's/\"//g')
|
||||
|
||||
export commands="./.buildkite/scripts/run-multi-node-test.sh /vllm-workspace/tests 2 2 ${image_name} '${command_node_0}' '${command_node_1}'"
|
||||
echo "COMMANDS: ${commands}"
|
||||
composite_command=$(echo "${composite_command} && ${commands}")
|
||||
done
|
||||
/bin/bash -c "${composite_command}"
|
||||
cleanup_network
|
||||
# Parse the bracket syntax: prefix ; [node0_cmds] && [node1_cmds]
|
||||
# BASH_REMATCH[1] = prefix (everything before first bracket)
|
||||
# BASH_REMATCH[2] = comma-separated node0 commands
|
||||
# BASH_REMATCH[3] = comma-separated node1 commands
|
||||
if [[ "$commands" =~ ^(.*)\[(.*)"] && ["(.*)\]$ ]]; then
|
||||
prefix=$(echo "${BASH_REMATCH[1]}" | sed 's/;//g')
|
||||
echo "PREFIX: ${prefix}"
|
||||
|
||||
export composite_command="(command rocm-smi || true)"
|
||||
saved_IFS=$IFS
|
||||
IFS=','
|
||||
read -ra node0 <<< "${BASH_REMATCH[2]}"
|
||||
read -ra node1 <<< "${BASH_REMATCH[3]}"
|
||||
IFS=$saved_IFS
|
||||
|
||||
if [[ ${#node0[@]} -ne ${#node1[@]} ]]; then
|
||||
echo "Warning: node0 has ${#node0[@]} commands, node1 has ${#node1[@]}. They will be paired by index."
|
||||
fi
|
||||
|
||||
for i in "${!node0[@]}"; do
|
||||
command_node_0=$(echo "${node0[i]}" | sed 's/\"//g')
|
||||
command_node_1=$(echo "${node1[i]}" | sed 's/\"//g')
|
||||
|
||||
step_cmd="./.buildkite/scripts/run-multi-node-test.sh /vllm-workspace/tests 2 2 ${image_name} '${command_node_0}' '${command_node_1}'"
|
||||
echo "COMMANDS: ${step_cmd}"
|
||||
composite_command="${composite_command} && ${step_cmd}"
|
||||
done
|
||||
|
||||
/bin/bash -c "${composite_command}"
|
||||
cleanup_network
|
||||
else
|
||||
echo "Failed to parse node commands! Exiting."
|
||||
cleanup_network
|
||||
exit 111
|
||||
echo "Multi-node job detected but failed to parse bracket command syntax."
|
||||
echo "Expected format: prefix ; [node0_cmd1, node0_cmd2] && [node1_cmd1, node1_cmd2]"
|
||||
echo "Got: $commands"
|
||||
cleanup_network
|
||||
exit 111
|
||||
fi
|
||||
else
|
||||
echo "--- Single-node job"
|
||||
echo "Render devices: $BUILDKITE_AGENT_META_DATA_RENDER_DEVICES"
|
||||
docker run \
|
||||
--device /dev/kfd $BUILDKITE_AGENT_META_DATA_RENDER_DEVICES \
|
||||
--network=host \
|
||||
--shm-size=16gb \
|
||||
--group-add "$render_gid" \
|
||||
--rm \
|
||||
-e HF_TOKEN \
|
||||
-e AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY \
|
||||
-v "${HF_CACHE}:${HF_MOUNT}" \
|
||||
-e "HF_HOME=${HF_MOUNT}" \
|
||||
-e "PYTHONPATH=${MYPYTHONPATH}" \
|
||||
--name "${container_name}" \
|
||||
"${image_name}" \
|
||||
/bin/bash -c "${commands}"
|
||||
--device /dev/kfd $BUILDKITE_AGENT_META_DATA_RENDER_DEVICES \
|
||||
--network=host \
|
||||
--shm-size=16gb \
|
||||
--group-add "$render_gid" \
|
||||
--rm \
|
||||
-e HF_TOKEN \
|
||||
-e AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY \
|
||||
-v "${HF_CACHE}:${HF_MOUNT}" \
|
||||
-e "HF_HOME=${HF_MOUNT}" \
|
||||
-e "PYTHONPATH=${MYPYTHONPATH}" \
|
||||
--name "${container_name}" \
|
||||
"${image_name}" \
|
||||
/bin/bash -c "${commands}"
|
||||
fi
|
||||
|
||||
@@ -340,6 +340,11 @@ steps:
|
||||
- vllm/
|
||||
- tests/v1/tracing
|
||||
commands:
|
||||
- "pip install \
|
||||
'opentelemetry-sdk>=1.26.0' \
|
||||
'opentelemetry-api>=1.26.0' \
|
||||
'opentelemetry-exporter-otlp>=1.26.0' \
|
||||
'opentelemetry-semantic-conventions-ai>=0.4.1'"
|
||||
- pytest -v -s v1/tracing
|
||||
|
||||
##### fast check tests #####
|
||||
@@ -1958,6 +1963,11 @@ steps:
|
||||
- vllm/
|
||||
- tests/v1/tracing
|
||||
commands:
|
||||
- "pip install \
|
||||
'opentelemetry-sdk>=1.26.0' \
|
||||
'opentelemetry-api>=1.26.0' \
|
||||
'opentelemetry-exporter-otlp>=1.26.0' \
|
||||
'opentelemetry-semantic-conventions-ai>=0.4.1'"
|
||||
- pytest -v -s v1/tracing
|
||||
|
||||
##### fast check tests #####
|
||||
|
||||
@@ -145,7 +145,7 @@ steps:
|
||||
num_devices: 2
|
||||
commands:
|
||||
- pytest -v -s tests/distributed/test_context_parallel.py
|
||||
- cd examples/offline_inference/new_weight_syncing && VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 rlhf_async_new_apis.py
|
||||
- VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/offline_inference/new_weight_syncing/rlhf_async_new_apis.py
|
||||
- VLLM_USE_DEEP_GEMM=1 VLLM_LOGGING_LEVEL=DEBUG python3 examples/offline_inference/data_parallel.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=deepep_high_throughput
|
||||
- pytest -v -s tests/v1/distributed/test_dbo.py
|
||||
|
||||
|
||||
@@ -28,3 +28,11 @@ steps:
|
||||
- pytest -v -s v1/engine/test_preprocess_error_handling.py
|
||||
# Run the rest of v1/engine tests
|
||||
- pytest -v -s v1/engine --ignore v1/engine/test_preprocess_error_handling.py
|
||||
mirror:
|
||||
amd:
|
||||
device: mi325_8
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
commands:
|
||||
- pytest -v -s v1/e2e
|
||||
- pytest -v -s v1/engine
|
||||
|
||||
@@ -24,11 +24,6 @@ steps:
|
||||
- pytest -v -s entrypoints/llm --ignore=entrypoints/llm/test_generate.py --ignore=entrypoints/llm/test_collective_rpc.py
|
||||
- pytest -v -s entrypoints/llm/test_generate.py # it needs a clean process
|
||||
- pytest -v -s entrypoints/offline_mode # Needs to avoid interference with other tests
|
||||
mirror:
|
||||
amd:
|
||||
device: mi325_1
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
|
||||
- label: Entrypoints Integration (API Server 1)
|
||||
timeout_in_minutes: 130
|
||||
@@ -65,6 +60,11 @@ steps:
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -v -s entrypoints/pooling
|
||||
mirror:
|
||||
amd:
|
||||
device: mi325_1
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
|
||||
- label: Entrypoints Integration (Responses API)
|
||||
timeout_in_minutes: 50
|
||||
|
||||
@@ -115,6 +115,7 @@ steps:
|
||||
- pytest -v -s tests/kernels/moe/test_nvfp4_moe.py
|
||||
- pytest -v -s tests/kernels/moe/test_ocp_mx_moe.py
|
||||
- pytest -v -s tests/kernels/moe/test_flashinfer.py
|
||||
- pytest -v -s tests/kernels/moe/test_flashinfer_moe.py
|
||||
- pytest -v -s tests/kernels/moe/test_cutedsl_moe.py
|
||||
# e2e
|
||||
- pytest -v -s tests/models/quantization/test_nvfp4.py
|
||||
@@ -156,14 +157,3 @@ steps:
|
||||
- pytest -v -s kernels/moe/test_deepep_moe.py
|
||||
- pytest -v -s kernels/moe/test_pplx_cutlass_moe.py
|
||||
# - pytest -v -s kernels/moe/test_pplx_moe.py - failing on main
|
||||
|
||||
- label: Kernels Fp4 MoE Test (B200)
|
||||
timeout_in_minutes: 60
|
||||
device: b200
|
||||
num_devices: 1
|
||||
optional: true
|
||||
commands:
|
||||
- pytest -v -s kernels/moe/test_cutedsl_moe.py
|
||||
- pytest -v -s kernels/moe/test_flashinfer_moe.py
|
||||
- pytest -v -s kernels/moe/test_nvfp4_moe.py
|
||||
- pytest -v -s kernels/moe/test_ocp_mx_moe.py
|
||||
|
||||
@@ -16,6 +16,7 @@ steps:
|
||||
- pytest -v -s v1/sample
|
||||
- pytest -v -s v1/logits_processors
|
||||
- pytest -v -s v1/worker
|
||||
# TODO: create another `optional` test group for slow tests
|
||||
- pytest -v -s -m 'not slow_test' v1/spec_decode
|
||||
- pytest -v -s -m 'not cpu_test' v1/kv_connector/unit
|
||||
- pytest -v -s -m 'not cpu_test' v1/metrics
|
||||
@@ -25,6 +26,11 @@ steps:
|
||||
# Integration test for streaming correctness (requires special branch).
|
||||
- pip install -U git+https://github.com/robertgshaw2-redhat/lm-evaluation-harness.git@streaming-api
|
||||
- pytest -v -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine
|
||||
mirror:
|
||||
amd:
|
||||
device: mi325_1
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
|
||||
- label: V1 Others (CPU)
|
||||
depends_on:
|
||||
@@ -88,6 +94,11 @@ steps:
|
||||
- vllm/
|
||||
- tests/v1/tracing
|
||||
commands:
|
||||
- "pip install \
|
||||
'opentelemetry-sdk>=1.26.0' \
|
||||
'opentelemetry-api>=1.26.0' \
|
||||
'opentelemetry-exporter-otlp>=1.26.0' \
|
||||
'opentelemetry-semantic-conventions-ai>=0.4.1'"
|
||||
- pytest -v -s v1/tracing
|
||||
|
||||
- label: Python-only Installation
|
||||
|
||||
@@ -55,6 +55,15 @@ steps:
|
||||
- uv pip install --system --no-build-isolation 'git+https://github.com/state-spaces/mamba@v2.3.0'
|
||||
- uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.5.2'
|
||||
- pytest -v -s models/language/generation -m '(not core_model) and (not hybrid_model)'
|
||||
mirror:
|
||||
amd:
|
||||
device: mi325_1
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
commands:
|
||||
- uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@fix-rocm-7.0-warp-size-constexpr'
|
||||
- uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.5.2'
|
||||
- pytest -v -s models/language/generation -m '(not core_model) and (not hybrid_model)'
|
||||
|
||||
- label: Language Models Test (PPL)
|
||||
timeout_in_minutes: 110
|
||||
@@ -73,6 +82,11 @@ steps:
|
||||
- tests/models/language/pooling
|
||||
commands:
|
||||
- pytest -v -s models/language/pooling -m 'not core_model'
|
||||
mirror:
|
||||
amd:
|
||||
device: mi325_1
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
|
||||
- label: Language Models Test (MTEB)
|
||||
timeout_in_minutes: 110
|
||||
|
||||
+4
-4
@@ -5,14 +5,14 @@
|
||||
/vllm/compilation @zou3519 @youkaichao @ProExpertProg
|
||||
/vllm/distributed/kv_transfer @NickLucche @ApostaC @orozery
|
||||
/vllm/lora @jeejeelee
|
||||
/vllm/model_executor/layers/attention @LucasWilkinson
|
||||
/vllm/model_executor/layers/attention @LucasWilkinson @MatthewBonanni
|
||||
/vllm/model_executor/layers/fused_moe @mgoin @pavanimajety
|
||||
/vllm/model_executor/layers/quantization @mgoin @robertgshaw2-redhat @tlrmchlsmth @yewentao256 @pavanimajety
|
||||
/vllm/model_executor/layers/mamba @tdoublep
|
||||
/vllm/model_executor/model_loader @22quinn
|
||||
/vllm/model_executor/layers/batch_invariant.py @yewentao256
|
||||
/vllm/multimodal @DarkLight1337 @ywang96 @NickLucche @tjtanaa
|
||||
/vllm/vllm_flash_attn @LucasWilkinson
|
||||
/vllm/vllm_flash_attn @LucasWilkinson @MatthewBonanni
|
||||
CMakeLists.txt @tlrmchlsmth @LucasWilkinson
|
||||
|
||||
# Any change to the VllmConfig changes can have a large user-facing impact,
|
||||
@@ -43,14 +43,14 @@ CMakeLists.txt @tlrmchlsmth @LucasWilkinson
|
||||
/vllm/tool_parsers @aarnphm @chaunceyjiang
|
||||
|
||||
# vLLM V1
|
||||
/vllm/v1/attention @LucasWilkinson
|
||||
/vllm/v1/attention @LucasWilkinson @MatthewBonanni
|
||||
/vllm/v1/attention/backend.py @WoosukKwon @zhuohan123 @youkaichao @alexm-redhat @njhill
|
||||
/vllm/v1/attention/backends/mla @pavanimajety
|
||||
/vllm/v1/attention/backends/flashinfer.py @mgoin @pavanimajety
|
||||
/vllm/v1/attention/backends/triton_attn.py @tdoublep
|
||||
/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
|
||||
/vllm/v1/spec_decode @benchislett @luccafong @MatthewBonanni
|
||||
/vllm/v1/structured_output @mgoin @russellb @aarnphm @benchislett
|
||||
/vllm/v1/kv_cache_interface.py @heheda12345
|
||||
/vllm/v1/kv_offload @ApostaC @orozery
|
||||
|
||||
@@ -16,6 +16,8 @@ torch::Tensor get_scheduler_metadata(
|
||||
isa = cpu_attention::ISA::VEC16;
|
||||
} else if (isa_hint == "neon") {
|
||||
isa = cpu_attention::ISA::NEON;
|
||||
} else if (isa_hint == "vxe") {
|
||||
isa = cpu_attention::ISA::VXE;
|
||||
} else {
|
||||
TORCH_CHECK(false, "Unsupported CPU attention ISA hint: " + isa_hint);
|
||||
}
|
||||
@@ -100,6 +102,8 @@ void cpu_attn_reshape_and_cache(
|
||||
return cpu_attention::ISA::VEC16;
|
||||
} else if (isa == "neon") {
|
||||
return cpu_attention::ISA::NEON;
|
||||
} else if (isa == "vxe") {
|
||||
return cpu_attention::ISA::VXE;
|
||||
} else {
|
||||
TORCH_CHECK(false, "Invalid ISA type: " + isa);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "cpu/utils.hpp"
|
||||
|
||||
namespace cpu_attention {
|
||||
enum class ISA { AMX, VEC, VEC16, NEON };
|
||||
enum class ISA { AMX, VEC, VEC16, NEON, VXE };
|
||||
|
||||
template <ISA isa, typename scalar_t, int64_t head_dim>
|
||||
class AttentionImpl {};
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
#ifndef CPU_ATTN_VXE_HPP
|
||||
#define CPU_ATTN_VXE_HPP
|
||||
|
||||
#include "cpu_attn_impl.hpp"
|
||||
#include <vecintrin.h>
|
||||
#include <type_traits>
|
||||
|
||||
namespace cpu_attention {
|
||||
|
||||
namespace {
|
||||
|
||||
// s390x Vector = 16 bytes (128 bits)
|
||||
#define BLOCK_SIZE_ALIGNMENT 32
|
||||
#define HEAD_SIZE_ALIGNMENT 32
|
||||
#define MAX_Q_HEAD_NUM_PER_ITER 16
|
||||
|
||||
template <typename kv_cache_t>
|
||||
FORCE_INLINE void load_row8_B_as_f32(const kv_cache_t* p, __vector float& b0,
|
||||
__vector float& b1);
|
||||
|
||||
// [1] Float Specialization
|
||||
template <>
|
||||
FORCE_INLINE void load_row8_B_as_f32<float>(const float* p, __vector float& b0,
|
||||
__vector float& b1) {
|
||||
// Explicitly cast to long long for offset, and float* for pointer
|
||||
b0 = vec_xl((long long)0, const_cast<float*>(p));
|
||||
b1 = vec_xl((long long)0, const_cast<float*>(p + 4));
|
||||
}
|
||||
|
||||
// [2] BFloat16 Specialization (Big Endian Fix)
|
||||
template <>
|
||||
FORCE_INLINE void load_row8_B_as_f32<c10::BFloat16>(const c10::BFloat16* p,
|
||||
__vector float& b0,
|
||||
__vector float& b1) {
|
||||
// 1. Load 8 BF16s (16 bytes) into one vector
|
||||
// Explicit cast to unsigned short* for vec_xl to return vector unsigned short
|
||||
__vector unsigned short raw = vec_xl((long long)0, (unsigned short*)p);
|
||||
|
||||
// 2. Prepare Zero vector
|
||||
__vector unsigned short zeros = vec_splat_u16(0);
|
||||
|
||||
// 3. Merge High/Low to expand BF16 -> Float32
|
||||
// On Big Endian, a float is [BF16_bits | 16_zero_bits]
|
||||
b0 = (__vector float)vec_mergeh(raw, zeros);
|
||||
b1 = (__vector float)vec_mergel(raw, zeros);
|
||||
}
|
||||
|
||||
template <>
|
||||
FORCE_INLINE void load_row8_B_as_f32<c10::Half>(const c10::Half* p,
|
||||
__vector float& b0,
|
||||
__vector float& b1) {
|
||||
alignas(16) float tmp[8];
|
||||
|
||||
// Manual unroll / conversion
|
||||
tmp[0] = static_cast<float>(p[0]);
|
||||
tmp[1] = static_cast<float>(p[1]);
|
||||
tmp[2] = static_cast<float>(p[2]);
|
||||
tmp[3] = static_cast<float>(p[3]);
|
||||
tmp[4] = static_cast<float>(p[4]);
|
||||
tmp[5] = static_cast<float>(p[5]);
|
||||
tmp[6] = static_cast<float>(p[6]);
|
||||
tmp[7] = static_cast<float>(p[7]);
|
||||
|
||||
// Explicit arguments for intrinsic: (long long offset, float* ptr)
|
||||
b0 = vec_xl((long long)0, (float*)tmp);
|
||||
b1 = vec_xl((long long)0, (float*)(tmp + 4));
|
||||
}
|
||||
|
||||
template <int32_t M, typename kv_cache_t>
|
||||
FORCE_INLINE void gemm_micro_s390x_Mx8_Ku4(
|
||||
const float* __restrict A, // [M x K]
|
||||
const kv_cache_t* __restrict B, // [K x 8]
|
||||
float* __restrict C, // [M x 8]
|
||||
int64_t lda, int64_t ldb, int64_t ldc, int32_t K, bool accumulate) {
|
||||
static_assert(1 <= M && M <= 8, "M must be in [1,8]");
|
||||
|
||||
// Helper macros to unroll codegen for M rows
|
||||
#define ROWS_APPLY(OP) OP(0) OP(1) OP(2) OP(3) OP(4) OP(5) OP(6) OP(7)
|
||||
#define IF_M(i) if constexpr (M > (i))
|
||||
|
||||
// 1. Define A pointers
|
||||
#define DECL_A(i) const float* a##i = A + (i) * lda;
|
||||
ROWS_APPLY(DECL_A)
|
||||
#undef DECL_A
|
||||
|
||||
// 2. Define Accumulators (2 vectors covers 8 columns)
|
||||
#define DECL_ACC(i) __vector float acc##i##_0, acc##i##_1;
|
||||
ROWS_APPLY(DECL_ACC)
|
||||
#undef DECL_ACC
|
||||
|
||||
// 3. Initialize Accumulators (Load C or Zero)
|
||||
#define INIT_ACC(i) \
|
||||
IF_M(i) { \
|
||||
if (accumulate) { \
|
||||
acc##i##_0 = \
|
||||
vec_xl((long long)0, const_cast<float*>(C + (i) * ldc + 0)); \
|
||||
acc##i##_1 = \
|
||||
vec_xl((long long)0, const_cast<float*>(C + (i) * ldc + 4)); \
|
||||
} else { \
|
||||
acc##i##_0 = vec_splats(0.0f); \
|
||||
acc##i##_1 = vec_splats(0.0f); \
|
||||
} \
|
||||
}
|
||||
ROWS_APPLY(INIT_ACC)
|
||||
#undef INIT_ACC
|
||||
|
||||
int32_t k = 0;
|
||||
|
||||
for (; k + 3 < K; k += 4) {
|
||||
// Load 4 values of A for each Row M: A[k...k+3]
|
||||
#define LOAD_A4(i) \
|
||||
__vector float a##i##v; \
|
||||
IF_M(i) a##i##v = vec_xl((long long)0, const_cast<float*>(a##i + k));
|
||||
ROWS_APPLY(LOAD_A4)
|
||||
#undef LOAD_A4
|
||||
|
||||
// Helper: FMA for specific lane L of A
|
||||
// s390x: vec_madd(b, vec_splat(a, lane), acc)
|
||||
#define FMAS_LANE(i, aiv, L) \
|
||||
IF_M(i) { \
|
||||
__vector float a_broad = vec_splat(aiv, L); \
|
||||
acc##i##_0 = vec_madd(b0, a_broad, acc##i##_0); \
|
||||
acc##i##_1 = vec_madd(b1, a_broad, acc##i##_1); \
|
||||
}
|
||||
|
||||
// Unroll K=0..3
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 0) * ldb, b0, b1);
|
||||
#define STEP_K0(i) FMAS_LANE(i, a##i##v, 0)
|
||||
ROWS_APPLY(STEP_K0)
|
||||
#undef STEP_K0
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 1) * ldb, b0, b1);
|
||||
#define STEP_K1(i) FMAS_LANE(i, a##i##v, 1)
|
||||
ROWS_APPLY(STEP_K1)
|
||||
#undef STEP_K1
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 2) * ldb, b0, b1);
|
||||
#define STEP_K2(i) FMAS_LANE(i, a##i##v, 2)
|
||||
ROWS_APPLY(STEP_K2)
|
||||
#undef STEP_K2
|
||||
}
|
||||
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 3) * ldb, b0, b1);
|
||||
#define STEP_K3(i) FMAS_LANE(i, a##i##v, 3)
|
||||
ROWS_APPLY(STEP_K3)
|
||||
#undef STEP_K3
|
||||
}
|
||||
#undef FMAS_LANE
|
||||
}
|
||||
|
||||
for (; k < K; ++k) {
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)k * ldb, b0, b1);
|
||||
#define TAIL_ROW(i) \
|
||||
IF_M(i) { \
|
||||
__vector float ai = vec_splats(*(a##i + k)); \
|
||||
acc##i##_0 = vec_madd(b0, ai, acc##i##_0); \
|
||||
acc##i##_1 = vec_madd(b1, ai, acc##i##_1); \
|
||||
}
|
||||
ROWS_APPLY(TAIL_ROW)
|
||||
#undef TAIL_ROW
|
||||
}
|
||||
|
||||
#define STORE_ROW(i) \
|
||||
IF_M(i) { \
|
||||
vec_xst(acc##i##_0, 0, C + (i) * ldc + 0); \
|
||||
vec_xst(acc##i##_1, 0, C + (i) * ldc + 4); \
|
||||
}
|
||||
ROWS_APPLY(STORE_ROW)
|
||||
#undef STORE_ROW
|
||||
|
||||
#undef ROWS_APPLY
|
||||
#undef IF_M
|
||||
}
|
||||
|
||||
template <int32_t N, typename kv_cache_t>
|
||||
FORCE_INLINE void gemm_macro_s390x_Mx8_Ku4(const float* __restrict A,
|
||||
const kv_cache_t* __restrict B,
|
||||
float* __restrict C, int32_t M,
|
||||
int32_t K, int64_t lda, int64_t ldb,
|
||||
int64_t ldc, bool accumulate) {
|
||||
static_assert(N % 8 == 0, "N must be a multiple of 8");
|
||||
for (int32_t m = 0; m < M;) {
|
||||
int32_t mb = (M - m >= 8) ? 8 : (M - m >= 4) ? 4 : (M - m >= 2) ? 2 : 1;
|
||||
const float* Ab = A + m * lda;
|
||||
float* Cb = C + m * ldc;
|
||||
|
||||
for (int32_t n = 0; n < N; n += 8) {
|
||||
const kv_cache_t* Bn = B + n;
|
||||
float* Cn = Cb + n;
|
||||
switch (mb) {
|
||||
case 8:
|
||||
gemm_micro_s390x_Mx8_Ku4<8, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc, K,
|
||||
accumulate);
|
||||
break;
|
||||
case 4:
|
||||
gemm_micro_s390x_Mx8_Ku4<4, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc, K,
|
||||
accumulate);
|
||||
break;
|
||||
case 2:
|
||||
gemm_micro_s390x_Mx8_Ku4<2, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc, K,
|
||||
accumulate);
|
||||
break;
|
||||
default:
|
||||
gemm_micro_s390x_Mx8_Ku4<1, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc, K,
|
||||
accumulate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m += mb;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename kv_cache_t>
|
||||
class TileGemmS390X {
|
||||
public:
|
||||
template <AttentionGemmPhase phase, int32_t k_size>
|
||||
FORCE_INLINE static void gemm(const int32_t m_size,
|
||||
float* __restrict__ a_tile,
|
||||
kv_cache_t* __restrict__ b_tile,
|
||||
float* __restrict__ c_tile, const int64_t lda,
|
||||
const int64_t ldb, const int64_t ldc,
|
||||
const int32_t block_size,
|
||||
const int32_t dynamic_k_size,
|
||||
const bool accum_c) {
|
||||
if constexpr (phase == AttentionGemmPhase::QK) {
|
||||
gemm_macro_s390x_Mx8_Ku4<BLOCK_SIZE_ALIGNMENT, kv_cache_t>(
|
||||
a_tile, b_tile, c_tile, m_size, k_size, lda, ldb, ldc, accum_c);
|
||||
} else {
|
||||
gemm_macro_s390x_Mx8_Ku4<HEAD_SIZE_ALIGNMENT, kv_cache_t>(
|
||||
a_tile, b_tile, c_tile, m_size, dynamic_k_size, lda, ldb, ldc,
|
||||
accum_c);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
template <typename scalar_t, int64_t head_dim>
|
||||
class AttentionImpl<ISA::VXE, scalar_t, head_dim> {
|
||||
public:
|
||||
using query_t = scalar_t;
|
||||
using q_buffer_t = float;
|
||||
using kv_cache_t = scalar_t;
|
||||
using logits_buffer_t = float;
|
||||
using partial_output_buffer_t = float;
|
||||
using prob_buffer_t = float;
|
||||
|
||||
constexpr static int64_t BlockSizeAlignment = BLOCK_SIZE_ALIGNMENT;
|
||||
constexpr static int64_t HeadDimAlignment = HEAD_SIZE_ALIGNMENT;
|
||||
constexpr static int64_t MaxQHeadNumPerIteration = MAX_Q_HEAD_NUM_PER_ITER;
|
||||
constexpr static int64_t HeadDim = head_dim;
|
||||
constexpr static ISA ISAType = ISA::VXE;
|
||||
constexpr static bool scale_on_logits =
|
||||
false; // Scale is applied to Q during copy
|
||||
|
||||
public:
|
||||
AttentionImpl() {}
|
||||
|
||||
template <template <typename tile_gemm_t> typename attention>
|
||||
FORCE_INLINE void execute_attention(DEFINE_CPU_ATTENTION_PARAMS) {
|
||||
attention<TileGemmS390X<kv_cache_t>> attention_iteration;
|
||||
attention_iteration(CPU_ATTENTION_PARAMS);
|
||||
}
|
||||
|
||||
// Strides for Memory Layout
|
||||
constexpr static int64_t k_cache_token_group_stride(
|
||||
const int32_t block_size) {
|
||||
return BlockSizeAlignment; // [head_dim, block_size] layout
|
||||
}
|
||||
|
||||
constexpr static int64_t v_cache_token_group_stride(
|
||||
const int32_t block_size) {
|
||||
return head_dim * BlockSizeAlignment;
|
||||
}
|
||||
|
||||
constexpr static int64_t v_cache_head_group_stride(const int32_t block_size) {
|
||||
return HeadDimAlignment;
|
||||
}
|
||||
|
||||
static void copy_q_heads_tile(scalar_t* __restrict__ src,
|
||||
float* __restrict__ q_buffer,
|
||||
const int32_t q_num,
|
||||
const int32_t q_heads_per_kv,
|
||||
const int64_t q_num_stride,
|
||||
const int64_t q_head_stride, float scale) {
|
||||
__vector float scale_vec = vec_splats(scale);
|
||||
constexpr bool is_bf16 = std::is_same<scalar_t, c10::BFloat16>::value;
|
||||
|
||||
// Process 8 elements at a time (32 bytes of float output)
|
||||
for (int32_t i = 0; i < q_num; ++i) {
|
||||
for (int32_t h = 0; h < q_heads_per_kv; ++h) {
|
||||
scalar_t* curr_src = src + i * q_num_stride + h * q_head_stride;
|
||||
float* curr_dst =
|
||||
q_buffer + i * q_heads_per_kv * head_dim + h * head_dim;
|
||||
|
||||
int32_t d = 0;
|
||||
for (; d <= head_dim - 8; d += 8) {
|
||||
if constexpr (is_bf16) {
|
||||
__vector float v0, v1;
|
||||
// Reuse our Big-Endian-Safe loader
|
||||
load_row8_B_as_f32<scalar_t>(curr_src + d, v0, v1);
|
||||
|
||||
v0 = vec_mul(v0, scale_vec);
|
||||
v1 = vec_mul(v1, scale_vec);
|
||||
|
||||
vec_xst(v0, 0, curr_dst + d);
|
||||
vec_xst(v1, 0, curr_dst + d + 4);
|
||||
} else {
|
||||
__vector float v0 = vec_xl((long long)0, (float*)curr_src + d);
|
||||
__vector float v1 = vec_xl((long long)0, (float*)curr_src + d + 4);
|
||||
|
||||
v0 = vec_mul(v0, scale_vec);
|
||||
v1 = vec_mul(v1, scale_vec);
|
||||
|
||||
vec_xst(v0, 0, curr_dst + d);
|
||||
vec_xst(v1, 0, curr_dst + d + 4);
|
||||
}
|
||||
}
|
||||
|
||||
for (; d < head_dim; ++d) {
|
||||
float val = static_cast<float>(curr_src[d]);
|
||||
curr_dst[d] = val * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void reshape_and_cache(
|
||||
const scalar_t* __restrict__ key, const scalar_t* __restrict__ value,
|
||||
scalar_t* __restrict__ key_cache, scalar_t* __restrict__ value_cache,
|
||||
const int64_t* __restrict__ slot_mapping, const int64_t token_num,
|
||||
const int64_t key_token_num_stride, const int64_t value_token_num_stride,
|
||||
const int64_t head_num, const int64_t key_head_num_stride,
|
||||
const int64_t value_head_num_stride, const int64_t num_blocks,
|
||||
const int64_t num_blocks_stride, const int64_t cache_head_num_stride,
|
||||
const int64_t block_size, const int64_t block_size_stride) {
|
||||
#pragma omp parallel for collapse(2)
|
||||
for (int64_t token_idx = 0; token_idx < token_num; ++token_idx) {
|
||||
for (int64_t head_idx = 0; head_idx < head_num; ++head_idx) {
|
||||
const int64_t pos = slot_mapping[token_idx];
|
||||
if (pos < 0) continue;
|
||||
|
||||
const int64_t block_idx = pos / block_size;
|
||||
const int64_t block_offset = pos % block_size;
|
||||
|
||||
{
|
||||
const scalar_t* key_src = key + token_idx * key_token_num_stride +
|
||||
head_idx * key_head_num_stride;
|
||||
scalar_t* key_dst = key_cache + block_idx * num_blocks_stride +
|
||||
head_idx * cache_head_num_stride + block_offset;
|
||||
|
||||
for (int64_t i = 0, j = 0; i < head_dim; ++i, j += block_size) {
|
||||
key_dst[j] = key_src[i];
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const scalar_t* val_src = value + token_idx * value_token_num_stride +
|
||||
head_idx * value_head_num_stride;
|
||||
scalar_t* val_dst = value_cache + block_idx * num_blocks_stride +
|
||||
head_idx * cache_head_num_stride +
|
||||
block_offset * head_dim;
|
||||
|
||||
std::memcpy(val_dst, val_src, sizeof(scalar_t) * head_dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace cpu_attention
|
||||
|
||||
#undef BLOCK_SIZE_ALIGNMENT
|
||||
#undef HEAD_SIZE_ALIGNMENT
|
||||
#undef MAX_Q_HEAD_NUM_PER_ITER
|
||||
|
||||
#endif
|
||||
@@ -19,10 +19,11 @@ ISA_TYPES = {
|
||||
"VEC": 1,
|
||||
"VEC16": 2,
|
||||
"NEON": 3,
|
||||
"VXE": 4,
|
||||
}
|
||||
|
||||
# ISAs supported for head_dims divisible by 32
|
||||
ISA_FOR_32 = ["AMX", "NEON", "VEC", "VEC16"]
|
||||
ISA_FOR_32 = ["AMX", "NEON", "VEC", "VEC16", "VXE"]
|
||||
|
||||
# ISAs supported for head_dims divisible by 16 only
|
||||
ISA_FOR_16 = ["VEC16"]
|
||||
@@ -118,6 +119,10 @@ def generate_header_file() -> str:
|
||||
#include "cpu_attn_neon.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef __s390x__
|
||||
#include "cpu_attn_vxe.hpp"
|
||||
#endif
|
||||
|
||||
"""
|
||||
|
||||
header += generate_helper_function()
|
||||
@@ -163,6 +168,25 @@ def generate_header_file() -> str:
|
||||
} \\
|
||||
}()
|
||||
|
||||
"""
|
||||
|
||||
# s390x with VXE
|
||||
header += """#elif defined(__s390x__)
|
||||
#define CPU_ATTN_DISPATCH(HEAD_DIM, ISA_TYPE, ...) \\
|
||||
[&] { \\
|
||||
int64_t encoded_params = encode_cpu_attn_params(HEAD_DIM, ISA_TYPE); \\
|
||||
switch (encoded_params) { \\
|
||||
"""
|
||||
header += generate_cases_for_isa_group(["VXE", "VEC", "VEC16"])
|
||||
header += """
|
||||
default: { \\
|
||||
TORCH_CHECK(false, "Unsupported CPU attention configuration: head_dim=" + \\
|
||||
std::to_string(HEAD_DIM) + " isa=" + \\
|
||||
std::to_string(static_cast<int>(ISA_TYPE))); \\
|
||||
} \\
|
||||
} \\
|
||||
}()
|
||||
|
||||
"""
|
||||
|
||||
# Fallback: VEC and VEC16 only
|
||||
@@ -182,7 +206,7 @@ def generate_header_file() -> str:
|
||||
} \\
|
||||
}()
|
||||
|
||||
#endif /* CPU_CAPABILITY_AMXBF16 / __aarch64__ */
|
||||
#endif /* CPU_CAPABILITY_AMXBF16 / __aarch64__ / __s390x__ */
|
||||
|
||||
#endif // CPU_ATTN_DISPATCH_GENERATED_H
|
||||
"""
|
||||
|
||||
@@ -172,7 +172,7 @@ __device__ void _moe_align_block_size(
|
||||
}
|
||||
}
|
||||
|
||||
// Fill remaining expert_ids with 0
|
||||
// Fill remaining expert_ids with -1
|
||||
const size_t fill_start_idx =
|
||||
cumsum[cumsum_offset + num_experts] / block_size + threadIdx.x;
|
||||
for (size_t i = fill_start_idx; i < max_num_m_blocks; i += blockDim.x) {
|
||||
@@ -265,7 +265,7 @@ __device__ void _moe_align_block_size_small_batch_expert(
|
||||
}
|
||||
}
|
||||
|
||||
// Fill remaining expert_ids with 0
|
||||
// Fill remaining expert_ids with -1
|
||||
const size_t fill_start_idx = cumsum[num_experts] / block_size + tid;
|
||||
for (size_t i = fill_start_idx; i < max_num_m_blocks; i += stride) {
|
||||
expert_ids[expert_ids_offset + i] = inactive_expert_id;
|
||||
@@ -332,7 +332,7 @@ __global__ void moe_align_block_size_kernel(
|
||||
topk_ids, sorted_token_ids, expert_ids, total_tokens_post_pad, expert_map,
|
||||
num_experts, padded_num_experts, experts_per_warp, block_size, numel,
|
||||
cumsum, max_num_tokens_padded, CEILDIV(max_num_tokens_padded, block_size),
|
||||
0, 0, topk_num, nullptr, has_expert_map);
|
||||
0, -1, topk_num, nullptr, has_expert_map);
|
||||
}
|
||||
|
||||
template <typename scalar_t>
|
||||
@@ -373,7 +373,7 @@ __global__ void moe_align_block_size_small_batch_expert_kernel(
|
||||
_moe_align_block_size_small_batch_expert<scalar_t, fill_threads>(
|
||||
topk_ids, sorted_token_ids, expert_ids, total_tokens_post_pad, expert_map,
|
||||
num_experts, block_size, numel, max_num_tokens_padded,
|
||||
CEILDIV(max_num_tokens_padded, block_size), 0, 0, topk_num, nullptr,
|
||||
CEILDIV(max_num_tokens_padded, block_size), -1, 0, topk_num, nullptr,
|
||||
has_expert_map);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,68 @@ namespace vllm {
|
||||
|
||||
using c3x::cutlass_gemm_caller;
|
||||
|
||||
// Custom wrapper to allow specifying EpilogueTile for small M
|
||||
template <typename ElementAB_, typename ElementD_,
|
||||
template <typename, typename, typename> typename Epilogue_,
|
||||
typename TileShape, typename ClusterShape, typename KernelSchedule,
|
||||
typename EpilogueSchedule, typename EpilogueTile>
|
||||
struct cutlass_3x_gemm_sm120_custom {
|
||||
using ElementAB = ElementAB_;
|
||||
using LayoutA = cutlass::layout::RowMajor;
|
||||
static constexpr int AlignmentA =
|
||||
128 / cutlass::sizeof_bits<ElementAB>::value;
|
||||
|
||||
using LayoutB = cutlass::layout::ColumnMajor;
|
||||
static constexpr int AlignmentB =
|
||||
128 / cutlass::sizeof_bits<ElementAB>::value;
|
||||
|
||||
using ElementC = void;
|
||||
using LayoutC = cutlass::layout::RowMajor;
|
||||
static constexpr int AlignmentC =
|
||||
128 / cutlass::sizeof_bits<ElementD_>::value;
|
||||
|
||||
using ElementD = ElementD_;
|
||||
using LayoutD = cutlass::layout::RowMajor;
|
||||
static constexpr int AlignmentD = AlignmentC;
|
||||
|
||||
using ElementAcc =
|
||||
typename std::conditional<std::is_same_v<ElementAB, int8_t>, int32_t,
|
||||
float>::type;
|
||||
using Epilogue = Epilogue_<ElementAcc, ElementD, TileShape>;
|
||||
|
||||
// MMA type
|
||||
using ElementAccumulator = float;
|
||||
|
||||
// Epilogue types
|
||||
using ElementBias = cutlass::half_t;
|
||||
using ElementCompute = float;
|
||||
using ElementAux = ElementD;
|
||||
using LayoutAux = LayoutD;
|
||||
using ElementAmax = float;
|
||||
|
||||
using EVTCompute = typename Epilogue::EVTCompute;
|
||||
|
||||
using CollectiveEpilogue =
|
||||
typename cutlass::epilogue::collective::CollectiveBuilder<
|
||||
cutlass::arch::Sm120, cutlass::arch::OpClassTensorOp, TileShape,
|
||||
ClusterShape, EpilogueTile, // Use custom EpilogueTile
|
||||
ElementAccumulator, ElementCompute, ElementC, LayoutC, AlignmentC,
|
||||
ElementD, LayoutD, AlignmentD, EpilogueSchedule,
|
||||
EVTCompute>::CollectiveOp;
|
||||
|
||||
using CollectiveMainloop =
|
||||
typename cutlass::gemm::collective::CollectiveBuilder<
|
||||
cutlass::arch::Sm120, cutlass::arch::OpClassTensorOp, ElementAB,
|
||||
LayoutA, AlignmentA, ElementAB, LayoutB, AlignmentB,
|
||||
ElementAccumulator, TileShape, ClusterShape,
|
||||
cutlass::gemm::collective::StageCountAutoCarveout<static_cast<int>(
|
||||
sizeof(typename CollectiveEpilogue::SharedStorage))>,
|
||||
KernelSchedule, void>::CollectiveOp;
|
||||
|
||||
using GemmKernel = enable_sm120_only<cutlass::gemm::kernel::GemmUniversal<
|
||||
Shape<int, int, int, int>, CollectiveMainloop, CollectiveEpilogue, void>>;
|
||||
};
|
||||
|
||||
template <typename InType, typename OutType,
|
||||
template <typename, typename, typename> typename Epilogue>
|
||||
struct sm120_fp8_config_default {
|
||||
@@ -25,6 +87,54 @@ struct sm120_fp8_config_default {
|
||||
KernelSchedule, EpilogueSchedule>;
|
||||
};
|
||||
|
||||
template <typename InType, typename OutType,
|
||||
template <typename, typename, typename> typename Epilogue>
|
||||
struct sm120_fp8_config_M64 {
|
||||
static_assert(std::is_same<InType, cutlass::float_e4m3_t>());
|
||||
// SM120 Cooperative kernel requires Tile M >= 128.
|
||||
// For M=64 tile, we use Pingpong schedule which is more flexible with small
|
||||
// tiles.
|
||||
using KernelSchedule = cutlass::gemm::KernelTmaWarpSpecializedPingpong;
|
||||
using EpilogueSchedule = cutlass::epilogue::collective::EpilogueScheduleAuto;
|
||||
using TileShape = Shape<_64, _64, _128>;
|
||||
// CUTLASS 3.x on SM120 currently restricts programmatic multicast (Cluster >
|
||||
// 1) for certain schedules/types. Reverting to 1x1x1 to ensure compilation.
|
||||
using ClusterShape = Shape<_1, _1, _1>;
|
||||
using Cutlass3xGemm =
|
||||
cutlass_3x_gemm_sm120<InType, OutType, Epilogue, TileShape, ClusterShape,
|
||||
KernelSchedule, EpilogueSchedule>;
|
||||
};
|
||||
|
||||
template <typename InType, typename OutType,
|
||||
template <typename, typename, typename> typename Epilogue>
|
||||
struct sm120_fp8_config_M32 {
|
||||
static_assert(std::is_same<InType, cutlass::float_e4m3_t>());
|
||||
using KernelSchedule = cutlass::gemm::KernelTmaWarpSpecializedPingpong;
|
||||
using EpilogueSchedule = cutlass::epilogue::collective::EpilogueScheduleAuto;
|
||||
using TileShape = Shape<_32, _64, _128>;
|
||||
using ClusterShape = Shape<_1, _1, _1>;
|
||||
// Use custom gemm to specify EpilogueTile M=32
|
||||
using Cutlass3xGemm =
|
||||
cutlass_3x_gemm_sm120_custom<InType, OutType, Epilogue, TileShape,
|
||||
ClusterShape, KernelSchedule,
|
||||
EpilogueSchedule, Shape<_32, _32>>;
|
||||
};
|
||||
|
||||
template <typename InType, typename OutType,
|
||||
template <typename, typename, typename> typename Epilogue>
|
||||
struct sm120_fp8_config_M16 {
|
||||
static_assert(std::is_same<InType, cutlass::float_e4m3_t>());
|
||||
using KernelSchedule = cutlass::gemm::KernelTmaWarpSpecializedPingpong;
|
||||
using EpilogueSchedule = cutlass::epilogue::collective::EpilogueScheduleAuto;
|
||||
using TileShape = Shape<_16, _64, _128>;
|
||||
using ClusterShape = Shape<_1, _1, _1>;
|
||||
// Use custom gemm to specify EpilogueTile M=16
|
||||
using Cutlass3xGemm =
|
||||
cutlass_3x_gemm_sm120_custom<InType, OutType, Epilogue, TileShape,
|
||||
ClusterShape, KernelSchedule,
|
||||
EpilogueSchedule, Shape<_16, _32>>;
|
||||
};
|
||||
|
||||
template <typename InType, typename OutType,
|
||||
template <typename, typename, typename> typename Epilogue,
|
||||
typename... EpilogueArgs>
|
||||
@@ -36,6 +146,28 @@ inline void cutlass_gemm_sm120_fp8_dispatch(torch::Tensor& out,
|
||||
TORCH_CHECK(a.dtype() == torch::kFloat8_e4m3fn);
|
||||
TORCH_CHECK(b.dtype() == torch::kFloat8_e4m3fn);
|
||||
|
||||
int M = a.size(0);
|
||||
|
||||
if (M <= 16) {
|
||||
using Cutlass3xGemmM16 =
|
||||
typename sm120_fp8_config_M16<InType, OutType, Epilogue>::Cutlass3xGemm;
|
||||
return cutlass_gemm_caller<Cutlass3xGemmM16>(
|
||||
out, a, b, std::forward<EpilogueArgs>(args)...);
|
||||
}
|
||||
if (M <= 32) {
|
||||
using Cutlass3xGemmM32 =
|
||||
typename sm120_fp8_config_M32<InType, OutType, Epilogue>::Cutlass3xGemm;
|
||||
return cutlass_gemm_caller<Cutlass3xGemmM32>(
|
||||
out, a, b, std::forward<EpilogueArgs>(args)...);
|
||||
}
|
||||
|
||||
if (M <= 256) {
|
||||
using Cutlass3xGemmM64 =
|
||||
typename sm120_fp8_config_M64<InType, OutType, Epilogue>::Cutlass3xGemm;
|
||||
return cutlass_gemm_caller<Cutlass3xGemmM64>(
|
||||
out, a, b, std::forward<EpilogueArgs>(args)...);
|
||||
}
|
||||
|
||||
using Cutlass3xGemmDefault =
|
||||
typename sm120_fp8_config_default<InType, OutType,
|
||||
Epilogue>::Cutlass3xGemm;
|
||||
@@ -64,4 +196,4 @@ void cutlass_scaled_mm_sm120_fp8_epilogue(torch::Tensor& out,
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace vllm
|
||||
} // namespace vllm
|
||||
|
||||
+14
-46
@@ -1902,7 +1902,7 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
|
||||
float sB = *s_B;
|
||||
|
||||
while (m < M) {
|
||||
floatx16 sum[N][YTILE] = {};
|
||||
scalar8 sum[N][YTILE] = {};
|
||||
for (uint32_t k1 = 0; k1 < K; k1 += THRDS * A_CHUNK * UNRL) {
|
||||
bigType bigA[N][UNRL] = {};
|
||||
bigType bigB[YTILE][UNRL];
|
||||
@@ -1936,7 +1936,7 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
|
||||
for (uint32_t n = 0; n < N; n++) {
|
||||
for (int i = 0; i < A_CHUNK; i += 8) {
|
||||
for (int y = 0; y < YTILE; ++y) {
|
||||
sum[n][y] = __builtin_amdgcn_mfma_f32_32x32x16_fp8_fp8(
|
||||
sum[n][y] = __builtin_amdgcn_mfma_f32_16x16x32_fp8_fp8(
|
||||
bigA[n][k2].l[i / 8], bigB[y][k2].l[i / 8], sum[n][y], 0, 0,
|
||||
0);
|
||||
}
|
||||
@@ -1949,31 +1949,15 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
|
||||
for (int n = 0; n < N; n++) {
|
||||
for (int y = 0; y < YTILE; y++) {
|
||||
float accm0 = sum[n][y][0];
|
||||
float accm16 = sum[n][y][8];
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][1], 0x101, 0xf, 0xf,
|
||||
1); // row_shl1
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][9], 0x101, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][2], 0x102, 0xf, 0xf,
|
||||
1); // row_shl2
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][10], 0x102, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][3], 0x103, 0xf, 0xf,
|
||||
1); // row_shl3
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][11], 0x103, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][4], 0x108, 0xf, 0xf,
|
||||
1); // row_shl8
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][12], 0x108, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][5], 0x109, 0xf, 0xf,
|
||||
1); // row_shl9
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][13], 0x109, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][6], 0x10a, 0xf, 0xf,
|
||||
1); // row_shl10
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][14], 0x10a, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][7], 0x10b, 0xf, 0xf,
|
||||
1); // row_shl11
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][15], 0x10b, 0xf, 0xf, 1);
|
||||
accm0 += __shfl(accm0, 36);
|
||||
accm16 += __shfl(accm16, 52);
|
||||
sum[n][y][0] = accm0 + __shfl(accm16, 16);
|
||||
accm0 += __shfl_down(accm0, 20);
|
||||
accm0 += __shfl_down(accm0, 40);
|
||||
sum[n][y][0] = accm0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2064,7 +2048,7 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
|
||||
float sB = *s_B;
|
||||
|
||||
while (m < M) {
|
||||
floatx16 sum[N][YTILE] = {};
|
||||
scalar8 sum[N][YTILE] = {};
|
||||
for (uint32_t k1 = 0; k1 < K; k1 += THRDS * A_CHUNK * UNRL) {
|
||||
bigType bigA[N][UNRL] = {};
|
||||
bigType bigB[YTILE][UNRL];
|
||||
@@ -2100,7 +2084,7 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
|
||||
for (uint32_t n = 0; n < N; n++) {
|
||||
for (int i = 0; i < A_CHUNK; i += 8) {
|
||||
for (int y = 0; y < YTILE; ++y) {
|
||||
sum[n][y] = __builtin_amdgcn_mfma_f32_32x32x16_fp8_fp8(
|
||||
sum[n][y] = __builtin_amdgcn_mfma_f32_16x16x32_fp8_fp8(
|
||||
bigA[n][k2].l[i / 8], bigB[y][k2].l[i / 8], sum[n][y], 0, 0,
|
||||
0);
|
||||
}
|
||||
@@ -2113,31 +2097,15 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
|
||||
for (int n = 0; n < N; n++) {
|
||||
for (int y = 0; y < YTILE; y++) {
|
||||
float accm0 = sum[n][y][0];
|
||||
float accm16 = sum[n][y][8];
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][1], 0x101, 0xf, 0xf,
|
||||
1); // row_shl1
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][9], 0x101, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][2], 0x102, 0xf, 0xf,
|
||||
1); // row_shl2
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][10], 0x102, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][3], 0x103, 0xf, 0xf,
|
||||
1); // row_shl3
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][11], 0x103, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][4], 0x108, 0xf, 0xf,
|
||||
1); // row_shl8
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][12], 0x108, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][5], 0x109, 0xf, 0xf,
|
||||
1); // row_shl9
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][13], 0x109, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][6], 0x10a, 0xf, 0xf,
|
||||
1); // row_shl10
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][14], 0x10a, 0xf, 0xf, 1);
|
||||
accm0 += __builtin_amdgcn_mov_dpp(sum[n][y][7], 0x10b, 0xf, 0xf,
|
||||
1); // row_shl11
|
||||
accm16 += __builtin_amdgcn_mov_dpp(sum[n][y][15], 0x10b, 0xf, 0xf, 1);
|
||||
accm0 += __shfl(accm0, 36);
|
||||
accm16 += __shfl(accm16, 52);
|
||||
sum[n][y][0] = accm0 + __shfl(accm16, 16);
|
||||
accm0 += __shfl_down(accm0, 20);
|
||||
accm0 += __shfl_down(accm0, 40);
|
||||
sum[n][y][0] = accm0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2242,16 +2210,16 @@ void wvSplitKQ(const at::Tensor& in_b, const at::Tensor& in_a,
|
||||
: nullptr;
|
||||
switch (N_in) {
|
||||
case 1:
|
||||
WVSPLITKQ(12, 2, 2, 2, 2, 1)
|
||||
WVSPLITKQ(16, 2, 2, 2, 2, 1)
|
||||
break;
|
||||
case 2:
|
||||
WVSPLITKQ(12, 2, 2, 2, 2, 2)
|
||||
WVSPLITKQ(16, 2, 2, 2, 2, 2)
|
||||
break;
|
||||
case 3:
|
||||
WVSPLITKQ(8, 2, 2, 1, 1, 3)
|
||||
WVSPLITKQ(16, 2, 2, 2, 2, 3)
|
||||
break;
|
||||
case 4:
|
||||
WVSPLITKQ(4, 2, 2, 1, 1, 4)
|
||||
WVSPLITKQ(16, 2, 2, 2, 2, 4)
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
|
||||
@@ -305,6 +305,14 @@ RUN --mount=type=bind,from=export_vllm,src=/,target=/install \
|
||||
RUN --mount=type=bind,from=build_rixl,src=/app/install,target=/rixl_install \
|
||||
uv pip install --system /rixl_install/*.whl
|
||||
|
||||
# RIXL/MoRIIO runtime dependencies (RDMA userspace libraries)
|
||||
RUN apt-get update -q -y && apt-get install -q -y \
|
||||
librdmacm1 \
|
||||
libibverbs1 \
|
||||
ibverbs-providers \
|
||||
ibverbs-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /vllm-workspace
|
||||
ARG COMMON_WORKDIR
|
||||
COPY --from=build_vllm ${COMMON_WORKDIR}/vllm /vllm-workspace
|
||||
@@ -330,6 +338,11 @@ RUN bash /tmp/install_torchcodec.sh \
|
||||
# Copy in the v1 package (for python-only install test group)
|
||||
COPY --from=export_vllm /vllm_v1 /usr/local/lib/python${PYTHON_VERSION}/dist-packages/vllm/v1
|
||||
|
||||
# Set MIOPEN ENVS to resolve performance regressions in MIOpen 3D convolution kernel
|
||||
# See: https://github.com/pytorch/pytorch/issues/169857
|
||||
ENV MIOPEN_DEBUG_CONV_DIRECT=0
|
||||
ENV MIOPEN_DEBUG_CONV_GEMM=0
|
||||
|
||||
# Source code is used in the `python_only_compile.sh` test
|
||||
# We hide it inside `src/` so that this source code
|
||||
# will not be imported by other tests
|
||||
|
||||
@@ -49,7 +49,13 @@ If you are developing vLLM's Python and CUDA/C++ code, install Pytorch first:
|
||||
uv pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu129
|
||||
```
|
||||
|
||||
then install vLLM using:
|
||||
Then install the necessary build dependencies from `requirements/build.txt`, skipping `torch` as it was installed in the previous step:
|
||||
|
||||
```bash
|
||||
grep -v '^torch==' requirements/build.txt | uv pip install -r -
|
||||
```
|
||||
|
||||
Finally install vLLM using:
|
||||
|
||||
```bash
|
||||
uv pip install -e . --no-build-isolation
|
||||
|
||||
@@ -208,9 +208,7 @@ configurations affect the class we ultimately get.
|
||||
|
||||
The following figure shows the class hierarchy of vLLM:
|
||||
|
||||
> <figure markdown="span">
|
||||
> { align="center" alt="query" width="100%" }
|
||||
> </figure>
|
||||

|
||||
|
||||
There are several important design choices behind this class hierarchy:
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ th {
|
||||
|
||||
| Backend | Output act. format | Quant. types | Quant. format | Async | Apply Weight On Input | Subclass |
|
||||
|---------|--------------------|--------------|---------------|-------|-----------------------|-----------|
|
||||
| naive | standard | all<sup>1</sup> | G,A,T | N | <sup>6</sup> | [layer.py][vllm.model_executor.layers.fused_moe.layer.FusedMoE |
|
||||
| naive | standard | all<sup>1</sup> | G,A,T | N | <sup>6</sup> | [layer.py][vllm.model_executor.layers.fused_moe.layer.FusedMoE] |
|
||||
| pplx | batched | fp8,int8 | G,A,T | Y | Y | [`PplxPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.pplx_prepare_finalize.PplxPrepareAndFinalize] |
|
||||
| deepep_high_throughput | standard | fp8 | G(128),A,T<sup>2</sup> | Y | Y | [`DeepEPLLPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.deepep_ll_prepare_finalize.DeepEPLLPrepareAndFinalize] |
|
||||
| deepep_low_latency | batched | fp8 | G(128),A,T<sup>3</sup> | Y | Y | [`DeepEPHTPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.deepep_ht_prepare_finalize.DeepEPHTPrepareAndFinalize] |
|
||||
| deepep_high_throughput | standard | fp8 | G(128),A,T<sup>2</sup> | Y | Y | [`DeepEPHTPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.deepep_ht_prepare_finalize.DeepEPHTPrepareAndFinalize] |
|
||||
| deepep_low_latency | batched | fp8 | G(128),A,T<sup>3</sup> | Y | Y | [`DeepEPLLPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.deepep_ll_prepare_finalize.DeepEPLLPrepareAndFinalize] |
|
||||
| flashinfer_all2allv | standard | nvfp4,fp8 | G,A,T | N | N | [`FlashInferA2APrepareAndFinalize`][vllm.model_executor.layers.fused_moe.flashinfer_a2a_prepare_finalize.FlashInferA2APrepareAndFinalize] |
|
||||
| MoEPrepareAndFinalizeNoEP<sup>5</sup> | standard | fp8,int8 | G,A,T | N | Y | [`MoEPrepareAndFinalizeNoEP`][vllm.model_executor.layers.fused_moe.prepare_finalize.MoEPrepareAndFinalizeNoEP] |
|
||||
| BatchedPrepareAndFinalize<sup>5</sup> | batched | fp8,int8 | G,A,T | N | Y | [`BatchedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.fused_batched_moe.BatchedPrepareAndFinalize] |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
It's recommended to use [uv](https://docs.astral.sh/uv/), a very fast Python environment manager, to create and manage Python environments. Please follow the [documentation](https://docs.astral.sh/uv/#getting-started) to install `uv`. After installing `uv`, you can create a new Python environment using the following commands:
|
||||
|
||||
```bash
|
||||
uv venv --python 3.12 --seed
|
||||
uv venv --python 3.12 --seed --managed-python
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
@@ -79,13 +79,15 @@ Specially, committers are almost all area owners. They author subsystems, review
|
||||
|
||||
For a full list of committers and their respective areas, see the [committers](./committers.md) page.
|
||||
|
||||
#### Nomination Process
|
||||
#### Committer Proposal Process
|
||||
|
||||
Any committer can nominate candidates via our private mailing list:
|
||||
Any committer can nominate candidates via our private committer mailing list. The process runs as follows:
|
||||
|
||||
1. **Nominate**: Any committer may nominate a candidate by email to the private maintainers’ list, citing evidence mapped to the pre‑existing standards with links to PRs, reviews, RFCs, issues, benchmarks, and adoption evidence.
|
||||
2. **Vote**: The lead maintainers will group voices support or concerns. Shared concerns can stop the process. The vote typically last 3 working days. For concerns, committers group discuss the clear criteria for such person to be nominated again. The lead maintainers will make the final decision.
|
||||
3. **Confirm**: The lead maintainers send invitation, update CODEOWNERS, assign permissions, add to communications channels (mailing list and Slack).
|
||||
1. **Nominate**: A committer sends email to the committer group to nominate a candidate, highlighting the candidate’s contributions (e.g., links to PRs, reviews, RFCs, issues, benchmarks, and adoption evidence) and how they map to the standards below.
|
||||
2. **Discuss and vote**: The committer group discusses the nomination, votes, and voices concerns if needed. Shared concerns can stop the process. For concerns, the group discusses clear criteria for the person to be nominated again. Most cases are decided by consensus; in contentious cases, the lead maintainers resolve conflicts and make the decision.
|
||||
3. **Feedback period**: After a two-week feedback period (allowing time for any last input or concerns), if no blocking concerns arise and the nominator confirms with lead maintainer group to move forward (via the mailing list or committers slack channel), the nominator sends an invitation to the candidate asking them to open a PR to update their code ownership (e.g., CODEOWNERS and committers list).
|
||||
4. **Permissions and onboarding**: In parallel, the lead maintainers assign the necessary permissions in GitHub and add the new member to the committer mailing list, the committer-only Slack channel, and other communications channels as appropriate.
|
||||
5. **Finalize**: Once the CODEOWNERS/committer PR is ready and permissions are in place, the PR is merged and the new committer is welcomed.
|
||||
|
||||
Committership is highly selective and merit based. The selection criteria requires:
|
||||
|
||||
|
||||
@@ -682,7 +682,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
|
||||
| `Blip2ForConditionalGeneration` | BLIP-2 | T + I<sup>E</sup> | `Salesforce/blip2-opt-2.7b`, `Salesforce/blip2-opt-6.7b`, etc. | ✅︎ | ✅︎ |
|
||||
| `ChameleonForConditionalGeneration` | Chameleon | T + I | `facebook/chameleon-7b`, etc. | | ✅︎ |
|
||||
| `Cohere2VisionForConditionalGeneration` | Command A Vision | T + I<sup>+</sup> | `CohereLabs/command-a-vision-07-2025`, etc. | | ✅︎ |
|
||||
| `DeepseekVLV2ForCausalLM`<sup>^</sup> | DeepSeek-VL2 | T + I<sup>+</sup> | `deepseek-ai/deepseek-vl2-tiny`, `deepseek-ai/deepseek-vl2-small`, `deepseek-ai/deepseek-vl2`, etc. | | ✅︎ |
|
||||
| `DeepseekVLV2ForCausalLM` | DeepSeek-VL2 | T + I<sup>+</sup> | `deepseek-ai/deepseek-vl2-tiny`, `deepseek-ai/deepseek-vl2-small`, `deepseek-ai/deepseek-vl2`, etc. | | ✅︎ |
|
||||
| `DeepseekOCRForCausalLM` | DeepSeek-OCR | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-OCR`, etc. | ✅︎ | ✅︎ |
|
||||
| `DeepseekOCR2ForCausalLM` | DeepSeek-OCR-2 | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-OCR-2`, etc. | ✅︎ | ✅︎ |
|
||||
| `Eagle2_5_VLForConditionalGeneration` | Eagle2.5-VL | T + I<sup>E+</sup> | `nvidia/Eagle2.5-8B`, etc. | ✅︎ | ✅︎ |
|
||||
@@ -762,10 +762,8 @@ Some models are supported only via the [Transformers modeling backend](#transfor
|
||||
|--------------|--------|--------|-------------------|-----------------------------|-----------------------------------------|
|
||||
| `Emu3ForConditionalGeneration` | Emu3 | T + I | `BAAI/Emu3-Chat-hf` | ✅︎ | ✅︎ |
|
||||
|
||||
<sup>^</sup> You need to set the architecture name via `--hf-overrides` to match the one in vLLM.
|
||||
• For example, to use DeepSeek-VL2 series models:
|
||||
`--hf-overrides '{"architectures": ["DeepseekVLV2ForCausalLM"]}'`
|
||||
<sup>E</sup> Pre-computed embeddings can be inputted for this modality.
|
||||
<sup>^</sup> You need to set the architecture name via `--hf-overrides` to match the one in vLLM.</br>
|
||||
<sup>E</sup> Pre-computed embeddings can be inputted for this modality.</br>
|
||||
<sup>+</sup> Multiple items can be inputted per text prompt for this modality.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -177,7 +177,10 @@ class QKRoPEKVCacheTestModel(torch.nn.Module):
|
||||
def ops_in_model_before(self) -> list[torch._ops.OpOverload]:
|
||||
ops = []
|
||||
if self.enable_rope_custom_op:
|
||||
ops.append(ROTARY_OP)
|
||||
if rocm_aiter_ops.is_triton_rotary_embed_enabled():
|
||||
ops.append(torch.ops.vllm.rocm_aiter_triton_rotary_embedding.default)
|
||||
else:
|
||||
ops.append(ROTARY_OP)
|
||||
else:
|
||||
ops.append(INDEX_SELECT_OP)
|
||||
ops.append(torch.ops.vllm.unified_kv_cache_update.default)
|
||||
@@ -196,6 +199,7 @@ class QKRoPEKVCacheTestModel(torch.nn.Module):
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("enable_rope_custom_op", [True]) # [True, False])
|
||||
@pytest.mark.parametrize("enable_aiter_triton_rope", [True, False])
|
||||
@pytest.mark.parametrize("num_heads", [64])
|
||||
@pytest.mark.parametrize("num_kv_heads", [8])
|
||||
@pytest.mark.parametrize("head_size", [64])
|
||||
@@ -210,6 +214,7 @@ class QKRoPEKVCacheTestModel(torch.nn.Module):
|
||||
def test_rope_kvcache_fusion(
|
||||
attn_backend: AttentionBackendEnum,
|
||||
enable_rope_custom_op: bool,
|
||||
enable_aiter_triton_rope: bool,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
@@ -245,6 +250,9 @@ def test_rope_kvcache_fusion(
|
||||
|
||||
with vllm.config.set_current_vllm_config(vllm_config), monkeypatch.context() as m:
|
||||
m.setenv("VLLM_ROCM_USE_AITER", "1")
|
||||
m.setenv(
|
||||
"VLLM_ROCM_USE_AITER_TRITON_ROPE", "1" if enable_aiter_triton_rope else "0"
|
||||
)
|
||||
rocm_aiter_ops.refresh_env_variables()
|
||||
|
||||
model = QKRoPEKVCacheTestModel(
|
||||
|
||||
@@ -447,7 +447,7 @@ def test_metrics_exist_run_batch():
|
||||
"--model",
|
||||
"intfloat/multilingual-e5-small",
|
||||
"--enable-metrics",
|
||||
"--url",
|
||||
"--host",
|
||||
base_url,
|
||||
"--port",
|
||||
port,
|
||||
|
||||
@@ -39,6 +39,7 @@ def pairs_of_event_types() -> dict[str, str]:
|
||||
"response.mcp_call.completed": "response.mcp_call.in_progress",
|
||||
"response.function_call_arguments.done": "response.function_call_arguments.delta", # noqa: E501
|
||||
"response.code_interpreter_call_code.done": "response.code_interpreter_call_code.delta", # noqa: E501
|
||||
"response.code_interpreter_call.completed": "response.code_interpreter_call.in_progress", # noqa: E501
|
||||
"response.web_search_call.completed": "response.web_search_call.in_progress",
|
||||
}
|
||||
# fmt: on
|
||||
@@ -108,29 +109,19 @@ def events_contain_type(events: list, type_substring: str) -> bool:
|
||||
return any(type_substring in getattr(e, "type", "") for e in events)
|
||||
|
||||
|
||||
def validate_streaming_event_stack(
|
||||
events: list, pairs_of_event_types: dict[str, str]
|
||||
) -> None:
|
||||
"""Validate that streaming events are properly nested/paired."""
|
||||
def _validate_event_pairing(events: list, pairs_of_event_types: dict[str, str]) -> None:
|
||||
"""Validate that streaming events are properly nested/paired.
|
||||
|
||||
Derives push/pop sets from *pairs_of_event_types* so that every
|
||||
start/end pair in the dict is handled automatically.
|
||||
"""
|
||||
start_events = set(pairs_of_event_types.values())
|
||||
end_events = set(pairs_of_event_types.keys())
|
||||
|
||||
stack: list[str] = []
|
||||
for event in events:
|
||||
etype = event.type
|
||||
if etype == "response.created":
|
||||
stack.append(etype)
|
||||
elif etype == "response.completed":
|
||||
assert stack and stack[-1] == pairs_of_event_types[etype], (
|
||||
f"Unexpected stack top for {etype}: "
|
||||
f"got {stack[-1] if stack else '<empty>'}"
|
||||
)
|
||||
stack.pop()
|
||||
elif etype.endswith("added") or etype == "response.mcp_call.in_progress":
|
||||
stack.append(etype)
|
||||
elif etype.endswith("delta"):
|
||||
if stack and stack[-1] == etype:
|
||||
continue
|
||||
stack.append(etype)
|
||||
elif etype.endswith("done") or etype == "response.mcp_call.completed":
|
||||
assert etype in pairs_of_event_types, f"Unknown done event: {etype}"
|
||||
if etype in end_events:
|
||||
expected_start = pairs_of_event_types[etype]
|
||||
assert stack and stack[-1] == expected_start, (
|
||||
f"Stack mismatch for {etype}: "
|
||||
@@ -138,9 +129,180 @@ def validate_streaming_event_stack(
|
||||
f"got {stack[-1] if stack else '<empty>'}"
|
||||
)
|
||||
stack.pop()
|
||||
elif etype in start_events:
|
||||
# Consecutive deltas of the same type share a single stack slot.
|
||||
if etype.endswith("delta") and stack and stack[-1] == etype:
|
||||
continue
|
||||
stack.append(etype)
|
||||
# else: passthrough event (e.g. response.in_progress,
|
||||
# web_search_call.searching, code_interpreter_call.interpreting)
|
||||
assert len(stack) == 0, f"Unclosed events on stack: {stack}"
|
||||
|
||||
|
||||
def _validate_event_ordering(events: list) -> None:
|
||||
"""Validate that envelope events appear in the correct positions."""
|
||||
assert len(events) >= 2, f"Expected at least 2 events, got {len(events)}"
|
||||
|
||||
# First event must be response.created
|
||||
assert events[0].type == "response.created", (
|
||||
f"First event must be response.created, got {events[0].type}"
|
||||
)
|
||||
# Last event must be response.completed
|
||||
assert events[-1].type == "response.completed", (
|
||||
f"Last event must be response.completed, got {events[-1].type}"
|
||||
)
|
||||
|
||||
# response.in_progress, if present, must be the second event
|
||||
in_progress_indices = [
|
||||
i for i, e in enumerate(events) if e.type == "response.in_progress"
|
||||
]
|
||||
if in_progress_indices:
|
||||
assert in_progress_indices == [1], (
|
||||
f"response.in_progress must be the second event, "
|
||||
f"found at indices {in_progress_indices}"
|
||||
)
|
||||
|
||||
# Exactly one created and one completed
|
||||
created_count = sum(1 for e in events if e.type == "response.created")
|
||||
completed_count = sum(1 for e in events if e.type == "response.completed")
|
||||
assert created_count == 1, (
|
||||
f"Expected exactly 1 response.created, got {created_count}"
|
||||
)
|
||||
assert completed_count == 1, (
|
||||
f"Expected exactly 1 response.completed, got {completed_count}"
|
||||
)
|
||||
|
||||
|
||||
def _validate_field_consistency(events: list) -> None:
|
||||
"""Validate item_id, output_index, and content_index consistency.
|
||||
|
||||
Tracks the active output item established by ``output_item.added``
|
||||
and verifies that all subsequent events for that item carry matching
|
||||
identifiers until ``output_item.done`` closes it.
|
||||
"""
|
||||
_SESSION_EVENTS = {
|
||||
"response.created",
|
||||
"response.in_progress",
|
||||
"response.completed",
|
||||
}
|
||||
|
||||
active_item_id: str | None = None
|
||||
active_output_index: int | None = None
|
||||
last_output_index: int = -1
|
||||
active_content_index: int | None = None
|
||||
|
||||
for event in events:
|
||||
etype = event.type
|
||||
|
||||
if etype in _SESSION_EVENTS:
|
||||
continue
|
||||
|
||||
# --- output_item.added: opens a new item ------------------
|
||||
if etype == "response.output_item.added":
|
||||
item = getattr(event, "item", None)
|
||||
output_index = getattr(event, "output_index", None)
|
||||
|
||||
assert item is not None, "output_item.added must have an item"
|
||||
item_id = getattr(item, "id", None)
|
||||
assert item_id, "output_item.added item must have an id"
|
||||
|
||||
# output_index must be non-decreasing across items
|
||||
if output_index is not None:
|
||||
assert output_index >= last_output_index, (
|
||||
f"output_index went backwards: {output_index} < {last_output_index}"
|
||||
)
|
||||
last_output_index = output_index
|
||||
|
||||
active_item_id = item_id
|
||||
active_output_index = output_index
|
||||
active_content_index = None
|
||||
continue
|
||||
|
||||
# --- output_item.done: closes the active item -------------
|
||||
if etype == "response.output_item.done":
|
||||
item = getattr(event, "item", None)
|
||||
output_index = getattr(event, "output_index", None)
|
||||
|
||||
assert item is not None, "output_item.done must have an item"
|
||||
done_item_id = getattr(item, "id", None)
|
||||
|
||||
if active_item_id is not None and done_item_id:
|
||||
assert done_item_id == active_item_id, (
|
||||
f"output_item.done item.id mismatch: "
|
||||
f"expected {active_item_id}, got {done_item_id}"
|
||||
)
|
||||
if active_output_index is not None and output_index is not None:
|
||||
assert output_index == active_output_index, (
|
||||
f"output_item.done output_index mismatch: "
|
||||
f"expected {active_output_index}, got {output_index}"
|
||||
)
|
||||
|
||||
active_item_id = None
|
||||
active_output_index = None
|
||||
active_content_index = None
|
||||
continue
|
||||
|
||||
# --- content_part / reasoning_part added: sets content_index
|
||||
if etype in (
|
||||
"response.content_part.added",
|
||||
"response.reasoning_part.added",
|
||||
):
|
||||
_assert_item_fields(event, etype, active_item_id, active_output_index)
|
||||
active_content_index = getattr(event, "content_index", None)
|
||||
continue
|
||||
|
||||
# --- all other item-level events --------------------------
|
||||
_assert_item_fields(event, etype, active_item_id, active_output_index)
|
||||
|
||||
# content_index (only meaningful on events that carry it)
|
||||
content_index = getattr(event, "content_index", None)
|
||||
if content_index is not None and active_content_index is not None:
|
||||
assert content_index == active_content_index, (
|
||||
f"{etype} content_index mismatch: "
|
||||
f"expected {active_content_index}, got {content_index}"
|
||||
)
|
||||
|
||||
|
||||
def _assert_item_fields(
|
||||
event,
|
||||
etype: str,
|
||||
active_item_id: str | None,
|
||||
active_output_index: int | None,
|
||||
) -> None:
|
||||
"""Check that *event*'s item_id and output_index match the active item."""
|
||||
event_item_id = getattr(event, "item_id", None)
|
||||
output_index = getattr(event, "output_index", None)
|
||||
|
||||
if active_item_id is not None and event_item_id is not None:
|
||||
assert event_item_id == active_item_id, (
|
||||
f"{etype} item_id mismatch: expected {active_item_id}, got {event_item_id}"
|
||||
)
|
||||
if active_output_index is not None and output_index is not None:
|
||||
assert output_index == active_output_index, (
|
||||
f"{etype} output_index mismatch: "
|
||||
f"expected {active_output_index}, got {output_index}"
|
||||
)
|
||||
|
||||
|
||||
def validate_streaming_event_stack(
|
||||
events: list, pairs_of_event_types: dict[str, str]
|
||||
) -> None:
|
||||
"""Validate streaming events: pairing, ordering, and field consistency.
|
||||
|
||||
Checks three aspects:
|
||||
1. **Event pairing** — start/end events are properly nested
|
||||
(stack-based matching derived from *pairs_of_event_types*).
|
||||
2. **Event ordering** — envelope events (``created``,
|
||||
``in_progress``, ``completed``) appear at the correct positions.
|
||||
3. **Field consistency** — ``item_id``, ``output_index``, and
|
||||
``content_index`` are consistent across related events within
|
||||
each output item's lifecycle.
|
||||
"""
|
||||
_validate_event_pairing(events, pairs_of_event_types)
|
||||
_validate_event_ordering(events)
|
||||
_validate_field_consistency(events)
|
||||
|
||||
|
||||
def log_response_diagnostics(
|
||||
response,
|
||||
*,
|
||||
|
||||
@@ -910,21 +910,25 @@ async def test_function_calling_no_code_interpreter_events(
|
||||
reason="This test is flaky in CI, needs investigation and "
|
||||
"potential fixes in the code interpreter MCP implementation."
|
||||
)
|
||||
async def test_mcp_code_interpreter_streaming(client: OpenAI, model_name: str, server):
|
||||
tools = [{"type": "mcp", "server_label": "code_interpreter"}]
|
||||
async def test_code_interpreter_streaming(
|
||||
client: OpenAI,
|
||||
model_name: str,
|
||||
pairs_of_event_types: dict[str, str],
|
||||
):
|
||||
tools = [{"type": "code_interpreter", "container": {"type": "auto"}}]
|
||||
input_text = (
|
||||
"Calculate 123 * 456 using python. "
|
||||
"The python interpreter is not stateful and you must "
|
||||
"print to see the output."
|
||||
)
|
||||
|
||||
def _has_mcp_call(evts: list) -> bool:
|
||||
return events_contain_type(evts, "mcp_call")
|
||||
def _has_code_interpreter(evts: list) -> bool:
|
||||
return events_contain_type(evts, "code_interpreter")
|
||||
|
||||
events = await retry_streaming_for(
|
||||
client,
|
||||
model=model_name,
|
||||
validate_events=_has_mcp_call,
|
||||
validate_events=_has_code_interpreter,
|
||||
input=input_text,
|
||||
tools=tools,
|
||||
temperature=0.0,
|
||||
@@ -936,59 +940,36 @@ async def test_mcp_code_interpreter_streaming(client: OpenAI, model_name: str, s
|
||||
event_types = [e.type for e in events]
|
||||
event_types_set = set(event_types)
|
||||
logger.info(
|
||||
"\n====== MCP Streaming Diagnostics ======\n"
|
||||
"\n====== Code Interpreter Streaming Diagnostics ======\n"
|
||||
"Event count: %d\n"
|
||||
"Event types (in order): %s\n"
|
||||
"Unique event types: %s\n"
|
||||
"=======================================",
|
||||
"====================================================",
|
||||
len(events),
|
||||
event_types,
|
||||
sorted(event_types_set),
|
||||
)
|
||||
|
||||
# Verify the full MCP streaming lifecycle
|
||||
assert "response.output_item.added" in event_types_set, (
|
||||
f"MCP call was not added. Events: {sorted(event_types_set)}"
|
||||
)
|
||||
assert "response.mcp_call.in_progress" in event_types_set, (
|
||||
f"MCP call in_progress not seen. Events: {sorted(event_types_set)}"
|
||||
)
|
||||
assert "response.mcp_call_arguments.delta" in event_types_set, (
|
||||
f"MCP arguments delta not seen. Events: {sorted(event_types_set)}"
|
||||
)
|
||||
assert "response.mcp_call_arguments.done" in event_types_set, (
|
||||
f"MCP arguments done not seen. Events: {sorted(event_types_set)}"
|
||||
)
|
||||
assert "response.mcp_call.completed" in event_types_set, (
|
||||
f"MCP call completed not seen. Events: {sorted(event_types_set)}"
|
||||
)
|
||||
assert "response.output_item.done" in event_types_set, (
|
||||
f"MCP item done not seen. Events: {sorted(event_types_set)}"
|
||||
)
|
||||
# Structural validation (pairing, ordering, field consistency)
|
||||
validate_streaming_event_stack(events, pairs_of_event_types)
|
||||
|
||||
# Validate specific MCP event details
|
||||
# Validate code interpreter item fields
|
||||
for event in events:
|
||||
if event.type == "response.output_item.added":
|
||||
if hasattr(event.item, "type") and event.item.type == "mcp_call":
|
||||
assert event.item.name == "python"
|
||||
assert event.item.server_label == "code_interpreter"
|
||||
elif event.type == "response.mcp_call_arguments.done":
|
||||
assert event.name == "python"
|
||||
assert event.arguments is not None
|
||||
if (
|
||||
event.type == "response.output_item.added"
|
||||
and hasattr(event.item, "type")
|
||||
and event.item.type == "code_interpreter_call"
|
||||
):
|
||||
assert event.item.status == "in_progress"
|
||||
elif event.type == "response.code_interpreter_call_code.done":
|
||||
assert event.code is not None
|
||||
elif (
|
||||
event.type == "response.output_item.done"
|
||||
and hasattr(event.item, "type")
|
||||
and event.item.type == "mcp_call"
|
||||
and event.item.type == "code_interpreter_call"
|
||||
):
|
||||
assert event.item.name == "python"
|
||||
assert event.item.status == "completed"
|
||||
|
||||
# code_interpreter events should NOT appear when using MCP type
|
||||
code_interp_events = [e.type for e in events if "code_interpreter" in e.type]
|
||||
assert not code_interp_events, (
|
||||
"Should not see code_interpreter events when using MCP type, "
|
||||
f"but got: {code_interp_events}"
|
||||
)
|
||||
assert event.item.code is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -241,81 +241,3 @@ class TestMCPEnabled:
|
||||
)
|
||||
|
||||
validate_streaming_event_stack(events, pairs_of_event_types)
|
||||
|
||||
assert events_contain_type(events, "mcp_call"), (
|
||||
f"No mcp_call events after retries. "
|
||||
f"Event types: {sorted({e.type for e in events})}"
|
||||
)
|
||||
|
||||
|
||||
class TestMCPDisabled:
|
||||
"""Tests that MCP tools are not executed when the env flag is unset."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def mcp_disabled_server(self):
|
||||
env_dict = {
|
||||
**BASE_TEST_ENV,
|
||||
"VLLM_ENABLE_RESPONSES_API_STORE": "1",
|
||||
"PYTHON_EXECUTION_BACKEND": "dangerously_use_uv",
|
||||
"VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS": "1",
|
||||
}
|
||||
with RemoteOpenAIServer(
|
||||
MODEL_NAME, list(_BASE_SERVER_ARGS), env_dict=env_dict
|
||||
) as remote_server:
|
||||
yield remote_server
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(self, mcp_disabled_server):
|
||||
async with mcp_disabled_server.get_async_client() as async_client:
|
||||
yield async_client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("model_name", [MODEL_NAME])
|
||||
async def test_mcp_disabled_server_does_not_execute(
|
||||
self, client: OpenAI, model_name: str
|
||||
):
|
||||
"""When MCP is disabled the model may still attempt tool calls
|
||||
(tool descriptions can remain in the prompt), but the server
|
||||
must NOT execute them."""
|
||||
response = await client.responses.create(
|
||||
model=model_name,
|
||||
input=(
|
||||
"Execute the following code if the tool is present: "
|
||||
"import random; print(random.randint(1, 1000000))"
|
||||
),
|
||||
tools=[
|
||||
{
|
||||
"type": "mcp",
|
||||
"server_label": "code_interpreter",
|
||||
"server_url": "http://localhost:8888",
|
||||
}
|
||||
],
|
||||
temperature=0.0,
|
||||
extra_body={"enable_response_messages": True},
|
||||
)
|
||||
assert response is not None
|
||||
assert response.status == "completed"
|
||||
|
||||
log_response_diagnostics(response, label="MCP Disabled")
|
||||
|
||||
# Server must not have executed any tool calls
|
||||
for message in response.output_messages:
|
||||
author = message.get("author", {})
|
||||
assert not (
|
||||
author.get("role") == "tool"
|
||||
and (author.get("name") or "").startswith("python")
|
||||
), (
|
||||
"Server executed a python tool call even though MCP is "
|
||||
f"disabled. Message: {message}"
|
||||
)
|
||||
|
||||
# No completed mcp_call output items
|
||||
for item in response.output:
|
||||
if getattr(item, "type", None) == "mcp_call":
|
||||
assert getattr(item, "status", None) != "completed", (
|
||||
"MCP call should not be completed when MCP is disabled"
|
||||
)
|
||||
|
||||
# No developer messages injected
|
||||
for message in response.input_messages:
|
||||
assert message.get("author", {}).get("role") != "developer"
|
||||
|
||||
@@ -219,3 +219,23 @@ async def test_completion_error_stream():
|
||||
f"Expected error message in chunks: {chunks}"
|
||||
)
|
||||
assert chunks[-1] == "data: [DONE]\n\n"
|
||||
|
||||
|
||||
def test_negative_prompt_token_ids_nested():
|
||||
"""Negative token IDs in prompt (nested list) should raise validation error."""
|
||||
with pytest.raises(Exception, match="greater than or equal to 0"):
|
||||
CompletionRequest(
|
||||
model=MODEL_NAME,
|
||||
prompt=[[-1]],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
|
||||
def test_negative_prompt_token_ids_flat():
|
||||
"""Negative token IDs in prompt (flat list) should raise validation error."""
|
||||
with pytest.raises(Exception, match="greater than or equal to 0"):
|
||||
CompletionRequest(
|
||||
model=MODEL_NAME,
|
||||
prompt=[-1],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
@@ -10,59 +10,361 @@ import pytest
|
||||
from vllm.assets.audio import AudioAsset
|
||||
from vllm.entrypoints.openai.run_batch import BatchRequestOutput
|
||||
|
||||
MODEL_NAME = "hmellor/tiny-random-LlamaForCausalLM"
|
||||
CHAT_MODEL_NAME = "hmellor/tiny-random-LlamaForCausalLM"
|
||||
EMBEDDING_MODEL_NAME = "intfloat/multilingual-e5-small"
|
||||
RERANKER_MODEL_NAME = "BAAI/bge-reranker-v2-m3"
|
||||
REASONING_MODEL_NAME = "Qwen/Qwen3-0.6B"
|
||||
SPEECH_LARGE_MODEL_NAME = "openai/whisper-large-v3"
|
||||
SPEECH_SMALL_MODEL_NAME = "openai/whisper-small"
|
||||
|
||||
# ruff: noqa: E501
|
||||
INPUT_BATCH = (
|
||||
'{{"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {{"model": "{0}", "messages": [{{"role": "system", "content": "You are a helpful assistant."}},{{"role": "user", "content": "Hello world!"}}],"max_tokens": 1000}}}}\n'
|
||||
'{{"custom_id": "request-2", "method": "POST", "url": "/v1/chat/completions", "body": {{"model": "{0}", "messages": [{{"role": "system", "content": "You are an unhelpful assistant."}},{{"role": "user", "content": "Hello world!"}}],"max_tokens": 1000}}}}\n'
|
||||
'{{"custom_id": "request-3", "method": "POST", "url": "/v1/chat/completions", "body": {{"model": "NonExistModel", "messages": [{{"role": "system", "content": "You are an unhelpful assistant."}},{{"role": "user", "content": "Hello world!"}}],"max_tokens": 1000}}}}\n'
|
||||
'{{"custom_id": "request-4", "method": "POST", "url": "/bad_url", "body": {{"model": "{0}", "messages": [{{"role": "system", "content": "You are an unhelpful assistant."}},{{"role": "user", "content": "Hello world!"}}],"max_tokens": 1000}}}}\n'
|
||||
'{{"custom_id": "request-5", "method": "POST", "url": "/v1/chat/completions", "body": {{"stream": "True", "model": "{0}", "messages": [{{"role": "system", "content": "You are an unhelpful assistant."}},{{"role": "user", "content": "Hello world!"}}],"max_tokens": 1000}}}}'
|
||||
).format(MODEL_NAME)
|
||||
|
||||
INVALID_INPUT_BATCH = (
|
||||
'{{"invalid_field": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {{"model": "{0}", "messages": [{{"role": "system", "content": "You are a helpful assistant."}},{{"role": "user", "content": "Hello world!"}}],"max_tokens": 1000}}}}\n'
|
||||
'{{"custom_id": "request-2", "method": "POST", "url": "/v1/chat/completions", "body": {{"model": "{0}", "messages": [{{"role": "system", "content": "You are an unhelpful assistant."}},{{"role": "user", "content": "Hello world!"}}],"max_tokens": 1000}}}}'
|
||||
).format(MODEL_NAME)
|
||||
|
||||
INPUT_EMBEDDING_BATCH = (
|
||||
'{"custom_id": "request-1", "method": "POST", "url": "/v1/embeddings", "body": {"model": "intfloat/multilingual-e5-small", "input": "You are a helpful assistant."}}\n'
|
||||
'{"custom_id": "request-2", "method": "POST", "url": "/v1/embeddings", "body": {"model": "intfloat/multilingual-e5-small", "input": "You are an unhelpful assistant."}}\n'
|
||||
'{"custom_id": "request-3", "method": "POST", "url": "/v1/embeddings", "body": {"model": "intfloat/multilingual-e5-small", "input": "Hello world!"}}\n'
|
||||
'{"custom_id": "request-4", "method": "POST", "url": "/v1/embeddings", "body": {"model": "NonExistModel", "input": "Hello world!"}}'
|
||||
INPUT_BATCH = "\n".join(
|
||||
json.dumps(req)
|
||||
for req in [
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": CHAT_MODEL_NAME,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant.",
|
||||
},
|
||||
{"role": "user", "content": "Hello world!"},
|
||||
],
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-2",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": CHAT_MODEL_NAME,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an unhelpful assistant.",
|
||||
},
|
||||
{"role": "user", "content": "Hello world!"},
|
||||
],
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-3",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": "NonExistModel",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an unhelpful assistant.",
|
||||
},
|
||||
{"role": "user", "content": "Hello world!"},
|
||||
],
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-4",
|
||||
"method": "POST",
|
||||
"url": "/bad_url",
|
||||
"body": {
|
||||
"model": CHAT_MODEL_NAME,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an unhelpful assistant.",
|
||||
},
|
||||
{"role": "user", "content": "Hello world!"},
|
||||
],
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-5",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"stream": "True",
|
||||
"model": CHAT_MODEL_NAME,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an unhelpful assistant.",
|
||||
},
|
||||
{"role": "user", "content": "Hello world!"},
|
||||
],
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
INPUT_SCORE_BATCH = """{"custom_id": "request-1", "method": "POST", "url": "/score", "body": {"model": "BAAI/bge-reranker-v2-m3", "queries": "What is the capital of France?", "documents": ["The capital of Brazil is Brasilia.", "The capital of France is Paris."]}}
|
||||
{"custom_id": "request-2", "method": "POST", "url": "/v1/score", "body": {"model": "BAAI/bge-reranker-v2-m3", "queries": "What is the capital of France?", "documents": ["The capital of Brazil is Brasilia.", "The capital of France is Paris."]}}"""
|
||||
INVALID_INPUT_BATCH = "\n".join(
|
||||
json.dumps(req)
|
||||
for req in [
|
||||
{
|
||||
"invalid_field": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": CHAT_MODEL_NAME,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello world!"},
|
||||
],
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-2",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": CHAT_MODEL_NAME,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are an unhelpful assistant."},
|
||||
{"role": "user", "content": "Hello world!"},
|
||||
],
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
INPUT_RERANK_BATCH = """{"custom_id": "request-1", "method": "POST", "url": "/rerank", "body": {"model": "BAAI/bge-reranker-v2-m3", "query": "What is the capital of France?", "documents": ["The capital of Brazil is Brasilia.", "The capital of France is Paris."]}}
|
||||
{"custom_id": "request-2", "method": "POST", "url": "/v1/rerank", "body": {"model": "BAAI/bge-reranker-v2-m3", "query": "What is the capital of France?", "documents": ["The capital of Brazil is Brasilia.", "The capital of France is Paris."]}}
|
||||
{"custom_id": "request-2", "method": "POST", "url": "/v2/rerank", "body": {"model": "BAAI/bge-reranker-v2-m3", "query": "What is the capital of France?", "documents": ["The capital of Brazil is Brasilia.", "The capital of France is Paris."]}}"""
|
||||
INPUT_EMBEDDING_BATCH = "\n".join(
|
||||
json.dumps(req)
|
||||
for req in [
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/embeddings",
|
||||
"body": {
|
||||
"model": EMBEDDING_MODEL_NAME,
|
||||
"input": "You are a helpful assistant.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-2",
|
||||
"method": "POST",
|
||||
"url": "/v1/embeddings",
|
||||
"body": {
|
||||
"model": EMBEDDING_MODEL_NAME,
|
||||
"input": "You are an unhelpful assistant.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-3",
|
||||
"method": "POST",
|
||||
"url": "/v1/embeddings",
|
||||
"body": {
|
||||
"model": EMBEDDING_MODEL_NAME,
|
||||
"input": "Hello world!",
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-4",
|
||||
"method": "POST",
|
||||
"url": "/v1/embeddings",
|
||||
"body": {
|
||||
"model": "NonExistModel",
|
||||
"input": "Hello world!",
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
INPUT_REASONING_BATCH = """{"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "Qwen/Qwen3-0.6B", "messages": [{"role": "system", "content": "You are a helpful assistant."},{"role": "user", "content": "Solve this math problem: 2+2=?"}]}}
|
||||
{"custom_id": "request-2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "Qwen/Qwen3-0.6B", "messages": [{"role": "system", "content": "You are a helpful assistant."},{"role": "user", "content": "What is the capital of France?"}]}}"""
|
||||
_SCORE_RERANK_DOCUMENTS = [
|
||||
"The capital of Brazil is Brasilia.",
|
||||
"The capital of France is Paris.",
|
||||
]
|
||||
|
||||
INPUT_SCORE_BATCH = "\n".join(
|
||||
json.dumps(req)
|
||||
for req in [
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/score",
|
||||
"body": {
|
||||
"model": RERANKER_MODEL_NAME,
|
||||
"queries": "What is the capital of France?",
|
||||
"documents": _SCORE_RERANK_DOCUMENTS,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-2",
|
||||
"method": "POST",
|
||||
"url": "/v1/score",
|
||||
"body": {
|
||||
"model": RERANKER_MODEL_NAME,
|
||||
"queries": "What is the capital of France?",
|
||||
"documents": _SCORE_RERANK_DOCUMENTS,
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
INPUT_RERANK_BATCH = "\n".join(
|
||||
json.dumps(req)
|
||||
for req in [
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/rerank",
|
||||
"body": {
|
||||
"model": RERANKER_MODEL_NAME,
|
||||
"query": "What is the capital of France?",
|
||||
"documents": _SCORE_RERANK_DOCUMENTS,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-2",
|
||||
"method": "POST",
|
||||
"url": "/v1/rerank",
|
||||
"body": {
|
||||
"model": RERANKER_MODEL_NAME,
|
||||
"query": "What is the capital of France?",
|
||||
"documents": _SCORE_RERANK_DOCUMENTS,
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-2",
|
||||
"method": "POST",
|
||||
"url": "/v2/rerank",
|
||||
"body": {
|
||||
"model": RERANKER_MODEL_NAME,
|
||||
"query": "What is the capital of France?",
|
||||
"documents": _SCORE_RERANK_DOCUMENTS,
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
INPUT_REASONING_BATCH = "\n".join(
|
||||
json.dumps(req)
|
||||
for req in [
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": REASONING_MODEL_NAME,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Solve this math problem: 2+2=?"},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"custom_id": "request-2",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": REASONING_MODEL_NAME,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "What is the capital of France?"},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# This is a valid but minimal audio file for testing
|
||||
MINIMAL_WAV_BASE64 = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA="
|
||||
INPUT_TRANSCRIPTION_BATCH = (
|
||||
'{{"custom_id": "request-1", "method": "POST", "url": "/v1/audio/transcriptions", '
|
||||
'"body": {{"model": "openai/whisper-large-v3", "file_url": "data:audio/wav;base64,{}", '
|
||||
'"response_format": "json"}}}}\n'
|
||||
).format(MINIMAL_WAV_BASE64)
|
||||
json.dumps(
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/audio/transcriptions",
|
||||
"body": {
|
||||
"model": SPEECH_LARGE_MODEL_NAME,
|
||||
"file_url": f"data:audio/wav;base64,{MINIMAL_WAV_BASE64}",
|
||||
"response_format": "json",
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
INPUT_TRANSCRIPTION_HTTP_BATCH = (
|
||||
'{{"custom_id": "request-1", "method": "POST", "url": "/v1/audio/transcriptions", '
|
||||
'"body": {{"model": "openai/whisper-large-v3", "file_url": "{}", '
|
||||
'"response_format": "json"}}}}\n'
|
||||
).format(AudioAsset("mary_had_lamb").url)
|
||||
json.dumps(
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/audio/transcriptions",
|
||||
"body": {
|
||||
"model": SPEECH_LARGE_MODEL_NAME,
|
||||
"file_url": AudioAsset("mary_had_lamb").url,
|
||||
"response_format": "json",
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
INPUT_TRANSLATION_BATCH = (
|
||||
'{{"custom_id": "request-1", "method": "POST", "url": "/v1/audio/translations", '
|
||||
'"body": {{"model": "openai/whisper-small", "file_url": "{}", '
|
||||
'"response_format": "text", "language": "it", "to_language": "en", '
|
||||
'"temperature": 0.0}}}}\n'
|
||||
).format(AudioAsset("mary_had_lamb").url)
|
||||
json.dumps(
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/audio/translations",
|
||||
"body": {
|
||||
"model": SPEECH_SMALL_MODEL_NAME,
|
||||
"file_url": AudioAsset("mary_had_lamb").url,
|
||||
"response_format": "text",
|
||||
"language": "it",
|
||||
"to_language": "en",
|
||||
"temperature": 0.0,
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
WEATHER_TOOL = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_weather",
|
||||
"description": "Get the current weather in a given location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["celsius", "fahrenheit"],
|
||||
},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
INPUT_TOOL_CALLING_BATCH = json.dumps(
|
||||
{
|
||||
"custom_id": "request-1",
|
||||
"method": "POST",
|
||||
"url": "/v1/chat/completions",
|
||||
"body": {
|
||||
"model": REASONING_MODEL_NAME,
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is the weather in San Francisco?"},
|
||||
],
|
||||
"tools": [WEATHER_TOOL],
|
||||
"tool_choice": "required",
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_empty_file():
|
||||
@@ -81,7 +383,7 @@ def test_empty_file():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
"intfloat/multilingual-e5-small",
|
||||
EMBEDDING_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -108,7 +410,7 @@ def test_completions():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
MODEL_NAME,
|
||||
CHAT_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -141,7 +443,7 @@ def test_completions_invalid_input():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
MODEL_NAME,
|
||||
CHAT_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -165,7 +467,7 @@ def test_embeddings():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
"intfloat/multilingual-e5-small",
|
||||
EMBEDDING_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -196,7 +498,7 @@ def test_score(input_batch):
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
"BAAI/bge-reranker-v2-m3",
|
||||
RERANKER_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -234,7 +536,7 @@ def test_reasoning_parser():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
"Qwen/Qwen3-0.6B",
|
||||
REASONING_MODEL_NAME,
|
||||
"--reasoning-parser",
|
||||
"qwen3",
|
||||
],
|
||||
@@ -278,7 +580,7 @@ def test_transcription():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
"openai/whisper-large-v3",
|
||||
SPEECH_LARGE_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -316,7 +618,7 @@ def test_transcription_http_url():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
"openai/whisper-large-v3",
|
||||
SPEECH_LARGE_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -356,7 +658,7 @@ def test_translation():
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
"openai/whisper-small",
|
||||
SPEECH_SMALL_MODEL_NAME,
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
@@ -378,3 +680,69 @@ def test_translation():
|
||||
translation_text = response_body["text"]
|
||||
translation_text_lower = str(translation_text).strip().lower()
|
||||
assert "mary" in translation_text_lower or "lamb" in translation_text_lower
|
||||
|
||||
|
||||
def test_tool_calling():
|
||||
"""
|
||||
Test that tool calling works correctly in run_batch.
|
||||
Verifies that requests with tools return tool_calls in the response.
|
||||
"""
|
||||
with (
|
||||
tempfile.NamedTemporaryFile("w") as input_file,
|
||||
tempfile.NamedTemporaryFile("r") as output_file,
|
||||
):
|
||||
input_file.write(INPUT_TOOL_CALLING_BATCH)
|
||||
input_file.flush()
|
||||
proc = subprocess.Popen(
|
||||
[
|
||||
"vllm",
|
||||
"run-batch",
|
||||
"-i",
|
||||
input_file.name,
|
||||
"-o",
|
||||
output_file.name,
|
||||
"--model",
|
||||
REASONING_MODEL_NAME,
|
||||
"--enable-auto-tool-choice",
|
||||
"--tool-call-parser",
|
||||
"hermes",
|
||||
],
|
||||
)
|
||||
proc.communicate()
|
||||
proc.wait()
|
||||
assert proc.returncode == 0, f"{proc=}"
|
||||
|
||||
contents = output_file.read()
|
||||
for line in contents.strip().split("\n"):
|
||||
if not line.strip(): # Skip empty lines
|
||||
continue
|
||||
# Ensure that the output format conforms to the openai api.
|
||||
# Validation should throw if the schema is wrong.
|
||||
BatchRequestOutput.model_validate_json(line)
|
||||
|
||||
# Ensure that there is no error in the response.
|
||||
line_dict = json.loads(line)
|
||||
assert isinstance(line_dict, dict)
|
||||
assert line_dict["error"] is None
|
||||
|
||||
# Check that tool_calls are present in the response
|
||||
# With tool_choice="required", the model must call a tool
|
||||
response_body = line_dict["response"]["body"]
|
||||
assert response_body is not None
|
||||
message = response_body["choices"][0]["message"]
|
||||
assert "tool_calls" in message
|
||||
tool_calls = message.get("tool_calls")
|
||||
# With tool_choice="required", tool_calls must be present and non-empty
|
||||
assert tool_calls is not None
|
||||
assert isinstance(tool_calls, list)
|
||||
assert len(tool_calls) > 0
|
||||
# Verify tool_calls have the expected structure
|
||||
for tool_call in tool_calls:
|
||||
assert "id" in tool_call
|
||||
assert "type" in tool_call
|
||||
assert tool_call["type"] == "function"
|
||||
assert "function" in tool_call
|
||||
assert "name" in tool_call["function"]
|
||||
assert "arguments" in tool_call["function"]
|
||||
# Verify the tool name matches our tool definition
|
||||
assert tool_call["function"]["name"] == "get_current_weather"
|
||||
|
||||
@@ -110,6 +110,65 @@ async def call_vllm_api(
|
||||
return "", 0
|
||||
|
||||
|
||||
def _build_gsm8k_prompts(
|
||||
num_questions: int = 1319,
|
||||
num_shots: int = 5,
|
||||
) -> tuple[list[str], list[int]]:
|
||||
"""Build few-shot GSM8K completion prompts and ground-truth labels."""
|
||||
if num_questions == 0:
|
||||
return [], []
|
||||
train_data, test_data = load_gsm8k_data()
|
||||
num_questions = min(num_questions, len(test_data))
|
||||
|
||||
few_shot_examples = ""
|
||||
for i in range(num_shots):
|
||||
few_shot_examples += (
|
||||
f"Question: {train_data[i]['question']}\n"
|
||||
f"Answer: {train_data[i]['answer']}\n\n"
|
||||
)
|
||||
|
||||
prompts = []
|
||||
labels = []
|
||||
for i in range(num_questions):
|
||||
prompts.append(
|
||||
few_shot_examples + f"Question: {test_data[i]['question']}\nAnswer:"
|
||||
)
|
||||
labels.append(get_answer_value(test_data[i]["answer"]))
|
||||
|
||||
assert all(label != INVALID for label in labels), "Some labels are invalid"
|
||||
return prompts, labels
|
||||
|
||||
|
||||
def _score_gsm8k(
|
||||
states: list[str],
|
||||
output_tokens: list[int],
|
||||
labels: list[int],
|
||||
num_shots: int,
|
||||
max_tokens: int,
|
||||
latency: float,
|
||||
) -> dict[str, float | int]:
|
||||
"""Score GSM8K responses and return a results dict."""
|
||||
num_questions = len(labels)
|
||||
preds = [get_answer_value(state) for state in states]
|
||||
accuracy = np.mean(np.array(preds) == np.array(labels))
|
||||
invalid_rate = np.mean(np.array(preds) == INVALID)
|
||||
total_output_tokens = sum(output_tokens)
|
||||
tokens_per_second = total_output_tokens / latency if latency > 0 else 0.0
|
||||
|
||||
return {
|
||||
"accuracy": accuracy,
|
||||
"invalid_rate": invalid_rate,
|
||||
"latency": latency,
|
||||
"questions_per_second": num_questions / latency if latency > 0 else 0.0,
|
||||
"total_output_tokens": total_output_tokens,
|
||||
"tokens_per_second": tokens_per_second,
|
||||
"num_questions": num_questions,
|
||||
"num_shots": num_shots,
|
||||
"max_tokens": max_tokens,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
|
||||
def evaluate_gsm8k(
|
||||
num_questions: int = 1319,
|
||||
num_shots: int = 5,
|
||||
@@ -125,40 +184,17 @@ def evaluate_gsm8k(
|
||||
Returns dict with accuracy, invalid_rate, latency, etc.
|
||||
"""
|
||||
base_url = f"{host}:{port}"
|
||||
prompts, labels = _build_gsm8k_prompts(num_questions, num_shots)
|
||||
num_questions = len(prompts)
|
||||
|
||||
# Load GSM8K train and test data
|
||||
train_data, test_data = load_gsm8k_data()
|
||||
|
||||
# Limit to available test questions
|
||||
num_questions = min(num_questions, len(test_data))
|
||||
|
||||
# Build few-shot examples from train split (like lm-eval does)
|
||||
few_shot_examples = ""
|
||||
for i in range(num_shots):
|
||||
few_shot_examples += (
|
||||
f"Question: {train_data[i]['question']}\n"
|
||||
f"Answer: {train_data[i]['answer']}\n\n"
|
||||
)
|
||||
|
||||
# Prepare test questions and labels from test split
|
||||
questions = []
|
||||
labels = []
|
||||
for i in range(num_questions):
|
||||
questions.append(f"Question: {test_data[i]['question']}\nAnswer:")
|
||||
labels.append(get_answer_value(test_data[i]["answer"]))
|
||||
|
||||
assert all(label != INVALID for label in labels), "Some labels are invalid"
|
||||
|
||||
# Run evaluation
|
||||
async def run_async_evaluation():
|
||||
states: list[str] = [""] * num_questions
|
||||
output_tokens: list[int] = [0] * num_questions
|
||||
|
||||
async def get_answer(session: aiohttp.ClientSession, i: int) -> tuple[str, int]:
|
||||
prompt = few_shot_examples + questions[i]
|
||||
answer, tokens = await call_vllm_api(
|
||||
session=session,
|
||||
prompt=prompt,
|
||||
prompt=prompts[i],
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stop=["Question", "Assistant:", "<|separator|>"],
|
||||
@@ -183,27 +219,43 @@ def evaluate_gsm8k(
|
||||
states, output_tokens = asyncio.run(run_async_evaluation())
|
||||
latency = time.perf_counter() - tic
|
||||
|
||||
# Compute metrics
|
||||
preds = [get_answer_value(state) for state in states]
|
||||
accuracy = np.mean(np.array(preds) == np.array(labels))
|
||||
invalid_rate = np.mean(np.array(preds) == INVALID)
|
||||
total_output_tokens = sum(output_tokens)
|
||||
tokens_per_second = total_output_tokens / latency if latency > 0 else 0.0
|
||||
return _score_gsm8k(states, output_tokens, labels, num_shots, max_tokens, latency)
|
||||
|
||||
result = {
|
||||
"accuracy": accuracy,
|
||||
"invalid_rate": invalid_rate,
|
||||
"latency": latency,
|
||||
"questions_per_second": num_questions / latency,
|
||||
"total_output_tokens": total_output_tokens,
|
||||
"tokens_per_second": tokens_per_second,
|
||||
"num_questions": num_questions,
|
||||
"num_shots": num_shots,
|
||||
"max_tokens": max_tokens,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
return result
|
||||
def evaluate_gsm8k_offline(
|
||||
llm,
|
||||
num_questions: int = 1319,
|
||||
num_shots: int = 5,
|
||||
max_tokens: int = 256,
|
||||
temperature: float = 0.0,
|
||||
) -> dict[str, float | int]:
|
||||
"""Evaluate GSM8K accuracy using an offline vllm.LLM object.
|
||||
|
||||
Same prompts and scoring as evaluate_gsm8k(), but runs generation
|
||||
directly via llm.generate() instead of calling a server over HTTP.
|
||||
"""
|
||||
from vllm import SamplingParams
|
||||
|
||||
prompts, labels = _build_gsm8k_prompts(num_questions, num_shots)
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stop=["Question", "Assistant:", "<|separator|>"],
|
||||
)
|
||||
|
||||
print(
|
||||
f"Running offline GSM8K evaluation: {len(prompts)} questions, {num_shots}-shot"
|
||||
)
|
||||
|
||||
tic = time.perf_counter()
|
||||
outputs = llm.generate(prompts, sampling_params)
|
||||
latency = time.perf_counter() - tic
|
||||
|
||||
states = [o.outputs[0].text for o in outputs]
|
||||
output_tokens = [len(o.outputs[0].token_ids) for o in outputs]
|
||||
|
||||
return _score_gsm8k(states, output_tokens, labels, num_shots, max_tokens, latency)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -9,6 +9,7 @@ import torch
|
||||
import vllm._custom_ops as ops
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.triton_utils import triton
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
|
||||
|
||||
def cal_diff(
|
||||
@@ -124,8 +125,7 @@ def test_cutlass_mla_decode(
|
||||
q_pe = q_pe_padded
|
||||
|
||||
kv_cache_flat = blocked_k.squeeze(2)
|
||||
device_properties = torch.cuda.get_device_properties(torch.device("cuda:0"))
|
||||
sm_count = device_properties.multi_processor_count
|
||||
sm_count = num_compute_units(device.index)
|
||||
workspace_size = ops.sm100_cutlass_mla_get_workspace_size(
|
||||
max_seqlen * block_size, b, sm_count, num_kv_splits=1
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
|
||||
batched_moe_align_block_size,
|
||||
moe_align_block_size,
|
||||
)
|
||||
from vllm.utils.math_utils import round_up
|
||||
from vllm.utils.math_utils import cdiv, round_up
|
||||
from vllm.utils.torch_utils import set_random_seed
|
||||
|
||||
NUM_TOKENS = [1, 3, 256, 2256, 4096]
|
||||
@@ -142,7 +142,9 @@ def torch_moe_align_block_size(
|
||||
device=topk_ids.device,
|
||||
)
|
||||
max_num_blocks = (max_num_tokens_padded + block_size - 1) // block_size
|
||||
expert_ids = torch.zeros(max_num_blocks, dtype=torch.int32, device=topk_ids.device)
|
||||
expert_ids = torch.full(
|
||||
(max_num_blocks,), -1, dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
|
||||
current_pos = 0
|
||||
current_block = 0
|
||||
@@ -234,9 +236,10 @@ def test_moe_align_block_size(
|
||||
assert len(valid_tokens) == total_tokens, (
|
||||
f"Should have exactly {total_tokens} valid tokens, got {len(valid_tokens)}"
|
||||
)
|
||||
assert (actual_expert_ids >= 0).all() and (actual_expert_ids < num_experts).all(), (
|
||||
"expert_ids should contain valid expert indices"
|
||||
)
|
||||
actual_num_blocks = cdiv(int(actual_num_tokens.item()), block_size)
|
||||
assert (actual_expert_ids[:actual_num_blocks] >= 0).all() and (
|
||||
actual_expert_ids[:actual_num_blocks] < num_experts
|
||||
).all(), "expert_ids should contain valid expert indices"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m", [16, 32, 2048])
|
||||
|
||||
@@ -13,6 +13,7 @@ from vllm.model_executor.layers.quantization.utils.allspark_utils import (
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import quantize_weights
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.scalar_type import scalar_types
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
|
||||
|
||||
def is_gptq_allspark_supported(min_capability: int, max_capability: int) -> bool:
|
||||
@@ -78,7 +79,7 @@ def test_gptq_allspark_gemm_ampere(mnk_factors, group_size, has_zp, dtype):
|
||||
if has_zp:
|
||||
zp = zp.to(dtype)
|
||||
properties = torch.cuda.get_device_properties(qw.device.index)
|
||||
sm_count = properties.multi_processor_count
|
||||
sm_count = num_compute_units(qw.device.index)
|
||||
sm_version = properties.major * 10 + properties.minor
|
||||
|
||||
n_32align = (n + 32 - 1) // 32 * 32
|
||||
|
||||
@@ -9,7 +9,7 @@ import vllm._custom_ops as ops
|
||||
from tests.kernels.quant_utils import ref_dynamic_per_tensor_fp8_quant
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.platforms.rocm import on_gfx950
|
||||
from vllm.utils.platform_utils import get_cu_count
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
|
||||
DTYPES = [torch.bfloat16, torch.float16]
|
||||
BIAS_MODES = [0, 1, 2]
|
||||
@@ -121,7 +121,7 @@ def pad_fp8(weight):
|
||||
@pytest.mark.skipif(not on_gfx950(), reason="only meant for gfx950")
|
||||
def test_rocm_wvsplitkrc_kernel(xnorm, n, k, m, dtype, seed, bias_mode):
|
||||
torch.manual_seed(seed)
|
||||
cu_count = get_cu_count()
|
||||
cu_count = num_compute_units()
|
||||
|
||||
# Next ^2 of n
|
||||
N_p2 = 1 << (n - 1).bit_length()
|
||||
@@ -186,7 +186,7 @@ def test_rocm_llmm1_kernel(n, k, m, dtype, rows_per_block, seed):
|
||||
@pytest.mark.skipif(not current_platform.is_rocm(), reason="only test for rocm")
|
||||
def test_rocm_wvsplitk_kernel(n, k, m, dtype, seed):
|
||||
torch.manual_seed(seed)
|
||||
cu_count = get_cu_count()
|
||||
cu_count = num_compute_units()
|
||||
|
||||
A = torch.rand(n, k, dtype=dtype, device="cuda") - 0.5
|
||||
B = torch.rand(m, k, dtype=dtype, device="cuda") - 0.5
|
||||
@@ -203,7 +203,7 @@ def test_rocm_wvsplitk_kernel(n, k, m, dtype, seed):
|
||||
@pytest.mark.skipif(not current_platform.is_rocm(), reason="only test for rocm")
|
||||
def test_rocm_wvsplitk_bias1D_kernel(n, k, m, dtype, seed):
|
||||
torch.manual_seed(seed)
|
||||
cu_count = get_cu_count()
|
||||
cu_count = num_compute_units()
|
||||
|
||||
xavier = math.sqrt(2 / k) # normalize to avoid large output-bias deltas
|
||||
A = (torch.rand(n, k, dtype=dtype, device="cuda") - 0.5) * xavier
|
||||
@@ -222,7 +222,7 @@ def test_rocm_wvsplitk_bias1D_kernel(n, k, m, dtype, seed):
|
||||
@pytest.mark.skipif(not current_platform.is_rocm(), reason="only test for rocm")
|
||||
def test_rocm_wvsplitk_bias2D_kernel(n, k, m, dtype, seed):
|
||||
torch.manual_seed(seed)
|
||||
cu_count = get_cu_count()
|
||||
cu_count = num_compute_units()
|
||||
|
||||
xavier = math.sqrt(2 / k) # normalize to avoid large output-bias deltas
|
||||
A = (torch.rand(n, k, dtype=dtype, device="cuda") - 0.5) * xavier
|
||||
@@ -267,7 +267,7 @@ def test_rocm_wvsplitk_fp8_kernel(
|
||||
ref_out = torch._scaled_mm(
|
||||
A, B.t(), out_dtype=dtype, scale_a=scale_a, scale_b=scale_b, bias=BIAS
|
||||
)
|
||||
out = ops.wvSplitKQ(B, A, dtype, scale_a, scale_b, get_cu_count(), BIAS)
|
||||
out = ops.wvSplitKQ(B, A, dtype, scale_a, scale_b, num_compute_units(), BIAS)
|
||||
|
||||
if xnorm:
|
||||
torch.testing.assert_close(out, ref_out, atol=1e-3, rtol=1e-8)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import types
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
|
||||
def _load_oink_ops_module():
|
||||
# Import the module normally (vllm is installed as an editable package in CI).
|
||||
from vllm import _oink_ops
|
||||
|
||||
return _oink_ops
|
||||
|
||||
|
||||
def test_oink_availability_checks(monkeypatch: pytest.MonkeyPatch):
|
||||
_oink_ops = _load_oink_ops_module()
|
||||
|
||||
# Ensure the ops namespace exists and is mutable for tests.
|
||||
monkeypatch.setattr(
|
||||
torch.ops,
|
||||
"oink",
|
||||
types.SimpleNamespace(rmsnorm=lambda x, w, eps: x),
|
||||
raising=False,
|
||||
)
|
||||
|
||||
# Case 1: CUDA not available.
|
||||
monkeypatch.setattr(torch.cuda, "is_available", lambda: False)
|
||||
assert _oink_ops.is_oink_available_for_device(0) is False
|
||||
|
||||
# Case 2: CUDA available but < SM100.
|
||||
monkeypatch.setattr(torch.cuda, "is_available", lambda: True)
|
||||
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda idx: (9, 0))
|
||||
assert _oink_ops.is_oink_available_for_device(0) is False
|
||||
|
||||
# Case 3: CUDA available and SM100, rmsnorm op registered.
|
||||
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda idx: (10, 0))
|
||||
assert _oink_ops.is_oink_available_for_device(0) is True
|
||||
|
||||
# fused op presence probe
|
||||
assert _oink_ops.has_fused_add_rms_norm() is False
|
||||
monkeypatch.setattr(
|
||||
torch.ops,
|
||||
"oink",
|
||||
types.SimpleNamespace(
|
||||
rmsnorm=lambda x, w, eps: x,
|
||||
fused_add_rms_norm=lambda x, residual, w, eps: None,
|
||||
),
|
||||
raising=False,
|
||||
)
|
||||
assert _oink_ops.has_fused_add_rms_norm() is True
|
||||
|
||||
|
||||
def test_can_view_as_2d_stride_guard():
|
||||
# Import the helper from the layernorm module.
|
||||
from vllm.model_executor.layers.layernorm import _can_view_as_2d
|
||||
|
||||
x = torch.zeros((2, 3, 4))
|
||||
assert _can_view_as_2d(x) is True
|
||||
|
||||
# Size-1 dims should be ignored by the viewability check.
|
||||
# Create a tensor where stride(0) != stride(1) * size(1) due to padding,
|
||||
# but view(-1, H) is still valid because dim 1 has size 1.
|
||||
base = torch.zeros((2, 10, 4))
|
||||
x_singleton = base[:, :1, :]
|
||||
x_singleton.view(-1, x_singleton.shape[-1])
|
||||
assert _can_view_as_2d(x_singleton) is True
|
||||
|
||||
# Middle-dimension stride break: view(-1, hidden) should be invalid.
|
||||
x2 = x[:, ::2, :]
|
||||
with pytest.raises(RuntimeError):
|
||||
x2.view(-1, x2.shape[-1])
|
||||
assert _can_view_as_2d(x2) is False
|
||||
@@ -715,7 +715,6 @@ _MULTIMODAL_EXAMPLE_MODELS = {
|
||||
extras={"fork": "Isotr0py/deepseek-vl2-tiny"},
|
||||
max_transformers_version="4.48",
|
||||
transformers_version_reason={"hf": "HF model is not compatible."},
|
||||
hf_overrides={"architectures": ["DeepseekVLV2ForCausalLM"]},
|
||||
),
|
||||
"DeepseekOCRForCausalLM": _HfExamplesInfo(
|
||||
"deepseek-ai/DeepSeek-OCR",
|
||||
@@ -1200,6 +1199,11 @@ _SPECULATIVE_DECODING_EXAMPLE_MODELS = {
|
||||
},
|
||||
is_available_online=False,
|
||||
),
|
||||
"NemotronHMTPModel": _HfExamplesInfo(
|
||||
"nvidia/Nemotron-Super-Placeholder",
|
||||
speculative_model="nvidia/Nemotron-Super-Placeholder",
|
||||
is_available_online=False,
|
||||
),
|
||||
}
|
||||
|
||||
_TRANSFORMERS_BACKEND_MODELS = {
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from tests.reasoning.utils import run_reasoning_extraction
|
||||
from vllm.reasoning import ReasoningParser, ReasoningParserManager
|
||||
|
||||
parser_name = "step3p5"
|
||||
start_token = "<think>"
|
||||
end_token = "</think>"
|
||||
|
||||
REASONING_MODEL_NAME = "stepfun-ai/Step-3.5-Flash"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def step3p5_tokenizer():
|
||||
return AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)
|
||||
|
||||
|
||||
SIMPLE_REASONING = {
|
||||
"output": "This is a reasoning section</think>This is the rest",
|
||||
"reasoning_content": "This is a reasoning section",
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
# need to get into parser again to remove newline after </think>
|
||||
COMPLETE_REASONING = {
|
||||
"output": "This is a reasoning section</think>",
|
||||
"reasoning_content": "This is a reasoning section",
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
NO_CONTENT = {
|
||||
"output": "This is content",
|
||||
"reasoning_content": "This is content",
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
NO_REASONING_STREAMING = {
|
||||
"output": "This is a reasoning section",
|
||||
"reasoning_content": "This is a reasoning section",
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
MULTIPLE_LINES = {
|
||||
"output": "This\nThat</think>This is the rest\nThat",
|
||||
"reasoning_content": "This\nThat",
|
||||
"content": "This is the rest\nThat",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
SHORTEST_REASONING_NO_STREAMING = {
|
||||
"output": "</think>This is the rest",
|
||||
"reasoning_content": None,
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
SHORTEST_REASONING = {
|
||||
"output": "</think>This is the rest",
|
||||
"reasoning_content": None,
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
REASONING_WITH_THINK = {
|
||||
"output": "<think>This is a reasoning section</think>This is the rest",
|
||||
"reasoning_content": "This is a reasoning section",
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
COMPLETE_REASONING_WITH_THINK = {
|
||||
"output": "<think>This is a reasoning section</think>",
|
||||
"reasoning_content": "This is a reasoning section",
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
MULTIPLE_LINES_WITH_THINK = {
|
||||
"output": "<think>This\nThat</think>This is the rest\nThat",
|
||||
"reasoning_content": "This\nThat",
|
||||
"content": "This is the rest\nThat",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
SHORTEST_REASONING_NO_STREAMING_WITH_THINK = {
|
||||
"output": "</think>This is the rest",
|
||||
"reasoning_content": None,
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
SHORTEST_REASONING_WITH_THINK = {
|
||||
"output": "</think>This is the rest",
|
||||
"reasoning_content": None,
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
THINK_NO_END = {
|
||||
"output": "<think>This is a reasoning section",
|
||||
"reasoning_content": "This is a reasoning section",
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
EMPTY = {
|
||||
"output": "",
|
||||
"reasoning_content": None,
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
EMPTY_STREAMING = {
|
||||
"output": "",
|
||||
"reasoning_content": None,
|
||||
"content": None,
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
NEW_LINE = {
|
||||
"output": "\n<think>This is a reasoning section</think>\nThis is the rest",
|
||||
"reasoning_content": "This is a reasoning section",
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
|
||||
NEW_LINE_STREAMING = {
|
||||
"output": "\n<think>This is a reasoning section\n</think>\nThis is the rest",
|
||||
"reasoning_content": "\nThis is a reasoning section",
|
||||
"content": "This is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
|
||||
NEW_LINE_STREAMING_COMPLEX_CONTENT = {
|
||||
"output": "\n This is a \n reasoning section\n\n\n</think>\n\nThis is the rest",
|
||||
"reasoning_content": "\n This is a \n reasoning section\n\n",
|
||||
"content": "\nThis is the rest",
|
||||
"is_reasoning_end": True,
|
||||
}
|
||||
|
||||
MULTI_TURN_PROMPT_CONTENT = {
|
||||
"output": "<think> This is last turn's reasoning section </think> hello <think>",
|
||||
"reasoning_content": "",
|
||||
"content": "",
|
||||
"is_reasoning_end": False,
|
||||
}
|
||||
|
||||
TEST_CASES = [
|
||||
pytest.param(
|
||||
False,
|
||||
SIMPLE_REASONING,
|
||||
id="simple_reasoning",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
SIMPLE_REASONING,
|
||||
id="simple_reasoning_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
COMPLETE_REASONING,
|
||||
id="complete_reasoning",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
COMPLETE_REASONING,
|
||||
id="complete_reasoning_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
NO_CONTENT,
|
||||
id="no_content_token",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
NO_REASONING_STREAMING,
|
||||
id="no_reasoning_token_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
MULTIPLE_LINES,
|
||||
id="multiple_lines",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
MULTIPLE_LINES,
|
||||
id="multiple_lines_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
SHORTEST_REASONING,
|
||||
id="shortest",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
SHORTEST_REASONING_NO_STREAMING,
|
||||
id="shortest_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
REASONING_WITH_THINK,
|
||||
id="reasoning_with_think",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
REASONING_WITH_THINK,
|
||||
id="reasoning_with_think_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
COMPLETE_REASONING_WITH_THINK,
|
||||
id="complete_reasoning_with_think",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
COMPLETE_REASONING_WITH_THINK,
|
||||
id="complete_reasoning_with_think_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
MULTIPLE_LINES_WITH_THINK,
|
||||
id="multiple_lines_with_think",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
MULTIPLE_LINES_WITH_THINK,
|
||||
id="multiple_lines_with_think_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
SHORTEST_REASONING_NO_STREAMING_WITH_THINK,
|
||||
id="shortest_with_think",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
SHORTEST_REASONING_WITH_THINK,
|
||||
id="shortest_with_think_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
THINK_NO_END,
|
||||
id="think_no_end",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
THINK_NO_END,
|
||||
id="think_no_end_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
EMPTY,
|
||||
id="empty",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
EMPTY_STREAMING,
|
||||
id="empty_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
NEW_LINE,
|
||||
id="new_line",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
NEW_LINE_STREAMING,
|
||||
id="new_line_streaming",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
NEW_LINE_STREAMING_COMPLEX_CONTENT,
|
||||
id="new_line_streaming_complex_content",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
MULTI_TURN_PROMPT_CONTENT,
|
||||
id="multi_turn_prompt_content",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("streaming, param_dict", TEST_CASES)
|
||||
def test_reasoning(
|
||||
streaming: bool,
|
||||
param_dict: dict,
|
||||
step3p5_tokenizer,
|
||||
request,
|
||||
):
|
||||
output = step3p5_tokenizer.tokenize(param_dict["output"])
|
||||
# decode everything to tokens
|
||||
output_tokens: list[str] = [
|
||||
step3p5_tokenizer.convert_tokens_to_string([token]) for token in output
|
||||
]
|
||||
parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
|
||||
step3p5_tokenizer
|
||||
)
|
||||
|
||||
reasoning, content = run_reasoning_extraction(
|
||||
parser, output_tokens, streaming=streaming
|
||||
)
|
||||
|
||||
print(f"reasoning: {reasoning}")
|
||||
print(f"content: {content}")
|
||||
test_id = request.node.callspec.id if hasattr(request.node, "callspec") else None
|
||||
if request.node.callspec.id != "multi_turn_prompt_content":
|
||||
assert reasoning == param_dict["reasoning_content"]
|
||||
assert content == param_dict["content"]
|
||||
|
||||
# Test is_reasoning_end
|
||||
output_ids = step3p5_tokenizer.convert_tokens_to_ids(output)
|
||||
if streaming:
|
||||
is_reasoning_end = parser.is_reasoning_end(output_ids)
|
||||
assert is_reasoning_end == param_dict["is_reasoning_end"]
|
||||
|
||||
# Test extract_content
|
||||
if param_dict["content"] is not None:
|
||||
content = parser.extract_content_ids(output_ids)
|
||||
# Fixed expected token ids for specific test cases
|
||||
test_id = (
|
||||
request.node.callspec.id if hasattr(request.node, "callspec") else None
|
||||
)
|
||||
# Match most specific first
|
||||
if test_id not in [
|
||||
"new_line_streaming_complex_content",
|
||||
"new_line_streaming",
|
||||
"new_line",
|
||||
"multi_turn_prompt_content",
|
||||
]:
|
||||
expected_content_ids = step3p5_tokenizer.convert_tokens_to_ids(
|
||||
step3p5_tokenizer.tokenize(param_dict["content"])
|
||||
)
|
||||
assert content == expected_content_ids
|
||||
else:
|
||||
content = parser.extract_content_ids(output)
|
||||
assert content == []
|
||||
|
||||
|
||||
def test_step3p5_streaming_drops_leading_newline(step3p5_tokenizer):
|
||||
parser_cls = ReasoningParserManager.get_reasoning_parser("step3p5")
|
||||
parser = parser_cls(step3p5_tokenizer)
|
||||
output = "<think>calc</think>\nAnswer"
|
||||
tokens = step3p5_tokenizer.tokenize(output)
|
||||
output_tokens = [
|
||||
step3p5_tokenizer.convert_tokens_to_string([token]) for token in tokens
|
||||
]
|
||||
|
||||
_, content = run_reasoning_extraction(parser, output_tokens, streaming=True)
|
||||
assert content == "Answer"
|
||||
+15
-6
@@ -926,12 +926,17 @@ def test_vllm_config_defaults(model_id, compiliation_config, optimization_level)
|
||||
# Verify other compilation_config defaults
|
||||
compilation_config_dict = default_config["compilation_config"]
|
||||
for k, v in compilation_config_dict.items():
|
||||
if k != "pass_config":
|
||||
actual = getattr(vllm_config.compilation_config, k)
|
||||
expected = v(vllm_config) if callable(v) else v
|
||||
assert actual == expected, (
|
||||
f"compilation_config.{k}: expected {expected}, got {actual}"
|
||||
)
|
||||
if k == "pass_config":
|
||||
continue
|
||||
actual = getattr(vllm_config.compilation_config, k)
|
||||
expected = v(vllm_config) if callable(v) else v
|
||||
# On platforms without static graph support, __post_init__ forces
|
||||
# cudagraph_mode to NONE; expect that instead of the level default.
|
||||
if k == "cudagraph_mode" and not current_platform.support_static_graph_mode():
|
||||
expected = CUDAGraphMode.NONE
|
||||
assert actual == expected, (
|
||||
f"compilation_config.{k}: expected {expected}, got {actual}"
|
||||
)
|
||||
|
||||
|
||||
def test_vllm_config_callable_defaults():
|
||||
@@ -969,6 +974,10 @@ def test_vllm_config_callable_defaults():
|
||||
assert enable_if_sequential(config_quantized) is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.support_static_graph_mode(),
|
||||
reason="Explicit overrides may be force-overwritten without static graph support.",
|
||||
)
|
||||
def test_vllm_config_explicit_overrides():
|
||||
"""Test that explicit property overrides work correctly with callable defaults.
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ def _make_vllm_config(block_size, max_model_len, max_num_seqs):
|
||||
cudagraph_mode=CUDAGraphMode.FULL,
|
||||
max_cudagraph_capture_size=None,
|
||||
),
|
||||
speculative_config=None,
|
||||
num_speculative_tokens=0,
|
||||
parallel_config=SimpleNamespace(decode_context_parallel_size=1),
|
||||
scheduler_config=SimpleNamespace(max_num_seqs=max_num_seqs),
|
||||
model_config=SimpleNamespace(max_model_len=max_model_len),
|
||||
)
|
||||
@@ -92,7 +95,10 @@ def test_update_block_table_copies_block_idx_to_persistent_buffers():
|
||||
has_initial_states_p=None,
|
||||
query_start_loc_p=None,
|
||||
num_computed_tokens_p=None,
|
||||
state_indices_tensor=builder_a.state_indices_tensor[:num_reqs],
|
||||
state_indices_tensor_p=None,
|
||||
query_start_loc_d=None,
|
||||
num_accepted_tokens=None,
|
||||
state_indices_tensor_d=builder_a.state_indices_tensor_d[:num_reqs],
|
||||
block_idx_last_scheduled_token=(
|
||||
builder_a.block_idx_last_scheduled_token[:num_reqs]
|
||||
),
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from contextlib import ExitStack
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -187,24 +189,33 @@ async def test_load(
|
||||
# =============================================================================
|
||||
# DP Pause/Resume Tests
|
||||
# =============================================================================
|
||||
# When expert_parallel=False: uses non-MoE model (DP replicas as separate engines).
|
||||
# When expert_parallel=True: uses MoE model + EP (DPEngineCoreProc, sync pause path).
|
||||
|
||||
DP_PAUSE_MODEL = "hmellor/tiny-random-LlamaForCausalLM"
|
||||
DP_PAUSE_MODEL_MOE = "ibm-research/PowerMoE-3b"
|
||||
DP_PAUSE_PROMPT = "This is a test of data parallel pause"
|
||||
|
||||
|
||||
def _get_dp_pause_engine_args(expert_parallel: bool) -> AsyncEngineArgs:
|
||||
"""Engine args for DP pause tests: MoE+EP when expert_parallel else small Llama."""
|
||||
model = DP_PAUSE_MODEL_MOE if expert_parallel else DP_PAUSE_MODEL
|
||||
return AsyncEngineArgs(
|
||||
model=model,
|
||||
enforce_eager=True,
|
||||
tensor_parallel_size=int(os.getenv("TP_SIZE", 1)),
|
||||
data_parallel_size=DP_SIZE,
|
||||
data_parallel_backend="mp",
|
||||
enable_expert_parallel=expert_parallel,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dp_pause_resume_basic():
|
||||
@pytest.mark.parametrize("expert_parallel", [False, True])
|
||||
async def test_dp_pause_resume_basic(expert_parallel: bool):
|
||||
"""Pausing from the client (one call) pauses all DP ranks; resume clears it."""
|
||||
if current_platform.is_rocm():
|
||||
pytest.skip("DP pause tests use mp backend only")
|
||||
with ExitStack() as after:
|
||||
engine_args = AsyncEngineArgs(
|
||||
model=DP_PAUSE_MODEL,
|
||||
enforce_eager=True,
|
||||
tensor_parallel_size=int(os.getenv("TP_SIZE", 1)),
|
||||
data_parallel_size=DP_SIZE,
|
||||
data_parallel_backend="mp",
|
||||
)
|
||||
engine_args = _get_dp_pause_engine_args(expert_parallel)
|
||||
engine = AsyncLLM.from_engine_args(engine_args)
|
||||
after.callback(engine.shutdown)
|
||||
|
||||
@@ -226,18 +237,11 @@ async def test_dp_pause_resume_basic():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dp_pause_abort():
|
||||
@pytest.mark.parametrize("expert_parallel", [False, True])
|
||||
async def test_dp_pause_abort(expert_parallel: bool):
|
||||
"""Pause with abort from one client aborts in-flight requests on all DP ranks."""
|
||||
if current_platform.is_rocm():
|
||||
pytest.skip("DP pause tests use mp backend only")
|
||||
with ExitStack() as after:
|
||||
engine_args = AsyncEngineArgs(
|
||||
model=DP_PAUSE_MODEL,
|
||||
enforce_eager=True,
|
||||
tensor_parallel_size=int(os.getenv("TP_SIZE", 1)),
|
||||
data_parallel_size=DP_SIZE,
|
||||
data_parallel_backend="mp",
|
||||
)
|
||||
engine_args = _get_dp_pause_engine_args(expert_parallel)
|
||||
engine = AsyncLLM.from_engine_args(engine_args)
|
||||
after.callback(engine.shutdown)
|
||||
|
||||
@@ -286,41 +290,111 @@ async def test_dp_pause_abort():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dp_pause_keep_then_resume():
|
||||
"""Pause with keep queues new requests; resume allows them to run."""
|
||||
if current_platform.is_rocm():
|
||||
pytest.skip("DP pause tests use mp backend only")
|
||||
@pytest.mark.parametrize("expert_parallel", [False, True])
|
||||
async def test_dp_pause_keep_then_resume(expert_parallel: bool):
|
||||
"""Start generation, pause after a few tokens (keep mode), resume; verify gap."""
|
||||
|
||||
pause_duration = 2.0
|
||||
min_tokens_before_pause = 3
|
||||
|
||||
with ExitStack() as after:
|
||||
engine_args = AsyncEngineArgs(
|
||||
model=DP_PAUSE_MODEL,
|
||||
enforce_eager=True,
|
||||
tensor_parallel_size=int(os.getenv("TP_SIZE", 1)),
|
||||
data_parallel_size=DP_SIZE,
|
||||
data_parallel_backend="mp",
|
||||
)
|
||||
engine_args = _get_dp_pause_engine_args(expert_parallel)
|
||||
engine = AsyncLLM.from_engine_args(engine_args)
|
||||
after.callback(engine.shutdown)
|
||||
|
||||
await engine.pause_generation(mode="keep")
|
||||
assert await engine.is_paused()
|
||||
sampling_params = SamplingParams(max_tokens=15, ignore_eos=True)
|
||||
token_times: list[tuple[int, float]] = []
|
||||
pause_token_idx = 0
|
||||
|
||||
request_done = asyncio.Event()
|
||||
|
||||
async def gen():
|
||||
async for out in engine.generate(
|
||||
request_id="queued-keep",
|
||||
async def generator_task():
|
||||
nonlocal pause_token_idx
|
||||
out = None
|
||||
async for output in engine.generate(
|
||||
request_id="keep-resume-req",
|
||||
prompt=DP_PAUSE_PROMPT,
|
||||
sampling_params=SamplingParams(max_tokens=5),
|
||||
sampling_params=sampling_params,
|
||||
):
|
||||
pass
|
||||
request_done.set()
|
||||
token_count = len(output.outputs[0].token_ids)
|
||||
token_times.append((token_count, time.monotonic()))
|
||||
out = output
|
||||
return out
|
||||
|
||||
task = asyncio.create_task(gen())
|
||||
await asyncio.sleep(0.2)
|
||||
assert not request_done.is_set()
|
||||
async def controller_task():
|
||||
nonlocal pause_token_idx
|
||||
while len(token_times) < min_tokens_before_pause:
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.pause_generation(mode="keep")
|
||||
await asyncio.sleep(pause_duration)
|
||||
pause_token_idx = len(token_times)
|
||||
await engine.resume_generation()
|
||||
|
||||
gen_task = asyncio.create_task(generator_task())
|
||||
ctrl_task = asyncio.create_task(controller_task())
|
||||
final_output, _ = await asyncio.gather(gen_task, ctrl_task)
|
||||
|
||||
assert final_output is not None and final_output.finished
|
||||
assert await engine.is_paused() is False
|
||||
assert pause_token_idx >= min_tokens_before_pause
|
||||
if pause_token_idx > 0 and pause_token_idx < len(token_times):
|
||||
pause_gap = (
|
||||
token_times[pause_token_idx][1] - token_times[pause_token_idx - 1][1]
|
||||
)
|
||||
assert pause_gap >= pause_duration * 0.8, (
|
||||
f"Expected gap ~{pause_duration}s after pause, got {pause_gap:.3f}s"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dp_pause_keep_race_staggered_engines():
|
||||
"""Race: send pause(keep) to engine 0, then add two requests,
|
||||
then pause(keep) to engine 1. Ensures no deadlock when pause
|
||||
requests are staggered and requests arrive in between."""
|
||||
if DP_SIZE != 2:
|
||||
pytest.skip("test_dp_pause_keep_race_staggered_engines requires DP_SIZE=2")
|
||||
|
||||
with ExitStack() as after:
|
||||
engine_args = _get_dp_pause_engine_args(expert_parallel=True)
|
||||
engine = AsyncLLM.from_engine_args(engine_args)
|
||||
after.callback(engine.shutdown)
|
||||
|
||||
client = engine.engine_core
|
||||
|
||||
original_call_utility = client.call_utility_async
|
||||
mid_pause_tasks: list[asyncio.Task] = []
|
||||
|
||||
async def staggered_pause_keep(method: str, *args) -> Any:
|
||||
if method != "pause_scheduler" or not args or args[0] != "keep":
|
||||
return await original_call_utility(method, *args)
|
||||
# Send pause(keep) to engine 0 first
|
||||
await client._call_utility_async(
|
||||
method, *args, engine=client.core_engines[0]
|
||||
)
|
||||
# In the middle: send two requests (race window)
|
||||
sp = SamplingParams(max_tokens=5, ignore_eos=True)
|
||||
|
||||
async def consume_gen(req_id: str) -> None:
|
||||
async for _ in engine.generate(
|
||||
request_id=req_id,
|
||||
prompt=DP_PAUSE_PROMPT,
|
||||
sampling_params=sp,
|
||||
):
|
||||
pass
|
||||
|
||||
t1 = asyncio.create_task(consume_gen("race-1"))
|
||||
t2 = asyncio.create_task(consume_gen("race-2"))
|
||||
mid_pause_tasks.extend([t1, t2])
|
||||
await asyncio.sleep(3)
|
||||
# Then send pause(keep) to engine 1
|
||||
result = await client._call_utility_async(
|
||||
method, *args, engine=client.core_engines[1]
|
||||
)
|
||||
return result
|
||||
|
||||
client.call_utility_async = staggered_pause_keep
|
||||
|
||||
await engine.pause_generation(mode="keep")
|
||||
assert await engine.is_paused()
|
||||
await engine.resume_generation()
|
||||
final = await asyncio.wait_for(task, timeout=10.0)
|
||||
assert final.finished
|
||||
assert not await engine.is_paused()
|
||||
# Let the two requests we sent mid-pause complete
|
||||
await asyncio.gather(*mid_pause_tasks)
|
||||
|
||||
+142
-100
@@ -8,6 +8,7 @@ from typing import Any
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.evals.gsm8k.gsm8k_eval import _build_gsm8k_prompts, evaluate_gsm8k_offline
|
||||
from tests.utils import get_attn_backend_list_based_on_platform, large_gpu_mark
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.assets.base import VLLM_S3_BUCKET_URL
|
||||
@@ -35,53 +36,57 @@ def _skip_if_insufficient_gpus_for_tp(tp_size: int):
|
||||
Messages = list[dict[str, Any]]
|
||||
|
||||
|
||||
def get_test_prompts(
|
||||
mm_enabled: bool, quiet: bool = False, num_prompts: int = 100
|
||||
) -> list[Messages]:
|
||||
prompt_types = ["repeat", "sentence"]
|
||||
def get_test_prompts(mm_enabled: bool, num_prompts: int = 100) -> list[Messages]:
|
||||
prompt_types = ["repeat", "gsm8k"]
|
||||
if mm_enabled:
|
||||
prompt_types.append("mm")
|
||||
prompts = []
|
||||
prompts: list[Messages] = []
|
||||
|
||||
random.seed(0)
|
||||
random_prompt_type_choices = random.choices(prompt_types, k=num_prompts)
|
||||
|
||||
if not quiet:
|
||||
print(f"Prompt types: {random_prompt_type_choices}")
|
||||
num_repeat_prompts = num_prompts // len(prompt_types)
|
||||
if mm_enabled:
|
||||
num_gsm8k_prompts = num_prompts // len(prompt_types)
|
||||
num_mm_prompts = num_prompts - num_repeat_prompts - num_gsm8k_prompts
|
||||
else:
|
||||
num_mm_prompts = 0
|
||||
num_gsm8k_prompts = num_prompts - num_repeat_prompts
|
||||
|
||||
# Generate a mixed batch of prompts, some of which can be easily
|
||||
# predicted by n-gram matching and some which likely cannot.
|
||||
for kind in random_prompt_type_choices:
|
||||
random.seed(0)
|
||||
for _ in range(num_repeat_prompts):
|
||||
word_choices = ["test", "temp", "hello", "where"]
|
||||
word = random.choice(word_choices)
|
||||
prompt: str | list[dict[str, Any]] = ""
|
||||
if kind == "repeat":
|
||||
prompt = f"""
|
||||
please repeat the word '{word}' 10 times.
|
||||
give no other output than the word at least ten times in a row,
|
||||
in lowercase with spaces between each word and without quotes.
|
||||
"""
|
||||
elif kind == "sentence":
|
||||
prompt = f"""
|
||||
please give a ten-word sentence that
|
||||
uses the word {word} at least once.
|
||||
give no other output than that simple sentence without quotes.
|
||||
"""
|
||||
elif kind == "mm":
|
||||
placeholders = [
|
||||
prompts.append(
|
||||
[
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"{VLLM_S3_BUCKET_URL}/{VLM_IMAGES_DIR}/stop_sign.jpg"
|
||||
},
|
||||
"role": "user",
|
||||
"content": f"""
|
||||
please repeat the word '{word}' 10 times.
|
||||
give no other output than the word at least ten times in a row,
|
||||
in lowercase with spaces between each word and without quotes.
|
||||
""",
|
||||
}
|
||||
]
|
||||
prompt = [
|
||||
*placeholders,
|
||||
{"type": "text", "text": "The meaning of the image is"},
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"Unknown prompt type: {kind}")
|
||||
)
|
||||
prompts.extend(
|
||||
[{"role": "user", "content": prompt}]
|
||||
for prompt in _build_gsm8k_prompts(
|
||||
num_questions=num_gsm8k_prompts, num_shots=5
|
||||
)[0]
|
||||
)
|
||||
for _ in range(num_mm_prompts):
|
||||
placeholders = [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"{VLLM_S3_BUCKET_URL}/{VLM_IMAGES_DIR}/stop_sign.jpg"
|
||||
},
|
||||
}
|
||||
]
|
||||
prompt = [
|
||||
*placeholders,
|
||||
{"type": "text", "text": "The meaning of the image is"},
|
||||
]
|
||||
prompts.append([{"role": "user", "content": prompt}])
|
||||
|
||||
return prompts
|
||||
@@ -113,6 +118,25 @@ def model_name():
|
||||
return "meta-llama/Llama-3.1-8B-Instruct"
|
||||
|
||||
|
||||
def evaluate_llm_for_gsm8k(llm: LLM, expected_accuracy_threshold: float = 0.70) -> None:
|
||||
"""Evaluate the LLM on GSM8K and check that accuracy is above a sanity threshold.
|
||||
|
||||
The default threshold assumes the LLM uses the same target model as the "model_name"
|
||||
fixture, with max model len == 4096. Precomputed reference value is 75% to 80%
|
||||
on GSM8K with greedy decoding, so we check that it's above a sanity threshold of 70%
|
||||
to verify that the model is correct.
|
||||
"""
|
||||
if expected_accuracy_threshold <= 0.0:
|
||||
print("Skipping GSM8K evaluation")
|
||||
return
|
||||
results = evaluate_gsm8k_offline(llm)
|
||||
accuracy = results["accuracy"]
|
||||
print(f"GSM8K accuracy: {accuracy:.3f}")
|
||||
assert accuracy >= expected_accuracy_threshold, (
|
||||
f"Expected GSM8K accuracy >= {expected_accuracy_threshold}, got {accuracy:.3f}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_torch_dynamo():
|
||||
"""Reset torch dynamo cache before each test"""
|
||||
@@ -138,41 +162,14 @@ def reset_torch_dynamo():
|
||||
)
|
||||
def test_ngram_and_suffix_correctness(
|
||||
speculative_config: dict,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
sampling_config: SamplingParams,
|
||||
model_name: str,
|
||||
):
|
||||
"""
|
||||
Compare the outputs of an original LLM and a speculative LLM
|
||||
should be the same when using ngram speculative decoding.
|
||||
"""
|
||||
test_prompts = get_test_prompts(mm_enabled=False)
|
||||
|
||||
ref_llm = LLM(model=model_name, max_model_len=1024)
|
||||
ref_outputs = ref_llm.chat(test_prompts, sampling_config)
|
||||
del ref_llm
|
||||
torch.cuda.empty_cache()
|
||||
cleanup_dist_env_and_memory()
|
||||
|
||||
spec_llm = LLM(
|
||||
model=model_name,
|
||||
speculative_config=speculative_config,
|
||||
max_model_len=1024,
|
||||
max_model_len=4096,
|
||||
)
|
||||
spec_outputs = spec_llm.chat(test_prompts, sampling_config)
|
||||
matches = 0
|
||||
misses = 0
|
||||
for ref_output, spec_output in zip(ref_outputs, spec_outputs):
|
||||
if ref_output.outputs[0].text == spec_output.outputs[0].text:
|
||||
matches += 1
|
||||
else:
|
||||
misses += 1
|
||||
print(f"ref_output: {ref_output.outputs[0].text}")
|
||||
print(f"spec_output: {spec_output.outputs[0].text}")
|
||||
|
||||
# Heuristic: expect at least 66% of the prompts to match exactly
|
||||
# Upon failure, inspect the outputs to check for inaccuracy.
|
||||
assert matches >= int(0.66 * len(ref_outputs))
|
||||
evaluate_llm_for_gsm8k(spec_llm)
|
||||
del spec_llm
|
||||
torch.cuda.empty_cache()
|
||||
cleanup_dist_env_and_memory()
|
||||
@@ -238,10 +235,10 @@ def test_suffix_decoding_acceptance(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_path",
|
||||
["model_path", "expected_accuracy_threshold"],
|
||||
[
|
||||
"RedHatAI/Llama-3.1-8B-Instruct-speculator.eagle3",
|
||||
"RedHatAI/Qwen3-8B-speculator.eagle3",
|
||||
("RedHatAI/Llama-3.1-8B-Instruct-speculator.eagle3", 0.7), # ref: 75%-80%
|
||||
("RedHatAI/Qwen3-8B-speculator.eagle3", 0.8), # ref: 87%-92%
|
||||
],
|
||||
ids=["llama3_eagle3_speculator", "qwen3_eagle3_speculator"],
|
||||
)
|
||||
@@ -249,6 +246,7 @@ def test_speculators_model_integration(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
sampling_config: SamplingParams,
|
||||
model_path: str,
|
||||
expected_accuracy_threshold: float,
|
||||
):
|
||||
"""
|
||||
Test that speculators models work with the simplified integration.
|
||||
@@ -262,7 +260,8 @@ def test_speculators_model_integration(
|
||||
2. Verifier model is extracted from speculator config
|
||||
3. Speculative decoding is automatically enabled
|
||||
4. Text generation works correctly
|
||||
5. Output matches reference (non-speculative) generation
|
||||
5. GSM8k accuracy of the model passes a sanity check when speculative decoding on
|
||||
6. Output matches reference (non-speculative) generation
|
||||
"""
|
||||
monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
|
||||
|
||||
@@ -270,7 +269,10 @@ def test_speculators_model_integration(
|
||||
test_prompts = get_test_prompts(mm_enabled=False)
|
||||
|
||||
# First run: Direct speculator model (simplified integration)
|
||||
spec_llm = LLM(model=model_path, max_model_len=1024)
|
||||
spec_llm = LLM(model=model_path, max_model_len=4096)
|
||||
evaluate_llm_for_gsm8k(
|
||||
spec_llm, expected_accuracy_threshold=expected_accuracy_threshold
|
||||
)
|
||||
spec_outputs = spec_llm.chat(test_prompts, sampling_config)
|
||||
|
||||
# Verify speculative config was auto-detected
|
||||
@@ -297,7 +299,7 @@ def test_speculators_model_integration(
|
||||
cleanup_dist_env_and_memory()
|
||||
|
||||
# Second run: Reference without speculative decoding
|
||||
ref_llm = LLM(model=verifier_model, max_model_len=1024)
|
||||
ref_llm = LLM(model=verifier_model, max_model_len=4096)
|
||||
ref_outputs = ref_llm.chat(test_prompts, sampling_config)
|
||||
del ref_llm
|
||||
torch.cuda.empty_cache()
|
||||
@@ -318,19 +320,27 @@ def test_speculators_model_integration(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["model_setup", "mm_enabled", "enable_chunked_prefill", "model_impl"],
|
||||
[
|
||||
"model_setup",
|
||||
"mm_enabled",
|
||||
"enable_chunked_prefill",
|
||||
"model_impl",
|
||||
"expected_accuracy_threshold",
|
||||
],
|
||||
[
|
||||
(
|
||||
("eagle3", "Qwen/Qwen3-8B", "AngelSlim/Qwen3-8B_eagle3", 1),
|
||||
False,
|
||||
False,
|
||||
"auto",
|
||||
0.8, # ref: 90%
|
||||
),
|
||||
(
|
||||
("eagle3", "Qwen/Qwen3-8B", "AngelSlim/Qwen3-8B_eagle3", 1),
|
||||
False,
|
||||
False,
|
||||
"transformers",
|
||||
0.8, # ref: 90%
|
||||
),
|
||||
pytest.param(
|
||||
(
|
||||
@@ -342,6 +352,7 @@ def test_speculators_model_integration(
|
||||
False,
|
||||
False,
|
||||
"auto",
|
||||
0.8, # ref: 90%
|
||||
marks=pytest.mark.skip(
|
||||
reason="architecture of its eagle3 is LlamaForCausalLMEagle3"
|
||||
),
|
||||
@@ -356,6 +367,7 @@ def test_speculators_model_integration(
|
||||
False,
|
||||
False,
|
||||
"auto",
|
||||
0.7, # TODO, update this with a reference value when re-enabling this case
|
||||
marks=pytest.mark.skip(
|
||||
reason="Skipping due to its head_dim not being a a multiple of 32"
|
||||
),
|
||||
@@ -370,6 +382,7 @@ def test_speculators_model_integration(
|
||||
False,
|
||||
True,
|
||||
"auto",
|
||||
0.7, # ref: 75%-80%
|
||||
marks=large_gpu_mark(min_gb=40),
|
||||
), # works on 4x H100
|
||||
(
|
||||
@@ -382,6 +395,7 @@ def test_speculators_model_integration(
|
||||
False,
|
||||
False,
|
||||
"auto",
|
||||
0.7, # ref: 75%-80%
|
||||
),
|
||||
pytest.param(
|
||||
(
|
||||
@@ -393,7 +407,8 @@ def test_speculators_model_integration(
|
||||
False,
|
||||
False,
|
||||
"auto",
|
||||
marks=large_gpu_mark(min_gb=80),
|
||||
0.8, # ref: 90%
|
||||
# marks=large_gpu_mark(min_gb=80),
|
||||
), # works on 4x H100
|
||||
pytest.param(
|
||||
(
|
||||
@@ -405,6 +420,7 @@ def test_speculators_model_integration(
|
||||
True,
|
||||
True,
|
||||
"auto",
|
||||
0.8, # ref: 90%
|
||||
marks=large_gpu_mark(min_gb=80),
|
||||
), # works on 4x H100
|
||||
(
|
||||
@@ -417,6 +433,7 @@ def test_speculators_model_integration(
|
||||
False,
|
||||
False,
|
||||
"auto",
|
||||
0.0, # dummy model, skip gsm8k check
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
@@ -437,10 +454,18 @@ def test_eagle_correctness(
|
||||
sampling_config: SamplingParams,
|
||||
model_setup: tuple[str, str, str, int],
|
||||
mm_enabled: bool,
|
||||
expected_accuracy_threshold: float,
|
||||
enable_chunked_prefill: bool,
|
||||
model_impl: str,
|
||||
attn_backend: str,
|
||||
):
|
||||
"""
|
||||
Compare the outputs of a original LLM and a speculative LLM
|
||||
which should be the same when using eagle speculative decoding. Due to some variance
|
||||
in the engine, it is possible for some outputs to differ, so we expect that at least
|
||||
6/10 output tokens match exactly, and that the GSM8k accuracy is above
|
||||
a precomputed reference threshold for each model.
|
||||
"""
|
||||
if attn_backend == "TREE_ATTN":
|
||||
# TODO: Fix this flaky test
|
||||
pytest.skip(
|
||||
@@ -461,11 +486,6 @@ def test_eagle_correctness(
|
||||
|
||||
# Generate test prompts inside the function instead of using fixture
|
||||
test_prompts = get_test_prompts(mm_enabled)
|
||||
"""
|
||||
Compare the outputs of a original LLM and a speculative LLM
|
||||
should be the same when using eagle speculative decoding.
|
||||
model_setup: (method, model_name, eagle_model_name, tp_size)
|
||||
"""
|
||||
# Determine attention config
|
||||
# Scout requires default backend selection because vision encoder has
|
||||
# head_dim 88 being incompatible with FLASH_ATTN and needs to fall back
|
||||
@@ -505,6 +525,9 @@ def test_eagle_correctness(
|
||||
tensor_parallel_size=tp_size,
|
||||
attention_config=attention_config,
|
||||
)
|
||||
evaluate_llm_for_gsm8k(
|
||||
ref_llm, expected_accuracy_threshold=expected_accuracy_threshold
|
||||
)
|
||||
ref_outputs = ref_llm.chat(test_prompts, sampling_config)
|
||||
del ref_llm
|
||||
torch.cuda.empty_cache()
|
||||
@@ -526,6 +549,9 @@ def test_eagle_correctness(
|
||||
model_impl=model_impl,
|
||||
attention_config=attention_config,
|
||||
)
|
||||
evaluate_llm_for_gsm8k(
|
||||
spec_llm, expected_accuracy_threshold=expected_accuracy_threshold
|
||||
)
|
||||
spec_outputs = spec_llm.chat(test_prompts, sampling_config)
|
||||
matches = 0
|
||||
misses = 0
|
||||
@@ -546,10 +572,10 @@ def test_eagle_correctness(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["model_setup", "mm_enabled"],
|
||||
["model_setup", "mm_enabled", "expected_accuracy_threshold"],
|
||||
[
|
||||
(("mtp", "XiaomiMiMo/MiMo-7B-Base", 1), False),
|
||||
(("mtp", "ZixiQi/DeepSeek-V3-4layers-MTP-FP8", 1), False),
|
||||
(("mtp", "XiaomiMiMo/MiMo-7B-Base", 1), False, 0.5), # ref: 65%-70%
|
||||
(("mtp", "ZixiQi/DeepSeek-V3-4layers-MTP-FP8", 1), False, 0.0), # dummy model
|
||||
],
|
||||
ids=["mimo", "deepseek"],
|
||||
)
|
||||
@@ -558,14 +584,17 @@ def test_mtp_correctness(
|
||||
sampling_config: SamplingParams,
|
||||
model_setup: tuple[str, str, int],
|
||||
mm_enabled: bool,
|
||||
expected_accuracy_threshold: float,
|
||||
):
|
||||
# Generate test prompts inside the function instead of using fixture
|
||||
test_prompts = get_test_prompts(mm_enabled)
|
||||
"""
|
||||
Compare the outputs of a original LLM and a speculative LLM
|
||||
should be the same when using MTP speculative decoding.
|
||||
model_setup: (method, model_name, tp_size)
|
||||
which should be the same when using MTP speculative decoding. Due to some variance
|
||||
in the engine, it is possible for some outputs to differ, so we expect that at least
|
||||
6/10 output tokens match exactly, and that the GSM8k accuracy is above a precomputed
|
||||
reference threshold for each model.
|
||||
"""
|
||||
# Generate test prompts inside the function instead of using fixture
|
||||
test_prompts = get_test_prompts(mm_enabled)
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("VLLM_MLA_DISABLE", "1")
|
||||
|
||||
@@ -579,6 +608,9 @@ def test_mtp_correctness(
|
||||
trust_remote_code=True,
|
||||
)
|
||||
ref_outputs = ref_llm.chat(test_prompts, sampling_config)
|
||||
evaluate_llm_for_gsm8k(
|
||||
ref_llm, expected_accuracy_threshold=expected_accuracy_threshold
|
||||
)
|
||||
del ref_llm
|
||||
torch.cuda.empty_cache()
|
||||
cleanup_dist_env_and_memory()
|
||||
@@ -594,6 +626,9 @@ def test_mtp_correctness(
|
||||
},
|
||||
max_model_len=2048,
|
||||
)
|
||||
evaluate_llm_for_gsm8k(
|
||||
spec_llm, expected_accuracy_threshold=expected_accuracy_threshold
|
||||
)
|
||||
spec_outputs = spec_llm.chat(test_prompts, sampling_config)
|
||||
matches = 0
|
||||
misses = 0
|
||||
@@ -621,12 +656,13 @@ class ArgsTest:
|
||||
num_speculative_tokens: int
|
||||
expected_acceptance_rate: float
|
||||
expected_acceptance_len: float
|
||||
expected_gsm8k_accuracy: float = 0.0 # skip by default
|
||||
# Defaults
|
||||
enforce_eager: bool = True
|
||||
parallel_drafting: bool = False
|
||||
target_tensor_parallel_size: int = 1
|
||||
draft_tensor_parallel_size: int = 1
|
||||
max_model_len: int = 1024
|
||||
max_model_len: int = 2048
|
||||
gpu_memory_utilization: float = 0.5
|
||||
dataset: str = "test_prompts"
|
||||
num_prompts: int = 100
|
||||
@@ -639,8 +675,9 @@ cases = [
|
||||
draft_model="Qwen/Qwen3-0.6B",
|
||||
sampling_config=greedy_sampling(),
|
||||
num_speculative_tokens=3, # K
|
||||
expected_acceptance_len=3 + 1, # K + 1
|
||||
expected_acceptance_rate=1.0,
|
||||
expected_acceptance_len=0.98 * (3 + 1), # epsilon discount of K + 1
|
||||
expected_acceptance_rate=0.98, # slight epsilon
|
||||
expected_gsm8k_accuracy=0.25, # ref: 35-40%
|
||||
),
|
||||
# Smaller draft model, stochastic sampling.
|
||||
ArgsTest(
|
||||
@@ -648,8 +685,9 @@ cases = [
|
||||
draft_model="Qwen/Qwen3-0.6B",
|
||||
sampling_config=stochastic_sampling(),
|
||||
num_speculative_tokens=3,
|
||||
expected_acceptance_len=2.8 + 1,
|
||||
expected_acceptance_rate=0.9,
|
||||
expected_acceptance_len=3.4, # ref: 3.7
|
||||
expected_acceptance_rate=0.80, # ref: 0.90
|
||||
expected_gsm8k_accuracy=0.5, # ref: 60%. Note gsm8k always runs greedy sampling
|
||||
),
|
||||
]
|
||||
|
||||
@@ -669,9 +707,8 @@ def test_draft_model_realistic_example():
|
||||
num_speculative_tokens=3,
|
||||
sampling_config=greedy_sampling(),
|
||||
enforce_eager=False,
|
||||
# values below are not derived, but just prevent a regression
|
||||
expected_acceptance_len=2.8,
|
||||
expected_acceptance_rate=0.55,
|
||||
expected_acceptance_len=2.6, # ref: 2.86
|
||||
expected_acceptance_rate=0.5, # ref: 0.62
|
||||
)
|
||||
assert_draft_model_correctness(args)
|
||||
|
||||
@@ -685,9 +722,8 @@ def test_draft_model_parallel_drafting():
|
||||
sampling_config=greedy_sampling(),
|
||||
parallel_drafting=True,
|
||||
enforce_eager=False,
|
||||
# values below are collected from a stable run, with ~5% tolerance
|
||||
expected_acceptance_len=2.375,
|
||||
expected_acceptance_rate=0.45,
|
||||
expected_acceptance_len=2.3, # ref: 2.52
|
||||
expected_acceptance_rate=0.4, # ref: 0.51
|
||||
)
|
||||
assert_draft_model_correctness(args)
|
||||
|
||||
@@ -723,6 +759,7 @@ def test_draft_model_tensor_parallelism():
|
||||
draft_tensor_parallel_size=2,
|
||||
**some_high_acceptance_metrics(),
|
||||
enforce_eager=False,
|
||||
expected_gsm8k_accuracy=0.5,
|
||||
)
|
||||
assert_draft_model_correctness(sd_case)
|
||||
|
||||
@@ -797,9 +834,14 @@ def assert_draft_model_correctness(args: ArgsTest):
|
||||
# we don't check the outputs, only check the metrics
|
||||
spec_llm.chat(test_prompts, args.sampling_config)
|
||||
metrics = spec_llm.get_metrics()
|
||||
|
||||
acceptance_rate: float = compute_acceptance_rate(metrics)
|
||||
acceptance_len: float = compute_acceptance_len(metrics)
|
||||
|
||||
# Need to evaluate after getting metrics to avoid polluting the AR
|
||||
evaluate_llm_for_gsm8k(
|
||||
spec_llm, expected_accuracy_threshold=args.expected_gsm8k_accuracy
|
||||
)
|
||||
|
||||
del spec_llm # CLEANUP
|
||||
torch.cuda.empty_cache()
|
||||
cleanup_dist_env_and_memory()
|
||||
@@ -817,7 +859,7 @@ def assert_draft_model_correctness(args: ArgsTest):
|
||||
|
||||
def get_messages(dataset: str, n: int) -> list[Messages]:
|
||||
if dataset == "test_prompts":
|
||||
return get_test_prompts(mm_enabled=False, quiet=True, num_prompts=n)
|
||||
return get_test_prompts(mm_enabled=False, num_prompts=n)
|
||||
elif dataset == "likaixin/InstructCoder":
|
||||
return get_instruct_coder_messages(n=n)
|
||||
else:
|
||||
@@ -828,8 +870,8 @@ def some_high_acceptance_metrics() -> dict:
|
||||
return {
|
||||
"sampling_config": greedy_sampling(),
|
||||
"num_speculative_tokens": 3,
|
||||
"expected_acceptance_len": 2.8 + 1,
|
||||
"expected_acceptance_rate": 0.90,
|
||||
"expected_acceptance_len": 3.4, # ref: 3.75
|
||||
"expected_acceptance_rate": 0.8, # ref: 0.9
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -280,20 +280,15 @@ def echo_dc_nested(
|
||||
|
||||
|
||||
def future_echo(self, value: Any, num_wait_loops: int = 2) -> Future:
|
||||
"""Utility that returns a Future completed by a per_step_hook after
|
||||
num_wait_loops engine steps (tests deferred utility path).
|
||||
"""Utility that returns a Future completed once the engine is idle
|
||||
(tests deferred utility path).
|
||||
"""
|
||||
future: Future = Future()
|
||||
remaining = [num_wait_loops]
|
||||
|
||||
def _step(engine: EngineCore) -> bool:
|
||||
remaining[0] -= 1
|
||||
if remaining[0] <= 0:
|
||||
future.set_result(value)
|
||||
return True # remove hook
|
||||
return False
|
||||
def idle(engine: EngineCore):
|
||||
future.set_result(value)
|
||||
|
||||
self.per_step_hooks.add(_step)
|
||||
self._idle_state_callbacks.append(idle)
|
||||
return future
|
||||
|
||||
|
||||
@@ -832,8 +827,8 @@ async def test_engine_core_client_future_utility_async(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
subprocess_future_echo_patch,
|
||||
):
|
||||
"""Test that a utility returning a Future (completed by a per_step_hook
|
||||
after N steps) completes when the future is done (engine uses add_done_callback).
|
||||
"""Test that a utility returning a Future completes when the future is done
|
||||
(engine uses add_done_callback).
|
||||
"""
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(EngineCore, "future_echo", future_echo, raising=False)
|
||||
|
||||
@@ -95,18 +95,6 @@ cleanup_instances() {
|
||||
sleep 2
|
||||
}
|
||||
|
||||
# Handle to get model-specific arguments for deepseek
|
||||
get_model_args() {
|
||||
local model_name=$1
|
||||
local extra_args=""
|
||||
|
||||
if [[ "$model_name" == "deepseek-ai/deepseek-vl2-tiny" ]]; then
|
||||
extra_args="--hf_overrides '{\"architectures\": [\"DeepseekVLV2ForCausalLM\"]}' --trust-remote-code"
|
||||
fi
|
||||
|
||||
echo "$extra_args"
|
||||
}
|
||||
|
||||
get_num_gpus() {
|
||||
if [[ "$SMI_BIN" == *"nvidia"* ]]; then
|
||||
$SMI_BIN --query-gpu=name --format=csv,noheader | wc -l
|
||||
@@ -127,9 +115,6 @@ run_tests_for_model() {
|
||||
echo "Testing model: $model_name"
|
||||
echo "================================"
|
||||
|
||||
# Get model-specific arguments
|
||||
local model_args=$(get_model_args "$model_name")
|
||||
|
||||
# Arrays to store all hosts and ports
|
||||
PREFILL_HOSTS=()
|
||||
PREFILL_PORTS=()
|
||||
@@ -172,11 +157,7 @@ run_tests_for_model() {
|
||||
BASE_CMD="${BASE_CMD} --attention-backend=$ATTENTION_BACKEND"
|
||||
fi
|
||||
|
||||
if [ -n "$model_args" ]; then
|
||||
FULL_CMD="$BASE_CMD $model_args"
|
||||
else
|
||||
FULL_CMD="$BASE_CMD"
|
||||
fi
|
||||
|
||||
eval "$FULL_CMD &"
|
||||
|
||||
@@ -227,11 +208,7 @@ run_tests_for_model() {
|
||||
--tensor-parallel-size 1 --enable-expert-parallel"
|
||||
fi
|
||||
|
||||
if [ -n "$model_args" ]; then
|
||||
FULL_CMD="$BASE_CMD $model_args"
|
||||
else
|
||||
FULL_CMD="$BASE_CMD"
|
||||
fi
|
||||
|
||||
eval "$FULL_CMD &"
|
||||
|
||||
|
||||
@@ -55,19 +55,6 @@ cleanup_instances() {
|
||||
sleep 2
|
||||
}
|
||||
|
||||
# Handle to get model-specific arguments for deepseek
|
||||
get_model_args() {
|
||||
local model_name=$1
|
||||
local extra_args=""
|
||||
|
||||
if [[ "$model_name" == "deepseek-ai/deepseek-vl2-tiny" ]]; then
|
||||
extra_args="--hf_overrides '{\"architectures\": [\"DeepseekVLV2ForCausalLM\"]}' --trust-remote-code"
|
||||
fi
|
||||
|
||||
echo "$extra_args"
|
||||
}
|
||||
|
||||
|
||||
# Function to run tests for a specific model
|
||||
run_tests_for_model() {
|
||||
local model_name=$1
|
||||
@@ -75,9 +62,6 @@ run_tests_for_model() {
|
||||
echo "Testing model: $model_name"
|
||||
echo "================================"
|
||||
|
||||
# Get model-specific arguments
|
||||
local model_args=$(get_model_args "$model_name")
|
||||
|
||||
# Start prefill instance
|
||||
PREFILL_PORT=8001
|
||||
|
||||
@@ -87,11 +71,7 @@ run_tests_for_model() {
|
||||
--gpu-memory-utilization 0.2 \
|
||||
--kv-transfer-config '$KV_CONFIG'"
|
||||
|
||||
if [ -n "$model_args" ]; then
|
||||
FULL_CMD="$BASE_CMD $model_args"
|
||||
else
|
||||
FULL_CMD="$BASE_CMD"
|
||||
fi
|
||||
|
||||
eval "$FULL_CMD &"
|
||||
|
||||
@@ -105,11 +85,7 @@ run_tests_for_model() {
|
||||
--gpu-memory-utilization 0.2 \
|
||||
--kv-transfer-config '$KV_CONFIG'"
|
||||
|
||||
if [ -n "$model_args" ]; then
|
||||
FULL_CMD="$BASE_CMD $model_args"
|
||||
else
|
||||
FULL_CMD="$BASE_CMD"
|
||||
fi
|
||||
|
||||
eval "$FULL_CMD &"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import msgspec
|
||||
@@ -40,6 +41,19 @@ from .utils import create_request, create_scheduler
|
||||
|
||||
aiter_available = importlib.util.find_spec("aiter") is not None
|
||||
mori_available = importlib.util.find_spec("mori") is not None
|
||||
|
||||
|
||||
def _rdma_available() -> bool:
|
||||
"""Check if RDMA devices are available."""
|
||||
try:
|
||||
result = subprocess.run(["ibv_devinfo"], capture_output=True, text=True)
|
||||
return "No IB devices found" not in result.stderr
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
rdma_available = _rdma_available()
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (current_platform.is_rocm() and mori_available),
|
||||
reason="MoRIIOs are only available on ROCm with aiter package installed",
|
||||
@@ -393,6 +407,7 @@ def test_read_mode_loads_remote_block_ids(moriio_read_mode):
|
||||
@pytest.mark.skipif(
|
||||
not aiter_available, reason="Requires aiter package for ROCm FlashAttention backend"
|
||||
)
|
||||
@pytest.mark.skipif(not rdma_available, reason="No RDMA devices available")
|
||||
def test_register_kv_caches(mock_parallel_groups):
|
||||
"""Test that MoRIIOConnector.register_kv_caches correctly registers kv caches."""
|
||||
ROLE = "kv_consumer"
|
||||
@@ -488,6 +503,7 @@ def test_register_kv_caches(mock_parallel_groups):
|
||||
@pytest.mark.skipif(
|
||||
not aiter_available, reason="Requires aiter package for ROCm FlashAttention backend"
|
||||
)
|
||||
@pytest.mark.skipif(not rdma_available, reason="No RDMA devices available")
|
||||
def test_moriio_handshake_returns_metadata(mock_parallel_groups):
|
||||
"""MoRIIO handshake socket returns valid agent metadata over ZMQ."""
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput
|
||||
from vllm.v1.worker.mamba_utils import preprocess_mamba
|
||||
|
||||
|
||||
def _make_scheduler_output(
|
||||
finished_req_ids: set[str],
|
||||
preempted_req_ids: set[str] | None,
|
||||
resumed_req_ids: set[str],
|
||||
) -> SchedulerOutput:
|
||||
cached = CachedRequestData.make_empty()
|
||||
cached.resumed_req_ids = resumed_req_ids
|
||||
return SchedulerOutput(
|
||||
scheduled_new_reqs=[],
|
||||
scheduled_cached_reqs=cached,
|
||||
num_scheduled_tokens={},
|
||||
total_num_scheduled_tokens=0,
|
||||
scheduled_spec_decode_tokens={},
|
||||
scheduled_encoder_inputs={},
|
||||
num_common_prefix_blocks=[],
|
||||
finished_req_ids=finished_req_ids,
|
||||
free_encoder_mm_hashes=[],
|
||||
preempted_req_ids=preempted_req_ids,
|
||||
)
|
||||
|
||||
|
||||
def test_resumed_req_ids_cleared_from_mamba_state_idx():
|
||||
"""When a request is force-preempted (e.g. reset_prefix_cache),
|
||||
it appears in resumed_req_ids but NOT in preempted_req_ids.
|
||||
preprocess_mamba must still clear its mamba_state_idx entry,
|
||||
otherwise stale indices can point beyond the new block allocation.
|
||||
"""
|
||||
spec = MagicMock(block_size=64, num_speculative_blocks=0)
|
||||
cache_config = MagicMock(enable_prefix_caching=True)
|
||||
input_batch = MagicMock(req_ids=[])
|
||||
|
||||
mamba_state_idx = {
|
||||
"finished": 1,
|
||||
"preempted": 2,
|
||||
"resumed": 3, # only in resumed_req_ids, NOT in preempted
|
||||
"keep": 99,
|
||||
}
|
||||
sched = _make_scheduler_output(
|
||||
finished_req_ids={"finished"},
|
||||
preempted_req_ids={"preempted"},
|
||||
resumed_req_ids={"resumed"},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"vllm.v1.worker.mamba_utils.get_mamba_groups",
|
||||
return_value=([0], spec),
|
||||
):
|
||||
preprocess_mamba(
|
||||
sched,
|
||||
MagicMock(),
|
||||
cache_config,
|
||||
mamba_state_idx,
|
||||
input_batch,
|
||||
{},
|
||||
{},
|
||||
(),
|
||||
)
|
||||
|
||||
assert mamba_state_idx == {"keep": 99}
|
||||
+65
-36
@@ -831,6 +831,59 @@ def _rocm_aiter_triton_add_rmsnorm_pad_fake(
|
||||
return out, residual_out
|
||||
|
||||
|
||||
def _triton_rotary_embedding_impl(
|
||||
positions: torch.Tensor,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
head_size: int,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
is_neox: bool,
|
||||
offsets: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
# Modifies query and key in-place
|
||||
from aiter.ops.triton.rope.rope import (
|
||||
rope_cached_thd_positions_offsets_2c_fwd_inplace,
|
||||
)
|
||||
|
||||
num_tokens = positions.numel()
|
||||
cos, sin = cos_sin_cache.chunk(2, dim=-1)
|
||||
query_shape = query.shape
|
||||
key_shape = key.shape
|
||||
rotate_style = 0 if is_neox else 1
|
||||
rotary_dim = head_size
|
||||
|
||||
query = query.view(num_tokens, -1, head_size)
|
||||
key = key.view(num_tokens, -1, head_size)
|
||||
query_ = query[..., :rotary_dim]
|
||||
key_ = key[..., :rotary_dim]
|
||||
positions = positions.view(*query.shape[:1])
|
||||
rope_cached_thd_positions_offsets_2c_fwd_inplace(
|
||||
query_,
|
||||
key_,
|
||||
cos,
|
||||
sin,
|
||||
positions,
|
||||
offsets,
|
||||
rotate_style,
|
||||
reuse_freqs_front_part=True,
|
||||
nope_first=False,
|
||||
)
|
||||
query = query.view(query_shape)
|
||||
key = key.view(key_shape)
|
||||
|
||||
|
||||
def _triton_rotary_embedding_fake(
|
||||
positions: torch.Tensor,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
head_size: int,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
is_neox_style: bool,
|
||||
offsets: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
return
|
||||
|
||||
|
||||
# Global flag to ensure ops are registered only once
|
||||
_OPS_REGISTERED = False
|
||||
|
||||
@@ -1178,6 +1231,14 @@ class rocm_aiter_ops:
|
||||
dispatch_key=current_platform.dispatch_key,
|
||||
)
|
||||
|
||||
# Register rocm aiter rotary embedding custom op
|
||||
direct_register_custom_op(
|
||||
op_name="rocm_aiter_triton_rotary_embedding",
|
||||
op_func=_triton_rotary_embedding_impl,
|
||||
mutates_args=["query", "key"], # These tensors are modified in-place
|
||||
fake_impl=_triton_rotary_embedding_fake,
|
||||
)
|
||||
|
||||
_OPS_REGISTERED = True
|
||||
|
||||
@staticmethod
|
||||
@@ -1220,6 +1281,10 @@ class rocm_aiter_ops:
|
||||
def get_triton_add_rmsnorm_pad_op() -> OpOverload:
|
||||
return torch.ops.vllm.rocm_aiter_triton_add_rmsnorm_pad.default
|
||||
|
||||
@staticmethod
|
||||
def get_triton_rotary_embedding_op() -> OpOverload:
|
||||
return torch.ops.vllm.rocm_aiter_triton_rotary_embedding.default
|
||||
|
||||
@staticmethod
|
||||
def rms_norm(
|
||||
x: torch.Tensor, weight: torch.Tensor, variance_epsilon: float
|
||||
@@ -1482,42 +1547,6 @@ class rocm_aiter_ops:
|
||||
gemm_afp4wfp4(x_q, weight, x_s, weight_scale.T, out_dtype, y)
|
||||
return y
|
||||
|
||||
@staticmethod
|
||||
def triton_rotary_embed(
|
||||
positions: torch.Tensor,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
head_size: int,
|
||||
rotary_dim: int,
|
||||
is_neox_style: bool,
|
||||
):
|
||||
from aiter.ops.triton.rope import rope_cached_thd_positions_2c_fwd_inplace
|
||||
|
||||
num_tokens = positions.numel()
|
||||
cos, sin = cos_sin_cache.chunk(2, dim=-1)
|
||||
query_shape = query.shape
|
||||
key_shape = key.shape
|
||||
rotate_style = 0 if is_neox_style else 1
|
||||
|
||||
query = query.view(num_tokens, -1, head_size)
|
||||
key = key.view(num_tokens, -1, head_size)
|
||||
query_ = query[..., :rotary_dim]
|
||||
key_ = key[..., :rotary_dim]
|
||||
positions = positions.view(*query.shape[:1])
|
||||
rope_cached_thd_positions_2c_fwd_inplace(
|
||||
query_,
|
||||
key_,
|
||||
cos,
|
||||
sin,
|
||||
positions,
|
||||
rotate_style,
|
||||
reuse_freqs_front_part=True,
|
||||
nope_first=False,
|
||||
)
|
||||
query = query.view(query_shape)
|
||||
key = key.view(key_shape)
|
||||
|
||||
@staticmethod
|
||||
def triton_rope_and_cache(
|
||||
query: torch.Tensor,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Small helper wrappers for external Oink Blackwell custom ops.
|
||||
|
||||
vLLM does not depend on the external Oink repository/package. When an external
|
||||
plugin registers torch.library.custom_op entrypoints under the `oink::`
|
||||
namespace (e.g. via vLLM's general_plugins mechanism) and
|
||||
`VLLM_USE_OINK_OPS=1` is set, vLLM can route eligible calls to those ops.
|
||||
|
||||
This module provides:
|
||||
- A single place to probe Oink op availability at module init time
|
||||
(outside torch.compile tracing), and
|
||||
- Thin wrappers around the torch.ops entrypoints for use in CUDA fast paths,
|
||||
without introducing graph breaks.
|
||||
|
||||
Important:
|
||||
Do not call the availability helpers in a compiled region. They may call
|
||||
functions decorated with `torch._dynamo.disable` to safely check
|
||||
conditions that should not be traced.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
|
||||
try:
|
||||
from torch._dynamo import disable as _dynamo_disable # type: ignore[attr-defined]
|
||||
except Exception: # pragma: no cover
|
||||
|
||||
def _dynamo_disable(fn: Callable): # type: ignore[misc]
|
||||
return fn
|
||||
|
||||
|
||||
def _has_oink_op(op_name: str) -> bool:
|
||||
"""Check if a specific oink op is registered."""
|
||||
return hasattr(torch.ops, "oink") and hasattr(torch.ops.oink, op_name)
|
||||
|
||||
|
||||
@_dynamo_disable
|
||||
def is_oink_available_for_device(device_index: int) -> bool:
|
||||
"""Return True if Oink ops are registered and device is SM100+.
|
||||
|
||||
This function is intended to be called during module initialization
|
||||
(e.g., in RMSNorm.__init__), not in the forward path.
|
||||
|
||||
External plugins are expected to gate registration on SM100+ and
|
||||
VLLM_USE_OINK_OPS=1, so if the ops are present they should be usable.
|
||||
"""
|
||||
if not torch.cuda.is_available():
|
||||
return False
|
||||
|
||||
try:
|
||||
major, minor = torch.cuda.get_device_capability(device_index)
|
||||
sm = 10 * major + minor
|
||||
if sm < 100:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return _has_oink_op("rmsnorm")
|
||||
|
||||
|
||||
def has_fused_add_rms_norm() -> bool:
|
||||
"""Return True if the in-place fused op is registered."""
|
||||
return _has_oink_op("fused_add_rms_norm")
|
||||
|
||||
|
||||
def rmsnorm(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
|
||||
"""Call `torch.ops.oink.rmsnorm`.
|
||||
|
||||
This wrapper is safe to call in torch.compile regions.
|
||||
"""
|
||||
return torch.ops.oink.rmsnorm(x, weight, eps)
|
||||
|
||||
|
||||
def fused_add_rms_norm_(
|
||||
x: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
eps: float,
|
||||
) -> None:
|
||||
"""Call `torch.ops.oink.fused_add_rms_norm` (mutates x and residual)."""
|
||||
torch.ops.oink.fused_add_rms_norm(x, residual, weight, eps)
|
||||
|
||||
|
||||
def fused_add_rms_norm(
|
||||
x: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
eps: float,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Convenience wrapper returning (x, residual) after in-place mutation."""
|
||||
fused_add_rms_norm_(x, residual, weight, eps)
|
||||
return x, residual
|
||||
@@ -449,10 +449,15 @@ def _support_torch_compile(
|
||||
self.was_aot_compile_fn_loaded_from_disk = True
|
||||
except Exception as e:
|
||||
if os.path.exists(aot_compilation_path):
|
||||
if isinstance(e, EOFError):
|
||||
message = "Compile cache file corrupted."
|
||||
else:
|
||||
message = str(e)
|
||||
logger.warning(
|
||||
"Cannot load aot compilation from path %s, error: %s",
|
||||
"Compiling model again due to a load failure from %s, "
|
||||
"reason: %s",
|
||||
aot_compilation_path,
|
||||
str(e),
|
||||
message,
|
||||
)
|
||||
if envs.VLLM_FORCE_AOT_LOAD:
|
||||
raise e
|
||||
|
||||
@@ -89,10 +89,13 @@ class MatcherRotaryEmbedding(MatcherCustomOp):
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
use_flashinfer: bool = False,
|
||||
match_rocm_aiter: bool | None = None,
|
||||
enabled: bool | None = None,
|
||||
) -> None:
|
||||
if enabled is None:
|
||||
enabled = RotaryEmbedding.enabled()
|
||||
if match_rocm_aiter is None:
|
||||
match_rocm_aiter = rocm_aiter_ops.is_triton_rotary_embed_enabled()
|
||||
|
||||
super().__init__(enabled)
|
||||
self.is_neox = is_neox
|
||||
@@ -104,6 +107,8 @@ class MatcherRotaryEmbedding(MatcherCustomOp):
|
||||
self.rotary_dim = head_size
|
||||
if use_flashinfer:
|
||||
self.rotary_op = FLASHINFER_ROTARY_OP
|
||||
elif match_rocm_aiter:
|
||||
self.rotary_op = rocm_aiter_ops.get_triton_rotary_embedding_op()
|
||||
else:
|
||||
self.rotary_op = ROTARY_OP
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ class ScatterSplitReplacementPass(VllmInductorPass):
|
||||
def __call__(self, graph: fx.Graph) -> None:
|
||||
count = 0
|
||||
|
||||
target_ops = [torch.ops._C.rotary_embedding.default]
|
||||
if hasattr(torch.ops.vllm, "rocm_aiter_triton_rotary_embedding"):
|
||||
target_ops.append(torch.ops.vllm.rocm_aiter_triton_rotary_embedding.default)
|
||||
|
||||
for node in graph.nodes:
|
||||
if not is_func(node, auto_functionalized):
|
||||
continue
|
||||
@@ -67,7 +71,7 @@ class ScatterSplitReplacementPass(VllmInductorPass):
|
||||
kwargs = node.kwargs
|
||||
at_target = node.args[0]
|
||||
|
||||
if at_target == torch.ops._C.rotary_embedding.default:
|
||||
if at_target in target_ops:
|
||||
query = kwargs["query"]
|
||||
key = kwargs["key"]
|
||||
getitem_nodes = {}
|
||||
|
||||
+13
-16
@@ -123,6 +123,8 @@ class PassConfig:
|
||||
"""Enable async TP."""
|
||||
fuse_allreduce_rms: bool = Field(default=None)
|
||||
"""Enable flashinfer allreduce fusion."""
|
||||
enable_qk_norm_rope_fusion: bool = False
|
||||
"""Enable fused Q/K RMSNorm + RoPE pass."""
|
||||
|
||||
# ROCm/AITER specific fusions
|
||||
fuse_act_padding: bool = Field(default=None)
|
||||
@@ -153,8 +155,6 @@ class PassConfig:
|
||||
8: 1, # 1MB
|
||||
},
|
||||
}, where key is the device capability"""
|
||||
enable_qk_norm_rope_fusion: bool = False
|
||||
"""Enable fused Q/K RMSNorm + RoPE pass."""
|
||||
|
||||
# TODO(luka) better pass enabling system.
|
||||
|
||||
@@ -834,23 +834,20 @@ class CompilationConfig:
|
||||
func if isinstance(func, InductorPass) else CallableInductorPass(func)
|
||||
)
|
||||
|
||||
if self.pass_config.enable_qk_norm_rope_fusion:
|
||||
if (
|
||||
self.pass_config.enable_qk_norm_rope_fusion
|
||||
and "+rotary_embedding" not in self.custom_ops
|
||||
):
|
||||
# TODO(zhuhaoran): support rope native forward match and remove this.
|
||||
# Linked issue: https://github.com/vllm-project/vllm/issues/28042
|
||||
self.custom_ops.append("+rotary_embedding")
|
||||
if self.pass_config.fuse_rope_kvcache:
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
|
||||
if rocm_aiter_ops.is_triton_rotary_embed_enabled():
|
||||
logger.warning(
|
||||
"Cannot use VLLM_ROCM_USE_AITER_TRITON_ROPE with "
|
||||
"fuse_rope_kvcache. Disabling fuse_rope_kvcache."
|
||||
)
|
||||
self.pass_config.fuse_rope_kvcache = False
|
||||
else:
|
||||
# TODO(Rohan138): support rope native forward match and remove this.
|
||||
# Linked issue: https://github.com/vllm-project/vllm/issues/28042
|
||||
self.custom_ops.append("+rotary_embedding")
|
||||
if (
|
||||
self.pass_config.fuse_rope_kvcache
|
||||
and "+rotary_embedding" not in self.custom_ops
|
||||
):
|
||||
# TODO(Rohan138): support rope native forward match and remove this.
|
||||
# Linked issue: https://github.com/vllm-project/vllm/issues/28042
|
||||
self.custom_ops.append("+rotary_embedding")
|
||||
|
||||
if (
|
||||
is_torch_equal_or_newer("2.9.0.dev")
|
||||
|
||||
@@ -36,6 +36,7 @@ MTPModelTypes = Literal[
|
||||
"glm4_moe_lite_mtp",
|
||||
"glm_ocr_mtp",
|
||||
"ernie_mtp",
|
||||
"nemotron_h_mtp",
|
||||
"exaone_moe_mtp",
|
||||
"qwen3_next_mtp",
|
||||
"qwen3_5_mtp",
|
||||
@@ -255,6 +256,19 @@ class SpeculativeConfig:
|
||||
{"n_predict": n_predict, "architectures": ["ErnieMTPModel"]}
|
||||
)
|
||||
|
||||
if (
|
||||
hf_config.model_type == "nemotron_h"
|
||||
and hasattr(hf_config, "num_nextn_predict_layers")
|
||||
and hf_config.num_nextn_predict_layers > 0
|
||||
):
|
||||
# Check if this is an MTP variant
|
||||
hf_config.model_type = "nemotron_h_mtp"
|
||||
if hf_config.model_type == "nemotron_h_mtp":
|
||||
n_predict = getattr(hf_config, "num_nextn_predict_layers", 1)
|
||||
hf_config.update(
|
||||
{"n_predict": n_predict, "architectures": ["NemotronHMTPModel"]}
|
||||
)
|
||||
|
||||
if hf_config.model_type == "qwen3_next":
|
||||
hf_config.model_type = "qwen3_next_mtp"
|
||||
if hf_config.model_type == "qwen3_next_mtp":
|
||||
@@ -325,7 +339,7 @@ class SpeculativeConfig:
|
||||
if self.target_model_config is None:
|
||||
raise ValueError("target_model_config must be present for mtp")
|
||||
if self.target_model_config.hf_text_config.model_type == "deepseek_v32":
|
||||
# FIXME(luccafong): cudgraph with v32 MTP is not supported,
|
||||
# FIXME(luccafong): cudagraph with v32 MTP is not supported,
|
||||
# remove this when the issue is fixed.
|
||||
self.enforce_eager = True
|
||||
# use the draft model from the same model:
|
||||
@@ -427,7 +441,7 @@ class SpeculativeConfig:
|
||||
self.method = "mtp"
|
||||
if self.num_speculative_tokens > 1:
|
||||
logger.warning(
|
||||
"Enabling num_speculative_tokens > 1 will run"
|
||||
"Enabling num_speculative_tokens > 1 will run "
|
||||
"multiple times of forward on same MTP layer"
|
||||
",which may result in lower acceptance rate"
|
||||
)
|
||||
@@ -712,6 +726,7 @@ class SpeculativeConfig:
|
||||
"hunyuan_vl",
|
||||
"hunyuan_v1_dense",
|
||||
"afmoe",
|
||||
"nemotron_h",
|
||||
]
|
||||
if (
|
||||
self.method == "eagle3"
|
||||
|
||||
+29
-3
@@ -126,14 +126,27 @@ def enable_allreduce_rms_fusion(cfg: "VllmConfig") -> bool:
|
||||
)
|
||||
|
||||
|
||||
def enable_rope_kvcache_fusion(cfg: "VllmConfig") -> bool:
|
||||
"""Enable if rotary embedding custom op is active and
|
||||
use_inductor_graph_partition is enabled.
|
||||
"""
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
|
||||
return (
|
||||
rocm_aiter_ops.is_enabled()
|
||||
and cfg.compilation_config.is_custom_op_enabled("rotary_embedding")
|
||||
and cfg.compilation_config.use_inductor_graph_partition
|
||||
)
|
||||
|
||||
|
||||
def enable_norm_pad_fusion(cfg: "VllmConfig") -> bool:
|
||||
"""Enable if using AITER RMSNorm and AITER Triton GEMMs
|
||||
and hidden size is 2880 i.e. gpt-oss; otherwise Inductor handles fusion."""
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
|
||||
return (
|
||||
envs.VLLM_ROCM_USE_AITER
|
||||
and envs.VLLM_ROCM_USE_AITER_RMSNORM
|
||||
and envs.VLLM_ROCM_USE_AITER_TRITON_GEMM
|
||||
rocm_aiter_ops.is_rmsnorm_enabled()
|
||||
and not rocm_aiter_ops.is_triton_gemm_enabled()
|
||||
and cfg.model_config is not None
|
||||
and cfg.model_config.get_hidden_size() == 2880
|
||||
)
|
||||
@@ -149,6 +162,7 @@ OPTIMIZATION_LEVEL_00 = {
|
||||
"enable_sp": False,
|
||||
"fuse_gemm_comms": False,
|
||||
"fuse_act_padding": False,
|
||||
"fuse_rope_kvcache": False,
|
||||
},
|
||||
"cudagraph_mode": CUDAGraphMode.NONE,
|
||||
"use_inductor_graph_partition": False,
|
||||
@@ -167,6 +181,7 @@ OPTIMIZATION_LEVEL_01 = {
|
||||
"enable_sp": False,
|
||||
"fuse_gemm_comms": False,
|
||||
"fuse_act_padding": enable_norm_pad_fusion,
|
||||
"fuse_rope_kvcache": enable_rope_kvcache_fusion,
|
||||
},
|
||||
"cudagraph_mode": CUDAGraphMode.PIECEWISE,
|
||||
"use_inductor_graph_partition": False,
|
||||
@@ -185,6 +200,7 @@ OPTIMIZATION_LEVEL_02 = {
|
||||
"enable_sp": IS_DENSE,
|
||||
"fuse_gemm_comms": IS_DENSE,
|
||||
"fuse_act_padding": enable_norm_pad_fusion,
|
||||
"fuse_rope_kvcache": enable_rope_kvcache_fusion,
|
||||
},
|
||||
"cudagraph_mode": CUDAGraphMode.FULL_AND_PIECEWISE,
|
||||
"use_inductor_graph_partition": False,
|
||||
@@ -203,6 +219,7 @@ OPTIMIZATION_LEVEL_03 = {
|
||||
"enable_sp": IS_DENSE,
|
||||
"fuse_gemm_comms": IS_DENSE,
|
||||
"fuse_act_padding": enable_norm_pad_fusion,
|
||||
"fuse_rope_kvcache": enable_rope_kvcache_fusion,
|
||||
},
|
||||
"cudagraph_mode": CUDAGraphMode.FULL_AND_PIECEWISE,
|
||||
"use_inductor_graph_partition": False,
|
||||
@@ -395,6 +412,15 @@ class VllmConfig:
|
||||
]
|
||||
return hash_str
|
||||
|
||||
@property
|
||||
def num_speculative_tokens(self) -> int:
|
||||
if (
|
||||
self.speculative_config is not None
|
||||
and self.speculative_config.num_speculative_tokens is not None
|
||||
):
|
||||
return self.speculative_config.num_speculative_tokens
|
||||
return 0
|
||||
|
||||
@property
|
||||
def needs_dp_coordinator(self) -> bool:
|
||||
"""
|
||||
|
||||
@@ -2017,21 +2017,20 @@ class EngineArgs:
|
||||
)
|
||||
|
||||
# Disable chunked prefill and prefix caching for:
|
||||
# POWER (ppc64le)/s390x/RISCV CPUs in V1
|
||||
# POWER (ppc64le)/RISCV CPUs in V1
|
||||
if current_platform.is_cpu() and current_platform.get_cpu_architecture() in (
|
||||
CpuArchEnum.POWERPC,
|
||||
CpuArchEnum.S390X,
|
||||
CpuArchEnum.RISCV,
|
||||
):
|
||||
logger.info(
|
||||
"Chunked prefill is not supported for POWER, "
|
||||
"S390X and RISC-V CPUs; "
|
||||
"and RISC-V CPUs; "
|
||||
"disabling it for V1 backend."
|
||||
)
|
||||
self.enable_chunked_prefill = False
|
||||
logger.info(
|
||||
"Prefix caching is not supported for POWER, "
|
||||
"S390X and RISC-V CPUs; "
|
||||
"and RISC-V CPUs; "
|
||||
"disabling it for V1 backend."
|
||||
)
|
||||
self.enable_prefix_caching = False
|
||||
|
||||
@@ -148,7 +148,7 @@ class EngineClient(ABC):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def sleep(self, level: int = 1) -> None:
|
||||
async def sleep(self, level: int = 1, mode: "PauseMode" = "abort") -> None:
|
||||
"""Sleep the engine"""
|
||||
...
|
||||
|
||||
|
||||
+60
-66
@@ -87,6 +87,7 @@ from vllm.usage.usage_lib import UsageContext
|
||||
from vllm.utils.counter import Counter
|
||||
from vllm.utils.mistral import is_mistral_tokenizer
|
||||
from vllm.utils.tqdm_utils import maybe_tqdm
|
||||
from vllm.v1.engine import PauseMode
|
||||
from vllm.v1.engine.llm_engine import LLMEngine
|
||||
from vllm.v1.sample.logits_processor import LogitsProcessor
|
||||
|
||||
@@ -441,8 +442,7 @@ class LLM:
|
||||
A list of `RequestOutput` objects containing the
|
||||
generated completions in the same order as the input prompts.
|
||||
"""
|
||||
model_config = self.model_config
|
||||
runner_type = model_config.runner_type
|
||||
runner_type = self.model_config.runner_type
|
||||
if runner_type != "generate":
|
||||
raise ValueError(
|
||||
"LLM.generate() is only supported for generative models. "
|
||||
@@ -489,46 +489,22 @@ class LLM:
|
||||
Returns:
|
||||
A list of request IDs for the enqueued requests.
|
||||
"""
|
||||
model_config = self.model_config
|
||||
runner_type = model_config.runner_type
|
||||
runner_type = self.model_config.runner_type
|
||||
if runner_type != "generate":
|
||||
raise ValueError("LLM.enqueue() is only supported for generative models.")
|
||||
|
||||
if sampling_params is None:
|
||||
sampling_params = self.get_default_sampling_params()
|
||||
|
||||
# Use the same preprocessing as _run_completion
|
||||
seq_prompts = prompt_to_seq(prompts)
|
||||
seq_params = self._params_to_seq(sampling_params, len(seq_prompts))
|
||||
seq_lora_requests = self._lora_request_to_seq(lora_request, len(seq_prompts))
|
||||
seq_tok_kwargs = [
|
||||
merge_kwargs(
|
||||
tokenization_kwargs,
|
||||
dict(truncate_prompt_tokens=param.truncate_prompt_tokens),
|
||||
)
|
||||
for param in seq_params
|
||||
]
|
||||
seq_priority = self._priority_to_seq(priority, len(prompts))
|
||||
|
||||
request_ids = self._render_and_add_requests(
|
||||
prompts=(
|
||||
self._preprocess_cmpl_one(prompt, tok_kwargs)
|
||||
for prompt, tok_kwargs in zip(
|
||||
maybe_tqdm(
|
||||
seq_prompts,
|
||||
use_tqdm=use_tqdm,
|
||||
desc="Rendering prompts",
|
||||
),
|
||||
seq_tok_kwargs,
|
||||
)
|
||||
),
|
||||
params=seq_params,
|
||||
lora_requests=seq_lora_requests,
|
||||
priorities=seq_priority,
|
||||
return self._add_completion_requests(
|
||||
prompts=prompts,
|
||||
params=sampling_params,
|
||||
use_tqdm=use_tqdm,
|
||||
lora_request=lora_request,
|
||||
priority=priority,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
)
|
||||
|
||||
return request_ids
|
||||
|
||||
@overload
|
||||
def wait_for_completion(
|
||||
self,
|
||||
@@ -1659,7 +1635,7 @@ class LLM:
|
||||
reset_running_requests, reset_connector
|
||||
)
|
||||
|
||||
def sleep(self, level: int = 1):
|
||||
def sleep(self, level: int = 1, mode: PauseMode = "abort"):
|
||||
"""
|
||||
Put the engine to sleep. The engine should not process any requests.
|
||||
The caller should guarantee that no requests are being processed
|
||||
@@ -1679,10 +1655,10 @@ class LLM:
|
||||
a different model or update the model, where
|
||||
previous model weights are not needed. It reduces
|
||||
CPU memory pressure.
|
||||
mode: How to handle any existing requests, can be "abort", "wait",
|
||||
or "keep".
|
||||
"""
|
||||
if level > 0:
|
||||
self.reset_prefix_cache()
|
||||
self.llm_engine.sleep(level=level)
|
||||
self.llm_engine.sleep(level=level, mode=mode)
|
||||
|
||||
def wake_up(self, tags: list[str] | None = None):
|
||||
"""
|
||||
@@ -1759,6 +1735,45 @@ class LLM:
|
||||
|
||||
return [0] * num_requests
|
||||
|
||||
def _add_completion_requests(
|
||||
self,
|
||||
prompts: PromptType | Sequence[PromptType],
|
||||
params: SamplingParams
|
||||
| PoolingParams
|
||||
| Sequence[SamplingParams | PoolingParams],
|
||||
*,
|
||||
use_tqdm: bool | Callable[..., tqdm] = True,
|
||||
lora_request: Sequence[LoRARequest] | LoRARequest | None = None,
|
||||
priority: list[int] | None = None,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
) -> list[str]:
|
||||
seq_prompts = prompt_to_seq(prompts)
|
||||
seq_params = self._params_to_seq(params, len(seq_prompts))
|
||||
seq_lora_requests = self._lora_request_to_seq(lora_request, len(seq_prompts))
|
||||
seq_tok_kwargs = [
|
||||
merge_kwargs(
|
||||
tokenization_kwargs,
|
||||
dict(truncate_prompt_tokens=param.truncate_prompt_tokens),
|
||||
)
|
||||
for param in seq_params
|
||||
]
|
||||
seq_priority = self._priority_to_seq(priority, len(prompts))
|
||||
|
||||
return self._render_and_add_requests(
|
||||
prompts=(
|
||||
self._preprocess_cmpl_one(prompt, tok_kwargs)
|
||||
for prompt, tok_kwargs in zip(
|
||||
maybe_tqdm(
|
||||
seq_prompts, use_tqdm=use_tqdm, desc="Rendering prompts"
|
||||
),
|
||||
seq_tok_kwargs,
|
||||
)
|
||||
),
|
||||
params=seq_params,
|
||||
lora_requests=seq_lora_requests,
|
||||
priorities=seq_priority,
|
||||
)
|
||||
|
||||
def _run_completion(
|
||||
self,
|
||||
prompts: PromptType | Sequence[PromptType],
|
||||
@@ -1772,36 +1787,15 @@ class LLM:
|
||||
priority: list[int] | None = None,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
):
|
||||
seq_prompts = prompt_to_seq(prompts)
|
||||
seq_params = self._params_to_seq(params, len(seq_prompts))
|
||||
seq_lora_requests = self._lora_request_to_seq(lora_request, len(seq_prompts))
|
||||
seq_tok_kwargs = [
|
||||
merge_kwargs(
|
||||
tokenization_kwargs,
|
||||
dict(truncate_prompt_tokens=param.truncate_prompt_tokens),
|
||||
)
|
||||
for param in seq_params
|
||||
]
|
||||
seq_priority = self._priority_to_seq(priority, len(prompts))
|
||||
|
||||
return self._render_and_run_requests(
|
||||
prompts=(
|
||||
self._preprocess_cmpl_one(prompt, tok_kwargs)
|
||||
for prompt, tok_kwargs in zip(
|
||||
maybe_tqdm(
|
||||
seq_prompts,
|
||||
use_tqdm=use_tqdm,
|
||||
desc="Rendering prompts",
|
||||
),
|
||||
seq_tok_kwargs,
|
||||
)
|
||||
),
|
||||
params=seq_params,
|
||||
output_type=output_type,
|
||||
self._add_completion_requests(
|
||||
prompts=prompts,
|
||||
params=params,
|
||||
use_tqdm=use_tqdm,
|
||||
lora_requests=seq_lora_requests,
|
||||
priorities=seq_priority,
|
||||
lora_request=lora_request,
|
||||
priority=priority,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
)
|
||||
return self._run_engine(use_tqdm=use_tqdm, output_type=output_type)
|
||||
|
||||
def _run_chat(
|
||||
self,
|
||||
|
||||
@@ -555,8 +555,16 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
||||
return data
|
||||
|
||||
structured_outputs_kwargs = data["structured_outputs"]
|
||||
# structured_outputs may arrive as a dict (from JSON/raw kwargs) or
|
||||
# as a StructuredOutputsParams dataclass instance.
|
||||
is_dataclass = isinstance(structured_outputs_kwargs, StructuredOutputsParams)
|
||||
count = sum(
|
||||
structured_outputs_kwargs.get(k) is not None
|
||||
(
|
||||
getattr(structured_outputs_kwargs, k, None)
|
||||
if is_dataclass
|
||||
else structured_outputs_kwargs.get(k)
|
||||
)
|
||||
is not None
|
||||
for k in ("json", "regex", "choice")
|
||||
)
|
||||
# you can only use one kind of constraints for structured outputs
|
||||
|
||||
+150
-110
@@ -67,7 +67,149 @@ class LoRAParserAction(argparse.Action):
|
||||
|
||||
|
||||
@config
|
||||
class FrontendArgs:
|
||||
class BaseFrontendArgs:
|
||||
"""Base arguments for the OpenAI-compatible frontend server.
|
||||
|
||||
This base class does not include host, port, and server-specific arguments
|
||||
like SSL, CORS, and HTTP server settings. Those arguments are added by
|
||||
the subclasses.
|
||||
"""
|
||||
|
||||
lora_modules: list[LoRAModulePath] | None = None
|
||||
"""LoRA modules configurations in either 'name=path' format or JSON format
|
||||
or JSON list format. Example (old format): `'name=path'` Example (new
|
||||
format): `{\"name\": \"name\", \"path\": \"lora_path\",
|
||||
\"base_model_name\": \"id\"}`"""
|
||||
chat_template: str | None = None
|
||||
"""The file path to the chat template, or the template in single-line form
|
||||
for the specified model."""
|
||||
chat_template_content_format: ChatTemplateContentFormatOption = "auto"
|
||||
"""The format to render message content within a chat template.
|
||||
|
||||
* "string" will render the content as a string. Example: `"Hello World"`
|
||||
* "openai" will render the content as a list of dictionaries, similar to
|
||||
OpenAI schema. Example: `[{"type": "text", "text": "Hello world!"}]`"""
|
||||
trust_request_chat_template: bool = False
|
||||
"""Whether to trust the chat template provided in the request. If False,
|
||||
the server will always use the chat template specified by `--chat-template`
|
||||
or the ones from tokenizer."""
|
||||
default_chat_template_kwargs: dict[str, Any] | None = None
|
||||
"""Default keyword arguments to pass to the chat template renderer.
|
||||
These will be merged with request-level chat_template_kwargs,
|
||||
with request values taking precedence. Useful for setting default
|
||||
behavior for reasoning models. Example: '{"enable_thinking": false}'
|
||||
to disable thinking mode by default for Qwen3/DeepSeek models."""
|
||||
response_role: str = "assistant"
|
||||
"""The role name to return if `request.add_generation_prompt=true`."""
|
||||
return_tokens_as_token_ids: bool = False
|
||||
"""When `--max-logprobs` is specified, represents single tokens as
|
||||
strings of the form 'token_id:{token_id}' so that tokens that are not
|
||||
JSON-encodable can be identified."""
|
||||
disable_frontend_multiprocessing: bool = False
|
||||
"""If specified, will run the OpenAI frontend server in the same process as
|
||||
the model serving engine."""
|
||||
enable_auto_tool_choice: bool = False
|
||||
"""Enable auto tool choice for supported models. Use `--tool-call-parser`
|
||||
to specify which parser to use."""
|
||||
exclude_tools_when_tool_choice_none: bool = False
|
||||
"""If specified, exclude tool definitions in prompts when
|
||||
tool_choice='none'."""
|
||||
tool_call_parser: str | None = None
|
||||
"""Select the tool call parser depending on the model that you're using.
|
||||
This is used to parse the model-generated tool call into OpenAI API format.
|
||||
Required for `--enable-auto-tool-choice`. You can choose any option from
|
||||
the built-in parsers or register a plugin via `--tool-parser-plugin`."""
|
||||
tool_parser_plugin: str = ""
|
||||
"""Special the tool parser plugin write to parse the model-generated tool
|
||||
into OpenAI API format, the name register in this plugin can be used in
|
||||
`--tool-call-parser`."""
|
||||
tool_server: str | None = None
|
||||
"""Comma-separated list of host:port pairs (IPv4, IPv6, or hostname).
|
||||
Examples: 127.0.0.1:8000, [::1]:8000, localhost:1234. Or `demo` for demo
|
||||
purpose."""
|
||||
log_config_file: str | None = envs.VLLM_LOGGING_CONFIG_PATH
|
||||
"""Path to logging config JSON file for both vllm and uvicorn"""
|
||||
max_log_len: int | None = None
|
||||
"""Max number of prompt characters or prompt ID numbers being printed in
|
||||
log. The default of None means unlimited."""
|
||||
enable_prompt_tokens_details: bool = False
|
||||
"""If set to True, enable prompt_tokens_details in usage."""
|
||||
enable_server_load_tracking: bool = False
|
||||
"""If set to True, enable tracking server_load_metrics in the app state."""
|
||||
enable_force_include_usage: bool = False
|
||||
"""If set to True, including usage on every request."""
|
||||
enable_tokenizer_info_endpoint: bool = False
|
||||
"""Enable the `/tokenizer_info` endpoint. May expose chat
|
||||
templates and other tokenizer configuration."""
|
||||
enable_log_outputs: bool = False
|
||||
"""If set to True, log model outputs (generations).
|
||||
Requires --enable-log-requests."""
|
||||
enable_log_deltas: bool = True
|
||||
"""If set to False, output deltas will not be logged. Relevant only if
|
||||
--enable-log-outputs is set.
|
||||
"""
|
||||
log_error_stack: bool = envs.VLLM_SERVER_DEV_MODE
|
||||
"""If set to True, log the stack trace of error responses"""
|
||||
tokens_only: bool = False
|
||||
"""
|
||||
If set to True, only enable the Tokens In<>Out endpoint.
|
||||
This is intended for use in a Disaggregated Everything setup.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _customize_cli_kwargs(
|
||||
cls,
|
||||
frontend_kwargs: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Customize argparse kwargs before arguments are registered.
|
||||
|
||||
Subclasses should override this and call
|
||||
``super()._customize_cli_kwargs(frontend_kwargs)`` first.
|
||||
"""
|
||||
# Special case: default_chat_template_kwargs needs json.loads type
|
||||
frontend_kwargs["default_chat_template_kwargs"]["type"] = json.loads
|
||||
|
||||
# Special case: LoRA modules need custom parser action and
|
||||
# optional_type(str)
|
||||
frontend_kwargs["lora_modules"]["type"] = optional_type(str)
|
||||
frontend_kwargs["lora_modules"]["action"] = LoRAParserAction
|
||||
|
||||
# Special case: Tool call parser shows built-in options.
|
||||
valid_tool_parsers = list(ToolParserManager.list_registered())
|
||||
parsers_str = ",".join(valid_tool_parsers)
|
||||
frontend_kwargs["tool_call_parser"]["metavar"] = (
|
||||
f"{{{parsers_str}}} or name registered in --tool-parser-plugin"
|
||||
)
|
||||
return frontend_kwargs
|
||||
|
||||
@classmethod
|
||||
def add_cli_args(cls, parser: FlexibleArgumentParser) -> FlexibleArgumentParser:
|
||||
"""Register CLI arguments for this frontend class.
|
||||
|
||||
Subclasses should override ``_customize_cli_kwargs`` instead of
|
||||
this method so that base-class postprocessing is always applied.
|
||||
"""
|
||||
from vllm.engine.arg_utils import get_kwargs
|
||||
|
||||
frontend_kwargs = get_kwargs(cls)
|
||||
frontend_kwargs = cls._customize_cli_kwargs(frontend_kwargs)
|
||||
|
||||
group_name = cls.__name__.replace("Args", "")
|
||||
frontend_group = parser.add_argument_group(
|
||||
title=group_name,
|
||||
description=cls.__doc__,
|
||||
)
|
||||
for key, value in frontend_kwargs.items():
|
||||
extra_flags = value.pop("flags", [])
|
||||
frontend_group.add_argument(
|
||||
*extra_flags, f"--{key.replace('_', '-')}", **value
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@config
|
||||
class FrontendArgs(BaseFrontendArgs):
|
||||
"""Arguments for the OpenAI-compatible frontend server."""
|
||||
|
||||
host: str | None = None
|
||||
@@ -99,32 +241,6 @@ class FrontendArgs:
|
||||
api_key: list[str] | None = None
|
||||
"""If provided, the server will require one of these keys to be presented in
|
||||
the header."""
|
||||
lora_modules: list[LoRAModulePath] | None = None
|
||||
"""LoRA modules configurations in either 'name=path' format or JSON format
|
||||
or JSON list format. Example (old format): `'name=path'` Example (new
|
||||
format): `{\"name\": \"name\", \"path\": \"lora_path\",
|
||||
\"base_model_name\": \"id\"}`"""
|
||||
chat_template: str | None = None
|
||||
"""The file path to the chat template, or the template in single-line form
|
||||
for the specified model."""
|
||||
chat_template_content_format: ChatTemplateContentFormatOption = "auto"
|
||||
"""The format to render message content within a chat template.
|
||||
|
||||
* "string" will render the content as a string. Example: `"Hello World"`
|
||||
* "openai" will render the content as a list of dictionaries, similar to
|
||||
OpenAI schema. Example: `[{"type": "text", "text": "Hello world!"}]`"""
|
||||
trust_request_chat_template: bool = False
|
||||
"""Whether to trust the chat template provided in the request. If False,
|
||||
the server will always use the chat template specified by `--chat-template`
|
||||
or the ones from tokenizer."""
|
||||
default_chat_template_kwargs: dict[str, Any] | None = None
|
||||
"""Default keyword arguments to pass to the chat template renderer.
|
||||
These will be merged with request-level chat_template_kwargs,
|
||||
with request values taking precedence. Useful for setting default
|
||||
behavior for reasoning models. Example: '{"enable_thinking": false}'
|
||||
to disable thinking mode by default for Qwen3/DeepSeek models."""
|
||||
response_role: str = "assistant"
|
||||
"""The role name to return if `request.add_generation_prompt=true`."""
|
||||
ssl_keyfile: str | None = None
|
||||
"""The file path to the SSL key file."""
|
||||
ssl_certfile: str | None = None
|
||||
@@ -146,81 +262,28 @@ class FrontendArgs:
|
||||
is provided, vLLM will add it to the server using
|
||||
`@app.middleware('http')`. If a class is provided, vLLM will
|
||||
add it to the server using `app.add_middleware()`."""
|
||||
return_tokens_as_token_ids: bool = False
|
||||
"""When `--max-logprobs` is specified, represents single tokens as
|
||||
strings of the form 'token_id:{token_id}' so that tokens that are not
|
||||
JSON-encodable can be identified."""
|
||||
disable_frontend_multiprocessing: bool = False
|
||||
"""If specified, will run the OpenAI frontend server in the same process as
|
||||
the model serving engine."""
|
||||
enable_request_id_headers: bool = False
|
||||
"""If specified, API server will add X-Request-Id header to responses."""
|
||||
enable_auto_tool_choice: bool = False
|
||||
"""Enable auto tool choice for supported models. Use `--tool-call-parser`
|
||||
to specify which parser to use."""
|
||||
exclude_tools_when_tool_choice_none: bool = False
|
||||
"""If specified, exclude tool definitions in prompts when
|
||||
tool_choice='none'."""
|
||||
tool_call_parser: str | None = None
|
||||
"""Select the tool call parser depending on the model that you're using.
|
||||
This is used to parse the model-generated tool call into OpenAI API format.
|
||||
Required for `--enable-auto-tool-choice`. You can choose any option from
|
||||
the built-in parsers or register a plugin via `--tool-parser-plugin`."""
|
||||
tool_parser_plugin: str = ""
|
||||
"""Special the tool parser plugin write to parse the model-generated tool
|
||||
into OpenAI API format, the name register in this plugin can be used in
|
||||
`--tool-call-parser`."""
|
||||
tool_server: str | None = None
|
||||
"""Comma-separated list of host:port pairs (IPv4, IPv6, or hostname).
|
||||
Examples: 127.0.0.1:8000, [::1]:8000, localhost:1234. Or `demo` for demo
|
||||
purpose."""
|
||||
log_config_file: str | None = envs.VLLM_LOGGING_CONFIG_PATH
|
||||
"""Path to logging config JSON file for both vllm and uvicorn"""
|
||||
max_log_len: int | None = None
|
||||
"""Max number of prompt characters or prompt ID numbers being printed in
|
||||
log. The default of None means unlimited."""
|
||||
disable_fastapi_docs: bool = False
|
||||
"""Disable FastAPI's OpenAPI schema, Swagger UI, and ReDoc endpoint."""
|
||||
enable_prompt_tokens_details: bool = False
|
||||
"""If set to True, enable prompt_tokens_details in usage."""
|
||||
enable_server_load_tracking: bool = False
|
||||
"""If set to True, enable tracking server_load_metrics in the app state."""
|
||||
enable_force_include_usage: bool = False
|
||||
"""If set to True, including usage on every request."""
|
||||
enable_tokenizer_info_endpoint: bool = False
|
||||
"""Enable the `/tokenizer_info` endpoint. May expose chat
|
||||
templates and other tokenizer configuration."""
|
||||
enable_log_outputs: bool = False
|
||||
"""If set to True, log model outputs (generations).
|
||||
Requires --enable-log-requests."""
|
||||
enable_log_deltas: bool = True
|
||||
"""If set to False, output deltas will not be logged. Relevant only if
|
||||
--enable-log-outputs is set.
|
||||
"""
|
||||
h11_max_incomplete_event_size: int = H11_MAX_INCOMPLETE_EVENT_SIZE_DEFAULT
|
||||
"""Maximum size (bytes) of an incomplete HTTP event (header or body) for
|
||||
h11 parser. Helps mitigate header abuse. Default: 4194304 (4 MB)."""
|
||||
h11_max_header_count: int = H11_MAX_HEADER_COUNT_DEFAULT
|
||||
"""Maximum number of HTTP headers allowed in a request for h11 parser.
|
||||
Helps mitigate header abuse. Default: 256."""
|
||||
log_error_stack: bool = envs.VLLM_SERVER_DEV_MODE
|
||||
"""If set to True, log the stack trace of error responses"""
|
||||
tokens_only: bool = False
|
||||
"""
|
||||
If set to True, only enable the Tokens In<>Out endpoint.
|
||||
This is intended for use in a Disaggregated Everything setup.
|
||||
"""
|
||||
enable_offline_docs: bool = False
|
||||
"""
|
||||
Enable offline FastAPI documentation for air-gapped environments.
|
||||
Uses vendored static assets bundled with vLLM.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_cli_args(parser: FlexibleArgumentParser) -> FlexibleArgumentParser:
|
||||
from vllm.engine.arg_utils import get_kwargs
|
||||
|
||||
frontend_kwargs = get_kwargs(FrontendArgs)
|
||||
@classmethod
|
||||
def _customize_cli_kwargs(
|
||||
cls,
|
||||
frontend_kwargs: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
frontend_kwargs = super()._customize_cli_kwargs(frontend_kwargs)
|
||||
|
||||
# Special case: allowed_origins, allowed_methods, allowed_headers all
|
||||
# need json.loads type
|
||||
@@ -232,14 +295,6 @@ class FrontendArgs:
|
||||
del frontend_kwargs["allowed_methods"]["nargs"]
|
||||
del frontend_kwargs["allowed_headers"]["nargs"]
|
||||
|
||||
# Special case: default_chat_template_kwargs needs json.loads type
|
||||
frontend_kwargs["default_chat_template_kwargs"]["type"] = json.loads
|
||||
|
||||
# Special case: LoRA modules need custom parser action and
|
||||
# optional_type(str)
|
||||
frontend_kwargs["lora_modules"]["type"] = optional_type(str)
|
||||
frontend_kwargs["lora_modules"]["action"] = LoRAParserAction
|
||||
|
||||
# Special case: Middleware needs to append action
|
||||
frontend_kwargs["middleware"]["action"] = "append"
|
||||
frontend_kwargs["middleware"]["type"] = str
|
||||
@@ -252,22 +307,7 @@ class FrontendArgs:
|
||||
if "nargs" in frontend_kwargs["disable_access_log_for_endpoints"]:
|
||||
del frontend_kwargs["disable_access_log_for_endpoints"]["nargs"]
|
||||
|
||||
# Special case: Tool call parser shows built-in options.
|
||||
valid_tool_parsers = list(ToolParserManager.list_registered())
|
||||
parsers_str = ",".join(valid_tool_parsers)
|
||||
frontend_kwargs["tool_call_parser"]["metavar"] = (
|
||||
f"{{{parsers_str}}} or name registered in --tool-parser-plugin"
|
||||
)
|
||||
|
||||
frontend_group = parser.add_argument_group(
|
||||
title="Frontend",
|
||||
description=FrontendArgs.__doc__,
|
||||
)
|
||||
|
||||
for key, value in frontend_kwargs.items():
|
||||
frontend_group.add_argument(f"--{key.replace('_', '-')}", **value)
|
||||
|
||||
return parser
|
||||
return frontend_kwargs
|
||||
|
||||
|
||||
def make_arg_parser(parser: FlexibleArgumentParser) -> FlexibleArgumentParser:
|
||||
|
||||
@@ -42,7 +42,13 @@ class CompletionRequest(OpenAIBaseModel):
|
||||
# Ordered by official OpenAI API documentation
|
||||
# https://platform.openai.com/docs/api-reference/completions/create
|
||||
model: str | None = None
|
||||
prompt: list[int] | list[list[int]] | str | list[str] | None = None
|
||||
prompt: (
|
||||
list[Annotated[int, Field(ge=0)]]
|
||||
| list[list[Annotated[int, Field(ge=0)]]]
|
||||
| str
|
||||
| list[str]
|
||||
| None
|
||||
) = None
|
||||
echo: bool | None = False
|
||||
frequency_penalty: float | None = 0.0
|
||||
logit_bias: dict[str, float] | None = None
|
||||
@@ -314,8 +320,16 @@ class CompletionRequest(OpenAIBaseModel):
|
||||
return data
|
||||
|
||||
structured_outputs_kwargs = data["structured_outputs"]
|
||||
# structured_outputs may arrive as a dict (from JSON/raw kwargs) or
|
||||
# as a StructuredOutputsParams dataclass instance.
|
||||
is_dataclass = isinstance(structured_outputs_kwargs, StructuredOutputsParams)
|
||||
count = sum(
|
||||
structured_outputs_kwargs.get(k) is not None
|
||||
(
|
||||
getattr(structured_outputs_kwargs, k, None)
|
||||
if is_dataclass
|
||||
else structured_outputs_kwargs.get(k)
|
||||
)
|
||||
is not None
|
||||
for k in ("json", "regex", "choice")
|
||||
)
|
||||
if count > 1:
|
||||
|
||||
@@ -89,7 +89,7 @@ from vllm.entrypoints.openai.responses.protocol import (
|
||||
StreamingResponsesResponse,
|
||||
)
|
||||
from vllm.entrypoints.openai.responses.streaming_events import (
|
||||
HarmonyStreamingState,
|
||||
StreamingState,
|
||||
emit_content_delta_events,
|
||||
emit_previous_item_done_events,
|
||||
emit_tool_action_events,
|
||||
@@ -1591,7 +1591,7 @@ class OpenAIServingResponses(OpenAIServing):
|
||||
[StreamingResponsesResponse], StreamingResponsesResponse
|
||||
],
|
||||
) -> AsyncGenerator[StreamingResponsesResponse, None]:
|
||||
state = HarmonyStreamingState()
|
||||
state = StreamingState()
|
||||
|
||||
async for ctx in result_generator:
|
||||
assert isinstance(ctx, StreamingHarmonyContext)
|
||||
|
||||
@@ -6,6 +6,13 @@ Streaming SSE event builders for the Responses API.
|
||||
Pure functions that translate streaming state + delta data into
|
||||
OpenAI Response API SSE events. Used by the streaming event
|
||||
processors in serving.py.
|
||||
|
||||
The file is organized as:
|
||||
1. StreamingState dataclass + utility helpers
|
||||
2. Shared leaf helpers — delta events (take plain strings, no context)
|
||||
3. Shared leaf helpers — done events (take plain strings, no context)
|
||||
4. Harmony-specific dispatchers (route ctx/previous_item → leaf helpers)
|
||||
5. Harmony-specific tool lifecycle helpers
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -47,6 +54,7 @@ from openai.types.responses.response_output_item import McpCall
|
||||
from openai.types.responses.response_reasoning_item import (
|
||||
Content as ResponseReasoningTextContent,
|
||||
)
|
||||
from openai_harmony import Message as HarmonyMessage
|
||||
|
||||
from vllm.entrypoints.mcp.tool_server import ToolServer
|
||||
from vllm.entrypoints.openai.responses.context import StreamingHarmonyContext
|
||||
@@ -64,13 +72,28 @@ TOOL_NAME_TO_MCP_SERVER_LABEL: Final[dict[str, str]] = {
|
||||
}
|
||||
|
||||
|
||||
def _resolve_mcp_name_label(recipient: str) -> tuple[str, str]:
|
||||
"""Resolve MCP tool name and server label from a recipient string.
|
||||
|
||||
- ``mcp.*`` recipients: strip prefix, use the bare name as both
|
||||
name and server_label.
|
||||
- Everything else: use the recipient as the name and look up the
|
||||
server_label in TOOL_NAME_TO_MCP_SERVER_LABEL.
|
||||
"""
|
||||
if recipient.startswith("mcp."):
|
||||
name = recipient[len("mcp.") :]
|
||||
return name, name
|
||||
return recipient, TOOL_NAME_TO_MCP_SERVER_LABEL.get(recipient, recipient)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HarmonyStreamingState:
|
||||
"""Mutable state for harmony streaming event processing."""
|
||||
class StreamingState:
|
||||
"""Mutable state for streaming event processing."""
|
||||
|
||||
current_content_index: int = -1
|
||||
current_output_index: int = 0
|
||||
current_item_id: str = ""
|
||||
current_call_id: str = ""
|
||||
sent_output_item_added: bool = False
|
||||
is_first_function_call_delta: bool = False
|
||||
|
||||
@@ -79,6 +102,7 @@ class HarmonyStreamingState:
|
||||
self.current_output_index += 1
|
||||
self.sent_output_item_added = False
|
||||
self.is_first_function_call_delta = False
|
||||
self.current_call_id = ""
|
||||
|
||||
|
||||
def is_mcp_tool_by_namespace(recipient: str | None) -> bool:
|
||||
@@ -96,213 +120,16 @@ def is_mcp_tool_by_namespace(recipient: str | None) -> bool:
|
||||
return not recipient.startswith("functions.")
|
||||
|
||||
|
||||
def emit_function_call_done_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
# =====================================================================
|
||||
# Shared leaf helpers — delta events
|
||||
# =====================================================================
|
||||
|
||||
|
||||
def emit_text_delta_events(
|
||||
delta: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when a function call completes."""
|
||||
function_name = previous_item.recipient[len("functions.") :]
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseFunctionCallArgumentsDoneEvent(
|
||||
type="response.function_call_arguments.done",
|
||||
arguments=previous_item.content[0].text,
|
||||
name=function_name,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
sequence_number=-1,
|
||||
)
|
||||
)
|
||||
function_call_item = ResponseFunctionToolCall(
|
||||
type="function_call",
|
||||
arguments=previous_item.content[0].text,
|
||||
name=function_name,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
sequence_number=-1,
|
||||
call_id=f"fc_{random_uuid()}",
|
||||
status="completed",
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=function_call_item,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_mcp_call_done_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when an MCP tool call completes."""
|
||||
server_label = TOOL_NAME_TO_MCP_SERVER_LABEL.get(
|
||||
previous_item.recipient, previous_item.recipient
|
||||
)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseMcpCallArgumentsDoneEvent(
|
||||
type="response.mcp_call_arguments.done",
|
||||
arguments=previous_item.content[0].text,
|
||||
name=previous_item.recipient,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
sequence_number=-1,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseMcpCallCompletedEvent(
|
||||
type="response.mcp_call.completed",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=McpCall(
|
||||
type="mcp_call",
|
||||
arguments=previous_item.content[0].text,
|
||||
name=previous_item.recipient,
|
||||
id=state.current_item_id,
|
||||
server_label=server_label,
|
||||
status="completed",
|
||||
),
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_reasoning_done_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when a reasoning (analysis) item completes."""
|
||||
content = ResponseReasoningTextContent(
|
||||
text=previous_item.content[0].text,
|
||||
type="reasoning_text",
|
||||
)
|
||||
reasoning_item = ResponseReasoningItem(
|
||||
type="reasoning",
|
||||
content=[content],
|
||||
status="completed",
|
||||
id=state.current_item_id,
|
||||
summary=[],
|
||||
)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseReasoningTextDoneEvent(
|
||||
type="response.reasoning_text.done",
|
||||
item_id=state.current_item_id,
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
text=previous_item.content[0].text,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseReasoningPartDoneEvent(
|
||||
type="response.reasoning_part.done",
|
||||
sequence_number=-1,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
part=content,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=reasoning_item,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_text_output_done_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when a final text output item completes."""
|
||||
text_content = ResponseOutputText(
|
||||
type="output_text",
|
||||
text=previous_item.content[0].text,
|
||||
annotations=[],
|
||||
)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseTextDoneEvent(
|
||||
type="response.output_text.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
text=previous_item.content[0].text,
|
||||
logprobs=[],
|
||||
item_id=state.current_item_id,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseContentPartDoneEvent(
|
||||
type="response.content_part.done",
|
||||
sequence_number=-1,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
part=text_content,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=ResponseOutputMessage(
|
||||
id=state.current_item_id,
|
||||
type="message",
|
||||
role="assistant",
|
||||
content=[text_content],
|
||||
status="completed",
|
||||
),
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_previous_item_done_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit done events for the previous item when expecting a new start."""
|
||||
if previous_item.recipient is not None:
|
||||
# Deal with tool call
|
||||
if previous_item.recipient.startswith("functions."):
|
||||
return emit_function_call_done_events(previous_item, state)
|
||||
elif (
|
||||
is_mcp_tool_by_namespace(previous_item.recipient)
|
||||
and state.current_item_id is not None
|
||||
and state.current_item_id.startswith("mcp_")
|
||||
):
|
||||
return emit_mcp_call_done_events(previous_item, state)
|
||||
elif previous_item.channel == "analysis":
|
||||
return emit_reasoning_done_events(previous_item, state)
|
||||
elif previous_item.channel == "final":
|
||||
return emit_text_output_done_events(previous_item, state)
|
||||
return []
|
||||
|
||||
|
||||
def emit_final_channel_delta_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for final channel text delta streaming."""
|
||||
"""Emit events for text content delta streaming."""
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
if not state.sent_output_item_added:
|
||||
state.sent_output_item_added = True
|
||||
@@ -344,7 +171,7 @@ def emit_final_channel_delta_events(
|
||||
content_index=state.current_content_index,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
delta=ctx.last_content_delta,
|
||||
delta=delta,
|
||||
# TODO, use logprobs from ctx.last_request_output
|
||||
logprobs=[],
|
||||
)
|
||||
@@ -352,11 +179,11 @@ def emit_final_channel_delta_events(
|
||||
return events
|
||||
|
||||
|
||||
def emit_analysis_channel_delta_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
def emit_reasoning_delta_events(
|
||||
delta: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for analysis channel reasoning delta streaming."""
|
||||
"""Emit events for reasoning text delta streaming."""
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
if not state.sent_output_item_added:
|
||||
state.sent_output_item_added = True
|
||||
@@ -394,20 +221,60 @@ def emit_analysis_channel_delta_events(
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
delta=ctx.last_content_delta,
|
||||
delta=delta,
|
||||
sequence_number=-1,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_mcp_tool_delta_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
def emit_function_call_delta_events(
|
||||
delta: str,
|
||||
function_name: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for function call argument deltas."""
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
if state.is_first_function_call_delta is False:
|
||||
state.is_first_function_call_delta = True
|
||||
state.current_item_id = f"fc_{random_uuid()}"
|
||||
state.current_call_id = f"call_{random_uuid()}"
|
||||
tool_call_item = ResponseFunctionToolCall(
|
||||
name=function_name,
|
||||
type="function_call",
|
||||
id=state.current_item_id,
|
||||
call_id=state.current_call_id,
|
||||
arguments="",
|
||||
status="in_progress",
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemAddedEvent(
|
||||
type="response.output_item.added",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=tool_call_item,
|
||||
)
|
||||
)
|
||||
# Always emit the delta (including on first call)
|
||||
events.append(
|
||||
ResponseFunctionCallArgumentsDeltaEvent(
|
||||
item_id=state.current_item_id,
|
||||
delta=delta,
|
||||
output_index=state.current_output_index,
|
||||
sequence_number=-1,
|
||||
type="response.function_call_arguments.delta",
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_mcp_delta_events(
|
||||
delta: str,
|
||||
state: StreamingState,
|
||||
recipient: str,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for MCP tool delta streaming."""
|
||||
server_label = TOOL_NAME_TO_MCP_SERVER_LABEL.get(recipient, recipient)
|
||||
name, server_label = _resolve_mcp_name_label(recipient)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
if not state.sent_output_item_added:
|
||||
state.sent_output_item_added = True
|
||||
@@ -420,7 +287,7 @@ def emit_mcp_tool_delta_events(
|
||||
item=McpCall(
|
||||
type="mcp_call",
|
||||
id=state.current_item_id,
|
||||
name=recipient,
|
||||
name=name,
|
||||
arguments="",
|
||||
server_label=server_label,
|
||||
status="in_progress",
|
||||
@@ -441,15 +308,15 @@ def emit_mcp_tool_delta_events(
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
delta=ctx.last_content_delta,
|
||||
delta=delta,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_code_interpreter_delta_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
delta: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for code interpreter delta streaming."""
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
@@ -485,151 +352,274 @@ def emit_code_interpreter_delta_events(
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
delta=ctx.last_content_delta,
|
||||
delta=delta,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_mcp_prefix_delta_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
# =====================================================================
|
||||
# Shared leaf helpers — done events
|
||||
# =====================================================================
|
||||
|
||||
|
||||
def emit_text_output_done_events(
|
||||
text: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for MCP prefix (mcp.*) delta streaming."""
|
||||
"""Emit events when a final text output item completes."""
|
||||
text_content = ResponseOutputText(
|
||||
type="output_text",
|
||||
text=text,
|
||||
annotations=[],
|
||||
)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
if not state.sent_output_item_added:
|
||||
state.sent_output_item_added = True
|
||||
state.current_item_id = f"mcp_{random_uuid()}"
|
||||
mcp_name = ctx.parser.current_recipient[len("mcp.") :]
|
||||
|
||||
events.append(
|
||||
ResponseOutputItemAddedEvent(
|
||||
type="response.output_item.added",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=McpCall(
|
||||
type="mcp_call",
|
||||
id=state.current_item_id,
|
||||
name=mcp_name,
|
||||
arguments="",
|
||||
server_label=mcp_name,
|
||||
status="in_progress",
|
||||
),
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseMcpCallInProgressEvent(
|
||||
type="response.mcp_call.in_progress",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
)
|
||||
)
|
||||
|
||||
events.append(
|
||||
ResponseMcpCallArgumentsDeltaEvent(
|
||||
type="response.mcp_call_arguments.delta",
|
||||
ResponseTextDoneEvent(
|
||||
type="response.output_text.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
text=text,
|
||||
logprobs=[],
|
||||
item_id=state.current_item_id,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseContentPartDoneEvent(
|
||||
type="response.content_part.done",
|
||||
sequence_number=-1,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
part=text_content,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=ResponseOutputMessage(
|
||||
id=state.current_item_id,
|
||||
type="message",
|
||||
role="assistant",
|
||||
content=[text_content],
|
||||
status="completed",
|
||||
),
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_reasoning_done_events(
|
||||
text: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when a reasoning (analysis) item completes."""
|
||||
content = ResponseReasoningTextContent(
|
||||
text=text,
|
||||
type="reasoning_text",
|
||||
)
|
||||
reasoning_item = ResponseReasoningItem(
|
||||
type="reasoning",
|
||||
content=[content],
|
||||
status="completed",
|
||||
id=state.current_item_id,
|
||||
summary=[],
|
||||
)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseReasoningTextDoneEvent(
|
||||
type="response.reasoning_text.done",
|
||||
item_id=state.current_item_id,
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
text=text,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseReasoningPartDoneEvent(
|
||||
type="response.reasoning_part.done",
|
||||
sequence_number=-1,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
content_index=state.current_content_index,
|
||||
part=content,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=reasoning_item,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_function_call_done_events(
|
||||
function_name: str,
|
||||
arguments: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when a function call completes."""
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseFunctionCallArgumentsDoneEvent(
|
||||
type="response.function_call_arguments.done",
|
||||
arguments=arguments,
|
||||
name=function_name,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
sequence_number=-1,
|
||||
)
|
||||
)
|
||||
function_call_item = ResponseFunctionToolCall(
|
||||
type="function_call",
|
||||
arguments=arguments,
|
||||
name=function_name,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
sequence_number=-1,
|
||||
call_id=state.current_call_id,
|
||||
status="completed",
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=function_call_item,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_mcp_completion_events(
|
||||
recipient: str,
|
||||
arguments: str,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when an MCP tool call completes."""
|
||||
name, server_label = _resolve_mcp_name_label(recipient)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseMcpCallArgumentsDoneEvent(
|
||||
type="response.mcp_call_arguments.done",
|
||||
arguments=arguments,
|
||||
name=name,
|
||||
item_id=state.current_item_id,
|
||||
output_index=state.current_output_index,
|
||||
sequence_number=-1,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseMcpCallCompletedEvent(
|
||||
type="response.mcp_call.completed",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
delta=ctx.last_content_delta,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_function_call_delta_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for developer function calls on commentary channel."""
|
||||
if not (
|
||||
ctx.parser.current_channel == "commentary"
|
||||
and ctx.parser.current_recipient
|
||||
and ctx.parser.current_recipient.startswith("functions.")
|
||||
):
|
||||
return []
|
||||
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
if state.is_first_function_call_delta is False:
|
||||
state.is_first_function_call_delta = True
|
||||
fc_name = ctx.parser.current_recipient[len("functions.") :]
|
||||
state.current_item_id = f"fc_{random_uuid()}"
|
||||
tool_call_item = ResponseFunctionToolCall(
|
||||
name=fc_name,
|
||||
type="function_call",
|
||||
id=state.current_item_id,
|
||||
call_id=f"call_{random_uuid()}",
|
||||
arguments="",
|
||||
status="in_progress",
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemAddedEvent(
|
||||
type="response.output_item.added",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=tool_call_item,
|
||||
)
|
||||
)
|
||||
# Always emit the delta (including on first call)
|
||||
events.append(
|
||||
ResponseFunctionCallArgumentsDeltaEvent(
|
||||
item_id=state.current_item_id,
|
||||
delta=ctx.last_content_delta,
|
||||
output_index=state.current_output_index,
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
type="response.function_call_arguments.delta",
|
||||
output_index=state.current_output_index,
|
||||
item=McpCall(
|
||||
type="mcp_call",
|
||||
arguments=arguments,
|
||||
name=name,
|
||||
id=state.current_item_id,
|
||||
server_label=server_label,
|
||||
status="completed",
|
||||
),
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Harmony-specific dispatchers
|
||||
# =====================================================================
|
||||
|
||||
|
||||
def emit_content_delta_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for content delta streaming based on channel type."""
|
||||
if not ctx.last_content_delta:
|
||||
"""Emit events for content delta streaming based on channel type.
|
||||
|
||||
This is a Harmony-specific dispatcher that extracts values from the
|
||||
Harmony context and delegates to shared leaf helpers.
|
||||
"""
|
||||
delta = ctx.last_content_delta
|
||||
if not delta:
|
||||
return []
|
||||
|
||||
if ctx.parser.current_channel == "final" and ctx.parser.current_recipient is None:
|
||||
return emit_final_channel_delta_events(ctx, state)
|
||||
elif (
|
||||
ctx.parser.current_channel == "analysis"
|
||||
and ctx.parser.current_recipient is None
|
||||
):
|
||||
return emit_analysis_channel_delta_events(ctx, state)
|
||||
channel = ctx.parser.current_channel
|
||||
recipient = ctx.parser.current_recipient
|
||||
|
||||
if channel == "final" and recipient is None:
|
||||
return emit_text_delta_events(delta, state)
|
||||
elif channel == "analysis" and recipient is None:
|
||||
return emit_reasoning_delta_events(delta, state)
|
||||
# built-in tools will be triggered on the analysis channel
|
||||
# However, occasionally built-in tools will
|
||||
# still be output to commentary.
|
||||
elif (
|
||||
ctx.parser.current_channel == "commentary"
|
||||
or ctx.parser.current_channel == "analysis"
|
||||
) and ctx.parser.current_recipient is not None:
|
||||
recipient = ctx.parser.current_recipient
|
||||
# Check for function calls first - they have their own event handling
|
||||
elif channel in ("commentary", "analysis") and recipient is not None:
|
||||
if recipient.startswith("functions."):
|
||||
return emit_function_call_delta_events(ctx, state)
|
||||
if is_mcp_tool_by_namespace(recipient):
|
||||
return emit_mcp_tool_delta_events(ctx, state, recipient)
|
||||
else:
|
||||
return emit_code_interpreter_delta_events(ctx, state)
|
||||
elif (
|
||||
(
|
||||
ctx.parser.current_channel == "commentary"
|
||||
or ctx.parser.current_channel == "analysis"
|
||||
)
|
||||
and ctx.parser.current_recipient is not None
|
||||
and ctx.parser.current_recipient.startswith("mcp.")
|
||||
):
|
||||
return emit_mcp_prefix_delta_events(ctx, state)
|
||||
function_name = recipient[len("functions.") :]
|
||||
return emit_function_call_delta_events(delta, function_name, state)
|
||||
elif recipient == "python":
|
||||
return emit_code_interpreter_delta_events(delta, state)
|
||||
elif recipient.startswith("mcp.") or is_mcp_tool_by_namespace(recipient):
|
||||
return emit_mcp_delta_events(delta, state, recipient)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def emit_previous_item_done_events(
|
||||
previous_item: HarmonyMessage,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit done events for the previous item when expecting a new start.
|
||||
|
||||
This is a Harmony-specific dispatcher that extracts values from the
|
||||
Harmony parser's message object and delegates to shared leaf helpers.
|
||||
"""
|
||||
text = previous_item.content[0].text
|
||||
if previous_item.recipient is not None:
|
||||
# Deal with tool call
|
||||
if previous_item.recipient.startswith("functions."):
|
||||
function_name = previous_item.recipient[len("functions.") :]
|
||||
return emit_function_call_done_events(function_name, text, state)
|
||||
elif previous_item.recipient == "python":
|
||||
return emit_code_interpreter_completion_events(previous_item, state)
|
||||
elif (
|
||||
is_mcp_tool_by_namespace(previous_item.recipient)
|
||||
and state.current_item_id is not None
|
||||
and state.current_item_id.startswith("mcp_")
|
||||
):
|
||||
return emit_mcp_completion_events(previous_item.recipient, text, state)
|
||||
elif previous_item.channel == "analysis":
|
||||
return emit_reasoning_done_events(text, state)
|
||||
elif previous_item.channel == "final":
|
||||
return emit_text_output_done_events(text, state)
|
||||
return []
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Harmony-specific tool lifecycle helpers
|
||||
# =====================================================================
|
||||
|
||||
|
||||
def emit_browser_tool_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
previous_item: HarmonyMessage,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for browser tool calls (web search)."""
|
||||
function_name = previous_item.recipient[len("browser.") :]
|
||||
@@ -714,53 +704,9 @@ def emit_browser_tool_events(
|
||||
return events
|
||||
|
||||
|
||||
def emit_mcp_tool_completion_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when an MCP tool completes during assistant action turn."""
|
||||
recipient = previous_item.recipient
|
||||
server_label = TOOL_NAME_TO_MCP_SERVER_LABEL.get(recipient, recipient)
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseMcpCallArgumentsDoneEvent(
|
||||
type="response.mcp_call_arguments.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
arguments=previous_item.content[0].text,
|
||||
name=recipient,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseMcpCallCompletedEvent(
|
||||
type="response.mcp_call.completed",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=McpCall(
|
||||
type="mcp_call",
|
||||
id=state.current_item_id,
|
||||
name=recipient,
|
||||
arguments=previous_item.content[0].text,
|
||||
server_label=server_label,
|
||||
status="completed",
|
||||
),
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_code_interpreter_completion_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
previous_item: HarmonyMessage,
|
||||
state: StreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when code interpreter completes."""
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
@@ -807,52 +753,9 @@ def emit_code_interpreter_completion_events(
|
||||
return events
|
||||
|
||||
|
||||
def emit_mcp_prefix_completion_events(
|
||||
previous_item,
|
||||
state: HarmonyStreamingState,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events when an MCP prefix tool (mcp.*) completes."""
|
||||
mcp_name = previous_item.recipient[len("mcp.") :]
|
||||
events: list[StreamingResponsesResponse] = []
|
||||
events.append(
|
||||
ResponseMcpCallArgumentsDoneEvent(
|
||||
type="response.mcp_call_arguments.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
arguments=previous_item.content[0].text,
|
||||
name=mcp_name,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseMcpCallCompletedEvent(
|
||||
type="response.mcp_call.completed",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item_id=state.current_item_id,
|
||||
)
|
||||
)
|
||||
events.append(
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
sequence_number=-1,
|
||||
output_index=state.current_output_index,
|
||||
item=McpCall(
|
||||
type="mcp_call",
|
||||
id=state.current_item_id,
|
||||
name=mcp_name,
|
||||
arguments=previous_item.content[0].text,
|
||||
server_label=mcp_name,
|
||||
status="completed",
|
||||
),
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def emit_tool_action_events(
|
||||
ctx: StreamingHarmonyContext,
|
||||
state: HarmonyStreamingState,
|
||||
state: StreamingState,
|
||||
tool_server: ToolServer | None,
|
||||
) -> list[StreamingResponsesResponse]:
|
||||
"""Emit events for tool action turn."""
|
||||
@@ -879,19 +782,13 @@ def emit_tool_action_events(
|
||||
and state.sent_output_item_added
|
||||
):
|
||||
recipient = previous_item.recipient
|
||||
# Handle MCP prefix tool completion first
|
||||
if recipient.startswith("mcp."):
|
||||
events.extend(emit_mcp_prefix_completion_events(previous_item, state))
|
||||
else:
|
||||
# Handle other MCP tool and code interpreter completion
|
||||
is_mcp_tool = is_mcp_tool_by_namespace(
|
||||
recipient
|
||||
) and state.current_item_id.startswith("mcp_")
|
||||
if is_mcp_tool:
|
||||
events.extend(emit_mcp_tool_completion_events(previous_item, state))
|
||||
else:
|
||||
events.extend(
|
||||
emit_code_interpreter_completion_events(previous_item, state)
|
||||
if recipient == "python":
|
||||
events.extend(emit_code_interpreter_completion_events(previous_item, state))
|
||||
elif recipient.startswith("mcp.") or is_mcp_tool_by_namespace(recipient):
|
||||
events.extend(
|
||||
emit_mcp_completion_events(
|
||||
recipient, previous_item.content[0].text, state
|
||||
)
|
||||
)
|
||||
|
||||
return events
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import sys
|
||||
import tempfile
|
||||
from argparse import Namespace
|
||||
from collections.abc import Awaitable, Callable
|
||||
@@ -17,23 +18,23 @@ from fastapi import UploadFile
|
||||
from prometheus_client import start_http_server
|
||||
from pydantic import Field, TypeAdapter, field_validator, model_validator
|
||||
from pydantic_core.core_schema import ValidationInfo
|
||||
from starlette.datastructures import State
|
||||
from tqdm import tqdm
|
||||
|
||||
from vllm.engine.arg_utils import AsyncEngineArgs, optional_type
|
||||
from vllm.config import config
|
||||
from vllm.engine.arg_utils import AsyncEngineArgs
|
||||
from vllm.engine.protocol import EngineClient
|
||||
from vllm.entrypoints.logger import RequestLogger
|
||||
from vllm.entrypoints.openai.api_server import init_app_state
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
)
|
||||
from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat
|
||||
from vllm.entrypoints.openai.cli_args import BaseFrontendArgs
|
||||
from vllm.entrypoints.openai.engine.protocol import (
|
||||
ErrorInfo,
|
||||
ErrorResponse,
|
||||
OpenAIBaseModel,
|
||||
)
|
||||
from vllm.entrypoints.openai.models.protocol import BaseModelPath
|
||||
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
|
||||
from vllm.entrypoints.openai.speech_to_text.protocol import (
|
||||
TranscriptionRequest,
|
||||
TranscriptionResponse,
|
||||
@@ -42,25 +43,18 @@ from vllm.entrypoints.openai.speech_to_text.protocol import (
|
||||
TranslationResponse,
|
||||
TranslationResponseVerbose,
|
||||
)
|
||||
from vllm.entrypoints.openai.speech_to_text.serving import (
|
||||
OpenAIServingTranscription,
|
||||
OpenAIServingTranslation,
|
||||
)
|
||||
from vllm.entrypoints.pooling.embed.protocol import (
|
||||
EmbeddingRequest,
|
||||
EmbeddingResponse,
|
||||
)
|
||||
from vllm.entrypoints.pooling.embed.serving import OpenAIServingEmbedding
|
||||
from vllm.entrypoints.pooling.score.protocol import (
|
||||
RerankRequest,
|
||||
RerankResponse,
|
||||
ScoreRequest,
|
||||
ScoreResponse,
|
||||
)
|
||||
from vllm.entrypoints.pooling.score.serving import ServingScores
|
||||
from vllm.logger import init_logger
|
||||
from vllm.reasoning import ReasoningParserManager
|
||||
from vllm.tasks import SupportedTask
|
||||
from vllm.utils import random_uuid
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
from vllm.version import __version__ as VLLM_VERSION
|
||||
@@ -219,87 +213,73 @@ class BatchRequestOutput(OpenAIBaseModel):
|
||||
error: Any | None
|
||||
|
||||
|
||||
@config
|
||||
class BatchFrontendArgs(BaseFrontendArgs):
|
||||
"""Arguments for the batch runner frontend."""
|
||||
|
||||
input_file: str | None = None
|
||||
"""The path or url to a single input file. Currently supports local file
|
||||
paths, or the http protocol (http or https). If a URL is specified,
|
||||
the file should be available via HTTP GET."""
|
||||
output_file: str | None = None
|
||||
"""The path or url to a single output file. Currently supports
|
||||
local file paths, or web (http or https) urls. If a URL is specified,
|
||||
the file should be available via HTTP PUT."""
|
||||
output_tmp_dir: str | None = None
|
||||
"""The directory to store the output file before uploading it
|
||||
to the output URL."""
|
||||
enable_metrics: bool = False
|
||||
"""Enable Prometheus metrics"""
|
||||
host: str | None = None
|
||||
"""Host name for the Prometheus metrics server
|
||||
(only needed if enable-metrics is set)."""
|
||||
port: int = 8000
|
||||
"""Port number for the Prometheus metrics server
|
||||
(only needed if enable-metrics is set)."""
|
||||
url: str = "0.0.0.0"
|
||||
"""[DEPRECATED] Host name for the Prometheus metrics server
|
||||
(only needed if enable-metrics is set). Use --host instead."""
|
||||
|
||||
@classmethod
|
||||
def _customize_cli_kwargs(
|
||||
cls,
|
||||
frontend_kwargs: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
frontend_kwargs = super()._customize_cli_kwargs(frontend_kwargs)
|
||||
|
||||
frontend_kwargs["input_file"]["flags"] = ["-i"]
|
||||
frontend_kwargs["input_file"]["required"] = True
|
||||
frontend_kwargs["output_file"]["flags"] = ["-o"]
|
||||
frontend_kwargs["output_file"]["required"] = True
|
||||
|
||||
frontend_kwargs["enable_metrics"]["action"] = "store_true"
|
||||
|
||||
frontend_kwargs["url"]["deprecated"] = True
|
||||
return frontend_kwargs
|
||||
|
||||
|
||||
def make_arg_parser(parser: FlexibleArgumentParser):
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-file",
|
||||
required=True,
|
||||
type=str,
|
||||
help="The path or url to a single input file. Currently supports local file "
|
||||
"paths, or the http protocol (http or https). If a URL is specified, "
|
||||
"the file should be available via HTTP GET.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
required=True,
|
||||
type=str,
|
||||
help="The path or url to a single output file. Currently supports "
|
||||
"local file paths, or web (http or https) urls. If a URL is specified,"
|
||||
" the file should be available via HTTP PUT.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-tmp-dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The directory to store the output file before uploading it "
|
||||
"to the output URL.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--response-role",
|
||||
type=optional_type(str),
|
||||
default="assistant",
|
||||
help="The role name to return if `request.add_generation_prompt=True`.",
|
||||
)
|
||||
|
||||
parser = BatchFrontendArgs.add_cli_args(parser)
|
||||
parser = AsyncEngineArgs.add_cli_args(parser)
|
||||
|
||||
parser.add_argument(
|
||||
"--max-log-len",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Max number of prompt characters or prompt "
|
||||
"ID numbers being printed in log."
|
||||
"\n\nDefault: Unlimited",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--enable-metrics", action="store_true", help="Enable Prometheus metrics"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--url",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="URL to the Prometheus metrics server "
|
||||
"(only needed if enable-metrics is set).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Port number for the Prometheus metrics server "
|
||||
"(only needed if enable-metrics is set).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-prompt-tokens-details",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If set to True, enable prompt_tokens_details in usage.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-force-include-usage",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If set to True, include usage on every request "
|
||||
"(even when stream_options is not specified)",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = FlexibleArgumentParser(description="vLLM OpenAI-Compatible batch runner.")
|
||||
return make_arg_parser(parser).parse_args()
|
||||
args = make_arg_parser(parser).parse_args()
|
||||
|
||||
# Backward compatibility: If --url is set, use it for host
|
||||
url_explicit = any(arg == "--url" or arg.startswith("--url=") for arg in sys.argv)
|
||||
host_explicit = any(
|
||||
arg == "--host" or arg.startswith("--host=") for arg in sys.argv
|
||||
)
|
||||
if url_explicit and hasattr(args, "url") and not host_explicit:
|
||||
args.host = args.url
|
||||
logger.warning_once(
|
||||
"Using --url for metrics is deprecated. Please use --host instead."
|
||||
)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
# explicitly use pure text format, with a newline at the end
|
||||
@@ -671,12 +651,9 @@ def make_transcription_wrapper(is_translation: bool) -> WrapperFn:
|
||||
return wrapper
|
||||
|
||||
|
||||
def build_endpoint_registry(
|
||||
async def build_endpoint_registry(
|
||||
engine_client: EngineClient,
|
||||
args: Namespace,
|
||||
base_model_paths: list[BaseModelPath],
|
||||
request_logger: RequestLogger | None,
|
||||
supported_tasks: tuple[SupportedTask, ...],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""
|
||||
Build the endpoint registry with all serving objects and handler configurations.
|
||||
@@ -684,90 +661,27 @@ def build_endpoint_registry(
|
||||
Args:
|
||||
engine_client: The engine client
|
||||
args: Command line arguments
|
||||
base_model_paths: List of base model paths
|
||||
request_logger: Optional request logger
|
||||
supported_tasks: Tuple of supported tasks
|
||||
|
||||
Returns:
|
||||
Dictionary mapping endpoint keys to their configurations
|
||||
"""
|
||||
model_config = engine_client.model_config
|
||||
supported_tasks = await engine_client.get_supported_tasks()
|
||||
logger.info("Supported tasks: %s", supported_tasks)
|
||||
|
||||
# Create the openai serving objects.
|
||||
openai_serving_models = OpenAIServingModels(
|
||||
engine_client=engine_client,
|
||||
base_model_paths=base_model_paths,
|
||||
lora_modules=None,
|
||||
)
|
||||
# Create a state object to hold serving objects
|
||||
state = State()
|
||||
|
||||
openai_serving_chat = (
|
||||
OpenAIServingChat(
|
||||
engine_client,
|
||||
openai_serving_models,
|
||||
args.response_role,
|
||||
request_logger=request_logger,
|
||||
chat_template=None,
|
||||
chat_template_content_format="auto",
|
||||
reasoning_parser=args.structured_outputs_config.reasoning_parser,
|
||||
enable_prompt_tokens_details=args.enable_prompt_tokens_details,
|
||||
enable_force_include_usage=args.enable_force_include_usage,
|
||||
default_chat_template_kwargs=getattr(
|
||||
args, "default_chat_template_kwargs", None
|
||||
),
|
||||
)
|
||||
if "generate" in supported_tasks
|
||||
else None
|
||||
)
|
||||
# Initialize all serving objects using init_app_state
|
||||
# This provides full functionality including chat template processing,
|
||||
# LoRA support, tool servers, etc.
|
||||
await init_app_state(engine_client, state, args, supported_tasks)
|
||||
|
||||
openai_serving_embedding = (
|
||||
OpenAIServingEmbedding(
|
||||
engine_client,
|
||||
openai_serving_models,
|
||||
request_logger=request_logger,
|
||||
chat_template=None,
|
||||
chat_template_content_format="auto",
|
||||
)
|
||||
if "embed" in supported_tasks
|
||||
else None
|
||||
)
|
||||
|
||||
enable_serving_reranking = (
|
||||
"classify" in supported_tasks
|
||||
and getattr(model_config.hf_config, "num_labels", 0) == 1
|
||||
)
|
||||
|
||||
openai_serving_scores = (
|
||||
ServingScores(
|
||||
engine_client,
|
||||
openai_serving_models,
|
||||
request_logger=request_logger,
|
||||
score_template=None,
|
||||
)
|
||||
if ("embed" in supported_tasks or enable_serving_reranking)
|
||||
else None
|
||||
)
|
||||
|
||||
openai_serving_transcription = (
|
||||
OpenAIServingTranscription(
|
||||
engine_client,
|
||||
openai_serving_models,
|
||||
request_logger=request_logger,
|
||||
enable_force_include_usage=args.enable_force_include_usage,
|
||||
)
|
||||
if "transcription" in supported_tasks
|
||||
else None
|
||||
)
|
||||
|
||||
openai_serving_translation = (
|
||||
OpenAIServingTranslation(
|
||||
engine_client,
|
||||
openai_serving_models,
|
||||
request_logger=request_logger,
|
||||
enable_force_include_usage=args.enable_force_include_usage,
|
||||
)
|
||||
if "transcription" in supported_tasks
|
||||
else None
|
||||
)
|
||||
# Get serving objects from state (defaulting to None if not set)
|
||||
openai_serving_chat = getattr(state, "openai_serving_chat", None)
|
||||
openai_serving_embedding = getattr(state, "openai_serving_embedding", None)
|
||||
openai_serving_scores = getattr(state, "openai_serving_scores", None)
|
||||
openai_serving_transcription = getattr(state, "openai_serving_transcription", None)
|
||||
openai_serving_translation = getattr(state, "openai_serving_translation", None)
|
||||
|
||||
# Registry of endpoint configurations
|
||||
endpoint_registry: dict[str, dict[str, Any]] = {
|
||||
@@ -845,29 +759,9 @@ async def run_batch(
|
||||
engine_client: EngineClient,
|
||||
args: Namespace,
|
||||
) -> None:
|
||||
if args.served_model_name is not None:
|
||||
served_model_names = args.served_model_name
|
||||
else:
|
||||
served_model_names = [args.model]
|
||||
|
||||
if args.enable_log_requests:
|
||||
request_logger = RequestLogger(max_log_len=args.max_log_len)
|
||||
else:
|
||||
request_logger = None
|
||||
|
||||
base_model_paths = [
|
||||
BaseModelPath(name=name, model_path=args.model) for name in served_model_names
|
||||
]
|
||||
|
||||
supported_tasks = await engine_client.get_supported_tasks()
|
||||
logger.info("Supported tasks: %s", supported_tasks)
|
||||
|
||||
endpoint_registry = build_endpoint_registry(
|
||||
endpoint_registry = await build_endpoint_registry(
|
||||
engine_client=engine_client,
|
||||
args=args,
|
||||
base_model_paths=base_model_paths,
|
||||
request_logger=request_logger,
|
||||
supported_tasks=supported_tasks,
|
||||
)
|
||||
|
||||
tracker = BatchProgressTracker()
|
||||
@@ -942,7 +836,7 @@ if __name__ == "__main__":
|
||||
# to publish metrics at the /metrics endpoint.
|
||||
if args.enable_metrics:
|
||||
logger.info("Prometheus metrics enabled")
|
||||
start_http_server(port=args.port, addr=args.url)
|
||||
start_http_server(port=args.port, addr=args.host)
|
||||
else:
|
||||
logger.info("Prometheus metrics disabled")
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ router = APIRouter()
|
||||
async def sleep(raw_request: Request):
|
||||
# get POST params
|
||||
level = raw_request.query_params.get("level", "1")
|
||||
await engine_client(raw_request).sleep(int(level))
|
||||
mode = raw_request.query_params.get("mode", "abort")
|
||||
await engine_client(raw_request).sleep(int(level), mode)
|
||||
# FIXME: in v0 with frontend multiprocessing, the sleep command
|
||||
# is sent but does not finish yet when we return a response.
|
||||
return Response(status_code=200)
|
||||
|
||||
+9
-3
@@ -97,6 +97,7 @@ if TYPE_CHECKING:
|
||||
VLLM_SKIP_P2P_CHECK: bool = False
|
||||
VLLM_DISABLED_KERNELS: list[str] = []
|
||||
VLLM_DISABLE_PYNCCL: bool = False
|
||||
VLLM_USE_OINK_OPS: bool = False
|
||||
VLLM_ROCM_USE_AITER: bool = False
|
||||
VLLM_ROCM_USE_AITER_PAGED_ATTN: bool = False
|
||||
VLLM_ROCM_USE_AITER_LINEAR: bool = True
|
||||
@@ -105,7 +106,7 @@ if TYPE_CHECKING:
|
||||
VLLM_ROCM_USE_AITER_MLA: bool = True
|
||||
VLLM_ROCM_USE_AITER_MHA: bool = True
|
||||
VLLM_ROCM_USE_AITER_FP4_ASM_GEMM: bool = False
|
||||
VLLM_ROCM_USE_AITER_TRITON_ROPE: bool = False
|
||||
VLLM_ROCM_USE_AITER_TRITON_ROPE: bool = True
|
||||
VLLM_ROCM_USE_AITER_FP8BMM: bool = True
|
||||
VLLM_ROCM_USE_AITER_FP4BMM: bool = True
|
||||
VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION: bool = False
|
||||
@@ -896,6 +897,11 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
"VLLM_DISABLE_PYNCCL": lambda: (
|
||||
os.getenv("VLLM_DISABLE_PYNCCL", "False").lower() in ("true", "1")
|
||||
),
|
||||
# Optional: enable external Oink custom ops (e.g., Blackwell RMSNorm).
|
||||
# Disabled by default.
|
||||
"VLLM_USE_OINK_OPS": lambda: (
|
||||
os.getenv("VLLM_USE_OINK_OPS", "False").lower() in ("true", "1")
|
||||
),
|
||||
# Disable aiter ops unless specifically enabled.
|
||||
# Acts as a parent switch to enable the rest of the other operations.
|
||||
"VLLM_ROCM_USE_AITER": lambda: (
|
||||
@@ -937,9 +943,9 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
os.getenv("VLLM_ROCM_USE_AITER_FP4_ASM_GEMM", "False").lower() in ("true", "1")
|
||||
),
|
||||
# Whether to use aiter rope.
|
||||
# By default is disabled.
|
||||
# By default is enabled.
|
||||
"VLLM_ROCM_USE_AITER_TRITON_ROPE": lambda: (
|
||||
os.getenv("VLLM_ROCM_USE_AITER_TRITON_ROPE", "False").lower() in ("true", "1")
|
||||
os.getenv("VLLM_ROCM_USE_AITER_TRITON_ROPE", "True").lower() in ("true", "1")
|
||||
),
|
||||
# Whether to use aiter triton fp8 bmm kernel
|
||||
# By default is enabled.
|
||||
|
||||
@@ -133,15 +133,19 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
|
||||
if getattr(self.base_layer.quant_method, "supports_internal_mk", False):
|
||||
# Use the existing modular kernel from the quant method
|
||||
m_fused_moe_fn = self.base_layer.quant_method.moe_mk
|
||||
# Don't let the kernel own shared experts so the runner can
|
||||
# overlap them with routed experts via a separate CUDA stream.
|
||||
m_fused_moe_fn.shared_experts = None
|
||||
else:
|
||||
# Create a new modular kernel via select_gemm_impl
|
||||
# Create a new modular kernel via select_gemm_impl.
|
||||
# Don't pass shared_experts to the kernel so the runner can
|
||||
# overlap them with routed experts via a separate CUDA stream.
|
||||
prepare_finalize = MoEPrepareAndFinalizeNoEP()
|
||||
m_fused_moe_fn = FusedMoEModularKernel(
|
||||
prepare_finalize,
|
||||
self.base_layer.quant_method.select_gemm_impl(
|
||||
prepare_finalize, self.base_layer
|
||||
),
|
||||
self.base_layer.shared_experts,
|
||||
)
|
||||
|
||||
if quant_config.use_mxfp4_w4a16:
|
||||
|
||||
@@ -11,6 +11,7 @@ from vllm.model_executor.layers.quantization.utils.allspark_utils import (
|
||||
check_allspark_supported_dtype_shape,
|
||||
)
|
||||
from vllm.model_executor.parameter import BasevLLMParameter, permute_param_layout_
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
|
||||
from .MPLinearKernel import MPLinearKernel, MPLinearLayerConfig
|
||||
|
||||
@@ -45,7 +46,7 @@ class AllSparkLinearKernel(MPLinearKernel):
|
||||
|
||||
# prepare the parameters required for the kernel
|
||||
properties = torch.cuda.get_device_properties(device.index)
|
||||
sm_count = properties.multi_processor_count
|
||||
sm_count = num_compute_units(device.index)
|
||||
sm_version = properties.major * 10 + properties.minor
|
||||
gemm_args = {}
|
||||
gemm_args["sm_count"] = sm_count
|
||||
|
||||
@@ -7,7 +7,7 @@ import torch
|
||||
import vllm.envs as envs
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.platform_utils import get_cu_count
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
from vllm.utils.torch_utils import direct_register_custom_op
|
||||
|
||||
from .ScaledMMLinearKernel import (
|
||||
@@ -36,7 +36,7 @@ def rocm_per_tensor_float_w8a8_scaled_mm_impl(
|
||||
out_dtype,
|
||||
As,
|
||||
Bs,
|
||||
get_cu_count(),
|
||||
num_compute_units(),
|
||||
bias,
|
||||
)
|
||||
# Fallback
|
||||
|
||||
@@ -9,6 +9,7 @@ import torch
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.triton_utils import tl, triton
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
from vllm.utils.torch_utils import is_torch_equal_or_newer
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
|
||||
@@ -147,7 +148,7 @@ def matmul_persistent(
|
||||
assert bias is None or bias.dim() == 1, (
|
||||
"Currently assuming bias is 1D, let Horace know if you run into this"
|
||||
)
|
||||
NUM_SMS = torch.cuda.get_device_properties("cuda").multi_processor_count
|
||||
NUM_SMS = num_compute_units(a.device.index)
|
||||
M, K = a.shape
|
||||
K, N = b.shape
|
||||
dtype = a.dtype
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
# This backward pass is faster for dimensions up to 8k, but after that it's much slower due to register spilling.
|
||||
# The models we train have hidden dim up to 8k anyway (e.g. Llama 70B), so this is fine.
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
@@ -22,6 +20,7 @@ from einops import rearrange
|
||||
|
||||
from vllm.triton_utils import tl, triton
|
||||
from vllm.utils.math_utils import cdiv, next_power_of_2
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
|
||||
from .utils import input_guard
|
||||
|
||||
@@ -162,15 +161,8 @@ def layer_norm_fwd_kernel(
|
||||
tl.store(Y_base, y, mask=mask)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _get_sm_count(device: torch.device) -> int:
|
||||
"""Get and cache the SM count for a given device."""
|
||||
props = torch.cuda.get_device_properties(device)
|
||||
return props.multi_processor_count
|
||||
|
||||
|
||||
def calc_rows_per_block(M: int, device: torch.device) -> int:
|
||||
sm_count = _get_sm_count(device)
|
||||
sm_count = num_compute_units(device.index)
|
||||
rows_per_block = next_power_of_2(cdiv(M, 2 * sm_count))
|
||||
rows_per_block = min(rows_per_block, 4)
|
||||
return rows_per_block
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"triton_version": "3.6.0",
|
||||
"1": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 64,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"2": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"4": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"8": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"16": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"24": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"32": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"48": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 256,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
},
|
||||
"64": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"96": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 256,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"128": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 256,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"256": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"512": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"1024": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"1536": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 256,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
},
|
||||
"2048": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 256,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
},
|
||||
"3072": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"4096": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 256,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 3
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from vllm import _oink_ops, envs
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.custom_op import CustomOp
|
||||
from vllm.model_executor.layers.batch_invariant import (
|
||||
rms_norm_batch_invariant,
|
||||
@@ -14,6 +16,41 @@ from vllm.model_executor.layers.batch_invariant import (
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _can_view_as_2d(x: torch.Tensor) -> bool:
|
||||
"""Return True if x.view(-1, x.shape[-1]) is viewable (no copy)."""
|
||||
if x.dim() < 2:
|
||||
return False
|
||||
if x.dim() == 2:
|
||||
return True
|
||||
# For a view(-1, N) to be valid, all leading dims must be contiguous with
|
||||
# respect to each other (size-1 dims are ignored).
|
||||
for dim in range(x.dim() - 1):
|
||||
# Strides for size-1 dims are irrelevant and can be arbitrary.
|
||||
if x.size(dim + 1) != 1 and x.stride(dim) != x.stride(dim + 1) * x.size(
|
||||
dim + 1
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_oink_stride_compatible_2d(x_2d: torch.Tensor) -> bool:
|
||||
"""Return True if x_2d meets Oink's pointer-path stride constraints."""
|
||||
if x_2d.dim() != 2:
|
||||
return False
|
||||
if x_2d.stride(1) != 1:
|
||||
return False
|
||||
# Match Oink's vectorization constraint: stride(0) divisible by 256b.
|
||||
if x_2d.dtype in (torch.float16, torch.bfloat16):
|
||||
divby = 16
|
||||
elif x_2d.dtype == torch.float32:
|
||||
divby = 8
|
||||
else:
|
||||
return False
|
||||
return (x_2d.stride(0) % divby) == 0
|
||||
|
||||
|
||||
def rms_norm(
|
||||
x: torch.Tensor, weight: torch.Tensor, variance_epsilon: float
|
||||
@@ -131,6 +168,57 @@ class RMSNorm(CustomOp):
|
||||
with_fused_add=True, dtype=weight_dtype, use_aiter=aiter_rmsnorm_enabled
|
||||
)
|
||||
|
||||
# Optional: enable Oink Blackwell RMSNorm custom-op fast path on
|
||||
# compatible CUDA devices (e.g., SM100) when the external Oink
|
||||
# package is available. This is detected once at construction time
|
||||
# to avoid per-call device queries in the hot path.
|
||||
self._use_oink_rmsnorm = False
|
||||
self._use_oink_fused_add_rmsnorm = False
|
||||
if (
|
||||
not current_platform.is_rocm()
|
||||
and torch.cuda.is_available()
|
||||
and bool(getattr(envs, "VLLM_USE_OINK_OPS", False))
|
||||
):
|
||||
# NOTE: vLLM disables custom ops by default when using Inductor.
|
||||
# If this op is disabled, CustomOp will dispatch to forward_native,
|
||||
# and the Oink path in forward_cuda will never run.
|
||||
if getattr(self._forward_method, "__func__", None) is getattr(
|
||||
self.forward_native, "__func__", None
|
||||
):
|
||||
try:
|
||||
from vllm.config import get_cached_compilation_config
|
||||
|
||||
custom_ops = get_cached_compilation_config().custom_ops
|
||||
except Exception:
|
||||
custom_ops = ["<unknown>"]
|
||||
logger.warning_once(
|
||||
"VLLM_USE_OINK_OPS=1 but the `rms_norm` custom op is "
|
||||
"disabled (CompilationConfig.custom_ops=%s). Enable it via "
|
||||
"`compilation_config={'custom_ops': ['none', '+rms_norm']}` "
|
||||
"(or `['all']`) to let vLLM call into torch.ops.oink.*.",
|
||||
custom_ops,
|
||||
)
|
||||
# Custom op disabled => forward_cuda won't run. Avoid doing any
|
||||
# external Oink initialization work in this case.
|
||||
else:
|
||||
try:
|
||||
device_index = torch.cuda.current_device()
|
||||
if _oink_ops.is_oink_available_for_device(device_index):
|
||||
self._use_oink_rmsnorm = True
|
||||
self._use_oink_fused_add_rmsnorm = (
|
||||
_oink_ops.has_fused_add_rms_norm()
|
||||
)
|
||||
except Exception as e:
|
||||
# If anything goes wrong (no Oink install, CPU-only env, etc.),
|
||||
# silently fall back to the built-in RMSNorm path.
|
||||
logger.warning_once(
|
||||
"VLLM_USE_OINK_OPS=1 but failed to initialize Oink "
|
||||
"RMSNorm; falling back to vLLM RMSNorm. Error: %s",
|
||||
e,
|
||||
)
|
||||
self._use_oink_rmsnorm = False
|
||||
self._use_oink_fused_add_rmsnorm = False
|
||||
|
||||
@staticmethod
|
||||
def forward_static(
|
||||
x: torch.Tensor,
|
||||
@@ -202,6 +290,73 @@ class RMSNorm(CustomOp):
|
||||
if self.variance_size_override is not None:
|
||||
return self.forward_native(x, residual)
|
||||
|
||||
# Optional Oink SM100 fast path (no residual). This path is
|
||||
# torch.compile-friendly via torch.ops.oink.rmsnorm and preserves
|
||||
# 2D layouts (including padded rows) when using the Oink
|
||||
# pointer-based kernel.
|
||||
if (
|
||||
residual is None
|
||||
and getattr(self, "_use_oink_rmsnorm", False)
|
||||
and x.is_cuda
|
||||
and x.dim() >= 2
|
||||
and self.has_weight
|
||||
and not vllm_is_batch_invariant()
|
||||
and self.weight.data.dtype == x.dtype
|
||||
and self.weight.data.is_contiguous()
|
||||
):
|
||||
orig_shape = x.shape
|
||||
hidden_size = orig_shape[-1]
|
||||
if _can_view_as_2d(x):
|
||||
x_2d = x.view(-1, hidden_size)
|
||||
if _is_oink_stride_compatible_2d(x_2d):
|
||||
y_2d = _oink_ops.rmsnorm(
|
||||
x_2d,
|
||||
self.weight.data,
|
||||
self.variance_epsilon,
|
||||
)
|
||||
return y_2d.view(orig_shape)
|
||||
|
||||
# Optional Oink SM100 fast path (fused residual-add + RMSNorm, in-place).
|
||||
# This mirrors vLLM's fused_add_rms_norm semantics by mutating both
|
||||
# `x` (normalized output) and `residual` (residual-out buffer).
|
||||
if (
|
||||
residual is not None
|
||||
and getattr(self, "_use_oink_fused_add_rmsnorm", False)
|
||||
and x.is_cuda
|
||||
and residual.is_cuda
|
||||
and x.shape == residual.shape
|
||||
and x.dtype == residual.dtype
|
||||
and x.dim() >= 2
|
||||
and self.has_weight
|
||||
and not vllm_is_batch_invariant()
|
||||
and self.weight.data.dtype == x.dtype
|
||||
and self.weight.data.is_contiguous()
|
||||
):
|
||||
orig_shape = x.shape
|
||||
hidden_size = orig_shape[-1]
|
||||
if _can_view_as_2d(x) and _can_view_as_2d(residual):
|
||||
x_2d = x.view(-1, hidden_size)
|
||||
res_2d = residual.view(-1, hidden_size)
|
||||
|
||||
# The Oink in-place pointer path supports the common vLLM
|
||||
# layout where:
|
||||
# - `x` may be strided/padded row-major (stride(1) == 1), and
|
||||
# - `residual` is contiguous row-major ([M, N] with stride(0) == N).
|
||||
# If these conditions are not met, fall back to vLLM's built-in
|
||||
# fused kernel.
|
||||
if (
|
||||
_is_oink_stride_compatible_2d(x_2d)
|
||||
and _is_oink_stride_compatible_2d(res_2d)
|
||||
and res_2d.is_contiguous()
|
||||
):
|
||||
_oink_ops.fused_add_rms_norm_(
|
||||
x_2d,
|
||||
res_2d,
|
||||
self.weight.data,
|
||||
self.variance_epsilon,
|
||||
)
|
||||
return x, residual
|
||||
|
||||
add_residual = residual is not None
|
||||
if add_residual:
|
||||
return fused_add_rms_norm(
|
||||
|
||||
@@ -66,15 +66,23 @@ WEIGHT_LOADER_V2_SUPPORTED = [
|
||||
]
|
||||
|
||||
|
||||
def adjust_marlin_shard(param, shard_size, shard_offset):
|
||||
marlin_tile_size = getattr(param, "marlin_tile_size", None)
|
||||
def adjust_marlin_shard(
|
||||
param: Parameter,
|
||||
shard_size: int,
|
||||
shard_offset: int,
|
||||
) -> tuple[int, int]:
|
||||
marlin_tile_size: int | None = getattr(param, "marlin_tile_size", None)
|
||||
if marlin_tile_size is None:
|
||||
return shard_size, shard_offset
|
||||
|
||||
return shard_size * marlin_tile_size, shard_offset * marlin_tile_size
|
||||
|
||||
|
||||
def adjust_block_scale_shard(weight_block_size, shard_size, shard_offset):
|
||||
def adjust_block_scale_shard(
|
||||
weight_block_size: tuple[int, ...] | None,
|
||||
shard_size: int,
|
||||
shard_offset: int,
|
||||
) -> tuple[int, int]:
|
||||
assert weight_block_size is not None
|
||||
block_n = weight_block_size[0]
|
||||
shard_offset = (shard_offset + block_n - 1) // block_n
|
||||
@@ -83,7 +91,9 @@ def adjust_block_scale_shard(weight_block_size, shard_size, shard_offset):
|
||||
|
||||
|
||||
def adjust_bitsandbytes_4bit_shard(
|
||||
param: Parameter, shard_offsets: dict[str, tuple[int, int]], loaded_shard_id: str
|
||||
param: Parameter,
|
||||
shard_offsets: dict[str, tuple[int, int]],
|
||||
loaded_shard_id: str,
|
||||
) -> tuple[int, int]:
|
||||
"""Adjust the quantization offsets and sizes for BitsAndBytes sharding."""
|
||||
|
||||
@@ -97,7 +107,11 @@ def adjust_bitsandbytes_4bit_shard(
|
||||
return quantized_size, quantized_offset
|
||||
|
||||
|
||||
def adjust_scalar_to_fused_array(param, loaded_weight, shard_id):
|
||||
def adjust_scalar_to_fused_array(
|
||||
param_data: torch.Tensor,
|
||||
loaded_weight: torch.Tensor,
|
||||
shard_id: int | str,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""For fused modules (QKV and MLP) we have an array of length
|
||||
N that holds 1 scale for each "logical" matrix. So the param
|
||||
is an array of length N. The loaded_weight corresponds to
|
||||
@@ -117,12 +131,14 @@ def adjust_scalar_to_fused_array(param, loaded_weight, shard_id):
|
||||
assert loaded_weight.shape[0] == 1
|
||||
loaded_weight = loaded_weight[0]
|
||||
|
||||
return param[shard_id], loaded_weight
|
||||
return param_data[shard_id], loaded_weight
|
||||
|
||||
|
||||
# TODO(Isotr0py): We might need a more flexible structure to handle
|
||||
# bitsandbytes shard offsets.
|
||||
def left_shift_bitsandbytes_4bit_shard(bnb_weight_attrs: dict[str, Any]):
|
||||
def left_shift_bitsandbytes_4bit_shard(
|
||||
bnb_weight_attrs: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""
|
||||
Separate the BitsAndBytes 4-bit shard.
|
||||
|
||||
@@ -681,12 +697,41 @@ class MergedColumnParallelLinear(ColumnParallelLinear):
|
||||
disable_tp=disable_tp,
|
||||
)
|
||||
|
||||
def validate_shard_id(self, loaded_shard_id: int | tuple[int, ...] | None):
|
||||
if loaded_shard_id is None:
|
||||
return
|
||||
if isinstance(loaded_shard_id, tuple):
|
||||
for idx in loaded_shard_id:
|
||||
if not (0 <= idx < len(self.output_sizes)):
|
||||
raise ValueError(
|
||||
f"Shard id index {idx} should be between 0 and "
|
||||
f"{len(self.output_sizes) - 1}. Got shard id {loaded_shard_id}."
|
||||
)
|
||||
if len(loaded_shard_id) > 1 and any(
|
||||
b - a != 1 for a, b in zip(loaded_shard_id[:-1], loaded_shard_id[1:])
|
||||
):
|
||||
raise ValueError(
|
||||
"Shard id with multiple indices should be consecutive. "
|
||||
f"Got shard id {loaded_shard_id}."
|
||||
)
|
||||
return
|
||||
elif isinstance(loaded_shard_id, int):
|
||||
if loaded_shard_id < 0 or loaded_shard_id >= len(self.output_sizes):
|
||||
raise ValueError(
|
||||
f"Shard id should be between 0 and {len(self.output_sizes) - 1}. "
|
||||
f"Got shard id {loaded_shard_id}."
|
||||
)
|
||||
return
|
||||
raise ValueError("This line should not be reached")
|
||||
|
||||
def weight_loader(
|
||||
self,
|
||||
param: Parameter,
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: tuple[int, ...] | int | None = None,
|
||||
):
|
||||
self.validate_shard_id(loaded_shard_id)
|
||||
# FIXME(Isotr0py): Enable tuple shard_id for BNB quantization.
|
||||
if isinstance(loaded_shard_id, tuple):
|
||||
raise NotImplementedError(
|
||||
"Shard id with multiple indices is not supported in weight_loader, "
|
||||
@@ -874,6 +919,7 @@ class MergedColumnParallelLinear(ColumnParallelLinear):
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: tuple[int, ...] | int | None = None,
|
||||
):
|
||||
self.validate_shard_id(loaded_shard_id)
|
||||
if loaded_shard_id is None or isinstance(loaded_shard_id, tuple):
|
||||
if isinstance(param, PerTensorScaleParameter):
|
||||
param.load_merged_column_weight(loaded_weight=loaded_weight, shard_id=0)
|
||||
@@ -1005,6 +1051,18 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
disable_tp=disable_tp,
|
||||
)
|
||||
|
||||
def validate_shard_id(self, loaded_shard_id: str | None):
|
||||
if loaded_shard_id is None:
|
||||
return
|
||||
if isinstance(loaded_shard_id, str):
|
||||
if loaded_shard_id not in ["q", "k", "v"]:
|
||||
raise ValueError(
|
||||
"Shard id for QKVParallelLinear should be 'q', 'k', or 'v', "
|
||||
f"got shard id {loaded_shard_id}."
|
||||
)
|
||||
return
|
||||
raise ValueError("This line should not be reached")
|
||||
|
||||
def _get_shard_offset_mapping(self, loaded_shard_id: str):
|
||||
shard_offset_mapping = {
|
||||
"q": 0,
|
||||
@@ -1073,6 +1131,7 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: str | None = None,
|
||||
):
|
||||
self.validate_shard_id(loaded_shard_id)
|
||||
if loaded_shard_id is None: # special case for certain models
|
||||
if isinstance(param, PerTensorScaleParameter):
|
||||
param.load_qkv_weight(
|
||||
@@ -1112,6 +1171,7 @@ class QKVParallelLinear(ColumnParallelLinear):
|
||||
loaded_weight: torch.Tensor,
|
||||
loaded_shard_id: str | None = None,
|
||||
):
|
||||
self.validate_shard_id(loaded_shard_id)
|
||||
# Special case for GGUF
|
||||
# initialize GGUF param after we know the quantize type
|
||||
is_gguf_weight = getattr(param, "is_gguf_weight", False)
|
||||
|
||||
@@ -41,14 +41,6 @@ class MambaBase(AttentionLayerBase):
|
||||
pass
|
||||
|
||||
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
|
||||
if (
|
||||
vllm_config.speculative_config is not None
|
||||
and vllm_config.model_config.hf_config.model_type
|
||||
not in ["qwen3_next", "qwen3_5", "qwen3_5_moe"]
|
||||
):
|
||||
raise NotImplementedError(
|
||||
"Mamba with speculative decoding is not supported yet."
|
||||
)
|
||||
mamba_block_size = vllm_config.cache_config.mamba_block_size
|
||||
page_size_padded = vllm_config.cache_config.mamba_page_size_padded
|
||||
return MambaSpec(
|
||||
|
||||
@@ -265,7 +265,8 @@ class MambaMixer(MambaBase, PluggableLayer):
|
||||
attn_metadata = attn_metadata[self.prefix]
|
||||
assert isinstance(attn_metadata, Mamba1AttentionMetadata)
|
||||
query_start_loc_p = attn_metadata.query_start_loc_p
|
||||
state_indices_tensor = attn_metadata.state_indices_tensor
|
||||
state_indices_tensor_p = attn_metadata.state_indices_tensor_p
|
||||
state_indices_tensor_d = attn_metadata.state_indices_tensor_d
|
||||
self_kv_cache = self.kv_cache[forward_context.virtual_engine]
|
||||
conv_state = self_kv_cache[0].transpose(-1, -2)
|
||||
ssm_state = self_kv_cache[1]
|
||||
@@ -295,17 +296,13 @@ class MambaMixer(MambaBase, PluggableLayer):
|
||||
prefill_decode_split = split_batch_to_prefill_and_decode(
|
||||
hidden_states_BC,
|
||||
gate,
|
||||
state_indices_tensor,
|
||||
num_prefill_tokens,
|
||||
num_prefills,
|
||||
num_decode_tokens,
|
||||
)
|
||||
hidden_states_BC_p = prefill_decode_split.hidden_states_BC_p
|
||||
hidden_states_BC_d = prefill_decode_split.hidden_states_BC_d
|
||||
gate_p = prefill_decode_split.gate_p
|
||||
gate_d = prefill_decode_split.gate_d
|
||||
state_indices_tensor_p = prefill_decode_split.state_indices_tensor_p
|
||||
state_indices_tensor_d = prefill_decode_split.state_indices_tensor_d
|
||||
|
||||
if is_mamba_cache_all:
|
||||
block_idx_last_computed_token_d, block_idx_last_computed_token_p = (
|
||||
@@ -477,16 +474,12 @@ class PrefillDecodeSplit(NamedTuple):
|
||||
hidden_states_BC_d: torch.Tensor
|
||||
gate_p: torch.Tensor
|
||||
gate_d: torch.Tensor
|
||||
state_indices_tensor_p: torch.Tensor
|
||||
state_indices_tensor_d: torch.Tensor
|
||||
|
||||
|
||||
def split_batch_to_prefill_and_decode(
|
||||
hidden_states_BC: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
state_indices_tensor: torch.Tensor,
|
||||
num_prefill_tokens: int,
|
||||
num_prefills: int,
|
||||
num_decode_tokens: int,
|
||||
) -> PrefillDecodeSplit:
|
||||
num_actual_tokens = num_prefill_tokens + num_decode_tokens
|
||||
@@ -501,20 +494,11 @@ def split_batch_to_prefill_and_decode(
|
||||
gate[..., :num_actual_tokens], [num_decode_tokens, num_prefill_tokens], dim=-1
|
||||
)
|
||||
|
||||
# num_decode_tokens accounts for CUDA graph padding when applicable
|
||||
state_indices_tensor_d, state_indices_tensor_p = torch.split(
|
||||
state_indices_tensor[: num_decode_tokens + num_prefills],
|
||||
[num_decode_tokens, num_prefills],
|
||||
dim=0,
|
||||
)
|
||||
|
||||
return PrefillDecodeSplit(
|
||||
hidden_states_BC_p=hidden_states_BC_p,
|
||||
hidden_states_BC_d=hidden_states_BC_d,
|
||||
gate_p=gate_p,
|
||||
gate_d=gate_d,
|
||||
state_indices_tensor_p=state_indices_tensor_p,
|
||||
state_indices_tensor_d=state_indices_tensor_d,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -477,7 +477,8 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
dim=-1,
|
||||
)
|
||||
|
||||
compilation_config = get_current_vllm_config().compilation_config
|
||||
vllm_config = get_current_vllm_config()
|
||||
compilation_config = vllm_config.compilation_config
|
||||
if prefix in compilation_config.static_forward_context:
|
||||
raise ValueError(f"Duplicate layer name: {prefix}")
|
||||
compilation_config.static_forward_context[prefix] = self
|
||||
@@ -488,6 +489,8 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
self.cache_config = cache_config
|
||||
self.prefix = prefix
|
||||
|
||||
self.num_spec = vllm_config.num_speculative_tokens
|
||||
|
||||
# Pre-compute sizes for forward pass
|
||||
self.tped_intermediate_size = self.intermediate_size // self.tp_size
|
||||
self.tped_conv_size = self.conv_dim // self.tp_size
|
||||
@@ -576,7 +579,6 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
# conv_state = (..., dim, width-1) yet contiguous along 'dim'
|
||||
conv_state = self_kv_cache[0].transpose(-1, -2)
|
||||
ssm_state = self_kv_cache[1]
|
||||
state_indices_tensor = attn_metadata.state_indices_tensor
|
||||
has_initial_states_p = attn_metadata.has_initial_states_p
|
||||
prep_initial_states = attn_metadata.prep_initial_states
|
||||
chunk_size = attn_metadata.chunk_size
|
||||
@@ -584,6 +586,12 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
query_start_loc_p = attn_metadata.query_start_loc_p
|
||||
cu_chunk_seqlen_p = attn_metadata.cu_chunk_seqlen_p
|
||||
last_chunk_indices_p = attn_metadata.last_chunk_indices_p
|
||||
state_indices_tensor_p = attn_metadata.state_indices_tensor_p
|
||||
state_indices_tensor_d = attn_metadata.state_indices_tensor_d
|
||||
num_accepted_tokens = attn_metadata.num_accepted_tokens
|
||||
query_start_loc_d = attn_metadata.query_start_loc_d
|
||||
num_decodes = attn_metadata.num_decodes
|
||||
num_decode_tokens = attn_metadata.num_decode_tokens
|
||||
|
||||
if attn_metadata is None:
|
||||
# profile run
|
||||
@@ -593,29 +601,21 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
hidden_states, _B, _C = self.split_hidden_states_B_C_fn(hidden_states_B_C)
|
||||
return hidden_states
|
||||
|
||||
num_prefills = attn_metadata.num_prefills # request count
|
||||
num_decodes = attn_metadata.num_decode_tokens # token count (=request)
|
||||
num_prefill_tokens = attn_metadata.num_prefill_tokens # token count
|
||||
num_prefills = attn_metadata.num_prefills
|
||||
num_prefill_tokens = attn_metadata.num_prefill_tokens
|
||||
has_prefill = num_prefills > 0
|
||||
has_decode = num_decodes > 0
|
||||
num_actual_tokens = num_prefill_tokens + num_decodes
|
||||
num_actual_tokens = num_prefill_tokens + num_decode_tokens
|
||||
|
||||
# Separate prefill and decode by splitting varlen input
|
||||
# Split along token dimension
|
||||
hidden_states_B_C_d, hidden_states_B_C_p = torch.split(
|
||||
hidden_states_B_C[:num_actual_tokens],
|
||||
[num_decodes, num_prefill_tokens],
|
||||
[num_decode_tokens, num_prefill_tokens],
|
||||
dim=0,
|
||||
)
|
||||
dt_d, dt_p = torch.split(
|
||||
dt[:num_actual_tokens],
|
||||
[num_decodes, num_prefill_tokens],
|
||||
dim=0,
|
||||
)
|
||||
# Split along batch dimension
|
||||
state_indices_tensor_d, state_indices_tensor_p = torch.split(
|
||||
state_indices_tensor[:num_actual_tokens],
|
||||
[num_decodes, num_prefills],
|
||||
[num_decode_tokens, num_prefill_tokens],
|
||||
dim=0,
|
||||
)
|
||||
|
||||
@@ -642,16 +642,16 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
)
|
||||
num_computed_tokens_p = attn_metadata.num_computed_tokens_p
|
||||
else:
|
||||
block_idx_last_computed_token_d = None
|
||||
block_idx_last_computed_token_p = None
|
||||
block_idx_last_scheduled_token_d = None
|
||||
block_idx_last_scheduled_token_p = None
|
||||
block_idx_first_scheduled_token_p = None
|
||||
block_idx_last_scheduled_token_d = None
|
||||
block_idx_last_computed_token_d = None
|
||||
num_computed_tokens_p = None
|
||||
|
||||
preallocated_ssm_out_d, preallocated_ssm_out_p = torch.split(
|
||||
output[:num_actual_tokens],
|
||||
[num_decodes, num_prefill_tokens],
|
||||
[num_decode_tokens, num_prefill_tokens],
|
||||
dim=0,
|
||||
)
|
||||
|
||||
@@ -709,6 +709,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
)
|
||||
|
||||
# NOTE: final output is an in-place update of out tensor
|
||||
assert preallocated_ssm_out_p is not None
|
||||
varlen_states = mamba_chunk_scan_combined_varlen(
|
||||
hidden_states_p.view(
|
||||
num_prefill_tokens, self.num_heads // self.tp_size, self.head_dim
|
||||
@@ -840,6 +841,9 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
conv_state_indices=state_indices_tensor_d,
|
||||
block_idx_last_scheduled_token=block_idx_last_scheduled_token_d,
|
||||
initial_state_idx=block_idx_last_computed_token_d,
|
||||
num_accepted_tokens=num_accepted_tokens,
|
||||
query_start_loc=query_start_loc_d,
|
||||
max_query_len=state_indices_tensor_d.size(-1),
|
||||
)
|
||||
|
||||
hidden_states_d, B_d, C_d = self.split_hidden_states_B_C_fn(
|
||||
@@ -862,6 +866,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
-1, self.num_heads // self.tp_size, self.head_dim
|
||||
)
|
||||
|
||||
assert preallocated_ssm_out_d is not None
|
||||
# - the hidden is reshaped into (bs, num_heads, head_dim)
|
||||
# - mamba_cache_params.ssm_state's slots will be selected
|
||||
# using state_indices_tensor_d
|
||||
@@ -879,7 +884,9 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
dt_softplus=True,
|
||||
state_batch_indices=state_indices_tensor_d_input,
|
||||
dst_state_batch_indices=state_indices_tensor_d_output,
|
||||
out=preallocated_ssm_out_d.view(num_decodes, -1, self.head_dim),
|
||||
out=preallocated_ssm_out_d.view(num_decode_tokens, -1, self.head_dim),
|
||||
num_accepted_tokens=num_accepted_tokens,
|
||||
cu_seqlens=query_start_loc_d,
|
||||
is_blackwell=self.is_blackwell,
|
||||
)
|
||||
|
||||
@@ -901,6 +908,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
|
||||
head_dim=self.head_dim,
|
||||
state_size=self.ssm_state_size,
|
||||
conv_kernel=self.conv_kernel_size,
|
||||
num_spec=self.num_spec,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -133,6 +133,7 @@ class MambaStateShapeCalculator:
|
||||
head_dim: int,
|
||||
state_size: int,
|
||||
conv_kernel: int,
|
||||
num_spec: int = 0,
|
||||
) -> tuple[tuple[int, int], tuple[int, int, int]]:
|
||||
# if n_groups is not divisible by world_size, need to extend the shards
|
||||
# to ensure all groups needed by a head is sharded along with it
|
||||
@@ -141,7 +142,7 @@ class MambaStateShapeCalculator:
|
||||
conv_dim = intermediate_size + 2 * n_groups * state_size
|
||||
|
||||
# contiguous along 'dim' axis
|
||||
conv_state_shape = (conv_kernel - 1, divide(conv_dim, tp_world_size))
|
||||
conv_state_shape = (conv_kernel - 1 + num_spec, divide(conv_dim, tp_world_size))
|
||||
|
||||
# These are not TP-ed as they depend on A, dt_bias, D
|
||||
# - they are typically small
|
||||
|
||||
@@ -1155,7 +1155,9 @@ def causal_conv1d_update(
|
||||
if conv_state_indices is None:
|
||||
assert conv_state.size(0) >= batch
|
||||
else:
|
||||
assert (batch,) == conv_state_indices.shape
|
||||
assert batch == conv_state_indices.shape[0], (
|
||||
f"ERROR: conv_state_indices should have shape ({batch},*) but got {conv_state_indices.shape}"
|
||||
)
|
||||
|
||||
assert num_cache_lines >= batch
|
||||
assert weight.stride(1) == 1 # Need this
|
||||
|
||||
@@ -119,7 +119,8 @@ class ShortConv(MambaBase, CustomOp):
|
||||
assert isinstance(attn_metadata, ShortConvAttentionMetadata)
|
||||
self_kv_cache = self.kv_cache[forward_context.virtual_engine]
|
||||
conv_state = self_kv_cache[0].transpose(-1, -2)
|
||||
state_indices_tensor = attn_metadata.state_indices_tensor
|
||||
state_indices_tensor_p = attn_metadata.state_indices_tensor_p
|
||||
state_indices_tensor_d = attn_metadata.state_indices_tensor_d
|
||||
has_initial_states_p = attn_metadata.has_initial_states_p
|
||||
query_start_loc_p = attn_metadata.query_start_loc_p
|
||||
|
||||
@@ -163,13 +164,6 @@ class ShortConv(MambaBase, CustomOp):
|
||||
[num_decodes, num_prefill_tokens],
|
||||
dim=0,
|
||||
)
|
||||
# Split along batch dimension
|
||||
state_indices_tensor_d, state_indices_tensor_p = torch.split(
|
||||
state_indices_tensor,
|
||||
[num_decodes, num_prefills],
|
||||
dim=0,
|
||||
)
|
||||
|
||||
conv_output_list = []
|
||||
|
||||
if has_prefill:
|
||||
|
||||
@@ -70,6 +70,7 @@ from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
|
||||
MXFP8_VALUE_DTYPE,
|
||||
Mxfp8LinearBackend,
|
||||
Mxfp8LinearOp,
|
||||
swizzle_mxfp8_scale,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
|
||||
apply_nvfp4_linear,
|
||||
@@ -1689,9 +1690,9 @@ class ModelOptMxFp8LinearMethod(LinearMethodBase):
|
||||
"Dynamic quantization is not supported."
|
||||
)
|
||||
|
||||
backend: Mxfp8LinearBackend = Mxfp8LinearBackend.EMULATION
|
||||
self.mxfp8_linear_op = Mxfp8LinearOp(backend=backend)
|
||||
logger.info_once("Using %s backend for MXFP8 GEMM", backend.value)
|
||||
self.backend: Mxfp8LinearBackend = Mxfp8LinearBackend.FLASHINFER_CUTLASS
|
||||
self.mxfp8_linear_op = Mxfp8LinearOp(backend=self.backend)
|
||||
logger.info_once("Using %s backend for MXFP8 GEMM", self.backend.value)
|
||||
|
||||
def create_weights(
|
||||
self,
|
||||
@@ -1749,7 +1750,38 @@ class ModelOptMxFp8LinearMethod(LinearMethodBase):
|
||||
)
|
||||
layer.register_parameter("weight_scale", weight_scale)
|
||||
|
||||
def _process_weights_after_loading_scale_2d(self, layer: torch.nn.Module) -> None:
|
||||
"""Not swizzled - MXFP8 GEMM emulation"""
|
||||
weight = layer.weight.data # [N, K]
|
||||
N, K = weight.shape
|
||||
scale_k = K // MXFP8_BLOCK_SIZE
|
||||
|
||||
# Slice weight_scale to match weight dimensions (handles padding)
|
||||
weight_scale = layer.weight_scale.data[:N, :scale_k].contiguous()
|
||||
|
||||
layer.weight = Parameter(weight.contiguous(), requires_grad=False)
|
||||
layer.weight_scale = Parameter(weight_scale, requires_grad=False)
|
||||
|
||||
def _process_weights_after_loading_scale_1d(self, layer: torch.nn.Module) -> None:
|
||||
"""Swizzled - MXFP8 GEMM Flashinfer CUTLASS"""
|
||||
weight = layer.weight.data # [N, K]
|
||||
N, K = weight.shape
|
||||
|
||||
# 2D weight scale
|
||||
weight_scale = layer.weight_scale.data
|
||||
|
||||
# Swizzle the weight scales
|
||||
scale_k = K // MXFP8_BLOCK_SIZE
|
||||
weight_scale_2d = weight_scale[:N, :scale_k].contiguous()
|
||||
weight_scale_swizzled = swizzle_mxfp8_scale(weight_scale_2d, M=N, K=K)
|
||||
|
||||
layer.weight = Parameter(weight.contiguous(), requires_grad=False)
|
||||
layer.weight_scale = Parameter(
|
||||
weight_scale_swizzled.contiguous(), requires_grad=False
|
||||
)
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
||||
# Validate weight tensor
|
||||
if layer.weight.ndim != 2:
|
||||
raise ValueError(
|
||||
f"MXFP8 weight must be 2D tensor [N, K], got {layer.weight.ndim}D "
|
||||
@@ -1763,15 +1795,23 @@ class ModelOptMxFp8LinearMethod(LinearMethodBase):
|
||||
f"quantized with MXFP8."
|
||||
)
|
||||
|
||||
weight = layer.weight.data # [N, K]
|
||||
N, K = weight.shape
|
||||
scale_k = K // MXFP8_BLOCK_SIZE
|
||||
# Validate weight scale tensor (should be 2D, not swizzled)
|
||||
assert layer.weight_scale.ndim == 2, (
|
||||
f"MXFP8 weight scale must be 2D, got {layer.weight_scale.ndim}D"
|
||||
)
|
||||
assert layer.weight_scale.dtype == MXFP8_SCALE_DTYPE, (
|
||||
f"MXFP8 weight scale must be {MXFP8_SCALE_DTYPE},"
|
||||
f" got {layer.weight_scale.dtype}"
|
||||
)
|
||||
|
||||
# Slice weight_scale to match weight dimensions (handles padding)
|
||||
weight_scale = layer.weight_scale.data[:N, :scale_k].contiguous()
|
||||
if self.backend == Mxfp8LinearBackend.EMULATION:
|
||||
# Swizzled layout is not used
|
||||
self._process_weights_after_loading_scale_2d(layer)
|
||||
return
|
||||
|
||||
layer.weight = Parameter(weight.contiguous(), requires_grad=False)
|
||||
layer.weight_scale = Parameter(weight_scale, requires_grad=False)
|
||||
assert self.backend == Mxfp8LinearBackend.FLASHINFER_CUTLASS
|
||||
# Swizzled layout is required for Flashinfer CUTLASS
|
||||
self._process_weights_after_loading_scale_1d(layer)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
|
||||
@@ -348,7 +348,7 @@ def flashinfer_trtllm_fp4_moe(
|
||||
hidden_states=hidden_states_fp4,
|
||||
hidden_states_scale=hidden_states_scale_linear_fp4.view(
|
||||
torch.float8_e4m3fn
|
||||
).flatten(),
|
||||
).reshape(*hidden_states_fp4.shape[:-1], -1),
|
||||
gemm1_weights=layer.w13_weight.data,
|
||||
gemm1_weights_scale=layer.w13_weight_scale.data.view(torch.float8_e4m3fn),
|
||||
gemm1_bias=None,
|
||||
@@ -432,7 +432,7 @@ def flashinfer_trtllm_fp4_routed_moe(
|
||||
hidden_states=hidden_states_fp4,
|
||||
hidden_states_scale=hidden_states_scale_linear_fp4.view(
|
||||
torch.float8_e4m3fn
|
||||
).flatten(),
|
||||
).reshape(*hidden_states_fp4.shape[:-1], -1),
|
||||
gemm1_weights=layer.w13_weight.data,
|
||||
gemm1_weights_scale=layer.w13_weight_scale.data.view(torch.float8_e4m3fn),
|
||||
gemm1_bias=None,
|
||||
|
||||
@@ -16,6 +16,7 @@ from vllm.model_executor.layers.quantization.utils.int8_utils import (
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.scalar_type import ScalarType, scalar_types
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
|
||||
from .quant_utils import pack_cols, unpack_cols
|
||||
|
||||
@@ -271,7 +272,7 @@ def marlin_make_workspace_new(
|
||||
) -> torch.Tensor:
|
||||
# In the new marlin kernel, we use the num of threadblocks as workspace
|
||||
# size. The num of threadblocks is sms_count * max_blocks_per_sm.
|
||||
sms = torch.cuda.get_device_properties(device).multi_processor_count
|
||||
sms = num_compute_units(device.index)
|
||||
return torch.zeros(
|
||||
sms * max_blocks_per_sm, dtype=torch.int, device=device, requires_grad=False
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from enum import Enum
|
||||
import torch
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.utils import flashinfer as vllm_flashinfer
|
||||
from vllm.utils.torch_utils import direct_register_custom_op
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -13,6 +14,7 @@ logger = init_logger(__name__)
|
||||
|
||||
class Mxfp8LinearBackend(Enum):
|
||||
EMULATION = "emulation"
|
||||
FLASHINFER_CUTLASS = "flashinfer-cutlass"
|
||||
|
||||
|
||||
# MXFP8 constants
|
||||
@@ -21,6 +23,30 @@ MXFP8_SCALE_DTYPE = torch.uint8
|
||||
MXFP8_BLOCK_SIZE = 32
|
||||
|
||||
|
||||
def swizzle_mxfp8_scale(sf: torch.Tensor, M: int, K: int) -> torch.Tensor:
|
||||
"""Swizzle MXFP8 scales from row-major 2D to F8_128x4 layout."""
|
||||
scaling_vector_size = MXFP8_BLOCK_SIZE # 32 for MXFP8
|
||||
factor = scaling_vector_size * 4 # 128
|
||||
|
||||
num_m_tiles = (M + 127) // 128
|
||||
num_k_tiles = (K + factor - 1) // factor
|
||||
|
||||
m_padded = num_m_tiles * 128
|
||||
k_scale_padded = num_k_tiles * 4
|
||||
|
||||
scale_cols = K // scaling_vector_size
|
||||
sf_padded = torch.zeros(
|
||||
(m_padded, k_scale_padded), dtype=sf.dtype, device=sf.device
|
||||
)
|
||||
sf_padded[:M, :scale_cols] = sf
|
||||
|
||||
sf_reshaped = sf_padded.view(num_m_tiles, 4, 32, num_k_tiles, 4)
|
||||
|
||||
sf_swizzled = sf_reshaped.transpose(1, 3)
|
||||
|
||||
return sf_swizzled.contiguous().view(-1)
|
||||
|
||||
|
||||
def _mxfp8_e4m3_quantize_impl(
|
||||
x: torch.Tensor, is_sf_swizzled_layout: bool = False
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
@@ -108,7 +134,7 @@ class Mxfp8LinearOp:
|
||||
|
||||
self.backend = backend
|
||||
|
||||
def apply(
|
||||
def _apply_emulation(
|
||||
self,
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
@@ -132,3 +158,79 @@ class Mxfp8LinearOp:
|
||||
|
||||
output = torch.nn.functional.linear(input, weight_bf16, bias)
|
||||
return output.to(out_dtype)
|
||||
|
||||
def _apply_flashinfer_cutlass(
|
||||
self,
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
out_dtype: torch.dtype,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
N, K = weight.shape
|
||||
|
||||
input_shape = input.shape
|
||||
input_2d = input.view(-1, K)
|
||||
M_orig = input_2d.shape[0]
|
||||
|
||||
# Minimum dimension size for F8_128x4 block scaling layout
|
||||
min_dim = 128
|
||||
|
||||
assert min_dim <= K, (
|
||||
f"mm_mxfp8 requires K >= {min_dim}, got K={K}. "
|
||||
f"in_features is too small for mm_mxfp8."
|
||||
)
|
||||
assert K % MXFP8_BLOCK_SIZE == 0, (
|
||||
f"mm_mxfp8 requires K to be divisible by {MXFP8_BLOCK_SIZE}, got K={K}."
|
||||
)
|
||||
assert min_dim <= N, (
|
||||
f"mm_mxfp8 requires N >= {min_dim}, got N={N}. "
|
||||
f"out_features is too small for mm_mxfp8."
|
||||
)
|
||||
|
||||
M_padded = ((M_orig + min_dim - 1) // min_dim) * min_dim
|
||||
if M_padded != M_orig:
|
||||
pad_rows = M_padded - M_orig
|
||||
input_2d = torch.nn.functional.pad(input_2d, (0, 0, 0, pad_rows))
|
||||
|
||||
input_mxfp8, input_scale = mxfp8_e4m3_quantize(
|
||||
input_2d,
|
||||
is_sf_swizzled_layout=True, # Swizzled for best accuracy
|
||||
)
|
||||
|
||||
if not weight.is_contiguous():
|
||||
weight = weight.contiguous()
|
||||
|
||||
output = vllm_flashinfer.mm_mxfp8(
|
||||
input_mxfp8,
|
||||
weight.t(),
|
||||
input_scale,
|
||||
weight_scale,
|
||||
out_dtype=out_dtype,
|
||||
backend="cutlass",
|
||||
)
|
||||
|
||||
if M_padded != M_orig:
|
||||
output = output[:M_orig, :]
|
||||
|
||||
if bias is not None:
|
||||
output = output + bias
|
||||
|
||||
output_shape = (*input_shape[:-1], N)
|
||||
return output.view(output_shape)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
out_dtype: torch.dtype,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
if self.backend == Mxfp8LinearBackend.EMULATION:
|
||||
return self._apply_emulation(input, weight, weight_scale, out_dtype, bias)
|
||||
|
||||
assert self.backend == Mxfp8LinearBackend.FLASHINFER_CUTLASS
|
||||
return self._apply_flashinfer_cutlass(
|
||||
input, weight, weight_scale, out_dtype, bias
|
||||
)
|
||||
|
||||
@@ -47,15 +47,20 @@ class RotaryEmbeddingBase(CustomOp):
|
||||
if not hasattr(self, "use_flashinfer"):
|
||||
self.use_flashinfer = False
|
||||
|
||||
self.use_aiter = (
|
||||
self.enabled() and rocm_aiter_ops.is_triton_rotary_embed_enabled()
|
||||
)
|
||||
if self.use_aiter:
|
||||
self.rocm_aiter_triton_rotary_embedding = (
|
||||
rocm_aiter_ops.get_triton_rotary_embedding_op()
|
||||
)
|
||||
|
||||
if init_cache:
|
||||
cache = self._compute_cos_sin_cache()
|
||||
if not self.use_flashinfer:
|
||||
cache = cache.to(dtype)
|
||||
self.cos_sin_cache: torch.Tensor
|
||||
self.register_buffer("cos_sin_cache", cache, persistent=False)
|
||||
self.is_rocm_triton_rotary_embed_enabled = (
|
||||
rocm_aiter_ops.is_triton_rotary_embed_enabled()
|
||||
)
|
||||
|
||||
self.apply_rotary_emb = ApplyRotaryEmb(
|
||||
is_neox_style=self.is_neox_style,
|
||||
@@ -231,15 +236,14 @@ class RotaryEmbedding(RotaryEmbeddingBase):
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
if self.is_rocm_triton_rotary_embed_enabled:
|
||||
if self.use_aiter:
|
||||
cos_sin_cache = self._match_cos_sin_cache_dtype(query)
|
||||
rocm_aiter_ops.triton_rotary_embed(
|
||||
self.rocm_aiter_triton_rotary_embedding(
|
||||
positions,
|
||||
query,
|
||||
key,
|
||||
cos_sin_cache,
|
||||
self.head_size,
|
||||
self.rotary_dim,
|
||||
cos_sin_cache,
|
||||
self.is_neox_style,
|
||||
)
|
||||
return query, key
|
||||
|
||||
@@ -11,7 +11,7 @@ from vllm import envs
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import CpuArchEnum, current_platform
|
||||
from vllm.utils.platform_utils import get_cu_count
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
from vllm.utils.torch_utils import direct_register_custom_op
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -149,7 +149,7 @@ def rocm_unquantized_gemm_impl(
|
||||
m = weight.shape[0]
|
||||
k = weight.shape[1]
|
||||
|
||||
cu_count = get_cu_count()
|
||||
cu_count = num_compute_units()
|
||||
if use_aiter_triton_gemm(n, m, k, x.dtype):
|
||||
from aiter.ops.triton.gemm_a16w16 import gemm_a16w16
|
||||
|
||||
@@ -199,7 +199,7 @@ def rocm_unquantized_gemm_impl(
|
||||
|
||||
x_view = x.reshape(-1, x.size(-1))
|
||||
if m > 8 and 0 < n <= 4:
|
||||
cu_count = get_cu_count()
|
||||
cu_count = num_compute_units()
|
||||
out = ops.wvSplitK(weight, x_view, cu_count, bias)
|
||||
return out.reshape(*x.shape[:-1], weight.shape[0])
|
||||
elif m % 4 == 0 and n == 1 and k <= 8192 and bias is None:
|
||||
|
||||
@@ -14,6 +14,7 @@ from transformers.utils import SAFE_WEIGHTS_INDEX_NAME
|
||||
from vllm.config import ModelConfig
|
||||
from vllm.config.load import LoadConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
|
||||
from vllm.model_executor.layers.quantization.torchao import torchao_version_at_least
|
||||
from vllm.model_executor.model_loader.base_loader import BaseModelLoader
|
||||
from vllm.model_executor.model_loader.weight_utils import (
|
||||
@@ -286,7 +287,6 @@ class DefaultModelLoader(BaseModelLoader):
|
||||
):
|
||||
self.load_config.safetensors_load_strategy = "torchao"
|
||||
|
||||
weights_to_load = {name for name, _ in model.named_parameters()}
|
||||
loaded_weights = model.load_weights(self.get_all_weights(model_config, model))
|
||||
|
||||
self.counter_after_loading_weights = time.perf_counter()
|
||||
@@ -295,9 +295,20 @@ class DefaultModelLoader(BaseModelLoader):
|
||||
self.counter_after_loading_weights - self.counter_before_loading_weights,
|
||||
scope="local",
|
||||
)
|
||||
# We only enable strict check for non-quantized models
|
||||
# that have loaded weights tracking currently.
|
||||
if model_config.quantization is None and loaded_weights is not None:
|
||||
self.track_weights_loading(model, loaded_weights)
|
||||
|
||||
def track_weights_loading(
|
||||
self, model: nn.Module, loaded_weights: set[str] | None
|
||||
) -> None:
|
||||
weights_to_load = {name for name, _ in model.named_parameters()}
|
||||
if loaded_weights is not None:
|
||||
for name, module in model.named_modules():
|
||||
quant_method = getattr(module, "quant_method", None)
|
||||
# ignore kv_cache scale, which can be missing in checkpoints
|
||||
if isinstance(quant_method, BaseKVCacheMethod):
|
||||
for param_name, _ in module.named_parameters():
|
||||
full_name = f"{name}.{param_name}" if name else param_name
|
||||
loaded_weights.add(full_name)
|
||||
weights_not_loaded = weights_to_load - loaded_weights
|
||||
if weights_not_loaded:
|
||||
raise ValueError(
|
||||
|
||||
@@ -421,7 +421,6 @@ class Ernie4_5_MoeModel(nn.Module):
|
||||
cache_config = vllm_config.cache_config
|
||||
quant_config = vllm_config.quant_config
|
||||
|
||||
self.padding_idx = config.pad_token_id
|
||||
self.vocab_size = config.vocab_size
|
||||
self.config = config
|
||||
parallel_config = vllm_config.parallel_config
|
||||
|
||||
@@ -523,7 +523,6 @@ class Ernie4_5_VLMoeModel(nn.Module):
|
||||
cache_config = vllm_config.cache_config
|
||||
quant_config = vllm_config.quant_config
|
||||
|
||||
self.padding_idx = config.pad_token_id
|
||||
self.vocab_size = config.vocab_size
|
||||
self.config = config
|
||||
|
||||
|
||||
@@ -157,7 +157,6 @@ class GraniteMoeSharedModel(nn.Module):
|
||||
|
||||
self.config = config
|
||||
self.quant_config = quant_config # Required by MixtralModel
|
||||
self.padding_idx = config.pad_token_id
|
||||
|
||||
self.vocab_size = config.vocab_size
|
||||
|
||||
|
||||
@@ -451,7 +451,6 @@ class Grok1Model(nn.Module):
|
||||
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
self.padding_idx = config.pad_token_id
|
||||
|
||||
# Store expert naming for weight loading
|
||||
self.ckpt_gate_proj_name = ckpt_gate_proj_name
|
||||
|
||||
@@ -600,7 +600,6 @@ class HunYuanModel(nn.Module):
|
||||
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
self.padding_idx = config.pad_token_id
|
||||
|
||||
self.vocab_size = config.vocab_size
|
||||
|
||||
|
||||
@@ -305,7 +305,6 @@ class Jais2Model(nn.Module):
|
||||
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
self.padding_idx = config.pad_token_id
|
||||
|
||||
self.vocab_size = config.vocab_size
|
||||
self.org_vocab_size = config.vocab_size
|
||||
|
||||
@@ -393,7 +393,6 @@ class KimiLinearModel(nn.Module):
|
||||
parallel_config = vllm_config.parallel_config
|
||||
self.config = config
|
||||
|
||||
self.padding_idx = config.pad_token_id
|
||||
self.vocab_size = config.vocab_size
|
||||
|
||||
if get_pp_group().is_first_rank:
|
||||
|
||||
@@ -486,7 +486,6 @@ class FlashModel(nn.Module):
|
||||
quant_config = vllm_config.quant_config
|
||||
self.config = config
|
||||
|
||||
self.padding_idx = getattr(config, "pad_token_id", None)
|
||||
self.vocab_size = config.vocab_size
|
||||
|
||||
if get_pp_group().is_first_rank:
|
||||
|
||||
@@ -228,6 +228,7 @@ class Mamba2ForCausalLM(
|
||||
head_dim=hf_config.head_dim,
|
||||
state_size=hf_config.state_size,
|
||||
conv_kernel=hf_config.conv_kernel,
|
||||
num_spec=vllm_config.num_speculative_tokens,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -495,7 +495,6 @@ class MiniMaxText01Model(nn.Module):
|
||||
cache_config = vllm_config.cache_config
|
||||
scheduler_config = vllm_config.scheduler_config
|
||||
|
||||
self.padding_idx = config.pad_token_id
|
||||
self.vocab_size = config.vocab_size
|
||||
|
||||
self.decoder_attention_types = getattr(
|
||||
|
||||
@@ -636,6 +636,9 @@ class NemotronHModel(nn.Module):
|
||||
hidden_states, _ = self.norm_f(hidden_states, residual)
|
||||
return hidden_states
|
||||
|
||||
def is_spec_layer(self, config: NemotronHConfig, weight_name: str) -> bool:
|
||||
return weight_name.startswith("mtp.")
|
||||
|
||||
def _get_max_n_routed_experts(self) -> int:
|
||||
"""Get max n_routed_experts from config or block_configs for puzzle models.
|
||||
|
||||
@@ -702,6 +705,10 @@ class NemotronHModel(nn.Module):
|
||||
if name is None:
|
||||
continue
|
||||
|
||||
# Skip MTP/spec decode layers early (before stacked params mapping)
|
||||
if name.startswith("mtp."):
|
||||
continue
|
||||
|
||||
# load stacked params
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
@@ -845,6 +852,7 @@ class NemotronHForCausalLM(
|
||||
head_dim=hf_config.mamba_head_dim,
|
||||
state_size=hf_config.ssm_state_size,
|
||||
conv_kernel=hf_config.conv_kernel,
|
||||
num_spec=vllm_config.num_speculative_tokens,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""NemotronH-MTP model with attention layers."""
|
||||
|
||||
import typing
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm.compilation.decorators import support_torch_compile
|
||||
from vllm.config import CacheConfig, ModelConfig, VllmConfig
|
||||
from vllm.config.parallel import ParallelConfig
|
||||
from vllm.model_executor.layers.fused_moe import FusedMoE
|
||||
from vllm.model_executor.layers.layernorm import RMSNorm
|
||||
from vllm.model_executor.layers.linear import ColumnParallelLinear
|
||||
from vllm.model_executor.layers.logits_processor import LogitsProcessor
|
||||
from vllm.model_executor.layers.quantization import QuantizationConfig
|
||||
from vllm.model_executor.layers.vocab_parallel_embedding import (
|
||||
ParallelLMHead,
|
||||
VocabParallelEmbedding,
|
||||
)
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.model_executor.models.utils import (
|
||||
make_empty_intermediate_tensors_factory,
|
||||
maybe_prefix,
|
||||
)
|
||||
from vllm.sequence import IntermediateTensors
|
||||
from vllm.transformers_utils.configs import NemotronHConfig
|
||||
|
||||
from .interfaces import SupportsPP
|
||||
from .nemotron_h import (
|
||||
NemotronHAttentionDecoderLayer,
|
||||
NemotronHMoEDecoderLayer,
|
||||
)
|
||||
|
||||
|
||||
class NemotronHMTPAttentionDecoderLayer(NemotronHAttentionDecoderLayer):
|
||||
def __init__(
|
||||
self,
|
||||
config: NemotronHConfig,
|
||||
layer_idx: int,
|
||||
model_config: ModelConfig | None = None,
|
||||
cache_config: CacheConfig | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
parallel_config: ParallelConfig | None = None,
|
||||
prefix: str = "",
|
||||
has_start_projections: bool = False,
|
||||
has_end_norm: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
config=config,
|
||||
layer_idx=layer_idx,
|
||||
model_config=model_config,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
parallel_config=parallel_config,
|
||||
prefix=prefix,
|
||||
)
|
||||
self.has_start_projections = has_start_projections
|
||||
self.has_end_norm = has_end_norm
|
||||
|
||||
if has_start_projections:
|
||||
self.enorm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
|
||||
self.hnorm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
|
||||
|
||||
# Fusion layer to combine embeddings with target hidden states
|
||||
self.eh_proj = ColumnParallelLinear(
|
||||
input_size=config.hidden_size * 2,
|
||||
output_size=config.hidden_size,
|
||||
bias=False,
|
||||
gather_output=True,
|
||||
params_dtype=config.dtype
|
||||
if hasattr(config, "dtype")
|
||||
else torch.bfloat16,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.eh_proj",
|
||||
)
|
||||
|
||||
if has_end_norm:
|
||||
self.final_layernorm = RMSNorm(
|
||||
config.hidden_size,
|
||||
eps=getattr(config, "layer_norm_epsilon", 1e-5),
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
inputs_embeds: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
residual: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
# Start projections (Fusion)
|
||||
if self.has_start_projections:
|
||||
# Normalize both inputs before fusion
|
||||
assert inputs_embeds is not None
|
||||
inputs_embeds_normed = self.enorm(inputs_embeds)
|
||||
previous_hidden_states_normed = self.hnorm(hidden_states)
|
||||
|
||||
# Fuse via concatenation and linear projection
|
||||
fused = torch.cat(
|
||||
[inputs_embeds_normed, previous_hidden_states_normed], dim=-1
|
||||
)
|
||||
hidden_states, _ = self.eh_proj(fused)
|
||||
|
||||
# Call parent forward (Attention)
|
||||
# Parent forward expects: hidden_states, residual
|
||||
hidden_states, residual = super().forward(
|
||||
positions=positions,
|
||||
hidden_states=hidden_states,
|
||||
residual=residual,
|
||||
)
|
||||
|
||||
# End norm
|
||||
if self.has_end_norm:
|
||||
if residual is not None:
|
||||
hidden_states = hidden_states + residual
|
||||
residual = None # Consumed residual
|
||||
|
||||
hidden_states = self.final_layernorm(hidden_states)
|
||||
|
||||
return hidden_states, residual
|
||||
|
||||
|
||||
class NemotronHMTPMoEDecoderLayer(NemotronHMoEDecoderLayer):
|
||||
def __init__(
|
||||
self,
|
||||
config: NemotronHConfig,
|
||||
layer_idx: int,
|
||||
model_config: ModelConfig | None = None,
|
||||
cache_config: CacheConfig | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
parallel_config: ParallelConfig | None = None,
|
||||
prefix: str = "",
|
||||
has_start_projections: bool = False,
|
||||
has_end_norm: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
config=config,
|
||||
layer_idx=layer_idx,
|
||||
model_config=model_config,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
parallel_config=parallel_config,
|
||||
prefix=prefix,
|
||||
)
|
||||
self.has_start_projections = has_start_projections
|
||||
self.has_end_norm = has_end_norm
|
||||
|
||||
if has_start_projections:
|
||||
self.enorm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
|
||||
self.hnorm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
|
||||
|
||||
# Fusion layer to combine embeddings with target hidden states
|
||||
self.eh_proj = ColumnParallelLinear(
|
||||
input_size=config.hidden_size * 2,
|
||||
output_size=config.hidden_size,
|
||||
bias=False,
|
||||
gather_output=True,
|
||||
params_dtype=config.dtype
|
||||
if hasattr(config, "dtype")
|
||||
else torch.bfloat16,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.eh_proj",
|
||||
)
|
||||
|
||||
if has_end_norm:
|
||||
self.final_layernorm = RMSNorm(
|
||||
config.hidden_size,
|
||||
eps=getattr(config, "layer_norm_epsilon", 1e-5),
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
inputs_embeds: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
residual: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
# Start projections (Fusion)
|
||||
if self.has_start_projections:
|
||||
# Normalize both inputs before fusion
|
||||
assert inputs_embeds is not None
|
||||
inputs_embeds_normed = self.enorm(inputs_embeds)
|
||||
previous_hidden_states_normed = self.hnorm(hidden_states)
|
||||
|
||||
# Fuse via concatenation and linear projection
|
||||
fused = torch.cat(
|
||||
[inputs_embeds_normed, previous_hidden_states_normed], dim=-1
|
||||
)
|
||||
hidden_states, _ = self.eh_proj(fused)
|
||||
|
||||
# Call parent forward (MoE)
|
||||
hidden_states, residual = super().forward(
|
||||
hidden_states=hidden_states,
|
||||
residual=residual,
|
||||
)
|
||||
|
||||
# End norm
|
||||
if self.has_end_norm:
|
||||
if residual is not None:
|
||||
hidden_states = hidden_states + residual
|
||||
residual = None # Consumed residual
|
||||
|
||||
hidden_states = self.final_layernorm(hidden_states)
|
||||
|
||||
return hidden_states, residual
|
||||
|
||||
|
||||
@support_torch_compile
|
||||
class NemotronHMultiTokenPredictor(nn.Module):
|
||||
"""MTP predictor with NemotronH layers."""
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
|
||||
config = vllm_config.model_config.hf_config
|
||||
|
||||
self.config = config
|
||||
self.vocab_size = config.vocab_size
|
||||
self.org_vocab_size = config.vocab_size
|
||||
|
||||
self.mtp_start_layer_idx = config.num_hidden_layers
|
||||
self.num_mtp_layers = getattr(config, "num_nextn_predict_layers", 1)
|
||||
assert self.num_mtp_layers == 1, (
|
||||
"Only one MTP layer is supported for NemotronH-MTP"
|
||||
)
|
||||
|
||||
self.pattern_str = config.mtp_hybrid_override_pattern
|
||||
self.pattern_len = len(self.pattern_str)
|
||||
assert self.pattern_len > 0
|
||||
|
||||
self.embed_tokens = VocabParallelEmbedding(
|
||||
self.vocab_size,
|
||||
config.hidden_size,
|
||||
)
|
||||
|
||||
# Build flat list of layers
|
||||
self.layers = torch.nn.ModuleDict()
|
||||
|
||||
# Total number of physical layers = num_steps * pattern_len
|
||||
total_layers = self.num_mtp_layers * self.pattern_len
|
||||
for i in range(total_layers):
|
||||
step_rel_idx = i % self.pattern_len
|
||||
|
||||
char = self.pattern_str[step_rel_idx]
|
||||
|
||||
is_start_of_step = step_rel_idx == 0
|
||||
is_end_of_step = step_rel_idx == self.pattern_len - 1
|
||||
|
||||
layer_prefix = f"{prefix}.layers.{i}"
|
||||
|
||||
# TODO smor- remove double layers formation
|
||||
common_kwargs = dict(
|
||||
config=config,
|
||||
layer_idx=self.mtp_start_layer_idx + i,
|
||||
model_config=vllm_config.model_config,
|
||||
cache_config=vllm_config.cache_config,
|
||||
quant_config=vllm_config.quant_config,
|
||||
parallel_config=vllm_config.parallel_config,
|
||||
prefix=layer_prefix,
|
||||
has_start_projections=is_start_of_step,
|
||||
has_end_norm=is_end_of_step,
|
||||
)
|
||||
|
||||
if char == "*":
|
||||
self.layers[str(i)] = NemotronHMTPAttentionDecoderLayer(**common_kwargs)
|
||||
elif char == "E":
|
||||
self.layers[str(i)] = NemotronHMTPMoEDecoderLayer(**common_kwargs)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Pattern char '{char}' in {self.pattern_str} not implemented"
|
||||
)
|
||||
|
||||
self.make_empty_intermediate_tensors: Callable[..., IntermediateTensors] = (
|
||||
make_empty_intermediate_tensors_factory(
|
||||
["hidden_states", "residual"], config.hidden_size
|
||||
)
|
||||
)
|
||||
|
||||
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
assert self.embed_tokens is not None, (
|
||||
"embed_tokens not initialized - must be shared from target model"
|
||||
)
|
||||
return self.embed_tokens(input_ids)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
if inputs_embeds is None:
|
||||
inputs_embeds = self.get_input_embeddings(input_ids)
|
||||
|
||||
residual = None
|
||||
|
||||
for i in range(self.pattern_len):
|
||||
hidden_states, residual = self.layers[str(i)](
|
||||
inputs_embeds=inputs_embeds,
|
||||
positions=positions,
|
||||
hidden_states=hidden_states,
|
||||
residual=residual,
|
||||
)
|
||||
return hidden_states
|
||||
|
||||
|
||||
class NemotronHMTP(nn.Module, SupportsPP):
|
||||
"""NemotronH MTP model."""
|
||||
|
||||
packed_modules_mapping = {
|
||||
"qkv_proj": [
|
||||
"q_proj",
|
||||
"k_proj",
|
||||
"v_proj",
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
config = vllm_config.model_config.hf_config
|
||||
self.vllm_config = vllm_config
|
||||
self.config = config
|
||||
self.quant_config = vllm_config.quant_config
|
||||
|
||||
# Needed for load_weights mapping
|
||||
self.mtp_start_layer_idx = config.num_hidden_layers
|
||||
|
||||
# EPLB config for experts
|
||||
self.num_redundant_experts = 0
|
||||
if vllm_config.parallel_config and vllm_config.parallel_config.eplb_config:
|
||||
self.num_redundant_experts = (
|
||||
vllm_config.parallel_config.eplb_config.num_redundant_experts
|
||||
)
|
||||
|
||||
# MTP predictor
|
||||
self.model = NemotronHMultiTokenPredictor(
|
||||
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "mtp")
|
||||
)
|
||||
|
||||
# LM head for generating logits
|
||||
self.lm_head = ParallelLMHead(
|
||||
self.config.vocab_size,
|
||||
self.config.hidden_size,
|
||||
prefix=maybe_prefix(prefix, "lm_head"),
|
||||
)
|
||||
|
||||
self.logits_processor = LogitsProcessor(self.config.vocab_size)
|
||||
|
||||
self.make_empty_intermediate_tensors = (
|
||||
self.model.make_empty_intermediate_tensors
|
||||
)
|
||||
|
||||
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.get_input_embeddings(input_ids)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
**kwargs: object,
|
||||
) -> torch.Tensor:
|
||||
"""Forward - applies attention-based MTP."""
|
||||
hidden_states = self.model(
|
||||
input_ids,
|
||||
positions,
|
||||
hidden_states,
|
||||
intermediate_tensors,
|
||||
inputs_embeds,
|
||||
)
|
||||
return hidden_states
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
) -> torch.Tensor | None:
|
||||
"""Compute logits for DRAFT token generation."""
|
||||
assert self.lm_head is not None, (
|
||||
"lm_head not initialized - must be shared from target model"
|
||||
)
|
||||
return self.logits_processor(self.lm_head, hidden_states)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load MTP weights with proper name remapping."""
|
||||
stacked_params_mapping = [
|
||||
# (param_name, shard_name, shard_id)
|
||||
("qkv_proj", "q_proj", "q"),
|
||||
("qkv_proj", "k_proj", "k"),
|
||||
("qkv_proj", "v_proj", "v"),
|
||||
]
|
||||
|
||||
expert_params_mapping = []
|
||||
if hasattr(self.config, "n_routed_experts") and self.config.n_routed_experts:
|
||||
expert_params_mapping = FusedMoE.make_expert_params_mapping(
|
||||
self,
|
||||
ckpt_gate_proj_name="up_proj",
|
||||
ckpt_down_proj_name="down_proj",
|
||||
ckpt_up_proj_name="", # Empty - non-gated MoE
|
||||
num_experts=self.config.n_routed_experts,
|
||||
num_redundant_experts=self.num_redundant_experts,
|
||||
)
|
||||
|
||||
params_dict = dict(self.named_parameters())
|
||||
loaded_params: set[str] = set()
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
# Only process MTP weights - skip all non-MTP weights
|
||||
if (
|
||||
not name.startswith("mtp.")
|
||||
and "embeddings" not in name
|
||||
and "lm_head" not in name
|
||||
):
|
||||
continue
|
||||
# Skip rotary embeddings (computed, not loaded)
|
||||
if "rotary_emb.inv_freq" in name:
|
||||
continue
|
||||
|
||||
name = name.replace("mtp.layers.", "model.layers.")
|
||||
|
||||
if "embeddings" in name:
|
||||
name = name.replace("embeddings", "embed_tokens")
|
||||
if name.startswith("backbone."):
|
||||
name = name.replace("backbone.", "model.")
|
||||
|
||||
# Handle stacked parameters (qkv_proj) for attention layers
|
||||
is_stacked = False
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
continue
|
||||
# Must be in a mixer (attention layer)
|
||||
if ".mixer." not in name:
|
||||
continue
|
||||
|
||||
is_stacked = True
|
||||
stacked_name = name.replace(weight_name, param_name)
|
||||
|
||||
if stacked_name.endswith(".bias") and stacked_name not in params_dict:
|
||||
continue
|
||||
|
||||
if stacked_name not in params_dict:
|
||||
# Might be that mapping failed or param doesn't exist
|
||||
continue
|
||||
|
||||
param = params_dict[stacked_name]
|
||||
weight_loader = getattr(param, "weight_loader", None)
|
||||
if weight_loader is not None:
|
||||
weight_loader(param, loaded_weight, shard_id)
|
||||
loaded_params.add(stacked_name)
|
||||
break
|
||||
|
||||
if is_stacked:
|
||||
continue
|
||||
|
||||
is_expert_weight = False
|
||||
for mapping in expert_params_mapping:
|
||||
param_name, weight_name, expert_id, shard_id = mapping
|
||||
# weight_name is like "experts.0.up_proj."
|
||||
if weight_name not in name:
|
||||
continue
|
||||
|
||||
is_expert_weight = True
|
||||
|
||||
# Replace the expert-specific weight name with fused parameter name
|
||||
# e.g., "experts.0.up_proj." -> "experts.w13_"
|
||||
name_mapped = name.replace(weight_name, param_name)
|
||||
|
||||
if name_mapped not in params_dict:
|
||||
continue
|
||||
|
||||
param = params_dict[name_mapped]
|
||||
weight_loader = typing.cast(Callable[..., bool], param.weight_loader)
|
||||
success = weight_loader(
|
||||
param,
|
||||
loaded_weight,
|
||||
name_mapped,
|
||||
shard_id=shard_id,
|
||||
expert_id=expert_id,
|
||||
return_success=True,
|
||||
)
|
||||
if success:
|
||||
loaded_params.add(name_mapped)
|
||||
break
|
||||
|
||||
if is_expert_weight:
|
||||
continue
|
||||
|
||||
if name.endswith(".bias") and name not in params_dict:
|
||||
continue
|
||||
|
||||
if name not in params_dict:
|
||||
continue
|
||||
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(name)
|
||||
|
||||
return loaded_params
|
||||
@@ -241,7 +241,6 @@ class DeciModel(nn.Module):
|
||||
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
self.padding_idx = config.pad_token_id
|
||||
|
||||
self.vocab_size = config.vocab_size
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user