forked from Karylab-cklius/vllm
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ea7cac55b | ||
|
|
be3476447f | ||
|
|
f1473092c4 | ||
|
|
4dcbae8670 | ||
|
|
65dac3a770 | ||
|
|
0ba2500ef0 | ||
|
|
ef576befd2 | ||
|
|
35e4a36107 | ||
|
|
da5803d46e | ||
|
|
75ddfaf909 | ||
|
|
f36fe52add | ||
|
|
391d918d4d | ||
|
|
fca040885b | ||
|
|
a7fd4c7482 | ||
|
|
5131691063 | ||
|
|
80e00e5ac6 | ||
|
|
ae10e855ab | ||
|
|
530ee36a0d | ||
|
|
d835ad572c | ||
|
|
47d0597ca2 | ||
|
|
818cf61e91 | ||
|
|
c01618fdc8 | ||
|
|
823eaf667d | ||
|
|
f1f1259692 | ||
|
|
df13b5aef5 | ||
|
|
4938d44a3b | ||
|
|
37bf988c2f | ||
|
|
9459fc6471 | ||
|
|
5245c80564 | ||
|
|
9bc266d923 | ||
|
|
5c9f6557d7 | ||
|
|
dcfebf93f4 | ||
|
|
752bd10647 | ||
|
|
2730b657c4 | ||
|
|
1dcbbd9cac | ||
|
|
ace9fda495 | ||
|
|
ef0aa7ca2f | ||
|
|
e6d1310b2a | ||
|
|
ac5f38a0f7 |
@@ -18,6 +18,8 @@ steps:
|
||||
- tests/kernels/quantization/test_cpu_fp8_scaled_mm.py
|
||||
- tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
|
||||
- tests/kernels/mamba/test_cpu_short_conv.py
|
||||
- tests/kernels/mamba/test_causal_conv1d.py
|
||||
- tests/kernels/mamba/test_mamba_ssm.py
|
||||
commands:
|
||||
- |
|
||||
bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 30m "
|
||||
@@ -28,7 +30,9 @@ steps:
|
||||
pytest -x -v -s tests/kernels/test_onednn.py
|
||||
pytest -x -v -s tests/kernels/test_awq_int4_to_int8.py
|
||||
pytest -x -v -s tests/kernels/quantization/test_cpu_fp8_scaled_mm.py
|
||||
pytest -x -v -s tests/kernels/mamba/cpu/test_cpu_gdn_ops.py"
|
||||
pytest -x -v -s tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
|
||||
pytest -x -v -s tests/kernels/mamba/test_causal_conv1d.py
|
||||
pytest -x -v -s tests/kernels/mamba/test_mamba_ssm.py"
|
||||
|
||||
# Note: SDE can't be downloaded from CI host because of AWS WAF
|
||||
# - label: CPU-Compatibility Tests
|
||||
|
||||
+340
-343
@@ -590,373 +590,370 @@ steps:
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
- block: "Unblock ROCm wheel/image prerequisites"
|
||||
- group: "Build ROCm Wheel / Image "
|
||||
key: "build-rocm-wheel-image"
|
||||
depends_on: ~
|
||||
key: block-build-rocm
|
||||
if: build.env("NIGHTLY") != "1"
|
||||
steps:
|
||||
# ROCm Job 1: Build ROCm Base Wheels (with S3 caching)
|
||||
- label: ":rocm: Build ROCm Base Image & Wheels"
|
||||
id: build-rocm-base-wheels
|
||||
depends_on: ~
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
# ROCm Job 1: Build ROCm Base Wheels (with S3 caching)
|
||||
- label: ":rocm: Build ROCm Base Image & Wheels"
|
||||
id: build-rocm-base-wheels
|
||||
depends_on:
|
||||
- step: block-build-rocm
|
||||
allow_failure: true
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- |
|
||||
set -euo pipefail
|
||||
# Generate cache key
|
||||
CACHE_KEY=$$(.buildkite/scripts/cache-rocm-base-wheels.sh key)
|
||||
ECR_CACHE_TAG="public.ecr.aws/q9t5s3a7/vllm-release-repo:$${CACHE_KEY}-rocm-base"
|
||||
|
||||
# Generate cache key
|
||||
CACHE_KEY=$$(.buildkite/scripts/cache-rocm-base-wheels.sh key)
|
||||
ECR_CACHE_TAG="public.ecr.aws/q9t5s3a7/vllm-release-repo:$${CACHE_KEY}-rocm-base"
|
||||
echo "========================================"
|
||||
echo "ROCm Base Build Configuration"
|
||||
echo "========================================"
|
||||
echo " CACHE_KEY: $${CACHE_KEY}"
|
||||
echo " ECR_CACHE_TAG: $${ECR_CACHE_TAG}"
|
||||
echo "========================================"
|
||||
|
||||
# Login to ECR
|
||||
aws ecr-public get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7
|
||||
|
||||
IMAGE_EXISTS=false
|
||||
WHEELS_EXIST=false
|
||||
|
||||
# Check ECR for Docker image
|
||||
|
||||
echo "========================================"
|
||||
echo "ROCm Base Build Configuration"
|
||||
echo "========================================"
|
||||
echo " CACHE_KEY: $${CACHE_KEY}"
|
||||
echo " ECR_CACHE_TAG: $${ECR_CACHE_TAG}"
|
||||
echo "========================================"
|
||||
|
||||
# Login to ECR
|
||||
aws ecr-public get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7
|
||||
|
||||
IMAGE_EXISTS=false
|
||||
WHEELS_EXIST=false
|
||||
|
||||
# Check ECR for Docker image
|
||||
if docker manifest inspect "$${ECR_CACHE_TAG}" > /dev/null 2>&1; then
|
||||
IMAGE_EXISTS=true
|
||||
echo "ECR image cache HIT"
|
||||
fi
|
||||
|
||||
# Check S3 for wheels
|
||||
WHEEL_CACHE_STATUS=$(.buildkite/scripts/cache-rocm-base-wheels.sh check)
|
||||
if [ "$${WHEEL_CACHE_STATUS}" = "hit" ]; then
|
||||
WHEELS_EXIST=true
|
||||
echo "S3 wheels cache HIT"
|
||||
fi
|
||||
|
||||
if docker manifest inspect "$${ECR_CACHE_TAG}" > /dev/null 2>&1; then
|
||||
IMAGE_EXISTS=true
|
||||
echo "ECR image cache HIT"
|
||||
fi
|
||||
|
||||
# Check S3 for wheels
|
||||
WHEEL_CACHE_STATUS=$(.buildkite/scripts/cache-rocm-base-wheels.sh check)
|
||||
if [ "$${WHEEL_CACHE_STATUS}" = "hit" ]; then
|
||||
WHEELS_EXIST=true
|
||||
echo "S3 wheels cache HIT"
|
||||
fi
|
||||
|
||||
# Scenario 1: Both cached (best case)
|
||||
if [ "$${IMAGE_EXISTS}" = "true" ] && [ "$${WHEELS_EXIST}" = "true" ]; then
|
||||
echo ""
|
||||
echo "FULL CACHE HIT - Reusing both image and wheels"
|
||||
echo ""
|
||||
|
||||
|
||||
# Scenario 1: Both cached (best case)
|
||||
if [ "$${IMAGE_EXISTS}" = "true" ] && [ "$${WHEELS_EXIST}" = "true" ]; then
|
||||
echo ""
|
||||
echo "FULL CACHE HIT - Reusing both image and wheels"
|
||||
echo ""
|
||||
# Download wheels
|
||||
.buildkite/scripts/cache-rocm-base-wheels.sh download
|
||||
|
||||
# Save ECR tag for downstream jobs
|
||||
buildkite-agent meta-data set "rocm-base-image-tag" "$${ECR_CACHE_TAG}"
|
||||
|
||||
# Scenario 2: Full rebuild needed
|
||||
else
|
||||
echo ""
|
||||
echo " CACHE MISS - Building from scratch..."
|
||||
echo ""
|
||||
|
||||
# Build full base image and push to ECR
|
||||
DOCKER_BUILDKIT=1 docker buildx build \
|
||||
--file docker/Dockerfile.rocm_base \
|
||||
--tag "$${ECR_CACHE_TAG}" \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
--push \
|
||||
.
|
||||
|
||||
# Build wheel extraction stage
|
||||
DOCKER_BUILDKIT=1 docker buildx build \
|
||||
--file docker/Dockerfile.rocm_base \
|
||||
--tag rocm-base-debs:$${BUILDKITE_BUILD_NUMBER} \
|
||||
--target debs_wheel_release \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
--load \
|
||||
.
|
||||
|
||||
# Extract and upload wheels
|
||||
mkdir -p artifacts/rocm-base-wheels
|
||||
cid=$(docker create rocm-base-debs:$${BUILDKITE_BUILD_NUMBER})
|
||||
docker cp $${cid}:/app/debs/. artifacts/rocm-base-wheels/
|
||||
docker rm $${cid}
|
||||
|
||||
.buildkite/scripts/cache-rocm-base-wheels.sh upload
|
||||
|
||||
# Download wheels
|
||||
.buildkite/scripts/cache-rocm-base-wheels.sh download
|
||||
|
||||
# Save ECR tag for downstream jobs
|
||||
buildkite-agent meta-data set "rocm-base-image-tag" "$${ECR_CACHE_TAG}"
|
||||
|
||||
# Scenario 2: Full rebuild needed
|
||||
else
|
||||
echo ""
|
||||
echo " CACHE MISS - Building from scratch..."
|
||||
echo ""
|
||||
|
||||
# Build full base image and push to ECR
|
||||
DOCKER_BUILDKIT=1 docker buildx build \
|
||||
--file docker/Dockerfile.rocm_base \
|
||||
--tag "$${ECR_CACHE_TAG}" \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
--push \
|
||||
.
|
||||
|
||||
# Build wheel extraction stage
|
||||
DOCKER_BUILDKIT=1 docker buildx build \
|
||||
--file docker/Dockerfile.rocm_base \
|
||||
--tag rocm-base-debs:$${BUILDKITE_BUILD_NUMBER} \
|
||||
--target debs_wheel_release \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
--load \
|
||||
.
|
||||
|
||||
# Extract and upload wheels
|
||||
mkdir -p artifacts/rocm-base-wheels
|
||||
cid=$(docker create rocm-base-debs:$${BUILDKITE_BUILD_NUMBER})
|
||||
docker cp $${cid}:/app/debs/. artifacts/rocm-base-wheels/
|
||||
docker rm $${cid}
|
||||
|
||||
.buildkite/scripts/cache-rocm-base-wheels.sh upload
|
||||
# Cache base docker image to ECR
|
||||
docker push "$${ECR_CACHE_TAG}"
|
||||
|
||||
buildkite-agent meta-data set "rocm-base-image-tag" "$${ECR_CACHE_TAG}"
|
||||
|
||||
echo ""
|
||||
echo " Build complete - Image and wheels cached"
|
||||
fi
|
||||
|
||||
# Cache base docker image to ECR
|
||||
docker push "$${ECR_CACHE_TAG}"
|
||||
|
||||
buildkite-agent meta-data set "rocm-base-image-tag" "$${ECR_CACHE_TAG}"
|
||||
|
||||
echo ""
|
||||
echo " Build complete - Image and wheels cached"
|
||||
fi
|
||||
artifact_paths:
|
||||
- "artifacts/rocm-base-wheels/*.whl"
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
|
||||
artifact_paths:
|
||||
- "artifacts/rocm-base-wheels/*.whl"
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
# ROCm Job 2: Build vLLM ROCm Wheel
|
||||
- label: ":python: Build vLLM ROCm Wheel - x86_64"
|
||||
id: build-rocm-vllm-wheel
|
||||
depends_on:
|
||||
- step: build-rocm-base-wheels
|
||||
allow_failure: false
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
# Download artifacts and prepare Docker image
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
# ROCm Job 2: Build vLLM ROCm Wheel
|
||||
- label: ":python: Build vLLM ROCm Wheel - x86_64"
|
||||
id: build-rocm-vllm-wheel
|
||||
depends_on:
|
||||
- step: build-rocm-base-wheels
|
||||
allow_failure: false
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
# Download artifacts and prepare Docker image
|
||||
- |
|
||||
set -euo pipefail
|
||||
# Ensure git tags are up-to-date (Buildkite's default fetch doesn't update tags)
|
||||
# This fixes version detection when tags are moved/force-pushed
|
||||
echo "Fetching latest tags from origin..."
|
||||
git fetch --tags --force origin
|
||||
|
||||
# Log tag information for debugging version detection
|
||||
echo "========================================"
|
||||
echo "Git Tag Verification"
|
||||
echo "========================================"
|
||||
echo "Current HEAD: $(git rev-parse HEAD)"
|
||||
echo "git describe --tags: $(git describe --tags 2>/dev/null || echo 'No tags found')"
|
||||
echo ""
|
||||
echo "Recent tags (pointing to commits near HEAD):"
|
||||
git tag -l --sort=-creatordate | head -5
|
||||
echo "setuptools_scm version detection:"
|
||||
pip install -q setuptools_scm 2>/dev/null || true
|
||||
python3 -c "import setuptools_scm; print(' Detected version:', setuptools_scm.get_version())" 2>/dev/null || echo " (setuptools_scm not available in this environment)"
|
||||
echo "========================================"
|
||||
|
||||
# Ensure git tags are up-to-date (Buildkite's default fetch doesn't update tags)
|
||||
# This fixes version detection when tags are moved/force-pushed
|
||||
echo "Fetching latest tags from origin..."
|
||||
git fetch --tags --force origin
|
||||
|
||||
# Log tag information for debugging version detection
|
||||
echo "========================================"
|
||||
echo "Git Tag Verification"
|
||||
echo "========================================"
|
||||
echo "Current HEAD: $(git rev-parse HEAD)"
|
||||
echo "git describe --tags: $(git describe --tags 2>/dev/null || echo 'No tags found')"
|
||||
echo ""
|
||||
echo "Recent tags (pointing to commits near HEAD):"
|
||||
git tag -l --sort=-creatordate | head -5
|
||||
echo "setuptools_scm version detection:"
|
||||
pip install -q setuptools_scm 2>/dev/null || true
|
||||
python3 -c "import setuptools_scm; print(' Detected version:', setuptools_scm.get_version())" 2>/dev/null || echo " (setuptools_scm not available in this environment)"
|
||||
echo "========================================"
|
||||
# Download wheel artifacts from current build
|
||||
echo "Downloading wheel artifacts from current build"
|
||||
buildkite-agent artifact download "artifacts/rocm-base-wheels/*.whl" .
|
||||
|
||||
# Download wheel artifacts from current build
|
||||
echo "Downloading wheel artifacts from current build"
|
||||
buildkite-agent artifact download "artifacts/rocm-base-wheels/*.whl" .
|
||||
# Get ECR image tag from metadata (set by build-rocm-base-wheels)
|
||||
ECR_IMAGE_TAG="$$(buildkite-agent meta-data get rocm-base-image-tag 2>/dev/null || echo '')"
|
||||
if [ -z "$${ECR_IMAGE_TAG}" ]; then
|
||||
echo "ERROR: rocm-base-image-tag metadata not found"
|
||||
echo "This should have been set by the build-rocm-base-wheels job"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pulling base Docker image from ECR: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Login to ECR
|
||||
aws ecr-public get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7
|
||||
|
||||
# Pull base Docker image from ECR
|
||||
docker pull "$${ECR_IMAGE_TAG}"
|
||||
|
||||
echo "Loaded base image: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Prepare base wheels for Docker build context
|
||||
mkdir -p docker/context/base-wheels
|
||||
touch docker/context/base-wheels/.keep
|
||||
cp artifacts/rocm-base-wheels/*.whl docker/context/base-wheels/
|
||||
echo "Base wheels for vLLM build:"
|
||||
ls -lh docker/context/base-wheels/
|
||||
|
||||
# Get ECR image tag from metadata (set by build-rocm-base-wheels)
|
||||
ECR_IMAGE_TAG="$$(buildkite-agent meta-data get rocm-base-image-tag 2>/dev/null || echo '')"
|
||||
if [ -z "$${ECR_IMAGE_TAG}" ]; then
|
||||
echo "ERROR: rocm-base-image-tag metadata not found"
|
||||
echo "This should have been set by the build-rocm-base-wheels job"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pulling base Docker image from ECR: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Login to ECR
|
||||
aws ecr-public get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7
|
||||
|
||||
# Pull base Docker image from ECR
|
||||
docker pull "$${ECR_IMAGE_TAG}"
|
||||
|
||||
echo "Loaded base image: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Prepare base wheels for Docker build context
|
||||
mkdir -p docker/context/base-wheels
|
||||
touch docker/context/base-wheels/.keep
|
||||
cp artifacts/rocm-base-wheels/*.whl docker/context/base-wheels/
|
||||
echo "Base wheels for vLLM build:"
|
||||
ls -lh docker/context/base-wheels/
|
||||
echo "========================================"
|
||||
echo "Building vLLM wheel with:"
|
||||
echo " BUILDKITE_COMMIT: $${BUILDKITE_COMMIT}"
|
||||
echo " BUILDKITE_BRANCH: $${BUILDKITE_BRANCH}"
|
||||
echo " BASE_IMAGE: $${ECR_IMAGE_TAG}"
|
||||
echo "========================================"
|
||||
|
||||
echo "========================================"
|
||||
echo "Building vLLM wheel with:"
|
||||
echo " BUILDKITE_COMMIT: $${BUILDKITE_COMMIT}"
|
||||
echo " BUILDKITE_BRANCH: $${BUILDKITE_BRANCH}"
|
||||
echo " BASE_IMAGE: $${ECR_IMAGE_TAG}"
|
||||
echo "========================================"
|
||||
# Build vLLM wheel using local checkout (REMOTE_VLLM=0)
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--file docker/Dockerfile.rocm \
|
||||
--target export_vllm_wheel_release \
|
||||
--output type=local,dest=rocm-dist \
|
||||
--build-arg BASE_IMAGE="$${ECR_IMAGE_TAG}" \
|
||||
--build-arg REMOTE_VLLM=0 \
|
||||
--build-arg GIT_REPO_CHECK=1 \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
.
|
||||
echo "Built vLLM wheel:"
|
||||
ls -lh rocm-dist/*.whl
|
||||
# Copy wheel to artifacts directory
|
||||
mkdir -p artifacts/rocm-vllm-wheel
|
||||
cp rocm-dist/*.whl artifacts/rocm-vllm-wheel/
|
||||
echo "Final vLLM wheel:"
|
||||
ls -lh artifacts/rocm-vllm-wheel/
|
||||
artifact_paths:
|
||||
- "artifacts/rocm-vllm-wheel/*.whl"
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
|
||||
# Build vLLM wheel using local checkout (REMOTE_VLLM=0)
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--file docker/Dockerfile.rocm \
|
||||
--target export_vllm_wheel_release \
|
||||
--output type=local,dest=rocm-dist \
|
||||
--build-arg BASE_IMAGE="$${ECR_IMAGE_TAG}" \
|
||||
--build-arg REMOTE_VLLM=0 \
|
||||
--build-arg GIT_REPO_CHECK=1 \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
.
|
||||
echo "Built vLLM wheel:"
|
||||
ls -lh rocm-dist/*.whl
|
||||
# Copy wheel to artifacts directory
|
||||
mkdir -p artifacts/rocm-vllm-wheel
|
||||
cp rocm-dist/*.whl artifacts/rocm-vllm-wheel/
|
||||
echo "Final vLLM wheel:"
|
||||
ls -lh artifacts/rocm-vllm-wheel/
|
||||
artifact_paths:
|
||||
- "artifacts/rocm-vllm-wheel/*.whl"
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
# ROCm Job 3: Upload Wheels to S3
|
||||
- label: ":s3: Upload ROCm Wheels to S3"
|
||||
id: upload-rocm-wheels
|
||||
depends_on:
|
||||
- step: build-rocm-vllm-wheel
|
||||
allow_failure: false
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
# Download all wheel artifacts and run upload
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
# ROCm Job 3: Upload Wheels to S3
|
||||
- label: ":s3: Upload ROCm Wheels to S3"
|
||||
id: upload-rocm-wheels
|
||||
depends_on:
|
||||
- step: build-rocm-vllm-wheel
|
||||
allow_failure: false
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
# Download all wheel artifacts and run upload
|
||||
- |
|
||||
set -euo pipefail
|
||||
# Download artifacts from current build
|
||||
echo "Downloading artifacts from current build"
|
||||
# buildkite-agent artifact download "artifacts/rocm-base-wheels/*.whl" .
|
||||
# buildkite-agent artifact download "artifacts/rocm-vllm-wheel/*.whl" .
|
||||
|
||||
# Download artifacts from current build
|
||||
echo "Downloading artifacts from current build"
|
||||
buildkite-agent artifact download "artifacts/rocm-base-wheels/*.whl" .
|
||||
buildkite-agent artifact download "artifacts/rocm-vllm-wheel/*.whl" .
|
||||
# # Run upload script
|
||||
bash .buildkite/scripts/upload-rocm-wheels.sh
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
|
||||
# Run upload script
|
||||
bash .buildkite/scripts/upload-rocm-wheels.sh
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
# ROCm Job 4: Annotate ROCm Wheel Release
|
||||
- label: ":memo: Annotate ROCm wheel release"
|
||||
id: annotate-rocm-release
|
||||
depends_on:
|
||||
- upload-rocm-wheels
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/annotate-rocm-release.sh"
|
||||
env:
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
|
||||
# ROCm Job 4: Annotate ROCm Wheel Release
|
||||
- label: ":memo: Annotate ROCm wheel release"
|
||||
id: annotate-rocm-release
|
||||
depends_on:
|
||||
- upload-rocm-wheels
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/annotate-rocm-release.sh"
|
||||
env:
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
# ROCm Job 5: Generate Root Index for ROCm Wheels (for release only)
|
||||
# This is the job to create https://wheels.vllm.ai/rocm/ index allowing
|
||||
# users to install with `uv pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/`
|
||||
- block: "Generate Root Index for ROCm Wheels for Release"
|
||||
key: block-generate-root-index-rocm-wheels
|
||||
depends_on: upload-rocm-wheels
|
||||
|
||||
# ROCm Job 5: Generate Root Index for ROCm Wheels (for release only)
|
||||
# This is the job to create https://wheels.vllm.ai/rocm/ index allowing
|
||||
# users to install with `uv pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/`
|
||||
- block: "Generate Root Index for ROCm Wheels for Release"
|
||||
key: block-generate-root-index-rocm-wheels
|
||||
depends_on: upload-rocm-wheels
|
||||
- label: ":package: Generate Root Index for ROCm Wheels for Release"
|
||||
depends_on: block-generate-root-index-rocm-wheels
|
||||
id: generate-root-index-rocm-wheels
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- "bash tools/vllm-rocm/generate-rocm-wheels-root-index.sh"
|
||||
env:
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
VARIANT: "rocm723"
|
||||
|
||||
- label: ":package: Generate Root Index for ROCm Wheels for Release"
|
||||
depends_on: block-generate-root-index-rocm-wheels
|
||||
id: generate-root-index-rocm-wheels
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- "bash tools/vllm-rocm/generate-rocm-wheels-root-index.sh"
|
||||
env:
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
VARIANT: "rocm723"
|
||||
# ROCm Job 6: Build ROCm Release Docker Image
|
||||
- label: ":docker: Build release image - x86_64 - ROCm"
|
||||
id: build-rocm-release-image
|
||||
depends_on:
|
||||
- step: block-build-release-images
|
||||
allow_failure: true
|
||||
- step: build-rocm-base-wheels
|
||||
allow_failure: false
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
# Login to ECR
|
||||
aws ecr-public get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7
|
||||
|
||||
# Get ECR image tag from metadata (set by build-rocm-base-wheels)
|
||||
ECR_IMAGE_TAG="$$(buildkite-agent meta-data get rocm-base-image-tag 2>/dev/null || echo '')"
|
||||
if [ -z "$${ECR_IMAGE_TAG}" ]; then
|
||||
echo "ERROR: rocm-base-image-tag metadata not found"
|
||||
echo "This should have been set by the build-rocm-base-wheels job"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pulling base Docker image from ECR: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Pull base Docker image from ECR
|
||||
docker pull "$${ECR_IMAGE_TAG}"
|
||||
|
||||
echo "Loaded base image: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Pass the base image ECR tag to downstream steps (nightly publish)
|
||||
buildkite-agent meta-data set "rocm-base-ecr-tag" "$${ECR_IMAGE_TAG}"
|
||||
|
||||
echo "========================================"
|
||||
echo "Building vLLM ROCm release image with:"
|
||||
echo " BASE_IMAGE: $${ECR_IMAGE_TAG}"
|
||||
echo " BUILDKITE_COMMIT: $${BUILDKITE_COMMIT}"
|
||||
echo "========================================"
|
||||
|
||||
# Build vLLM ROCm release image using cached base
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--build-arg max_jobs=16 \
|
||||
--build-arg BASE_IMAGE="$${ECR_IMAGE_TAG}" \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
--tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$${BUILDKITE_COMMIT}-rocm \
|
||||
--target vllm-openai \
|
||||
--progress plain \
|
||||
-f docker/Dockerfile.rocm .
|
||||
|
||||
# Push to ECR
|
||||
docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$${BUILDKITE_COMMIT}-rocm
|
||||
|
||||
# ROCm Job 6: Build ROCm Release Docker Image
|
||||
- label: ":docker: Build release image - x86_64 - ROCm"
|
||||
id: build-rocm-release-image
|
||||
depends_on:
|
||||
- step: block-build-release-images
|
||||
allow_failure: true
|
||||
- step: build-rocm-base-wheels
|
||||
allow_failure: false
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
# Login to ECR
|
||||
aws ecr-public get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7
|
||||
|
||||
# Get ECR image tag from metadata (set by build-rocm-base-wheels)
|
||||
ECR_IMAGE_TAG="$$(buildkite-agent meta-data get rocm-base-image-tag 2>/dev/null || echo '')"
|
||||
if [ -z "$${ECR_IMAGE_TAG}" ]; then
|
||||
echo "ERROR: rocm-base-image-tag metadata not found"
|
||||
echo "This should have been set by the build-rocm-base-wheels job"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pulling base Docker image from ECR: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Pull base Docker image from ECR
|
||||
docker pull "$${ECR_IMAGE_TAG}"
|
||||
|
||||
echo "Loaded base image: $${ECR_IMAGE_TAG}"
|
||||
|
||||
# Pass the base image ECR tag to downstream steps (nightly publish)
|
||||
buildkite-agent meta-data set "rocm-base-ecr-tag" "$${ECR_IMAGE_TAG}"
|
||||
|
||||
echo "========================================"
|
||||
echo "Building vLLM ROCm release image with:"
|
||||
echo " BASE_IMAGE: $${ECR_IMAGE_TAG}"
|
||||
echo " BUILDKITE_COMMIT: $${BUILDKITE_COMMIT}"
|
||||
echo "========================================"
|
||||
|
||||
# Build vLLM ROCm release image using cached base
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--build-arg max_jobs=16 \
|
||||
--build-arg BASE_IMAGE="$${ECR_IMAGE_TAG}" \
|
||||
--build-arg USE_SCCACHE=1 \
|
||||
--build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \
|
||||
--build-arg SCCACHE_REGION_NAME=us-west-2 \
|
||||
--build-arg SCCACHE_S3_NO_CREDENTIALS=0 \
|
||||
--tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$${BUILDKITE_COMMIT}-rocm \
|
||||
--target vllm-openai \
|
||||
--progress plain \
|
||||
-f docker/Dockerfile.rocm .
|
||||
|
||||
# Push to ECR
|
||||
docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$${BUILDKITE_COMMIT}-rocm
|
||||
echo ""
|
||||
echo " Successfully built and pushed ROCm release image"
|
||||
echo " Image: public.ecr.aws/q9t5s3a7/vllm-release-repo:$${BUILDKITE_COMMIT}-rocm"
|
||||
echo ""
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
|
||||
echo ""
|
||||
echo " Successfully built and pushed ROCm release image"
|
||||
echo " Image: public.ecr.aws/q9t5s3a7/vllm-release-repo:$${BUILDKITE_COMMIT}-rocm"
|
||||
echo ""
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
- label: "Publish nightly XPU image to DockerHub"
|
||||
depends_on:
|
||||
- create-manifest-xpu
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
agents:
|
||||
queue: small_cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/xpu/push-nightly-builds-xpu.sh"
|
||||
- "bash .buildkite/scripts/cleanup-nightly-builds.sh nightly- vllm/vllm-openai-xpu"
|
||||
plugins:
|
||||
- docker-login#v3.0.0:
|
||||
username: vllmbot
|
||||
password-env: DOCKERHUB_TOKEN
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
DOCKERHUB_USERNAME: "vllmbot"
|
||||
|
||||
- label: "Publish nightly XPU image to DockerHub"
|
||||
depends_on:
|
||||
- create-manifest-xpu
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
agents:
|
||||
queue: small_cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/xpu/push-nightly-builds-xpu.sh"
|
||||
- "bash .buildkite/scripts/cleanup-nightly-builds.sh nightly- vllm/vllm-openai-xpu"
|
||||
plugins:
|
||||
- docker-login#v3.0.0:
|
||||
username: vllmbot
|
||||
password-env: DOCKERHUB_TOKEN
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
DOCKERHUB_USERNAME: "vllmbot"
|
||||
|
||||
- label: "Publish nightly ROCm image to DockerHub"
|
||||
depends_on:
|
||||
- build-rocm-release-image
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
agents:
|
||||
queue: small_cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/push-nightly-builds-rocm.sh"
|
||||
# Clean up old nightly builds (keep only last 14)
|
||||
- "bash .buildkite/scripts/cleanup-nightly-builds.sh nightly- vllm/vllm-openai-rocm"
|
||||
- "bash .buildkite/scripts/cleanup-nightly-builds.sh base-nightly- vllm/vllm-openai-rocm"
|
||||
plugins:
|
||||
- docker-login#v3.0.0:
|
||||
username: vllmbot
|
||||
password-env: DOCKERHUB_TOKEN
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
DOCKERHUB_USERNAME: "vllmbot"
|
||||
- label: "Publish nightly ROCm image to DockerHub"
|
||||
depends_on:
|
||||
- build-rocm-release-image
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
agents:
|
||||
queue: small_cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/push-nightly-builds-rocm.sh"
|
||||
# Clean up old nightly builds (keep only last 14)
|
||||
- "bash .buildkite/scripts/cleanup-nightly-builds.sh nightly- vllm/vllm-openai-rocm"
|
||||
- "bash .buildkite/scripts/cleanup-nightly-builds.sh base-nightly- vllm/vllm-openai-rocm"
|
||||
plugins:
|
||||
- docker-login#v3.0.0:
|
||||
username: vllmbot
|
||||
password-env: DOCKERHUB_TOKEN
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
DOCKERHUB_USERNAME: "vllmbot"
|
||||
|
||||
# =============================================================================
|
||||
# Publish to DockerHub and PyPI (at the end so all builds complete first)
|
||||
|
||||
@@ -40,7 +40,9 @@ function cpu_tests() {
|
||||
pytest -x -v -s tests/kernels/moe/test_cpu_fused_moe.py
|
||||
pytest -x -v -s tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
|
||||
pytest -x -v -s tests/kernels/moe/test_cpu_int4_moe.py
|
||||
pytest -x -v -s tests/kernels/mamba/test_cpu_short_conv.py"
|
||||
pytest -x -v -s tests/kernels/mamba/test_cpu_short_conv.py
|
||||
pytest -x -v -s tests/kernels/mamba/test_causal_conv1d.py
|
||||
pytest -x -v -s tests/kernels/mamba/test_mamba_ssm.py"
|
||||
|
||||
# skip tests requiring model downloads if HF_TOKEN is not set
|
||||
# due to rate-limits
|
||||
@@ -97,3 +99,4 @@ function cpu_tests() {
|
||||
# All of CPU tests are expected to be finished less than 40 mins.
|
||||
export -f cpu_tests
|
||||
timeout 2h bash -c cpu_tests
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ steps:
|
||||
- pytest -v -s cuda/test_platform_no_cuda_init.py
|
||||
|
||||
- label: Cudagraph
|
||||
device: h200_35gb
|
||||
key: cudagraph
|
||||
timeout_in_minutes: 30
|
||||
source_file_dependencies:
|
||||
@@ -28,4 +29,4 @@ steps:
|
||||
commands:
|
||||
- pytest -v -s v1/cudagraph/test_cudagraph_dispatch.py
|
||||
- pytest -v -s v1/cudagraph/test_cudagraph_mode.py
|
||||
- pytest -v -s v1/cudagraph/test_breakable_cudagraph.py
|
||||
- pytest -v -s v1/cudagraph/test_breakable_cudagraph.py
|
||||
|
||||
@@ -57,6 +57,7 @@ steps:
|
||||
- image-build-amd
|
||||
|
||||
- label: Entrypoints Integration (API Server OpenAI - Part 1)
|
||||
device: h200_35gb
|
||||
key: entrypoints-integration-api-server-openai-part-1
|
||||
timeout_in_minutes: 45
|
||||
working_dir: "/vllm-workspace/tests"
|
||||
@@ -75,6 +76,7 @@ steps:
|
||||
- image-build-amd
|
||||
|
||||
- label: Entrypoints Integration (API Server OpenAI - Part 2)
|
||||
device: h200_35gb
|
||||
key: entrypoints-integration-api-server-openai-part-2
|
||||
timeout_in_minutes: 45
|
||||
working_dir: "/vllm-workspace/tests"
|
||||
@@ -94,6 +96,7 @@ steps:
|
||||
- image-build-amd
|
||||
|
||||
- label: Entrypoints Integration (API Server Generate)
|
||||
device: h200_35gb
|
||||
key: entrypoints-integration-api-server-generate
|
||||
timeout_in_minutes: 50
|
||||
working_dir: "/vllm-workspace/tests"
|
||||
@@ -151,6 +154,7 @@ steps:
|
||||
- pytest -v -s entrypoints/multimodal
|
||||
|
||||
- label: Entrypoints Integration (Pooling)
|
||||
device: h200_35gb
|
||||
key: entrypoints-integration-pooling
|
||||
timeout_in_minutes: 50
|
||||
working_dir: "/vllm-workspace/tests"
|
||||
|
||||
@@ -15,6 +15,7 @@ steps:
|
||||
- pytest -v -s tests/kernels/ir
|
||||
|
||||
- label: Kernels Core Operation Test
|
||||
device: h200_35gb
|
||||
key: kernels-core-operation-test
|
||||
timeout_in_minutes: 120
|
||||
source_file_dependencies:
|
||||
@@ -163,6 +164,7 @@ steps:
|
||||
- image-build-amd
|
||||
|
||||
- label: Kernels Mamba Test
|
||||
device: h200_35gb
|
||||
key: kernels-mamba-test
|
||||
timeout_in_minutes: 40
|
||||
source_file_dependencies:
|
||||
@@ -235,6 +237,11 @@ steps:
|
||||
- vllm/model_executor/kernels/linear/cute_dsl/ll_bf16.py
|
||||
- vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_dotprod.py
|
||||
- vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_splitk.py
|
||||
- vllm/cute_utils/
|
||||
- vllm/model_executor/layers/mamba/ops/gdn_chunk_cutedsl/
|
||||
- vllm/model_executor/layers/fused_moe/router/bf16x3_router_gemm_cutedsl.py
|
||||
- tests/kernels/mamba/test_gdn_prefill_cutedsl.py
|
||||
- tests/kernels/test_bf16x3_router_gemm_cutedsl.py
|
||||
- tests/kernels/test_ll_bf16_gemm.py
|
||||
- tests/kernels/test_top_k_per_row.py
|
||||
commands:
|
||||
@@ -264,6 +271,8 @@ steps:
|
||||
- pytest -v -s tests/kernels/moe/test_flashinfer_moe.py
|
||||
- pytest -v -s tests/kernels/moe/test_trtllm_nvfp4_moe.py
|
||||
- pytest -v -s tests/kernels/moe/test_cutedsl_moe.py
|
||||
- pytest -v -s tests/kernels/mamba/test_gdn_prefill_cutedsl.py
|
||||
- pytest -v -s tests/kernels/test_bf16x3_router_gemm_cutedsl.py
|
||||
- pytest -v -s tests/kernels/test_ll_bf16_gemm.py
|
||||
# e2e
|
||||
- pytest -v -s tests/models/quantization/test_nvfp4.py
|
||||
|
||||
@@ -64,8 +64,9 @@ steps:
|
||||
- image-build-amd
|
||||
|
||||
- label: V1 Core + KV + Metrics
|
||||
device: h200_35gb
|
||||
key: v1-core-kv-metrics
|
||||
timeout_in_minutes: 60
|
||||
timeout_in_minutes: 80
|
||||
source_file_dependencies:
|
||||
- vllm/config/
|
||||
- vllm/distributed/
|
||||
|
||||
@@ -3,6 +3,7 @@ depends_on:
|
||||
- image-build
|
||||
steps:
|
||||
- label: Model Executor
|
||||
device: h200_35gb
|
||||
key: model-executor
|
||||
timeout_in_minutes: 45
|
||||
source_file_dependencies:
|
||||
|
||||
@@ -21,6 +21,7 @@ steps:
|
||||
- image-build-amd
|
||||
|
||||
- label: Language Models Tests (Extra Standard) %N
|
||||
device: h200_35gb
|
||||
key: language-models-tests-extra-standard
|
||||
timeout_in_minutes: 40
|
||||
source_file_dependencies:
|
||||
@@ -51,8 +52,8 @@ steps:
|
||||
- tests/models/language/pooling/test_classification.py
|
||||
- vllm/_aiter_ops.py
|
||||
- vllm/platforms/rocm.py
|
||||
|
||||
- label: Language Models Tests (Hybrid) %N
|
||||
device: h200_35gb
|
||||
key: language-models-tests-hybrid
|
||||
timeout_in_minutes: 65
|
||||
source_file_dependencies:
|
||||
@@ -63,8 +64,8 @@ steps:
|
||||
# Note: also needed to run plamo2 model in vLLM
|
||||
- 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.6.0'
|
||||
# Shard hybrid language model tests
|
||||
- pytest -v -s models/language/generation -m hybrid_model --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB
|
||||
# Shard the hybrid language model tests that are numerically stable on Hopper.
|
||||
- pytest -v -s models/language/generation -m hybrid_model -k 'not granite-4.0-tiny-preview' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB
|
||||
parallelism: 2
|
||||
mirror:
|
||||
amd:
|
||||
@@ -77,6 +78,20 @@ steps:
|
||||
- uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0'
|
||||
- pytest -v -s models/language/generation -m hybrid_model --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB
|
||||
|
||||
# Granite 4 hybrid generation is sensitive to hardware-specific Triton SSD
|
||||
# autotuning (https://github.com/vllm-project/vllm/issues/25194). Keep this one
|
||||
# correctness test on L4 until its H200 output matches the Transformers reference.
|
||||
- label: Language Models Tests (Granite L4 Compatibility)
|
||||
key: language-models-tests-granite-l4-compatibility
|
||||
timeout_in_minutes: 65
|
||||
source_file_dependencies:
|
||||
- vllm/
|
||||
- tests/models/language/generation
|
||||
commands:
|
||||
- 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.6.0'
|
||||
- pytest -v -s models/language/generation -m hybrid_model -k 'granite-4.0-tiny-preview'
|
||||
|
||||
- label: Language Models Test (Extended Generation) # 80min
|
||||
device: h200_35gb
|
||||
key: language-models-test-extended-generation
|
||||
|
||||
@@ -119,6 +119,7 @@ steps:
|
||||
- vllm/model_executor/model_loader/
|
||||
|
||||
- label: Multi-Modal Models (Extended Generation 1)
|
||||
device: h200_35gb
|
||||
key: multi-modal-models-extended-generation-1
|
||||
optional: true
|
||||
source_file_dependencies:
|
||||
|
||||
@@ -116,8 +116,9 @@ steps:
|
||||
- image-build-amd
|
||||
|
||||
- label: PyTorch Fullgraph Smoke Test
|
||||
device: h200_35gb
|
||||
key: pytorch-fullgraph-smoke-test
|
||||
timeout_in_minutes: 60
|
||||
timeout_in_minutes: 90
|
||||
source_file_dependencies:
|
||||
- vllm/__init__.py
|
||||
- vllm/_aiter_ops.py
|
||||
@@ -149,7 +150,42 @@ steps:
|
||||
# as it is a heavy test that is covered in other steps.
|
||||
# Use `find` to launch multiple instances of pytest so that
|
||||
# they do not suffer from https://github.com/vllm-project/vllm/issues/28965
|
||||
- "find compile/fullgraph/ -name 'test_*.py' -not -name 'test_full_graph.py' -print0 | xargs -0 -n1 -I{} pytest -s -v '{}'"
|
||||
- "find compile/fullgraph/ -name 'test_*.py' -not -name 'test_full_cudagraph.py' -not -name 'test_full_graph.py' -print0 | xargs -0 -n1 -I{} pytest -s -v '{}'"
|
||||
|
||||
# Hopper-only DeepSeek-V2-Lite cases in this file require two 29.3-GiB model
|
||||
# instances and cannot fit a 35GB MIG slice. L4 retains the original coverage:
|
||||
# those SM90 cases skip while the architecture-compatible cases still run.
|
||||
- label: PyTorch Fullgraph CUDAGraph (L4 Compatibility)
|
||||
key: pytorch-fullgraph-cudagraph-l4-compatibility
|
||||
timeout_in_minutes: 60
|
||||
source_file_dependencies:
|
||||
- vllm/__init__.py
|
||||
- vllm/_aiter_ops.py
|
||||
- vllm/_custom_ops.py
|
||||
- vllm/compilation/
|
||||
- vllm/config/
|
||||
- vllm/distributed/
|
||||
- vllm/engine/
|
||||
- vllm/env_override.py
|
||||
- vllm/envs.py
|
||||
- vllm/forward_context.py
|
||||
- vllm/inputs/
|
||||
- vllm/ir/
|
||||
- vllm/kernels/
|
||||
- vllm/logger.py
|
||||
- vllm/model_executor/
|
||||
- vllm/multimodal/
|
||||
- vllm/platforms/
|
||||
- vllm/plugins/
|
||||
- vllm/sampling_params.py
|
||||
- vllm/sequence.py
|
||||
- vllm/transformers_utils/
|
||||
- vllm/triton_utils/
|
||||
- vllm/utils/
|
||||
- vllm/v1/
|
||||
- tests/compile
|
||||
commands:
|
||||
- pytest -s -v compile/fullgraph/test_full_cudagraph.py
|
||||
|
||||
- label: PyTorch Fullgraph
|
||||
key: pytorch-fullgraph
|
||||
|
||||
@@ -3,8 +3,11 @@ depends_on:
|
||||
- image-build
|
||||
steps:
|
||||
- label: Quantization
|
||||
device: h200_35gb
|
||||
key: quantization
|
||||
timeout_in_minutes: 60
|
||||
timeout_in_minutes: 75
|
||||
env:
|
||||
VLLM_USE_V2_MODEL_RUNNER: "0"
|
||||
source_file_dependencies:
|
||||
- csrc/
|
||||
- vllm/model_executor/layers/quantization
|
||||
@@ -19,9 +22,13 @@ steps:
|
||||
# TODO(jerryzh168): resolve the above comment
|
||||
- uv pip install --system torchao==0.17.0 --index-url https://download.pytorch.org/whl/cu130
|
||||
- uv pip install --system conch-triton-kernels
|
||||
- VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py
|
||||
# The SM90-only checkpoint currently contains a removed weight_chan_scale
|
||||
# parameter. It was not exercised by the previous L4 job.
|
||||
- VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py -k 'not test_compressed_tensors_w4a8_fp8' --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
|
||||
parallelism: 8
|
||||
|
||||
- label: Quantized Fusions
|
||||
device: h200_35gb
|
||||
key: quantized-fusions
|
||||
timeout_in_minutes: 20
|
||||
source_file_dependencies:
|
||||
@@ -52,10 +59,14 @@ steps:
|
||||
- pytest -s -v tests/quantization/test_blackwell_moe.py
|
||||
|
||||
- label: Quantized Models Test
|
||||
device: h200_35gb
|
||||
key: quantized-models-test
|
||||
timeout_in_minutes: 50
|
||||
timeout_in_minutes: 65
|
||||
env:
|
||||
VLLM_USE_V2_MODEL_RUNNER: "0"
|
||||
source_file_dependencies:
|
||||
- vllm/model_executor/layers/quantization
|
||||
- tests/models/quantization
|
||||
commands:
|
||||
- pytest -v -s models/quantization
|
||||
- pytest -v -s models/quantization --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT
|
||||
parallelism: 3
|
||||
|
||||
@@ -81,6 +81,7 @@ steps:
|
||||
- pytest -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine
|
||||
|
||||
- label: Rust Frontend Tool Use
|
||||
device: h200_35gb
|
||||
timeout_in_minutes: 25
|
||||
working_dir: "/vllm-workspace/tests"
|
||||
source_file_dependencies:
|
||||
|
||||
@@ -430,6 +430,7 @@ set(VLLM_EXT_SRC
|
||||
"csrc/cpu/layernorm.cpp"
|
||||
"csrc/cpu/mla_decode.cpp"
|
||||
"csrc/cpu/pos_encoding.cpp"
|
||||
"csrc/cpu/mamba_cpu.cpp"
|
||||
"csrc/moe/dynamic_4bit_int_moe_cpu.cpp"
|
||||
"csrc/cpu/cpu_attn.cpp"
|
||||
"csrc/cpu/torch_bindings.cpp")
|
||||
@@ -489,6 +490,7 @@ if (ENABLE_X86_ISA)
|
||||
"csrc/cpu/spec_decode_utils.cpp"
|
||||
"csrc/cpu/cpu_attn.cpp"
|
||||
"csrc/cpu/dnnl_kernels.cpp"
|
||||
"csrc/cpu/mamba_cpu.cpp"
|
||||
"csrc/cpu/torch_bindings.cpp"
|
||||
# TODO: Remove these files
|
||||
"csrc/cpu/activation.cpp"
|
||||
@@ -502,6 +504,7 @@ if (ENABLE_X86_ISA)
|
||||
"csrc/cpu/utils.cpp"
|
||||
"csrc/cpu/spec_decode_utils.cpp"
|
||||
"csrc/cpu/cpu_attn.cpp"
|
||||
"csrc/cpu/mamba_cpu.cpp"
|
||||
"csrc/cpu/dnnl_kernels.cpp"
|
||||
"csrc/cpu/torch_bindings.cpp"
|
||||
# TODO: Remove these files
|
||||
|
||||
@@ -336,13 +336,14 @@ struct FP32Vec8 : public Vec<FP32Vec8> {
|
||||
reg.val[1] = fp16_to_fp32_bits(raw_lo);
|
||||
}
|
||||
float reduce_sum() const {
|
||||
AliasReg ar;
|
||||
ar.reg = reg;
|
||||
float result = 0;
|
||||
unroll_loop<int, VEC_ELEM_NUM>(
|
||||
[&result, &ar](int i) { result += ar.values[i]; });
|
||||
|
||||
return result;
|
||||
// VSX horizontal reduction: 3 vector ops instead of 8 scalar adds.
|
||||
// Step 1: pairwise sum of the two 4-wide halves
|
||||
__vector float s = vec_add(reg.val[0], reg.val[1]);
|
||||
// Step 2: rotate by 8 bytes (2 floats) and add
|
||||
s = vec_add(s, vec_sld(s, s, 8));
|
||||
// Step 3: rotate by 4 bytes (1 float) and add => all lanes hold total
|
||||
s = vec_add(s, vec_sld(s, s, 4));
|
||||
return vec_extract(s, 0);
|
||||
}
|
||||
FP32Vec8 exp() const {
|
||||
f32x4x2_t out;
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
//
|
||||
// CPU at::Tensor wrappers for Mamba decode-step kernels defined in
|
||||
// mamba_kernels.hpp.
|
||||
|
||||
#include "cpu/mamba_kernels.hpp"
|
||||
|
||||
#include <ATen/ATen.h>
|
||||
#include <torch/library.h>
|
||||
#include <c10/util/Optional.h>
|
||||
|
||||
#include "cpu_types.hpp"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// causal_conv1d_update
|
||||
// ---------------------------------------------------------------------------
|
||||
at::Tensor causal_conv1d_update_cpu_impl(
|
||||
at::Tensor& x, at::Tensor& conv_state, const at::Tensor& weight,
|
||||
const c10::optional<at::Tensor>& bias,
|
||||
const c10::optional<std::string>& activation,
|
||||
const c10::optional<at::Tensor>& conv_state_indices,
|
||||
const c10::optional<at::Tensor>& query_start_loc, int64_t pad_slot_id) {
|
||||
bool do_silu = false;
|
||||
if (activation.has_value()) {
|
||||
const std::string& act = activation.value();
|
||||
do_silu = (act == "silu" || act == "swish");
|
||||
}
|
||||
|
||||
at::ScalarType dtype = x.scalar_type();
|
||||
|
||||
// Input x: contiguous in native dtype.
|
||||
at::Tensor x_c = x.is_contiguous() ? x : x.contiguous();
|
||||
|
||||
// conv_state: NEVER copy the full paged tensor just for layout reasons.
|
||||
// If the dtype matches we work directly on conv_state (contiguous or not)
|
||||
// by extracting strides and passing them to the kernel.
|
||||
// Only a dtype-conversion copy is made when types differ (rare for BF16).
|
||||
bool state_type_ok = (conv_state.scalar_type() == dtype);
|
||||
at::Tensor state_c = state_type_ok ? conv_state : conv_state.to(dtype);
|
||||
// state_c and conv_state may be non-contiguous — that is intentional.
|
||||
|
||||
// Weight: coerce to same dtype if needed (should match in practice)
|
||||
at::Tensor w_c =
|
||||
(weight.scalar_type() != dtype)
|
||||
? weight.to(dtype).contiguous()
|
||||
: (weight.is_contiguous() ? weight : weight.contiguous());
|
||||
|
||||
// Bias stays float32 (small scalar, used only for fp32 accumulation)
|
||||
at::Tensor bias_f32;
|
||||
if (bias.has_value() && bias.value().defined())
|
||||
bias_f32 = bias.value().to(at::kFloat).contiguous();
|
||||
|
||||
int64_t batch = x_c.size(0);
|
||||
int64_t dim = x_c.size(1);
|
||||
int64_t seqlen = (x_c.dim() == 3) ? x_c.size(2) : 1;
|
||||
int64_t width = w_c.size(1);
|
||||
int64_t state_len = state_c.size(2);
|
||||
|
||||
// Extract strides — works for contiguous AND non-contiguous (transposed)
|
||||
// state. stride(0): between cache slots (e.g. num_slots × dim × width-1 in
|
||||
// contiguous) stride(1): between conv channels (dim stride) stride(2):
|
||||
// between state elements (=1 when contiguous, =dim when transposed)
|
||||
int64_t stride_s_slot = state_c.stride(0);
|
||||
int64_t stride_s_dim = state_c.stride(1);
|
||||
int64_t stride_s_state = state_c.stride(2);
|
||||
|
||||
at::Tensor out = x_c.clone(); // native dtype, no float32 alloc
|
||||
|
||||
const int32_t* cache_idx_ptr = nullptr;
|
||||
at::Tensor cache_idx_int;
|
||||
if (conv_state_indices.has_value()) {
|
||||
cache_idx_int = conv_state_indices.value().to(at::kInt).contiguous();
|
||||
cache_idx_ptr = cache_idx_int.data_ptr<int32_t>();
|
||||
}
|
||||
|
||||
VLLM_DISPATCH_FLOATING_TYPES(dtype, "causal_conv1d_update", [&] {
|
||||
mamba_cpu::causal_conv1d_update_kernel<scalar_t>(
|
||||
x_c.data_ptr<scalar_t>(), state_c.data_ptr<scalar_t>(), stride_s_slot,
|
||||
stride_s_dim, stride_s_state, w_c.data_ptr<scalar_t>(),
|
||||
bias_f32.defined() ? bias_f32.data_ptr<float>() : nullptr,
|
||||
out.data_ptr<scalar_t>(), cache_idx_ptr,
|
||||
static_cast<int32_t>(pad_slot_id), batch, dim, seqlen, width, state_len,
|
||||
do_silu);
|
||||
});
|
||||
|
||||
// Write back only when a type-conversion copy was made.
|
||||
// Layout-only non-contiguity is handled via strides above — no copy needed.
|
||||
if (!state_type_ok) conv_state.copy_(state_c);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// selective_state_update
|
||||
// ---------------------------------------------------------------------------
|
||||
void selective_state_update_cpu_impl(
|
||||
at::Tensor& state, // (nstates, nheads, dim, dstate)
|
||||
const at::Tensor& x, // (N, nheads, dim)
|
||||
const at::Tensor& dt, const at::Tensor& A, const at::Tensor& B,
|
||||
const at::Tensor& C, const c10::optional<at::Tensor>& D,
|
||||
const c10::optional<at::Tensor>& z,
|
||||
const c10::optional<at::Tensor>& dt_bias, bool dt_softplus,
|
||||
const c10::optional<at::Tensor>& state_batch_indices,
|
||||
const c10::optional<at::Tensor>& dst_state_batch_indices,
|
||||
int64_t null_block_id, at::Tensor& out,
|
||||
const c10::optional<at::Tensor>& num_accepted_tokens,
|
||||
const c10::optional<at::Tensor>& cu_seqlens) {
|
||||
at::ScalarType state_type = state.scalar_type();
|
||||
at::ScalarType input_type = x.scalar_type();
|
||||
|
||||
// x, B, C must be contiguous and match input_type
|
||||
auto ensure_input = [input_type](const at::Tensor& t) -> at::Tensor {
|
||||
at::Tensor r = (t.scalar_type() != input_type) ? t.to(input_type) : t;
|
||||
return r.is_contiguous() ? r : r.contiguous();
|
||||
};
|
||||
at::Tensor x_in = ensure_input(x);
|
||||
at::Tensor B_in = ensure_input(B);
|
||||
at::Tensor C_in = ensure_input(C);
|
||||
at::Tensor z_in;
|
||||
if (z.has_value() && z.value().defined()) z_in = ensure_input(z.value());
|
||||
|
||||
// A, D, dt_bias are float32 model parameters that arrive here as expanded
|
||||
// tensors, e.g. A is (nheads, head_dim, dstate) with strides (1, 0, 0).
|
||||
// We need just the scalar value per head as a (nheads,) 1-D array so that
|
||||
// A_ptr[h] in the kernel correctly reads head h's value.
|
||||
//
|
||||
// Strategy: peel trailing expanded (stride=0) dims via .select(), which is
|
||||
// a zero-copy view. For A: (nheads, head_dim, dstate) strides (1,0,0)
|
||||
// → .select(2,0) → (nheads, head_dim) strides (1,0)
|
||||
// → .select(1,0) → (nheads,) stride (1,) ← contiguous, free.
|
||||
// No allocation, no type conversion (A is already float32).
|
||||
auto to_per_head_1d_f32 = [](const at::Tensor& t) -> at::Tensor {
|
||||
at::Tensor r = t;
|
||||
// Peel trailing dimensions that are broadcast (stride=0 or size=1)
|
||||
while (r.dim() > 1) r = r.select(r.dim() - 1, 0);
|
||||
if (r.scalar_type() != at::kFloat) r = r.to(at::kFloat);
|
||||
return r.is_contiguous() ? r : r.contiguous();
|
||||
};
|
||||
|
||||
at::Tensor A_f32 = to_per_head_1d_f32(A); // (nheads,) float32
|
||||
at::Tensor D_f32, dt_bias_f32;
|
||||
if (D.has_value() && D.value().defined())
|
||||
D_f32 = to_per_head_1d_f32(D.value());
|
||||
if (dt_bias.has_value() && dt_bias.value().defined())
|
||||
dt_bias_f32 = to_per_head_1d_f32(dt_bias.value());
|
||||
|
||||
// dt: reduce (N, nheads, head_dim) expanded tensor → (N, nheads) BEFORE
|
||||
// the type conversion so we convert head_dim x fewer elements.
|
||||
at::Tensor dt_f32;
|
||||
{
|
||||
// If dt was expanded to (N, nheads, head_dim) with stride-0 in dim 2,
|
||||
// take a zero-copy view of index 0 along that dim first.
|
||||
at::Tensor t2 = (dt.dim() == 3) ? dt.select(2, 0) : dt; // (N, nheads)
|
||||
at::Tensor t3 = (t2.scalar_type() != at::kFloat) ? t2.to(at::kFloat) : t2;
|
||||
dt_f32 = t3.is_contiguous() ? t3 : t3.contiguous();
|
||||
}
|
||||
|
||||
int64_t nheads = state.size(1);
|
||||
int64_t dim = state.size(2);
|
||||
int64_t dstate = state.size(3);
|
||||
int64_t N = (cu_seqlens.has_value() && cu_seqlens.value().defined())
|
||||
? cu_seqlens.value().size(0) - 1
|
||||
: x_in.size(0);
|
||||
int64_t ngroups = B_in.size(1);
|
||||
|
||||
// Strides
|
||||
int64_t stride_state_n = state.stride(0);
|
||||
int64_t stride_state_h = state.stride(1);
|
||||
int64_t stride_state_d = state.stride(2);
|
||||
int64_t stride_x_n = x_in.stride(0);
|
||||
int64_t stride_x_h = x_in.stride(1);
|
||||
int64_t stride_dt_n = dt_f32.stride(0); // dt is (N, nheads)
|
||||
int64_t stride_BC_n = B_in.stride(0);
|
||||
int64_t stride_BC_g = B_in.stride(1);
|
||||
int64_t stride_out_n = out.stride(0);
|
||||
int64_t stride_out_h = out.stride(1);
|
||||
|
||||
// Optional index pointers
|
||||
auto get_int32_ptr =
|
||||
[](const c10::optional<at::Tensor>& opt) -> const int32_t* {
|
||||
return (opt.has_value() && opt.value().defined())
|
||||
? opt.value().data_ptr<int32_t>()
|
||||
: nullptr;
|
||||
};
|
||||
const int32_t* sbi_ptr = get_int32_ptr(state_batch_indices);
|
||||
const int32_t* dsbi_ptr = get_int32_ptr(dst_state_batch_indices);
|
||||
const int32_t* nat_ptr = get_int32_ptr(num_accepted_tokens);
|
||||
const int32_t* csl_ptr = get_int32_ptr(cu_seqlens);
|
||||
|
||||
// Dispatch on (state_t, input_t, out_t): write directly into `out`
|
||||
// without any intermediate float32 buffer.
|
||||
VLLM_DISPATCH_FLOATING_TYPES(state_type, "ssu_state", [&] {
|
||||
using state_t = scalar_t;
|
||||
VLLM_DISPATCH_FLOATING_TYPES(input_type, "ssu_input", [&] {
|
||||
using input_t = scalar_t;
|
||||
VLLM_DISPATCH_FLOATING_TYPES(out.scalar_type(), "ssu_out", [&] {
|
||||
using out_t = scalar_t;
|
||||
mamba_cpu::selective_state_update_kernel<state_t, input_t, out_t>(
|
||||
state.data_ptr<state_t>(), stride_state_n, stride_state_h,
|
||||
stride_state_d, x_in.data_ptr<input_t>(), stride_x_n, stride_x_h,
|
||||
dt_f32.data_ptr<float>(), stride_dt_n, A_f32.data_ptr<float>(),
|
||||
B_in.data_ptr<input_t>(), C_in.data_ptr<input_t>(), stride_BC_n,
|
||||
stride_BC_g, D_f32.defined() ? D_f32.data_ptr<float>() : nullptr,
|
||||
z_in.defined() ? z_in.data_ptr<input_t>() : nullptr,
|
||||
dt_bias_f32.defined() ? dt_bias_f32.data_ptr<float>() : nullptr,
|
||||
out.data_ptr<out_t>(), stride_out_n, stride_out_h, sbi_ptr,
|
||||
dsbi_ptr, static_cast<int32_t>(null_block_id), nat_ptr, csl_ptr, N,
|
||||
nheads, ngroups, dim, dstate, dt_softplus);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mamba_chunk_scan_fwd_cpu
|
||||
// ---------------------------------------------------------------------------
|
||||
void mamba_chunk_scan_fwd_cpu_impl(
|
||||
at::Tensor& out, // [seqlen, nheads, headdim] — pre-allocated by caller
|
||||
at::Tensor&
|
||||
final_states, // [batch, nheads, headdim, dstate] float32 contiguous
|
||||
const at::Tensor& x, // [seqlen, nheads, headdim]
|
||||
const at::Tensor&
|
||||
dt, // [seqlen, nheads] float32 (preprocessed: bias+softplus+clamp)
|
||||
const at::Tensor& A, // [nheads] float32
|
||||
const at::Tensor& B, // [seqlen, ngroups, dstate]
|
||||
const at::Tensor& C, // [seqlen, ngroups, dstate]
|
||||
const c10::optional<at::Tensor>& D, // [nheads] float32 (optional)
|
||||
const c10::optional<at::Tensor>& z, // [seqlen, nheads, headdim] (optional)
|
||||
const at::Tensor& cu_seqlens // [batch+1] int32
|
||||
) {
|
||||
const at::ScalarType input_type = x.scalar_type();
|
||||
|
||||
auto ensure_contig = [input_type](const at::Tensor& t) -> at::Tensor {
|
||||
at::Tensor r = (t.scalar_type() != input_type) ? t.to(input_type) : t;
|
||||
return r.is_contiguous() ? r : r.contiguous();
|
||||
};
|
||||
at::Tensor x_in = ensure_contig(x);
|
||||
at::Tensor B_in = ensure_contig(B);
|
||||
at::Tensor C_in = ensure_contig(C);
|
||||
at::Tensor z_in;
|
||||
if (z.has_value() && z.value().defined()) z_in = ensure_contig(z.value());
|
||||
|
||||
// A and D are float32 model parameters, potentially broadcast-expanded.
|
||||
// Strip trailing broadcast dims to get a contiguous (nheads,) array.
|
||||
auto to_per_head_f32 = [](const at::Tensor& t) -> at::Tensor {
|
||||
at::Tensor r = t;
|
||||
while (r.dim() > 1) r = r.select(r.dim() - 1, 0);
|
||||
if (r.scalar_type() != at::kFloat) r = r.to(at::kFloat);
|
||||
return r.is_contiguous() ? r : r.contiguous();
|
||||
};
|
||||
at::Tensor A_f32 = to_per_head_f32(A);
|
||||
at::Tensor D_f32;
|
||||
if (D.has_value() && D.value().defined()) D_f32 = to_per_head_f32(D.value());
|
||||
|
||||
// dt: [seqlen, nheads] float32 — caller has applied bias+softplus+clamp in
|
||||
// Python.
|
||||
at::Tensor dt_c = dt.is_contiguous() ? dt : dt.contiguous();
|
||||
if (dt_c.scalar_type() != at::kFloat) dt_c = dt_c.to(at::kFloat);
|
||||
|
||||
at::Tensor cu_int = cu_seqlens.to(at::kInt).contiguous();
|
||||
|
||||
const int64_t batch = final_states.size(0);
|
||||
const int64_t nheads = final_states.size(1);
|
||||
const int64_t headdim = final_states.size(2);
|
||||
const int64_t dstate = final_states.size(3);
|
||||
const int64_t ngroups = B_in.size(1);
|
||||
|
||||
TORCH_CHECK(final_states.is_contiguous(),
|
||||
"mamba_chunk_scan_fwd_cpu: final_states must be contiguous");
|
||||
TORCH_CHECK(out.is_contiguous(),
|
||||
"mamba_chunk_scan_fwd_cpu: out must be contiguous (writes via "
|
||||
"raw data_ptr)");
|
||||
|
||||
VLLM_DISPATCH_FLOATING_TYPES(input_type, "mamba_chunk_scan_fwd_cpu", [&] {
|
||||
mamba_cpu::mamba_chunk_scan_fwd_kernel<scalar_t>(
|
||||
final_states.data_ptr<float>(), x_in.data_ptr<scalar_t>(),
|
||||
dt_c.data_ptr<float>(), A_f32.data_ptr<float>(),
|
||||
B_in.data_ptr<scalar_t>(), C_in.data_ptr<scalar_t>(),
|
||||
D_f32.defined() ? D_f32.data_ptr<float>() : nullptr,
|
||||
z_in.defined() ? z_in.data_ptr<scalar_t>() : nullptr,
|
||||
out.data_ptr<scalar_t>(), cu_int.data_ptr<int32_t>(), batch, nheads,
|
||||
ngroups, headdim, dstate);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
//
|
||||
// Fused CPU vector kernels for Mamba decode-step hotspots:
|
||||
// - causal_conv1d_update (depthwise 1-D conv state roll + compute)
|
||||
// - selective_state_update (SSM recurrence, single-step)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cpu_types.hpp"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
|
||||
namespace mamba_cpu {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// causal_conv1d_update — templated for native BF16/FP32
|
||||
//
|
||||
// state_ptr may point to a NON-CONTIGUOUS paged KV cache tensor.
|
||||
// Explicit strides are passed so the kernel writes directly into the
|
||||
// correct memory locations without making a contiguous copy of the full
|
||||
// paged tensor (which was the source of the 34-41% direct_copy_kernel).
|
||||
//
|
||||
// stride_s_slot = state.stride(0) — between cache slots
|
||||
// stride_s_dim = state.stride(1) — between conv_dim channels
|
||||
// stride_s_state = state.stride(2) — between state elements
|
||||
//
|
||||
// When stride_s_state == 1 (contiguous), the memmove fast path is used.
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename scalar_t>
|
||||
inline void causal_conv1d_update_kernel(
|
||||
const scalar_t* __restrict__ x_ptr, scalar_t* __restrict__ state_ptr,
|
||||
int64_t stride_s_slot, int64_t stride_s_dim, int64_t stride_s_state,
|
||||
const scalar_t* __restrict__ weight_ptr, const float* __restrict__ bias_ptr,
|
||||
scalar_t* __restrict__ out_ptr, const int32_t* __restrict__ cache_idxs,
|
||||
int32_t pad_slot_id, int64_t batch, int64_t dim, int64_t seqlen,
|
||||
int64_t width, int64_t state_len, bool do_silu) {
|
||||
#pragma omp parallel for
|
||||
for (int64_t b = 0; b < batch; ++b) {
|
||||
int64_t cache_idx = (cache_idxs != nullptr) ? cache_idxs[b] : b;
|
||||
if (cache_idx == pad_slot_id) continue;
|
||||
|
||||
for (int64_t t = 0; t < seqlen; ++t) {
|
||||
const scalar_t* x_b = x_ptr + (b * dim * seqlen + t);
|
||||
scalar_t* out_b = out_ptr + (b * dim * seqlen + t);
|
||||
// Base of this slot in the (possibly non-contiguous) paged state
|
||||
scalar_t* s_base = state_ptr + cache_idx * stride_s_slot;
|
||||
|
||||
for (int64_t d = 0; d < dim; ++d) {
|
||||
float x_val = static_cast<float>(x_b[d * seqlen]);
|
||||
scalar_t* sd = s_base + d * stride_s_dim; // start of this dim's state
|
||||
const scalar_t* w = weight_ptr + d * width;
|
||||
|
||||
// Accumulate in float32 for precision
|
||||
float acc = (bias_ptr != nullptr) ? bias_ptr[d] : 0.0f;
|
||||
for (int64_t k = 0; k < state_len; ++k) {
|
||||
acc += static_cast<float>(w[k]) *
|
||||
static_cast<float>(sd[k * stride_s_state]);
|
||||
}
|
||||
acc += static_cast<float>(w[state_len]) * x_val;
|
||||
|
||||
// Shift state left and append new input.
|
||||
// Use memmove when contiguous (stride==1); element loop otherwise.
|
||||
if (stride_s_state == 1) {
|
||||
if (state_len > 1)
|
||||
std::memmove(sd, sd + 1, (state_len - 1) * sizeof(scalar_t));
|
||||
if (state_len > 0) sd[state_len - 1] = static_cast<scalar_t>(x_val);
|
||||
} else {
|
||||
for (int64_t k = 0; k < state_len - 1; ++k)
|
||||
sd[k * stride_s_state] = sd[(k + 1) * stride_s_state];
|
||||
if (state_len > 0)
|
||||
sd[(state_len - 1) * stride_s_state] = static_cast<scalar_t>(x_val);
|
||||
}
|
||||
|
||||
if (do_silu) {
|
||||
float sigmoid = (acc >= 0) ? 1.0f / (1.0f + std::exp(-acc))
|
||||
: std::exp(acc) / (1.0f + std::exp(acc));
|
||||
acc *= sigmoid;
|
||||
}
|
||||
out_b[d * seqlen] = static_cast<scalar_t>(acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// selective_state_update
|
||||
//
|
||||
// Template parameters:
|
||||
// state_t - dtype of ssm_state cache (typically BFloat16)
|
||||
// input_t - dtype of x, B, C (typically BFloat16)
|
||||
// out_t - dtype of output tensor (typically BFloat16)
|
||||
// Write directly — no float32 intermediate buffer needed.
|
||||
//
|
||||
// A, D, dt_bias are accepted as const float* (they are always float32
|
||||
// model parameters in Mamba2). This eliminates the per-call float32→BF16
|
||||
// conversion and the .contiguous() materialisation of the broadcast-expand.
|
||||
//
|
||||
// dt is accepted as a (N, nheads) scalar-per-head tensor, not as the
|
||||
// (N, nheads, head_dim) expansion, so no .contiguous() copy is needed.
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename state_t, typename input_t, typename out_t = float>
|
||||
inline void selective_state_update_kernel(
|
||||
state_t* __restrict__ state_ptr, int64_t stride_state_n,
|
||||
int64_t stride_state_h, int64_t stride_state_d,
|
||||
const input_t* __restrict__ x_ptr, int64_t stride_x_n, int64_t stride_x_h,
|
||||
// dt: (N, nheads) — scalar per head, NOT expanded to head_dim
|
||||
const float* __restrict__ dt_ptr, int64_t stride_dt_n,
|
||||
// A: (nheads,) float32 — scalar per head
|
||||
const float* __restrict__ A_ptr, const input_t* __restrict__ B_ptr,
|
||||
const input_t* __restrict__ C_ptr, int64_t stride_BC_n, int64_t stride_BC_g,
|
||||
// D: (nheads,) float32 — scalar per head (nullptr if not used)
|
||||
const float* __restrict__ D_ptr,
|
||||
// z: same shape as x (optional)
|
||||
const input_t* __restrict__ z_ptr,
|
||||
// dt_bias: (nheads,) float32 — scalar per head (nullptr if not used)
|
||||
const float* __restrict__ dt_bias_ptr, out_t* __restrict__ out_ptr,
|
||||
int64_t stride_out_n, int64_t stride_out_h,
|
||||
const int32_t* __restrict__ state_batch_indices,
|
||||
const int32_t* __restrict__ dst_state_batch_indices, int32_t null_block_id,
|
||||
const int32_t* __restrict__ num_accepted_tokens,
|
||||
const int32_t* __restrict__ cu_seqlens, int64_t N, int64_t nheads,
|
||||
int64_t ngroups, int64_t dim, int64_t dstate, bool dt_softplus) {
|
||||
using state_vec_t = vec_op::vec_t<state_t>;
|
||||
using input_vec_t = vec_op::vec_t<input_t>;
|
||||
constexpr int VEC_ELEM_NUM = 8;
|
||||
|
||||
int64_t nheads_per_group = nheads / ngroups;
|
||||
|
||||
for (int64_t seq_idx = 0; seq_idx < N; ++seq_idx) {
|
||||
int64_t bos, seq_len;
|
||||
if (cu_seqlens != nullptr) {
|
||||
bos = cu_seqlens[seq_idx];
|
||||
seq_len = cu_seqlens[seq_idx + 1] - bos;
|
||||
} else {
|
||||
bos = seq_idx;
|
||||
seq_len = 1;
|
||||
}
|
||||
|
||||
int64_t state_read_idx = (state_batch_indices != nullptr)
|
||||
? state_batch_indices[seq_idx]
|
||||
: seq_idx;
|
||||
if (state_read_idx == null_block_id) continue;
|
||||
|
||||
int64_t state_write_idx = (num_accepted_tokens == nullptr)
|
||||
? ((dst_state_batch_indices != nullptr)
|
||||
? dst_state_batch_indices[seq_idx]
|
||||
: state_read_idx)
|
||||
: -1;
|
||||
|
||||
state_t* s = state_ptr + state_read_idx * stride_state_n;
|
||||
|
||||
for (int64_t t = 0; t < seq_len; ++t) {
|
||||
int64_t token_idx = bos + t;
|
||||
const input_t* x_tok = x_ptr + token_idx * stride_x_n;
|
||||
// dt: (N, nheads) — one float per head per token
|
||||
const float* dt_tok = dt_ptr + token_idx * stride_dt_n;
|
||||
const input_t* B_tok = B_ptr + token_idx * stride_BC_n;
|
||||
const input_t* C_tok = C_ptr + token_idx * stride_BC_n;
|
||||
out_t* out_tok = out_ptr + token_idx * stride_out_n;
|
||||
|
||||
#pragma omp parallel for
|
||||
for (int64_t h = 0; h < nheads; ++h) {
|
||||
int64_t g = h / nheads_per_group;
|
||||
const input_t* x_h = x_tok + h * stride_x_h;
|
||||
const input_t* B_g = B_tok + g * stride_BC_g;
|
||||
const input_t* C_g = C_tok + g * stride_BC_g;
|
||||
out_t* out_h = out_tok + h * stride_out_h;
|
||||
state_t* s_h = s + h * stride_state_h;
|
||||
|
||||
// Read scalars-per-head (A, dt, dt_bias, D) — no per-dim indexing
|
||||
float dt_val = dt_tok[h];
|
||||
if (dt_bias_ptr != nullptr) dt_val += dt_bias_ptr[h];
|
||||
if (dt_softplus) {
|
||||
dt_val = (dt_val <= 20.0f) ? std::log1p(std::exp(dt_val)) : dt_val;
|
||||
}
|
||||
const float A_val = A_ptr[h]; // scalar: same for all dim, dstate
|
||||
const float D_val = (D_ptr != nullptr) ? D_ptr[h] : 0.0f;
|
||||
|
||||
const input_t* z_h =
|
||||
(z_ptr != nullptr) ? z_ptr + token_idx * stride_x_n + h * stride_x_h
|
||||
: nullptr;
|
||||
|
||||
vec_op::FP32Vec8 dt_vec(dt_val);
|
||||
// dA = exp(A * dt): A and dt are SCALARS per head, so compute once
|
||||
// and broadcast. This saves 7 redundant std::exp() calls that
|
||||
// FP32Vec8::exp() would otherwise make on the broadcast vector.
|
||||
const float dA_scalar = std::exp(A_val * dt_val);
|
||||
vec_op::FP32Vec8 dA(dA_scalar); // broadcast
|
||||
|
||||
for (int64_t d = 0; d < dim; ++d) {
|
||||
float x_val = static_cast<float>(x_h[d]);
|
||||
|
||||
vec_op::FP32Vec8 out_vec(0.0f);
|
||||
state_t* s_hd = s_h + d * stride_state_d;
|
||||
const input_t* B_g_base = B_g;
|
||||
const input_t* C_g_base = C_g;
|
||||
|
||||
vec_op::FP32Vec8 x_vec(x_val);
|
||||
// dBx = B * x * dt — same dA for all dstate (A is scalar)
|
||||
// s_new = s * dA + B * x * dt
|
||||
|
||||
int64_t n = 0;
|
||||
for (; n <= dstate - VEC_ELEM_NUM; n += VEC_ELEM_NUM) {
|
||||
vec_op::FP32Vec8 B_v((input_vec_t(B_g_base + n)));
|
||||
vec_op::FP32Vec8 C_v((input_vec_t(C_g_base + n)));
|
||||
vec_op::FP32Vec8 s_v((state_vec_t(s_hd + n)));
|
||||
|
||||
vec_op::FP32Vec8 dBx = B_v * x_vec * dt_vec;
|
||||
vec_op::FP32Vec8 s_new = s_v * dA + dBx;
|
||||
|
||||
state_vec_t(s_new).save(s_hd + n);
|
||||
out_vec = out_vec + s_new * C_v;
|
||||
}
|
||||
|
||||
float out_val = out_vec.reduce_sum();
|
||||
for (; n < dstate; ++n) {
|
||||
// Reuse dA_scalar computed once per head — no exp() re-call
|
||||
float dBx = static_cast<float>(B_g[n]) * x_val * dt_val;
|
||||
float s_new = static_cast<float>(s_hd[n]) * dA_scalar + dBx;
|
||||
s_hd[n] = static_cast<state_t>(s_new);
|
||||
out_val += s_new * static_cast<float>(C_g[n]);
|
||||
}
|
||||
|
||||
if (D_ptr != nullptr) out_val += x_val * D_val;
|
||||
if (z_h != nullptr) {
|
||||
float z_val = static_cast<float>(z_h[d]);
|
||||
float sigmoid = (z_val >= 0)
|
||||
? 1.0f / (1.0f + std::exp(-z_val))
|
||||
: std::exp(z_val) / (1.0f + std::exp(z_val));
|
||||
out_val *= z_val * sigmoid;
|
||||
}
|
||||
out_h[d] = static_cast<out_t>(out_val);
|
||||
}
|
||||
}
|
||||
|
||||
if (num_accepted_tokens != nullptr &&
|
||||
dst_state_batch_indices != nullptr) {
|
||||
int64_t token_dst_idx = dst_state_batch_indices[seq_idx * seq_len + t];
|
||||
if (token_dst_idx != null_block_id && token_dst_idx != state_read_idx) {
|
||||
state_t* dst_s = state_ptr + token_dst_idx * stride_state_n;
|
||||
std::memmove(dst_s, s, nheads * stride_state_h * sizeof(state_t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (num_accepted_tokens == nullptr && state_write_idx != null_block_id &&
|
||||
state_write_idx != state_read_idx) {
|
||||
state_t* dst_s = state_ptr + state_write_idx * stride_state_n;
|
||||
std::memmove(dst_s, s, nheads * stride_state_h * sizeof(state_t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mamba_chunk_scan_fwd
|
||||
//
|
||||
// Prefill SSM recurrence for Mamba2 / SSD models.
|
||||
//
|
||||
// Key difference from selective_state_update_kernel (decode path):
|
||||
// - #pragma omp parallel for collapse(2) is OUTSIDE the time loop.
|
||||
// Each thread owns a (batch, head) slice and runs the entire token
|
||||
// sequence without any per-token OpenMP synchronisation overhead.
|
||||
// For seqlen=256, this eliminates 256 thread-barrier launches per batch.
|
||||
//
|
||||
// `dt` arrives already processed (float32, after bias + softplus + clamp)
|
||||
// to keep this kernel simple. Preprocessing is done in the Python wrapper.
|
||||
//
|
||||
// `states_ptr` points to the [batch, nheads, headdim, dstate] float32 output
|
||||
// tensor, pre-initialised by the caller (zero or from initial_states).
|
||||
// Each (b, h) slice is private to exactly one thread via collapse(2), so
|
||||
// there are no write conflicts.
|
||||
//
|
||||
// D is treated as a scalar per head ([nheads] float32).
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename input_t>
|
||||
inline void mamba_chunk_scan_fwd_kernel(
|
||||
float* __restrict__ states_ptr, // [batch, nheads, headdim, dstate] f32
|
||||
const input_t* __restrict__ x_ptr, // [seqlen, nheads, headdim]
|
||||
const float* __restrict__ dt_ptr, // [seqlen, nheads] f32 (preprocessed)
|
||||
const float* __restrict__ A_ptr, // [nheads] f32
|
||||
const input_t* __restrict__ B_ptr, // [seqlen, ngroups, dstate]
|
||||
const input_t* __restrict__ C_ptr, // [seqlen, ngroups, dstate]
|
||||
const float* __restrict__ D_ptr, // [nheads] f32 (nullable)
|
||||
const input_t* __restrict__ z_ptr, // [seqlen, nheads, headdim] (nullable)
|
||||
input_t* __restrict__ out_ptr, // [seqlen, nheads, headdim]
|
||||
const int32_t* __restrict__ cu_seqlens, // [batch+1] int32
|
||||
int64_t batch, int64_t nheads, int64_t ngroups, int64_t headdim,
|
||||
int64_t dstate) {
|
||||
using input_vec_t = vec_op::vec_t<input_t>;
|
||||
constexpr int VEC_ELEM_NUM = 8;
|
||||
|
||||
const int64_t nheads_per_group = nheads / ngroups;
|
||||
// states layout: [batch, nheads, headdim, dstate] contiguous (caller
|
||||
// guarantee)
|
||||
const int64_t stride_s_b = nheads * headdim * dstate;
|
||||
const int64_t stride_s_h = headdim * dstate;
|
||||
// stride_s_d = dstate, stride_s_n = 1
|
||||
|
||||
#pragma omp parallel for collapse(2) schedule(static)
|
||||
for (int64_t b = 0; b < batch; ++b) {
|
||||
for (int64_t h = 0; h < nheads; ++h) {
|
||||
const int64_t seq_start = cu_seqlens[b];
|
||||
const int64_t seq_end = cu_seqlens[b + 1];
|
||||
const int64_t g = h / nheads_per_group;
|
||||
|
||||
const float A_val = A_ptr[h];
|
||||
const float D_val = (D_ptr != nullptr) ? D_ptr[h] : 0.0f;
|
||||
|
||||
// Working state slice: states[b, h, :, :] — float32, headdim * dstate.
|
||||
// Fits in L1/L2 for typical dims (e.g. 64*128*4 = 32 KB).
|
||||
float* s_bh = states_ptr + b * stride_s_b + h * stride_s_h;
|
||||
|
||||
for (int64_t t = seq_start; t < seq_end; ++t) {
|
||||
const input_t* x_h = x_ptr + t * nheads * headdim + h * headdim;
|
||||
const float* dt_h = dt_ptr + t * nheads + h;
|
||||
const input_t* B_g = B_ptr + t * ngroups * dstate + g * dstate;
|
||||
const input_t* C_g = C_ptr + t * ngroups * dstate + g * dstate;
|
||||
const input_t* z_h = (z_ptr != nullptr)
|
||||
? z_ptr + t * nheads * headdim + h * headdim
|
||||
: nullptr;
|
||||
input_t* out_h = out_ptr + t * nheads * headdim + h * headdim;
|
||||
|
||||
const float dt_val = *dt_h;
|
||||
const float dA_val = std::exp(A_val * dt_val);
|
||||
const vec_op::FP32Vec8 dA_vec(dA_val); // broadcast scalar
|
||||
const vec_op::FP32Vec8 dt_vec(dt_val);
|
||||
|
||||
for (int64_t d = 0; d < headdim; ++d) {
|
||||
const float x_val = static_cast<float>(x_h[d]);
|
||||
float* s_bhd = s_bh + d * dstate; // [dstate] contiguous float32
|
||||
|
||||
// Vectorised SSM update + readout over dstate:
|
||||
// s_new = s * dA + x * dt * B
|
||||
// y += s_new * C
|
||||
int64_t n = 0;
|
||||
vec_op::FP32Vec8 y_vec(0.0f);
|
||||
const vec_op::FP32Vec8 x_vec(x_val);
|
||||
|
||||
for (; n <= dstate - VEC_ELEM_NUM; n += VEC_ELEM_NUM) {
|
||||
const vec_op::FP32Vec8 B_v((input_vec_t(B_g + n)));
|
||||
const vec_op::FP32Vec8 C_v((input_vec_t(C_g + n)));
|
||||
const vec_op::FP32Vec8 s_v(s_bhd + n);
|
||||
|
||||
const vec_op::FP32Vec8 s_new = s_v * dA_vec + x_vec * dt_vec * B_v;
|
||||
s_new.save(s_bhd + n);
|
||||
y_vec = y_vec + s_new * C_v;
|
||||
}
|
||||
|
||||
float y_val = y_vec.reduce_sum();
|
||||
|
||||
// Scalar tail for remaining dstate elements
|
||||
for (; n < dstate; ++n) {
|
||||
const float B_n = static_cast<float>(B_g[n]);
|
||||
const float C_n = static_cast<float>(C_g[n]);
|
||||
const float s_new = s_bhd[n] * dA_val + x_val * dt_val * B_n;
|
||||
s_bhd[n] = s_new;
|
||||
y_val += s_new * C_n;
|
||||
}
|
||||
|
||||
// D skip connection (scalar per head)
|
||||
if (D_ptr != nullptr) y_val += x_val * D_val;
|
||||
|
||||
// z gating: out = y * z * sigmoid(z) (SiLU)
|
||||
if (z_h != nullptr) {
|
||||
const float z_val = static_cast<float>(z_h[d]);
|
||||
const float sigmoid =
|
||||
(z_val >= 0.0f) ? 1.0f / (1.0f + std::exp(-z_val))
|
||||
: std::exp(z_val) / (1.0f + std::exp(z_val));
|
||||
y_val *= z_val * sigmoid;
|
||||
}
|
||||
|
||||
out_h[d] = static_cast<input_t>(y_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mamba_cpu
|
||||
@@ -213,6 +213,32 @@ void compute_slot_mapping_kernel_impl(const torch::Tensor query_start_loc,
|
||||
torch::Tensor slot_mapping,
|
||||
const int64_t block_size);
|
||||
|
||||
at::Tensor causal_conv1d_update_cpu_impl(
|
||||
at::Tensor& x, at::Tensor& conv_state, const at::Tensor& weight,
|
||||
const c10::optional<at::Tensor>& bias,
|
||||
const c10::optional<std::string>& activation,
|
||||
const c10::optional<at::Tensor>& conv_state_indices,
|
||||
const c10::optional<at::Tensor>& query_start_loc, int64_t pad_slot_id);
|
||||
|
||||
void selective_state_update_cpu_impl(
|
||||
at::Tensor& state, const at::Tensor& x, const at::Tensor& dt,
|
||||
const at::Tensor& A, const at::Tensor& B, const at::Tensor& C,
|
||||
const c10::optional<at::Tensor>& D, const c10::optional<at::Tensor>& z,
|
||||
const c10::optional<at::Tensor>& dt_bias, bool dt_softplus,
|
||||
const c10::optional<at::Tensor>& state_batch_indices,
|
||||
const c10::optional<at::Tensor>& dst_state_batch_indices,
|
||||
int64_t null_block_id, at::Tensor& out,
|
||||
const c10::optional<at::Tensor>& num_accepted_tokens,
|
||||
const c10::optional<at::Tensor>& cu_seqlens);
|
||||
|
||||
void mamba_chunk_scan_fwd_cpu_impl(at::Tensor& out, at::Tensor& final_states,
|
||||
const at::Tensor& x, const at::Tensor& dt,
|
||||
const at::Tensor& A, const at::Tensor& B,
|
||||
const at::Tensor& C,
|
||||
const c10::optional<at::Tensor>& D,
|
||||
const c10::optional<at::Tensor>& z,
|
||||
const at::Tensor& cu_seqlens);
|
||||
|
||||
void init_cpu_memory_env(std::vector<int64_t> node_ids);
|
||||
|
||||
namespace cpu_utils {
|
||||
@@ -595,6 +621,30 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
"block_size) -> ()",
|
||||
&compute_slot_mapping_kernel_impl);
|
||||
|
||||
// Mamba CPU kernels
|
||||
ops.def(
|
||||
"causal_conv1d_update_cpu_vec("
|
||||
"Tensor(a0!) x, Tensor(a1!) conv_state, Tensor weight, "
|
||||
"Tensor? bias, str? activation, Tensor? conv_state_indices, "
|
||||
"Tensor? query_start_loc, SymInt pad_slot_id) -> Tensor",
|
||||
&causal_conv1d_update_cpu_impl);
|
||||
|
||||
ops.def(
|
||||
"selective_state_update_cpu("
|
||||
"Tensor(a0!) state, Tensor x, Tensor dt, Tensor A, Tensor B, Tensor C, "
|
||||
"Tensor? D, Tensor? z, Tensor? dt_bias, bool dt_softplus, "
|
||||
"Tensor? state_batch_indices, Tensor? dst_state_batch_indices, "
|
||||
"SymInt null_block_id, Tensor(a13!) out, "
|
||||
"Tensor? num_accepted_tokens, Tensor? cu_seqlens) -> ()",
|
||||
&selective_state_update_cpu_impl);
|
||||
|
||||
ops.def(
|
||||
"mamba_chunk_scan_fwd_cpu("
|
||||
"Tensor(a0!) out, Tensor(a1!) final_states, "
|
||||
"Tensor x, Tensor dt, Tensor A, Tensor B, Tensor C, "
|
||||
"Tensor? D, Tensor? z, Tensor cu_seqlens) -> ()",
|
||||
&mamba_chunk_scan_fwd_cpu_impl);
|
||||
|
||||
ops.def("init_cpu_memory_env(SymInt[] node_ids) -> ()", &init_cpu_memory_env);
|
||||
|
||||
// Speculative decoding kernels
|
||||
|
||||
@@ -306,7 +306,7 @@ Supported quantization scheme/hardware combinations:
|
||||
|
||||
- Pass: [`vllm/compilation/passes/fusion/rms_quant_fusion.py`](https://github.com/vllm-project/vllm/blob/main/vllm/compilation/passes/fusion/rms_quant_fusion.py)
|
||||
- ROCm AITER pass: [`vllm/compilation/passes/fusion/rocm_aiter_fusion.py`](https://github.com/vllm-project/vllm/blob/main/vllm/compilation/passes/fusion/rocm_aiter_fusion.py)
|
||||
- CUDA/HIP kernels: [`csrc/layernorm_quant_kernels.cu`](https://github.com/vllm-project/vllm/blob/main/csrc/layernorm_quant_kernels.cu)
|
||||
- CUDA/HIP kernels: [`csrc/libtorch_stable/layernorm_quant_kernels.cu`](https://github.com/vllm-project/vllm/blob/main/csrc/libtorch_stable/layernorm_quant_kernels.cu)
|
||||
|
||||
### SiLU+Mul + Quantization (`fuse_act_quant`)
|
||||
|
||||
@@ -332,7 +332,7 @@ Supported quantization scheme/hardware combinations:
|
||||
- Pass: [`vllm/compilation/passes/fusion/act_quant_fusion.py`](https://github.com/vllm-project/vllm/blob/main/vllm/compilation/passes/fusion/act_quant_fusion.py)
|
||||
- ROCm AITER pass: [`vllm/compilation/passes/fusion/rocm_aiter_fusion.py`](https://github.com/vllm-project/vllm/blob/main/vllm/compilation/passes/fusion/rocm_aiter_fusion.py)
|
||||
- CUDA/HIP kernels: [`csrc/quantization/`](https://github.com/vllm-project/vllm/blob/main/csrc/quantization/)
|
||||
- Fused SiLU+Mul+BlockQuant kernel: [`csrc/quantization/fused_kernels/fused_silu_mul_block_quant.cu`](https://github.com/vllm-project/vllm/blob/main/csrc/quantization/fused_kernels/fused_silu_mul_block_quant.cu)
|
||||
- Fused SiLU+Mul+BlockQuant kernel: [`csrc/libtorch_stable/quantization/fused_kernels/fused_silu_mul_block_quant.cu`](https://github.com/vllm-project/vllm/blob/main/csrc/libtorch_stable/quantization/fused_kernels/fused_silu_mul_block_quant.cu)
|
||||
|
||||
### RMSNorm + Padding (`fuse_act_padding`)
|
||||
|
||||
|
||||
@@ -68,13 +68,14 @@ vllm serve <model> \
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `spec_name` | no | `CPUOffloadingSpec` | both | Set to `TieringOffloadingSpec` for multi-tier. |
|
||||
| `cpu_bytes_to_use` | yes | — | both | Total bytes of host memory reserved for the CPU tier across all workers (not per-worker). |
|
||||
| `block_size` | no | GPU block size | both | Offloaded block size in tokens; must be a multiple of the GPU block size. |
|
||||
| `block_size` | no | GPU block size | both | Offloaded block size in tokens; must be a multiple of the GPU block size. Mutually exclusive with `blocks_per_chunk`. |
|
||||
| `blocks_per_chunk` | no | `1` | both | Offloaded chunk size in GPU blocks; must be > 0. Alternative to `block_size` for models whose KV cache groups have different block sizes. |
|
||||
| `eviction_policy` | no | `lru` | both | Primary tier policy: `lru` or `arc`. |
|
||||
| `store_threshold` | no | `0` | single-tier | Min lookups before a block is offloaded. Values ≥ 2 are rejected by `TieringOffloadingSpec`. |
|
||||
| `max_tracker_size` | no | `64000` | single-tier | Max entries in the lookup tracker. |
|
||||
| `secondary_tiers` | no | `[]` | multi-tier | List of secondary tier configs (see below). |
|
||||
| `offload_prompt_only` | no | `true` | both | If `true`, only prompt (prefill) blocks are offloaded; decode blocks are skipped. |
|
||||
| `self_describing_kv_events` | no | `false` | single-tier | Opt-in. When `true` *and* KV cache events are enabled (`--kv-events-config` with `enable_kv_cache_events`), the connector emits self-describing block-granular `BlockStored`/`BlockRemoved` payloads (constituent block hashes, whole-chunk `token_ids`, per-block `block_size`, parent hash, LoRA + group/cache-spec metadata) instead of the placeholder fallback, so external KV-event consumers can index offloaded blocks. Inert unless events are enabled. Currently rejected by `TieringOffloadingSpec`. Full-attention groups only; sliding-window/SSM groups keep the placeholder fallback. In chunk mode (`block_size` > GPU block size), overlapping chunks re-announce shared per-block hashes, so consumers must reference-count (deduplicate) repeated store/remove announcements. |
|
||||
| `self_describing_kv_events` | no | `false` | single-tier | Opt-in. When `true` *and* KV cache events are enabled (`--kv-events-config` with `enable_kv_cache_events`), the connector emits self-describing block-granular `BlockStored`/`BlockRemoved` payloads (constituent block hashes, whole-chunk `token_ids`, per-block `block_size`, parent hash, LoRA + group/cache-spec metadata) instead of the placeholder fallback, so external KV-event consumers can index offloaded blocks. Inert unless events are enabled. Currently rejected by `TieringOffloadingSpec`. Full-attention groups only; sliding-window/SSM groups keep the placeholder fallback. In chunk mode (`block_size` > GPU block size, or `blocks_per_chunk` > 1), overlapping chunks re-announce shared per-block hashes, so consumers must reference-count (deduplicate) repeated store/remove announcements. |
|
||||
| `spec_module_path` | no | — | both | Python import path for a custom `OffloadingSpec` not in the built-in registry. Required only when `spec_name` is not built-in (advanced). |
|
||||
|
||||
## Secondary Tiers
|
||||
@@ -179,7 +180,7 @@ Rather than embedding `host`/`port` in each `secondary_tiers` entry, set them on
|
||||
|
||||
- `cpu_bytes_to_use`: a bigger CPU tier means fewer trips to slower secondary tiers and a higher hit rate. The value is total across all workers, not per-worker. Leave headroom for the rest of the host workload.
|
||||
- For single-tier (CPU-only) setups, set `cpu_bytes_to_use` larger than the aggregate GPU KV cache. Because offloading is immediate, a smaller CPU tier just mirrors what the GPU already holds and adds no hit rate.
|
||||
- `block_size`: larger offloaded blocks reduce per-block bookkeeping overhead but increase the granularity of lookups. Must be a multiple of the GPU block size.
|
||||
- `block_size` / `blocks_per_chunk`: larger offloaded chunks reduce per-block bookkeeping overhead but increase the granularity of lookups.
|
||||
- FS thread counts: tune `n_read_threads` and `n_write_threads` to the parallelism your storage can sustain. Reads are latency-sensitive on the prefill path, so prefer more read threads when prefill hit rates are high.
|
||||
- Sharing `root_dir` across runs: runs with the same model, `block_size`, parallelism layout, and dtype share files under the same `<digest>` subdirectory. Changing any of these produces a new subdirectory; old ones are orphaned but harmless. Delete them to reclaim disk.
|
||||
|
||||
|
||||
@@ -31,10 +31,8 @@
|
||||
| THUDM/CodeGeex4-All-9B | CodeGeexForCausalLM | ✅ | | |
|
||||
| chuhac/TeleChat2-35B | LlamaForCausalLM (TeleChat2 based on Llama arch) | ✅ | | |
|
||||
| 01-ai/Yi1.5-34B-Chat | YiForCausalLM | ✅ | | |
|
||||
| THUDM/CodeGeex4-All-9B | CodeGeexForCausalLM | ✅ | | |
|
||||
| deepseek-ai/DeepSeek-Coder-33B-base | DeepSeekCoderForCausalLM | ✅ | | |
|
||||
| meta-llama/Llama-2-13b-chat-hf | LlamaForCausalLM | ✅ | | |
|
||||
| THUDM/CodeGeex4-All-9B | CodeGeexForCausalLM | ✅ | | |
|
||||
| Qwen/Qwen1.5-14B-Chat | QwenForCausalLM | ✅ | | |
|
||||
| Qwen/Qwen1.5-32B-Chat | QwenForCausalLM | ✅ | | |
|
||||
| RedHatAI/Meta-Llama-3.1-8B-Instruct-FP8-dynamic | LlamaForCausalLM | | ✅ | |
|
||||
|
||||
@@ -17,6 +17,7 @@ from transformers import AutoProcessor, AutoTokenizer
|
||||
from vllm import LLM, EngineArgs, SamplingParams
|
||||
from vllm.lora.request import LoRARequest
|
||||
from vllm.multimodal.utils import fetch_image
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
QUESTION = "What is the content of each image?"
|
||||
@@ -1443,6 +1444,8 @@ def run_generate(
|
||||
engine_args.seed = seed
|
||||
if tensor_parallel_size is not None:
|
||||
engine_args.tensor_parallel_size = tensor_parallel_size
|
||||
if current_platform.is_rocm():
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
llm = LLM.from_engine_args(engine_args)
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
@@ -1484,6 +1487,8 @@ def run_chat(
|
||||
engine_args.seed = seed
|
||||
if tensor_parallel_size is not None:
|
||||
engine_args.tensor_parallel_size = tensor_parallel_size
|
||||
if current_platform.is_rocm():
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
llm = LLM.from_engine_args(engine_args)
|
||||
|
||||
sampling_params = (
|
||||
|
||||
@@ -21,6 +21,7 @@ from vllm.assets.image import ImageAsset
|
||||
from vllm.assets.video import VideoAsset
|
||||
from vllm.lora.request import LoRARequest
|
||||
from vllm.multimodal.image import convert_image_mode
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
|
||||
@@ -2646,6 +2647,8 @@ def main(args):
|
||||
if args.tensor_parallel_size is not None:
|
||||
engine_args.tensor_parallel_size = args.tensor_parallel_size
|
||||
engine_args = maybe_add_vit_cuda_graph_compilation_config(args, engine_args)
|
||||
if current_platform.is_rocm():
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
llm = LLM.from_engine_args(engine_args)
|
||||
|
||||
# Don't want to check the flag multiple times, so just hijack `prompts`.
|
||||
|
||||
Generated
+1
@@ -5560,6 +5560,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"vllm-bench",
|
||||
"vllm-chat",
|
||||
"vllm-engine-core-client",
|
||||
"vllm-managed-engine",
|
||||
|
||||
@@ -132,6 +132,7 @@ trait-set = "0.3.0"
|
||||
url = "2.5.7"
|
||||
uuid = { version = "1.22.0", features = ["v4"] }
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
vllm-bench = { path = "src/bench" }
|
||||
vllm-chat = { path = "src/chat" }
|
||||
vllm-engine-core-client = { path = "src/engine-core-client" }
|
||||
vllm-llm = { path = "src/llm" }
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
/// Backend type for the benchmark endpoint.
|
||||
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BackendKind {
|
||||
@@ -77,7 +75,7 @@ pub enum DatasetName {
|
||||
ShareGpt,
|
||||
#[value(name = "sonnet")]
|
||||
Sonnet,
|
||||
#[value(name = "speed-bench")]
|
||||
#[value(name = "speed-bench", alias = "speed_bench")]
|
||||
SpeedBench,
|
||||
#[value(name = "hf")]
|
||||
Hf,
|
||||
@@ -144,13 +142,8 @@ impl fmt::Display for SpeedBenchConfig {
|
||||
}
|
||||
|
||||
/// High-performance benchmark client for vLLM serving endpoints.
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(
|
||||
name = "vllm-bench",
|
||||
about = "Benchmark online serving throughput",
|
||||
version
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[derive(clap::Args, Debug, Clone)]
|
||||
pub struct BenchServeArgs {
|
||||
/// The type of backend or endpoint to use for the benchmark.
|
||||
#[arg(long, default_value = "openai")]
|
||||
pub backend: BackendKind,
|
||||
@@ -659,7 +652,7 @@ pub struct Cli {
|
||||
pub lora_assignment: LoraAssignment,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
impl BenchServeArgs {
|
||||
/// Resolve the base URL from explicit --base-url or from --host/--port.
|
||||
pub fn resolve_base_url(&self) -> String {
|
||||
if let Some(ref base) = self.base_url {
|
||||
|
||||
+212
-188
@@ -4,7 +4,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::cli::{BackendKind, Cli, DatasetName, LoraAssignment, RampUpStrategy, SpeedBenchConfig};
|
||||
use crate::cli::{
|
||||
BackendKind, BenchServeArgs, DatasetName, LoraAssignment, RampUpStrategy, SpeedBenchConfig,
|
||||
};
|
||||
use crate::datasets::random_mm::{MmBucketKey, MmLimitPerPrompt};
|
||||
use crate::error::{BenchError, Result};
|
||||
|
||||
@@ -215,63 +217,63 @@ pub struct BenchConfig {
|
||||
}
|
||||
|
||||
impl BenchConfig {
|
||||
pub fn from_cli(cli: &Cli) -> Result<Self> {
|
||||
if cli.burstiness <= 0.0 {
|
||||
pub fn from_args(args: &BenchServeArgs) -> Result<Self> {
|
||||
if args.burstiness <= 0.0 {
|
||||
return Err(BenchError::Config("Burstiness must be positive".into()));
|
||||
}
|
||||
|
||||
if cli.num_prompts == 0 {
|
||||
if args.num_prompts == 0 {
|
||||
return Err(BenchError::Config(
|
||||
"--num-prompts must be at least 1".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if cli.request_rate <= 0.0 && !cli.request_rate.is_infinite() {
|
||||
if args.request_rate <= 0.0 && !args.request_rate.is_infinite() {
|
||||
return Err(BenchError::Config(
|
||||
"--request-rate must be positive (or inf)".into(),
|
||||
));
|
||||
}
|
||||
if cli.max_model_len == Some(0) {
|
||||
if args.max_model_len == Some(0) {
|
||||
return Err(BenchError::Config(
|
||||
"--max-model-len must be at least 1".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let base_url = cli.resolve_base_url();
|
||||
let api_url = cli.resolve_api_url();
|
||||
let base_url = args.resolve_base_url();
|
||||
let api_url = args.resolve_api_url();
|
||||
|
||||
let extra_headers = cli.parse_headers()?;
|
||||
let mut extra_body = cli.parse_extra_body()?;
|
||||
let extra_headers = args.parse_headers()?;
|
||||
let mut extra_body = args.parse_extra_body()?;
|
||||
|
||||
// Merge sampling parameters into extra_body (matches Python behavior).
|
||||
// Python collects non-None sampling params and merges them UNDER extra_body,
|
||||
// meaning extra_body keys take precedence over sampling params.
|
||||
{
|
||||
let mut sampling_params = serde_json::Map::new();
|
||||
if let Some(v) = cli.top_p {
|
||||
if let Some(v) = args.top_p {
|
||||
sampling_params.insert("top_p".into(), serde_json::json!(v));
|
||||
}
|
||||
if let Some(v) = cli.top_k {
|
||||
if let Some(v) = args.top_k {
|
||||
sampling_params.insert("top_k".into(), serde_json::json!(v));
|
||||
}
|
||||
if let Some(v) = cli.min_p {
|
||||
if let Some(v) = args.min_p {
|
||||
sampling_params.insert("min_p".into(), serde_json::json!(v));
|
||||
}
|
||||
if let Some(v) = cli.temperature {
|
||||
if let Some(v) = args.temperature {
|
||||
sampling_params.insert("temperature".into(), serde_json::json!(v));
|
||||
}
|
||||
if let Some(v) = cli.frequency_penalty {
|
||||
if let Some(v) = args.frequency_penalty {
|
||||
sampling_params.insert("frequency_penalty".into(), serde_json::json!(v));
|
||||
}
|
||||
if let Some(v) = cli.presence_penalty {
|
||||
if let Some(v) = args.presence_penalty {
|
||||
sampling_params.insert("presence_penalty".into(), serde_json::json!(v));
|
||||
}
|
||||
if let Some(v) = cli.repetition_penalty {
|
||||
if let Some(v) = args.repetition_penalty {
|
||||
sampling_params.insert("repetition_penalty".into(), serde_json::json!(v));
|
||||
}
|
||||
|
||||
if !sampling_params.is_empty() {
|
||||
if !cli.backend.is_openai_compatible() {
|
||||
if !args.backend.is_openai_compatible() {
|
||||
return Err(BenchError::Config(
|
||||
"Sampling parameters are only supported by openai-compatible backends."
|
||||
.into(),
|
||||
@@ -299,7 +301,7 @@ impl BenchConfig {
|
||||
}
|
||||
|
||||
// Parse metadata
|
||||
let metadata = match &cli.metadata {
|
||||
let metadata = match &args.metadata {
|
||||
None => None,
|
||||
Some(items) => {
|
||||
let mut pairs = Vec::new();
|
||||
@@ -314,24 +316,24 @@ impl BenchConfig {
|
||||
};
|
||||
|
||||
// Parse goodput SLOs
|
||||
let goodput = parse_goodput(&cli.goodput)?;
|
||||
let goodput = parse_goodput(&args.goodput)?;
|
||||
|
||||
// Parse ramp-up config
|
||||
let ramp_up = parse_ramp_up(cli)?;
|
||||
let ramp_up = parse_ramp_up(args)?;
|
||||
|
||||
// Default percentile metrics based on backend type
|
||||
let default_percentile_metrics = if cli.backend.is_pooling() {
|
||||
let default_percentile_metrics = if args.backend.is_pooling() {
|
||||
"e2el"
|
||||
} else {
|
||||
"ttft,tpot,itl,e2el"
|
||||
};
|
||||
let percentile_metrics_str =
|
||||
cli.percentile_metrics.as_deref().unwrap_or(default_percentile_metrics);
|
||||
args.percentile_metrics.as_deref().unwrap_or(default_percentile_metrics);
|
||||
let selected_percentile_metrics: Vec<String> =
|
||||
percentile_metrics_str.split(',').map(|s| s.trim().to_string()).collect();
|
||||
|
||||
let metric_percentiles = parse_percentiles(&cli.metric_percentiles, false)?;
|
||||
let sweep_summary_percentiles = cli
|
||||
let metric_percentiles = parse_percentiles(&args.metric_percentiles, false)?;
|
||||
let sweep_summary_percentiles = args
|
||||
.sweep_summary_percentiles
|
||||
.as_deref()
|
||||
.map(|raw| parse_percentiles(raw, true))
|
||||
@@ -344,38 +346,38 @@ impl BenchConfig {
|
||||
selected_percentiles.push(90.0);
|
||||
}
|
||||
|
||||
let tokenizer_id = if cli.skip_tokenizer_init {
|
||||
let tokenizer_id = if args.skip_tokenizer_init {
|
||||
None
|
||||
} else {
|
||||
Some(cli.tokenizer.clone().or_else(|| cli.model.clone()).unwrap_or_default())
|
||||
args.tokenizer.clone().or_else(|| args.model.clone())
|
||||
};
|
||||
|
||||
// Resolve input/output lengths
|
||||
let random_input_len = cli.resolved_random_input_len();
|
||||
let random_output_len = cli.resolved_random_output_len();
|
||||
let per_turn_input_len = cli.resolved_per_turn_input_len();
|
||||
let random_input_len = args.resolved_random_input_len();
|
||||
let random_output_len = args.resolved_random_output_len();
|
||||
let per_turn_input_len = args.resolved_per_turn_input_len();
|
||||
|
||||
// Normalized multi-turn turn counts (computed in validation block below, defaults
|
||||
// to num_turns if multi-turn mode is not active)
|
||||
let mut multi_turn_min_turns = cli.multi_turn_num_turns;
|
||||
let mut multi_turn_max_turns = cli.multi_turn_num_turns;
|
||||
let mut multi_turn_min_turns = args.multi_turn_num_turns;
|
||||
let mut multi_turn_max_turns = args.multi_turn_num_turns;
|
||||
|
||||
// For random datasets with openai-compatible backends, default to ignore_eos.
|
||||
// Exception: multi-turn mode, where ignore_eos causes unbounded context growth
|
||||
// across turns. Multi-turn uses min_tokens instead for output length control.
|
||||
// Pooling backends don't generate tokens, so ignore_eos is irrelevant.
|
||||
let ignore_eos = if cli.backend.is_pooling() {
|
||||
let ignore_eos = if args.backend.is_pooling() {
|
||||
false
|
||||
} else {
|
||||
cli.ignore_eos
|
||||
|| ((cli.dataset_name == DatasetName::Random
|
||||
|| cli.dataset_name == DatasetName::RandomMm)
|
||||
&& cli.backend.is_openai_compatible()
|
||||
&& !cli.multi_turn)
|
||||
args.ignore_eos
|
||||
|| ((args.dataset_name == DatasetName::Random
|
||||
|| args.dataset_name == DatasetName::RandomMm)
|
||||
&& args.backend.is_openai_compatible()
|
||||
&& !args.multi_turn)
|
||||
};
|
||||
|
||||
// Pooling backends don't support multi-turn
|
||||
if cli.backend.is_pooling() && cli.multi_turn {
|
||||
if args.backend.is_pooling() && args.multi_turn {
|
||||
return Err(BenchError::Config(
|
||||
"Pooling/embedding backends do not support --multi-turn".into(),
|
||||
));
|
||||
@@ -383,7 +385,7 @@ impl BenchConfig {
|
||||
|
||||
// LoRA validation. Adapter names must be non-empty after trim; pooling
|
||||
// backends are out of scope (vLLM LoRA routing is for generative paths).
|
||||
let lora_modules = match cli.lora_modules.as_ref() {
|
||||
let lora_modules = match args.lora_modules.as_ref() {
|
||||
None => None,
|
||||
Some(names) => {
|
||||
if names.is_empty() {
|
||||
@@ -391,7 +393,7 @@ impl BenchConfig {
|
||||
"--lora-modules requires at least one adapter name".into(),
|
||||
));
|
||||
}
|
||||
if cli.backend.is_pooling() {
|
||||
if args.backend.is_pooling() {
|
||||
return Err(BenchError::Config(
|
||||
"--lora-modules is not supported for pooling/embedding backends".into(),
|
||||
));
|
||||
@@ -411,18 +413,18 @@ impl BenchConfig {
|
||||
};
|
||||
|
||||
// Random-MM validation and config parsing
|
||||
let (random_mm_limit, random_mm_buckets) = if cli.dataset_name == DatasetName::RandomMm {
|
||||
if cli.backend != BackendKind::OpenaiChat {
|
||||
let (random_mm_limit, random_mm_buckets) = if args.dataset_name == DatasetName::RandomMm {
|
||||
if args.backend != BackendKind::OpenaiChat {
|
||||
return Err(BenchError::Config(
|
||||
"Multi-modal content (images) is only supported on 'openai-chat' backend."
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
let limit = crate::datasets::random_mm::parse_limit_mm_per_prompt(
|
||||
&cli.random_mm_limit_mm_per_prompt,
|
||||
&args.random_mm_limit_mm_per_prompt,
|
||||
)?;
|
||||
let buckets =
|
||||
crate::datasets::random_mm::parse_bucket_config(&cli.random_mm_bucket_config)?;
|
||||
crate::datasets::random_mm::parse_bucket_config(&args.random_mm_bucket_config)?;
|
||||
(limit, buckets)
|
||||
} else {
|
||||
(MmLimitPerPrompt::default(), Vec::new())
|
||||
@@ -432,18 +434,18 @@ impl BenchConfig {
|
||||
// sonnet (uses built-in Shakespeare's sonnets).
|
||||
|
||||
// Range ratio (Python semantics: [len*(1-r), len*(1+r)], each r in [0,1))
|
||||
let random_range_ratio = RangeRatio::parse(&cli.random_range_ratio)?;
|
||||
let random_range_ratio = RangeRatio::parse(&args.random_range_ratio)?;
|
||||
|
||||
// Batched inputs only make sense for pooling backends (the generation
|
||||
// backends send one prompt per request).
|
||||
if cli.random_batch_size == 0 {
|
||||
if args.random_batch_size == 0 {
|
||||
return Err(BenchError::Config(
|
||||
"--random-batch-size must be at least 1".into(),
|
||||
));
|
||||
}
|
||||
if cli.random_batch_size > 1
|
||||
&& !cli.backend.is_pooling()
|
||||
&& cli.dataset_name != DatasetName::RandomRerank
|
||||
if args.random_batch_size > 1
|
||||
&& !args.backend.is_pooling()
|
||||
&& args.dataset_name != DatasetName::RandomRerank
|
||||
{
|
||||
return Err(BenchError::Config(
|
||||
"--random-batch-size > 1 is only supported with embeddings/pooling backends".into(),
|
||||
@@ -451,16 +453,16 @@ impl BenchConfig {
|
||||
}
|
||||
|
||||
// random-rerank validation (mirrors Python RandomDatasetForReranking)
|
||||
let is_reranker = !cli.no_reranker;
|
||||
if cli.dataset_name == DatasetName::RandomRerank {
|
||||
if !cli.backend.is_pooling() {
|
||||
let is_reranker = !args.no_reranker;
|
||||
if args.dataset_name == DatasetName::RandomRerank {
|
||||
if !args.backend.is_pooling() {
|
||||
return Err(BenchError::Config(
|
||||
"--dataset-name random-rerank requires an embeddings/pooling backend \
|
||||
(e.g. --backend vllm-rerank)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
if !is_reranker && (cli.num_prompts < 2 || cli.random_batch_size < 2) {
|
||||
if !is_reranker && (args.num_prompts < 2 || args.random_batch_size < 2) {
|
||||
return Err(BenchError::Config(
|
||||
"--no-reranker requires --num-prompts > 1 and --random-batch-size > 1 \
|
||||
(the query is folded into the first batch slot)"
|
||||
@@ -470,8 +472,8 @@ impl BenchConfig {
|
||||
}
|
||||
|
||||
// Custom dataset validation
|
||||
if cli.dataset_name == DatasetName::Custom {
|
||||
match cli.dataset_path.as_deref() {
|
||||
if args.dataset_name == DatasetName::Custom {
|
||||
match args.dataset_path.as_deref() {
|
||||
None => {
|
||||
return Err(BenchError::Config(
|
||||
"--dataset-path is required for --dataset-name custom \
|
||||
@@ -486,7 +488,7 @@ impl BenchConfig {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if !cli.skip_chat_template {
|
||||
if !args.skip_chat_template {
|
||||
eprintln!(
|
||||
"NOTE: client-side chat template rendering is not supported; custom \
|
||||
dataset prompts are sent raw (equivalent to --skip-chat-template)."
|
||||
@@ -495,29 +497,29 @@ impl BenchConfig {
|
||||
}
|
||||
|
||||
// Prefix repetition validation
|
||||
if cli.dataset_name == DatasetName::PrefixRepetition {
|
||||
if cli.prefix_repetition_num_prefixes == 0 {
|
||||
if args.dataset_name == DatasetName::PrefixRepetition {
|
||||
if args.prefix_repetition_num_prefixes == 0 {
|
||||
return Err(BenchError::Config(
|
||||
"--prefix-repetition-num-prefixes must be at least 1".into(),
|
||||
));
|
||||
}
|
||||
if cli.num_prompts < cli.prefix_repetition_num_prefixes {
|
||||
if args.num_prompts < args.prefix_repetition_num_prefixes {
|
||||
return Err(BenchError::Config(format!(
|
||||
"--num-prompts ({}) must be >= --prefix-repetition-num-prefixes ({})",
|
||||
cli.num_prompts, cli.prefix_repetition_num_prefixes
|
||||
args.num_prompts, args.prefix_repetition_num_prefixes
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// HF dataset validation
|
||||
if cli.dataset_name == DatasetName::Hf && cli.dataset_path.is_none() {
|
||||
if args.dataset_name == DatasetName::Hf && args.dataset_path.is_none() {
|
||||
return Err(BenchError::Config(
|
||||
"--dataset-path is required for --dataset-name hf \
|
||||
(set to a HuggingFace dataset ID, e.g. 'allenai/WildChat-4.8M')"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
if let Some(len) = cli.hf_output_len
|
||||
if let Some(len) = args.hf_output_len
|
||||
&& len == 0
|
||||
{
|
||||
return Err(BenchError::Config(
|
||||
@@ -526,13 +528,13 @@ impl BenchConfig {
|
||||
}
|
||||
|
||||
// Multi-turn validation
|
||||
if cli.multi_turn {
|
||||
if cli.backend != BackendKind::OpenaiChat {
|
||||
if args.multi_turn {
|
||||
if args.backend != BackendKind::OpenaiChat {
|
||||
return Err(BenchError::Config(
|
||||
"--multi-turn requires --backend openai-chat".into(),
|
||||
));
|
||||
}
|
||||
if cli.multi_turn_num_turns == 0 {
|
||||
if args.multi_turn_num_turns == 0 {
|
||||
return Err(BenchError::Config(
|
||||
"--multi-turn-num-turns must be at least 1".into(),
|
||||
));
|
||||
@@ -541,18 +543,18 @@ impl BenchConfig {
|
||||
// Normalize and validate min/max turns. ShareGPT only consumes max_turns
|
||||
// (the loader walks all available turns up to the cap), so the
|
||||
// min/num/max coupling used for synthetic generation does not apply.
|
||||
if cli.dataset_name == DatasetName::ShareGpt {
|
||||
if cli.multi_turn_max_turns == 1 {
|
||||
if args.dataset_name == DatasetName::ShareGpt {
|
||||
if args.multi_turn_max_turns == 1 {
|
||||
return Err(BenchError::Config(
|
||||
"--multi-turn-max-turns must be at least 2 for ShareGPT multi-turn".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
(multi_turn_min_turns, multi_turn_max_turns) =
|
||||
match (cli.multi_turn_min_turns, cli.multi_turn_max_turns) {
|
||||
(0, 0) => (cli.multi_turn_num_turns, cli.multi_turn_num_turns),
|
||||
(m, 0) => (m, cli.multi_turn_num_turns),
|
||||
(0, x) => (cli.multi_turn_num_turns, x),
|
||||
match (args.multi_turn_min_turns, args.multi_turn_max_turns) {
|
||||
(0, 0) => (args.multi_turn_num_turns, args.multi_turn_num_turns),
|
||||
(m, 0) => (m, args.multi_turn_num_turns),
|
||||
(0, x) => (args.multi_turn_num_turns, x),
|
||||
(m, x) => (m, x),
|
||||
};
|
||||
if multi_turn_min_turns < 1 {
|
||||
@@ -575,8 +577,8 @@ impl BenchConfig {
|
||||
}
|
||||
|
||||
// Validate prefix sharing ratios
|
||||
let pg = cli.multi_turn_prefix_global_ratio;
|
||||
let pc = cli.multi_turn_prefix_conversation_ratio;
|
||||
let pg = args.multi_turn_prefix_global_ratio;
|
||||
let pc = args.multi_turn_prefix_conversation_ratio;
|
||||
if !(0.0..=1.0).contains(&pg) {
|
||||
return Err(BenchError::Config(
|
||||
"--multi-turn-prefix-global-ratio must be in [0.0, 1.0]".into(),
|
||||
@@ -592,20 +594,20 @@ impl BenchConfig {
|
||||
"--multi-turn-prefix-global-ratio + --multi-turn-prefix-conversation-ratio must be < 1.0 (unique suffix required)".into(),
|
||||
));
|
||||
}
|
||||
if (pg > 0.0 || pc > 0.0) && cli.dataset_name != DatasetName::Random {
|
||||
if (pg > 0.0 || pc > 0.0) && args.dataset_name != DatasetName::Random {
|
||||
return Err(BenchError::Config(
|
||||
"Prefix sharing (--multi-turn-prefix-global-ratio / --multi-turn-prefix-conversation-ratio) only works with --dataset-name random".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !(cli.steady_state_threshold > 0.0 && cli.steady_state_threshold <= 1.0) {
|
||||
if !(args.steady_state_threshold > 0.0 && args.steady_state_threshold <= 1.0) {
|
||||
return Err(BenchError::Config(format!(
|
||||
"--steady-state-threshold must be in (0.0, 1.0], got {}",
|
||||
cli.steady_state_threshold
|
||||
args.steady_state_threshold
|
||||
)));
|
||||
}
|
||||
if let Some(mw) = cli.steady_state_min_window
|
||||
if let Some(mw) = args.steady_state_min_window
|
||||
&& mw < 0.0
|
||||
{
|
||||
return Err(BenchError::Config(format!(
|
||||
@@ -613,122 +615,122 @@ impl BenchConfig {
|
||||
)));
|
||||
}
|
||||
|
||||
if cli.profile_batch_threshold.is_some() && !cli.profile {
|
||||
if args.profile_batch_threshold.is_some() && !args.profile {
|
||||
return Err(BenchError::Config(
|
||||
"--profile-batch-threshold requires --profile".into(),
|
||||
));
|
||||
}
|
||||
if cli.profile_duration <= 0.0 {
|
||||
if args.profile_duration <= 0.0 {
|
||||
return Err(BenchError::Config(
|
||||
"--profile-duration must be positive".into(),
|
||||
));
|
||||
}
|
||||
if cli.profile_batch_threshold.is_none() && cli.profile_duration != 5.0 {
|
||||
if args.profile_batch_threshold.is_none() && args.profile_duration != 5.0 {
|
||||
return Err(BenchError::Config(
|
||||
"--profile-duration requires --profile-batch-threshold".into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(BenchConfig {
|
||||
backend: cli.backend,
|
||||
backend: args.backend,
|
||||
base_url,
|
||||
api_url,
|
||||
model: cli.model.clone(),
|
||||
model_name: cli.served_model_name.clone(),
|
||||
model: args.model.clone(),
|
||||
model_name: args.served_model_name.clone(),
|
||||
tokenizer_id,
|
||||
tokenizer_mode: cli.tokenizer_mode.clone(),
|
||||
trust_remote_code: cli.trust_remote_code,
|
||||
skip_tokenizer_init: cli.skip_tokenizer_init,
|
||||
dataset_name: cli.dataset_name,
|
||||
dataset_path: cli.dataset_path.clone(),
|
||||
max_model_len: cli.max_model_len,
|
||||
tokenizer_mode: args.tokenizer_mode.clone(),
|
||||
trust_remote_code: args.trust_remote_code,
|
||||
skip_tokenizer_init: args.skip_tokenizer_init,
|
||||
dataset_name: args.dataset_name,
|
||||
dataset_path: args.dataset_path.clone(),
|
||||
max_model_len: args.max_model_len,
|
||||
random_input_len,
|
||||
random_output_len,
|
||||
random_prefix_len: cli.random_prefix_len,
|
||||
random_prefix_len: args.random_prefix_len,
|
||||
random_range_ratio,
|
||||
random_batch_size: cli.random_batch_size,
|
||||
random_batch_size: args.random_batch_size,
|
||||
is_reranker,
|
||||
custom_output_len: cli.output_len.map(|v| v as i64).unwrap_or(cli.custom_output_len),
|
||||
prefix_repetition_prefix_len: cli.prefix_repetition_prefix_len,
|
||||
prefix_repetition_suffix_len: cli.prefix_repetition_suffix_len,
|
||||
prefix_repetition_num_prefixes: cli.prefix_repetition_num_prefixes,
|
||||
prefix_repetition_output_len: cli
|
||||
custom_output_len: args.output_len.map(|v| v as i64).unwrap_or(args.custom_output_len),
|
||||
prefix_repetition_prefix_len: args.prefix_repetition_prefix_len,
|
||||
prefix_repetition_suffix_len: args.prefix_repetition_suffix_len,
|
||||
prefix_repetition_num_prefixes: args.prefix_repetition_num_prefixes,
|
||||
prefix_repetition_output_len: args
|
||||
.output_len
|
||||
.unwrap_or(cli.prefix_repetition_output_len),
|
||||
random_cache_hit_fraction: cli.random_cache_hit_fraction,
|
||||
random_cache_ratio: cli.random_cache_ratio,
|
||||
sharegpt_output_len: cli.sharegpt_output_len,
|
||||
sonnet_input_len: cli.sonnet_input_len,
|
||||
sonnet_output_len: cli.sonnet_output_len,
|
||||
sonnet_prefix_len: cli.sonnet_prefix_len,
|
||||
no_oversample: cli.no_oversample,
|
||||
disable_shuffle: cli.disable_shuffle,
|
||||
num_prompts: cli.num_prompts,
|
||||
request_rate: cli.request_rate,
|
||||
burstiness: cli.burstiness,
|
||||
max_concurrency: cli.max_concurrency,
|
||||
steady_state_threshold: cli.steady_state_threshold,
|
||||
steady_state_min_window: cli.steady_state_min_window,
|
||||
no_steady_state: cli.no_steady_state,
|
||||
disable_tqdm: cli.disable_tqdm,
|
||||
num_warmups: cli.num_warmups,
|
||||
profile: cli.profile,
|
||||
profile_batch_threshold: cli.profile_batch_threshold,
|
||||
profile_duration: cli.profile_duration,
|
||||
save_result: cli.save_result,
|
||||
save_detailed: cli.save_detailed,
|
||||
append_result: cli.append_result,
|
||||
result_dir: cli.result_dir.clone(),
|
||||
result_filename: cli.result_filename.clone(),
|
||||
seed: cli.seed,
|
||||
.unwrap_or(args.prefix_repetition_output_len),
|
||||
random_cache_hit_fraction: args.random_cache_hit_fraction,
|
||||
random_cache_ratio: args.random_cache_ratio,
|
||||
sharegpt_output_len: args.sharegpt_output_len,
|
||||
sonnet_input_len: args.sonnet_input_len,
|
||||
sonnet_output_len: args.sonnet_output_len,
|
||||
sonnet_prefix_len: args.sonnet_prefix_len,
|
||||
no_oversample: args.no_oversample,
|
||||
disable_shuffle: args.disable_shuffle,
|
||||
num_prompts: args.num_prompts,
|
||||
request_rate: args.request_rate,
|
||||
burstiness: args.burstiness,
|
||||
max_concurrency: args.max_concurrency,
|
||||
steady_state_threshold: args.steady_state_threshold,
|
||||
steady_state_min_window: args.steady_state_min_window,
|
||||
no_steady_state: args.no_steady_state,
|
||||
disable_tqdm: args.disable_tqdm,
|
||||
num_warmups: args.num_warmups,
|
||||
profile: args.profile,
|
||||
profile_batch_threshold: args.profile_batch_threshold,
|
||||
profile_duration: args.profile_duration,
|
||||
save_result: args.save_result,
|
||||
save_detailed: args.save_detailed,
|
||||
append_result: args.append_result,
|
||||
result_dir: args.result_dir.clone(),
|
||||
result_filename: args.result_filename.clone(),
|
||||
seed: args.seed,
|
||||
ignore_eos,
|
||||
insecure: cli.insecure,
|
||||
insecure: args.insecure,
|
||||
selected_percentile_metrics,
|
||||
selected_percentiles,
|
||||
sweep_summary_percentiles,
|
||||
label: cli.label.clone(),
|
||||
logprobs: cli.logprobs,
|
||||
request_id_prefix: cli.get_request_id_prefix(),
|
||||
ready_check_timeout_sec: cli.ready_check_timeout_sec,
|
||||
label: args.label.clone(),
|
||||
logprobs: args.logprobs,
|
||||
request_id_prefix: args.get_request_id_prefix(),
|
||||
ready_check_timeout_sec: args.ready_check_timeout_sec,
|
||||
extra_headers,
|
||||
extra_body,
|
||||
metadata,
|
||||
dry_run: cli.dry_run,
|
||||
dry_run: args.dry_run,
|
||||
goodput,
|
||||
ramp_up,
|
||||
multi_turn: cli.multi_turn,
|
||||
multi_turn_num_turns: cli.multi_turn_num_turns,
|
||||
multi_turn: args.multi_turn,
|
||||
multi_turn_num_turns: args.multi_turn_num_turns,
|
||||
multi_turn_min_turns,
|
||||
multi_turn_max_turns,
|
||||
sharegpt_multi_turn_max_turns: if cli.multi_turn
|
||||
&& cli.dataset_name == DatasetName::ShareGpt
|
||||
&& cli.multi_turn_max_turns != 0
|
||||
sharegpt_multi_turn_max_turns: if args.multi_turn
|
||||
&& args.dataset_name == DatasetName::ShareGpt
|
||||
&& args.multi_turn_max_turns != 0
|
||||
{
|
||||
Some(cli.multi_turn_max_turns)
|
||||
Some(args.multi_turn_max_turns)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
per_turn_input_len,
|
||||
multi_turn_concurrency: cli.multi_turn_concurrency,
|
||||
multi_turn_delay_ms: cli.multi_turn_delay_ms,
|
||||
multi_turn_prefix_global_ratio: cli.multi_turn_prefix_global_ratio,
|
||||
multi_turn_prefix_conversation_ratio: cli.multi_turn_prefix_conversation_ratio,
|
||||
speed_bench_config: cli.speed_bench_config,
|
||||
speed_bench_category: cli.speed_bench_category.clone(),
|
||||
speed_bench_max_input_len: cli.speed_bench_max_input_len,
|
||||
hf_split: cli.hf_split.clone(),
|
||||
hf_subset: cli.hf_subset.clone(),
|
||||
hf_output_len: cli.hf_output_len,
|
||||
hf_text_column: cli.hf_text_column.clone(),
|
||||
reset_prefix_cache: cli.reset_prefix_cache,
|
||||
prompt_token_ids: cli.prompt_token_ids,
|
||||
random_mm_base_items_per_request: cli.random_mm_base_items_per_request,
|
||||
random_mm_num_mm_items_range_ratio: cli.random_mm_num_mm_items_range_ratio,
|
||||
multi_turn_concurrency: args.multi_turn_concurrency,
|
||||
multi_turn_delay_ms: args.multi_turn_delay_ms,
|
||||
multi_turn_prefix_global_ratio: args.multi_turn_prefix_global_ratio,
|
||||
multi_turn_prefix_conversation_ratio: args.multi_turn_prefix_conversation_ratio,
|
||||
speed_bench_config: args.speed_bench_config,
|
||||
speed_bench_category: args.speed_bench_category.clone(),
|
||||
speed_bench_max_input_len: args.speed_bench_max_input_len,
|
||||
hf_split: args.hf_split.clone(),
|
||||
hf_subset: args.hf_subset.clone(),
|
||||
hf_output_len: args.hf_output_len,
|
||||
hf_text_column: args.hf_text_column.clone(),
|
||||
reset_prefix_cache: args.reset_prefix_cache,
|
||||
prompt_token_ids: args.prompt_token_ids,
|
||||
random_mm_base_items_per_request: args.random_mm_base_items_per_request,
|
||||
random_mm_num_mm_items_range_ratio: args.random_mm_num_mm_items_range_ratio,
|
||||
random_mm_limit,
|
||||
random_mm_buckets,
|
||||
enable_multimodal_chat: cli.enable_multimodal_chat,
|
||||
enable_multimodal_chat: args.enable_multimodal_chat,
|
||||
lora_modules,
|
||||
lora_assignment: cli.lora_assignment,
|
||||
lora_assignment: args.lora_assignment,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -811,17 +813,17 @@ fn parse_goodput(goodput_args: &Option<Vec<String>>) -> Result<GoodputConfig> {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn parse_ramp_up(cli: &Cli) -> Result<Option<RampUpConfig>> {
|
||||
let strategy = match cli.ramp_up_strategy {
|
||||
fn parse_ramp_up(args: &BenchServeArgs) -> Result<Option<RampUpConfig>> {
|
||||
let strategy = match args.ramp_up_strategy {
|
||||
None => return Ok(None),
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
let start_rps = cli.ramp_up_start_rps.ok_or_else(|| {
|
||||
let start_rps = args.ramp_up_start_rps.ok_or_else(|| {
|
||||
BenchError::Config("--ramp-up-start-rps is required when --ramp-up-strategy is set".into())
|
||||
})?;
|
||||
|
||||
let end_rps = cli.ramp_up_end_rps.ok_or_else(|| {
|
||||
let end_rps = args.ramp_up_end_rps.ok_or_else(|| {
|
||||
BenchError::Config("--ramp-up-end-rps is required when --ramp-up-strategy is set".into())
|
||||
})?;
|
||||
|
||||
@@ -843,7 +845,21 @@ mod tests {
|
||||
use clap::Parser;
|
||||
|
||||
use super::*;
|
||||
use crate::cli::Cli;
|
||||
use crate::cli::BenchServeArgs;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct TestCli {
|
||||
#[command(flatten)]
|
||||
args: BenchServeArgs,
|
||||
}
|
||||
|
||||
fn parse_args<I, T>(args: I) -> BenchServeArgs
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<std::ffi::OsString> + Clone,
|
||||
{
|
||||
TestCli::parse_from(args).args
|
||||
}
|
||||
|
||||
fn base_multi_turn_args() -> Vec<&'static str> {
|
||||
vec![
|
||||
@@ -859,8 +875,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_prefix_sharing_defaults_to_zero() {
|
||||
let args = base_multi_turn_args();
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
assert_eq!(config.multi_turn_prefix_global_ratio, 0.0);
|
||||
assert_eq!(config.multi_turn_prefix_conversation_ratio, 0.0);
|
||||
}
|
||||
@@ -874,8 +890,8 @@ mod tests {
|
||||
"--multi-turn-prefix-conversation-ratio",
|
||||
"0.8",
|
||||
]);
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
assert!((config.multi_turn_prefix_global_ratio - 0.1).abs() < 1e-10);
|
||||
assert!((config.multi_turn_prefix_conversation_ratio - 0.8).abs() < 1e-10);
|
||||
}
|
||||
@@ -889,8 +905,8 @@ mod tests {
|
||||
"--multi-turn-prefix-conversation-ratio",
|
||||
"0.6",
|
||||
]);
|
||||
let cli = Cli::parse_from(args);
|
||||
assert!(BenchConfig::from_cli(&cli).is_err());
|
||||
let args = parse_args(args);
|
||||
assert!(BenchConfig::from_args(&args).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -902,16 +918,16 @@ mod tests {
|
||||
"--multi-turn-prefix-conversation-ratio",
|
||||
"0.5",
|
||||
]);
|
||||
let cli = Cli::parse_from(args);
|
||||
assert!(BenchConfig::from_cli(&cli).is_err());
|
||||
let args = parse_args(args);
|
||||
assert!(BenchConfig::from_args(&args).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix_sharing_out_of_range_fails() {
|
||||
let mut args = base_multi_turn_args();
|
||||
args.extend(["--multi-turn-prefix-global-ratio", "1.5"]);
|
||||
let cli = Cli::parse_from(args);
|
||||
assert!(BenchConfig::from_cli(&cli).is_err());
|
||||
let args = parse_args(args);
|
||||
assert!(BenchConfig::from_args(&args).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -928,8 +944,8 @@ mod tests {
|
||||
"--multi-turn-prefix-global-ratio",
|
||||
"0.1",
|
||||
];
|
||||
let cli = Cli::parse_from(args);
|
||||
assert!(BenchConfig::from_cli(&cli).is_err());
|
||||
let args = parse_args(args);
|
||||
assert!(BenchConfig::from_args(&args).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -944,8 +960,8 @@ mod tests {
|
||||
"--dataset-name",
|
||||
"sharegpt",
|
||||
];
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
|
||||
assert_eq!(config.multi_turn_max_turns, 3);
|
||||
assert_eq!(config.sharegpt_multi_turn_max_turns, None);
|
||||
@@ -968,8 +984,8 @@ mod tests {
|
||||
"--multi-turn-max-turns",
|
||||
"2",
|
||||
];
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
assert_eq!(config.sharegpt_multi_turn_max_turns, Some(2));
|
||||
}
|
||||
|
||||
@@ -987,8 +1003,8 @@ mod tests {
|
||||
"--multi-turn-max-turns",
|
||||
"1",
|
||||
];
|
||||
let cli = Cli::parse_from(args);
|
||||
let err = BenchConfig::from_cli(&cli).unwrap_err().to_string();
|
||||
let args = parse_args(args);
|
||||
let err = BenchConfig::from_args(&args).unwrap_err().to_string();
|
||||
assert!(
|
||||
err.contains("at least 2 for ShareGPT"),
|
||||
"expected ShareGPT-specific error, got: {err}"
|
||||
@@ -1009,8 +1025,8 @@ mod tests {
|
||||
"--multi-turn-max-turns",
|
||||
"20",
|
||||
];
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
|
||||
assert_eq!(config.sharegpt_multi_turn_max_turns, Some(20));
|
||||
}
|
||||
@@ -1018,8 +1034,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_sweep_summary_percentiles_default_empty() {
|
||||
let args = base_multi_turn_args();
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
|
||||
assert!(config.sweep_summary_percentiles.is_empty());
|
||||
assert_eq!(config.selected_percentiles, vec![99.0, 90.0]);
|
||||
@@ -1034,8 +1050,8 @@ mod tests {
|
||||
"--sweep-summary-percentiles",
|
||||
"90,95,90",
|
||||
]);
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
|
||||
assert_eq!(config.sweep_summary_percentiles, vec![90.0, 95.0]);
|
||||
assert_eq!(config.selected_percentiles, vec![99.0, 95.0, 90.0]);
|
||||
@@ -1045,8 +1061,8 @@ mod tests {
|
||||
fn test_invalid_sweep_summary_percentile_fails() {
|
||||
let mut args = base_multi_turn_args();
|
||||
args.extend(["--sweep-summary-percentiles", "101"]);
|
||||
let cli = Cli::parse_from(args);
|
||||
assert!(BenchConfig::from_cli(&cli).is_err());
|
||||
let args = parse_args(args);
|
||||
assert!(BenchConfig::from_args(&args).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1058,12 +1074,20 @@ mod tests {
|
||||
"--max-model-len",
|
||||
"4096",
|
||||
];
|
||||
let cli = Cli::parse_from(args);
|
||||
let config = BenchConfig::from_cli(&cli).unwrap();
|
||||
let args = parse_args(args);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
|
||||
assert_eq!(config.max_model_len, Some(4096));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenizer_id_deferred_when_model_is_unspecified() {
|
||||
let args = parse_args(["vllm-bench"]);
|
||||
let config = BenchConfig::from_args(&args).unwrap();
|
||||
|
||||
assert_eq!(config.tokenizer_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_max_model_len_fails() {
|
||||
let args = vec![
|
||||
@@ -1073,9 +1097,9 @@ mod tests {
|
||||
"--max-model-len",
|
||||
"0",
|
||||
];
|
||||
let cli = Cli::parse_from(args);
|
||||
let args = parse_args(args);
|
||||
|
||||
assert!(BenchConfig::from_cli(&cli).is_err());
|
||||
assert!(BenchConfig::from_args(&args).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn test_range_ratio_parse_float() {
|
||||
|
||||
@@ -40,8 +40,11 @@ impl HubRepo {
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build download runtime: {e}"))?;
|
||||
rt.block_on(async move {
|
||||
let api = hf_hub::api::tokio::Api::new()
|
||||
.map_err(|e| format!("Failed to init HF API: {e}"))?;
|
||||
let mut builder = hf_hub::api::tokio::ApiBuilder::from_env();
|
||||
if let Ok(token) = std::env::var("HF_TOKEN") {
|
||||
builder = builder.with_token(Some(token));
|
||||
}
|
||||
let api = builder.build().map_err(|e| format!("Failed to init HF API: {e}"))?;
|
||||
api.repo(repo).get(&filename).await.map_err(|e| format!("{e}"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
mod backends;
|
||||
mod benchmark;
|
||||
mod cli;
|
||||
mod compare;
|
||||
mod config;
|
||||
mod datasets;
|
||||
mod error;
|
||||
mod hub;
|
||||
mod metrics;
|
||||
mod multi_run;
|
||||
mod multi_turn;
|
||||
mod output;
|
||||
mod rate_control;
|
||||
mod ready_checker;
|
||||
mod sweep;
|
||||
mod tiktoken;
|
||||
mod tokenizer;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
pub use cli::{
|
||||
BackendKind, BenchServeArgs, DatasetName, LoraAssignment, RampUpStrategy, SpeedBenchConfig,
|
||||
};
|
||||
use config::BenchConfig;
|
||||
|
||||
/// Prepare process-wide resources for a benchmark run.
|
||||
pub fn prepare_process() {
|
||||
// Raise the open-file soft limit to the hard limit. High-concurrency
|
||||
// benchmarks (1024+ requests) easily exceed the default 1024 fd soft limit.
|
||||
if let Ok(new) = rlimit::increase_nofile_limit(u64::MAX)
|
||||
&& new > 1024
|
||||
{
|
||||
eprintln!("Open-file limit: {new}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the online serving benchmark.
|
||||
pub async fn run(args: BenchServeArgs) -> anyhow::Result<()> {
|
||||
// --- Compare mode: no server needed, just diff two JSON files ---
|
||||
if let Some(ref files) = args.compare {
|
||||
return compare::compare_results(&files[0], &files[1]).context("Comparison failed");
|
||||
}
|
||||
|
||||
let config = BenchConfig::from_args(&args).context("Configuration error")?;
|
||||
|
||||
async {
|
||||
if config.multi_turn {
|
||||
if let Some(ref sweep_mc) = args.sweep_max_concurrency {
|
||||
// --- Sweep over concurrency in multi-turn mode ---
|
||||
let values = sweep::parse_concurrency_values(sweep_mc)
|
||||
.context("Invalid --sweep-max-concurrency")?;
|
||||
sweep::run_multi_turn_concurrency_sweep(
|
||||
&config,
|
||||
&values,
|
||||
args.sweep_num_prompts_factor,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// --- Single multi-turn conversation benchmark ---
|
||||
multi_turn::run_multi_turn_benchmark(&config).await?;
|
||||
}
|
||||
} else if let Some(ref sweep_mc) = args.sweep_max_concurrency {
|
||||
// --- Sweep over max-concurrency ---
|
||||
let values = sweep::parse_concurrency_values(sweep_mc)
|
||||
.context("Invalid --sweep-max-concurrency")?;
|
||||
sweep::run_concurrency_sweep(&config, &values, args.sweep_num_prompts_factor).await?;
|
||||
} else if let Some(ref sweep_rate) = args.sweep_request_rate {
|
||||
// --- Sweep over request-rate ---
|
||||
let values =
|
||||
sweep::parse_rate_values(sweep_rate).context("Invalid --sweep-request-rate")?;
|
||||
sweep::run_rate_sweep(&config, &values).await?;
|
||||
} else if args.num_runs > 1 {
|
||||
// --- Multi-run with statistical aggregation ---
|
||||
multi_run::run_multi(&config, args.num_runs).await?;
|
||||
} else {
|
||||
// --- Normal single benchmark ---
|
||||
benchmark::run_benchmark(&config).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await
|
||||
.context("Benchmark failed")
|
||||
}
|
||||
+14
-74
@@ -1,92 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
mod backends;
|
||||
mod benchmark;
|
||||
mod cli;
|
||||
mod compare;
|
||||
mod config;
|
||||
mod datasets;
|
||||
mod error;
|
||||
mod hub;
|
||||
mod metrics;
|
||||
mod multi_run;
|
||||
mod multi_turn;
|
||||
mod output;
|
||||
mod rate_control;
|
||||
mod ready_checker;
|
||||
mod sweep;
|
||||
mod tiktoken;
|
||||
mod tokenizer;
|
||||
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use cli::Cli;
|
||||
use config::BenchConfig;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "vllm-bench",
|
||||
about = "Benchmark online serving throughput",
|
||||
version
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(flatten)]
|
||||
args: vllm_bench::BenchServeArgs,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
// Raise the open-file soft limit to the hard limit. High-concurrency
|
||||
// benchmarks (1024+ requests) easily exceed the default 1024 fd soft limit.
|
||||
if let Ok(new) = rlimit::increase_nofile_limit(u64::MAX)
|
||||
&& new > 1024
|
||||
{
|
||||
eprintln!("Open-file limit: {new}");
|
||||
}
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
// --- Compare mode: no server needed, just diff two JSON files ---
|
||||
if let Some(ref files) = cli.compare {
|
||||
return compare::compare_results(&files[0], &files[1]).context("Comparison failed");
|
||||
}
|
||||
|
||||
let config = BenchConfig::from_cli(&cli).context("Configuration error")?;
|
||||
vllm_bench::prepare_process();
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to build tokio runtime");
|
||||
.context("Failed to build tokio runtime")?;
|
||||
|
||||
runtime
|
||||
.block_on(async {
|
||||
if config.multi_turn {
|
||||
if let Some(ref sweep_mc) = cli.sweep_max_concurrency {
|
||||
// --- Sweep over concurrency in multi-turn mode ---
|
||||
let values = sweep::parse_concurrency_values(sweep_mc)
|
||||
.context("Invalid --sweep-max-concurrency")?;
|
||||
sweep::run_multi_turn_concurrency_sweep(
|
||||
&config,
|
||||
&values,
|
||||
cli.sweep_num_prompts_factor,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// --- Single multi-turn conversation benchmark ---
|
||||
multi_turn::run_multi_turn_benchmark(&config).await?;
|
||||
}
|
||||
} else if let Some(ref sweep_mc) = cli.sweep_max_concurrency {
|
||||
// --- Sweep over max-concurrency ---
|
||||
let values = sweep::parse_concurrency_values(sweep_mc)
|
||||
.context("Invalid --sweep-max-concurrency")?;
|
||||
sweep::run_concurrency_sweep(&config, &values, cli.sweep_num_prompts_factor)
|
||||
.await?;
|
||||
} else if let Some(ref sweep_rate) = cli.sweep_request_rate {
|
||||
// --- Sweep over request-rate ---
|
||||
let values =
|
||||
sweep::parse_rate_values(sweep_rate).context("Invalid --sweep-request-rate")?;
|
||||
sweep::run_rate_sweep(&config, &values).await?;
|
||||
} else if cli.num_runs > 1 {
|
||||
// --- Multi-run with statistical aggregation ---
|
||||
multi_run::run_multi(&config, cli.num_runs).await?;
|
||||
} else {
|
||||
// --- Normal single benchmark ---
|
||||
benchmark::run_benchmark(&config).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.context("Benchmark failed")
|
||||
runtime.block_on(vllm_bench::run(cli.args))
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub(super) fn build_batched_items(
|
||||
let keep_on_cpu = spec.keep_on_cpu_keys.contains(key);
|
||||
let (value, field) = match spec.field_layout_for(key) {
|
||||
Some(FieldLayout::Batched) => (
|
||||
tensor.batched_value_at(index)?,
|
||||
tensor.batched_wire_value_at(index)?,
|
||||
MmField::Batched(MmBatchedField { keep_on_cpu }),
|
||||
),
|
||||
Some(FieldLayout::Flat { sizes_key }) => {
|
||||
@@ -47,7 +47,7 @@ pub(super) fn build_batched_items(
|
||||
})?;
|
||||
let (start, end) = tensor::flat_range_for_index(sizes, sizes_key, index)?;
|
||||
(
|
||||
tensor.flat_value_range(start, end)?,
|
||||
tensor.flat_wire_value_range(start, end)?,
|
||||
MmField::Flat(MmFlatField {
|
||||
slices: vec![MmSlice::Slice(SliceSpec {
|
||||
start: Some(0),
|
||||
@@ -60,7 +60,7 @@ pub(super) fn build_batched_items(
|
||||
)
|
||||
}
|
||||
None => (
|
||||
tensor.clone(),
|
||||
tensor.try_into()?,
|
||||
MmField::Shared(MmSharedField {
|
||||
batch_size: len,
|
||||
keep_on_cpu,
|
||||
@@ -71,7 +71,7 @@ pub(super) fn build_batched_items(
|
||||
data.insert(
|
||||
key.clone(),
|
||||
MmFieldElem {
|
||||
data: Some(value.try_into()?),
|
||||
data: Some(value),
|
||||
field,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ use vllm_engine_core_client::protocol::tensor::{ShapeExt as _, WireTensor};
|
||||
use crate::error::{Error, Result, bail_multimodal, multimodal};
|
||||
|
||||
/// Representation for multimodal kwarg values for transformation.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub(super) enum KwargValue {
|
||||
/// Float tensor with row-major flat data and shape.
|
||||
F32Tensor { data: Vec<f32>, shape: Vec<usize> },
|
||||
@@ -107,28 +107,19 @@ impl KwargValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<KwargValue> for ProtocolKwargValue {
|
||||
impl TryFrom<&KwargValue> for ProtocolKwargValue {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: KwargValue) -> Result<Self> {
|
||||
match value {
|
||||
KwargValue::F32Tensor { data, shape } => Ok(Self::Tensor(
|
||||
WireTensor::from_f32(shape, data).map_err(Error::Multimodal)?,
|
||||
)),
|
||||
KwargValue::F16Tensor { data, shape } => Ok(Self::Tensor(
|
||||
WireTensor::from_f16(shape, data).map_err(Error::Multimodal)?,
|
||||
)),
|
||||
KwargValue::Bf16Tensor { data, shape } => Ok(Self::Tensor(
|
||||
WireTensor::from_bf16(shape, data).map_err(Error::Multimodal)?,
|
||||
)),
|
||||
KwargValue::I64Tensor { data, shape } => Ok(Self::Tensor(
|
||||
WireTensor::from_i64(shape, data).map_err(Error::Multimodal)?,
|
||||
)),
|
||||
KwargValue::U32Tensor { data, shape } => Ok(Self::Tensor(
|
||||
WireTensor::from_u32(shape, data).map_err(Error::Multimodal)?,
|
||||
)),
|
||||
KwargValue::Passthrough(value) => Ok(value),
|
||||
}
|
||||
fn try_from(value: &KwargValue) -> Result<Self> {
|
||||
let tensor = match value {
|
||||
KwargValue::F32Tensor { data, shape } => WireTensor::from_f32(shape.clone(), data),
|
||||
KwargValue::F16Tensor { data, shape } => WireTensor::from_f16(shape.clone(), data),
|
||||
KwargValue::Bf16Tensor { data, shape } => WireTensor::from_bf16(shape.clone(), data),
|
||||
KwargValue::I64Tensor { data, shape } => WireTensor::from_i64(shape.clone(), data),
|
||||
KwargValue::U32Tensor { data, shape } => WireTensor::from_u32(shape.clone(), data),
|
||||
KwargValue::Passthrough(value) => return Ok(value.clone()),
|
||||
};
|
||||
tensor.map(ProtocolKwargValue::Tensor).map_err(Error::Multimodal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,63 +136,55 @@ impl KwargValue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract one media item from a batched tensor field.
|
||||
/// Convert one media item from a batched tensor field to wire bytes.
|
||||
///
|
||||
/// Batched fields use their first axis as media-item index and drop that
|
||||
/// axis in the per-feature value, matching vLLM's batched-field semantics.
|
||||
pub(super) fn batched_value_at(&self, index: usize) -> Result<Self> {
|
||||
match self {
|
||||
Self::F32Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, index, index + 1, true)?;
|
||||
Ok(Self::F32Tensor { data, shape })
|
||||
}
|
||||
Self::F16Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, index, index + 1, true)?;
|
||||
Ok(Self::F16Tensor { data, shape })
|
||||
}
|
||||
Self::Bf16Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, index, index + 1, true)?;
|
||||
Ok(Self::Bf16Tensor { data, shape })
|
||||
}
|
||||
Self::I64Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, index, index + 1, true)?;
|
||||
Ok(Self::I64Tensor { data, shape })
|
||||
}
|
||||
Self::U32Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, index, index + 1, true)?;
|
||||
Ok(Self::U32Tensor { data, shape })
|
||||
}
|
||||
Self::Passthrough(value) => Ok(Self::Passthrough(value.clone())),
|
||||
}
|
||||
pub(super) fn batched_wire_value_at(&self, index: usize) -> Result<ProtocolKwargValue> {
|
||||
self.wire_value_range(index, index + 1, true)
|
||||
}
|
||||
|
||||
/// Extract one media item's variable-length range from a flat tensor field.
|
||||
/// Convert one media item's flat tensor range directly to wire bytes.
|
||||
///
|
||||
/// Flat fields keep the first axis as the sliced length for this item.
|
||||
pub(super) fn flat_value_range(&self, start: usize, end: usize) -> Result<Self> {
|
||||
match self {
|
||||
pub(super) fn flat_wire_value_range(
|
||||
&self,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> Result<ProtocolKwargValue> {
|
||||
self.wire_value_range(start, end, false)
|
||||
}
|
||||
|
||||
fn wire_value_range(
|
||||
&self,
|
||||
start: usize,
|
||||
end: usize,
|
||||
drop_axis: bool,
|
||||
) -> Result<ProtocolKwargValue> {
|
||||
let tensor = match self {
|
||||
Self::F32Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, false)?;
|
||||
Ok(Self::F32Tensor { data, shape })
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, drop_axis)?;
|
||||
WireTensor::from_f32(shape, data)
|
||||
}
|
||||
Self::F16Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, false)?;
|
||||
Ok(Self::F16Tensor { data, shape })
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, drop_axis)?;
|
||||
WireTensor::from_f16(shape, data)
|
||||
}
|
||||
Self::Bf16Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, false)?;
|
||||
Ok(Self::Bf16Tensor { data, shape })
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, drop_axis)?;
|
||||
WireTensor::from_bf16(shape, data)
|
||||
}
|
||||
Self::I64Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, false)?;
|
||||
Ok(Self::I64Tensor { data, shape })
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, drop_axis)?;
|
||||
WireTensor::from_i64(shape, data)
|
||||
}
|
||||
Self::U32Tensor { data, shape } => {
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, false)?;
|
||||
Ok(Self::U32Tensor { data, shape })
|
||||
let (shape, data) = slice_first_axis_range(shape, data, start, end, drop_axis)?;
|
||||
WireTensor::from_u32(shape, data)
|
||||
}
|
||||
Self::Passthrough(value) => Ok(Self::Passthrough(value.clone())),
|
||||
}
|
||||
Self::Passthrough(value) => return Ok(value.clone()),
|
||||
};
|
||||
tensor.map(ProtocolKwargValue::Tensor).map_err(Error::Multimodal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,13 +223,13 @@ fn tensor_as_usize_vec(tensor: &KwargValue) -> Result<Vec<usize>> {
|
||||
}
|
||||
|
||||
/// Slice a flat row-major tensor along its first axis.
|
||||
fn slice_first_axis_range<T: Clone>(
|
||||
fn slice_first_axis_range<'a, T>(
|
||||
shape: &[usize],
|
||||
data: &[T],
|
||||
data: &'a [T],
|
||||
start: usize,
|
||||
end: usize,
|
||||
drop_axis: bool,
|
||||
) -> Result<(Vec<usize>, Vec<T>)> {
|
||||
) -> Result<(Vec<usize>, &'a [T])> {
|
||||
let first_dim = *shape.first().ok_or_else(|| multimodal!("tensor has no first dimension"))?;
|
||||
if start > end || end > first_dim {
|
||||
bail_multimodal!("invalid tensor slice {start}..{end} for first dimension {first_dim}");
|
||||
@@ -270,7 +253,7 @@ fn slice_first_axis_range<T: Clone>(
|
||||
shape[0] = end - start;
|
||||
shape
|
||||
};
|
||||
Ok((out_shape, data[data_start..data_end].to_vec()))
|
||||
Ok((out_shape, &data[data_start..data_end]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -278,35 +261,39 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batched_value_at_drops_first_axis() {
|
||||
fn batched_wire_value_at_drops_first_axis() {
|
||||
let value = KwargValue::F32Tensor {
|
||||
data: vec![1.0, 2.0, 3.0, 4.0],
|
||||
shape: vec![2, 2],
|
||||
};
|
||||
|
||||
let value = value.batched_value_at(1).unwrap();
|
||||
let ProtocolKwargValue::Tensor(tensor) = value.batched_wire_value_at(1).unwrap() else {
|
||||
panic!("expected tensor");
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
value,
|
||||
KwargValue::F32Tensor { data, shape }
|
||||
if shape == vec![2] && data == vec![3.0, 4.0]
|
||||
));
|
||||
assert_eq!(tensor.shape, vec![2]);
|
||||
assert_eq!(
|
||||
tensor.data.into_raw_view().unwrap(),
|
||||
[3.0_f32, 4.0].into_iter().flat_map(f32::to_ne_bytes).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_value_range_keeps_first_axis() {
|
||||
fn flat_wire_value_range_keeps_first_axis() {
|
||||
let value = KwargValue::U32Tensor {
|
||||
data: (0..10).collect(),
|
||||
shape: vec![5, 2],
|
||||
};
|
||||
|
||||
let value = value.flat_value_range(1, 3).unwrap();
|
||||
let ProtocolKwargValue::Tensor(tensor) = value.flat_wire_value_range(1, 3).unwrap() else {
|
||||
panic!("expected tensor");
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
value,
|
||||
KwargValue::U32Tensor { data, shape }
|
||||
if shape == vec![2, 2] && data == vec![2, 3, 4, 5]
|
||||
));
|
||||
assert_eq!(tensor.shape, vec![2, 2]);
|
||||
assert_eq!(
|
||||
tensor.data.into_raw_view().unwrap(),
|
||||
[2_u32, 3, 4, 5].into_iter().flat_map(u32::to_ne_bytes).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -336,7 +323,7 @@ mod tests {
|
||||
let value =
|
||||
KwargValue::from_f32_tensor(vec![1.0, -1.0], vec![2], ModelDtype::BFloat16).unwrap();
|
||||
|
||||
let ProtocolKwargValue::Tensor(tensor) = ProtocolKwargValue::try_from(value).unwrap()
|
||||
let ProtocolKwargValue::Tensor(tensor) = ProtocolKwargValue::try_from(&value).unwrap()
|
||||
else {
|
||||
panic!("expected tensor");
|
||||
};
|
||||
@@ -351,7 +338,7 @@ mod tests {
|
||||
let value =
|
||||
KwargValue::from_f32_tensor(vec![1.0, -1.0], vec![2], ModelDtype::Float16).unwrap();
|
||||
|
||||
let ProtocolKwargValue::Tensor(tensor) = ProtocolKwargValue::try_from(value).unwrap()
|
||||
let ProtocolKwargValue::Tensor(tensor) = ProtocolKwargValue::try_from(&value).unwrap()
|
||||
else {
|
||||
panic!("expected tensor");
|
||||
};
|
||||
|
||||
@@ -130,7 +130,7 @@ fn build_video_item(
|
||||
let keep_on_cpu = support.spec.keep_on_cpu_keys.contains(&key);
|
||||
let (value, field) = match support.spec.field_layout_for(&key) {
|
||||
Some(FieldLayout::Batched) => (
|
||||
tensor.batched_value_at(0)?,
|
||||
tensor.batched_wire_value_at(0)?,
|
||||
MmField::Batched(MmBatchedField { keep_on_cpu }),
|
||||
),
|
||||
Some(FieldLayout::Flat { .. }) => {
|
||||
@@ -138,7 +138,7 @@ fn build_video_item(
|
||||
.first_dim()
|
||||
.ok_or_else(|| multimodal!("flat video input `{key}` is not a tensor"))?;
|
||||
(
|
||||
tensor,
|
||||
(&tensor).try_into()?,
|
||||
MmField::Flat(MmFlatField {
|
||||
slices: vec![MmSlice::Slice(SliceSpec {
|
||||
start: Some(0),
|
||||
@@ -151,7 +151,7 @@ fn build_video_item(
|
||||
)
|
||||
}
|
||||
None => (
|
||||
tensor,
|
||||
(&tensor).try_into()?,
|
||||
MmField::Shared(MmSharedField {
|
||||
batch_size: 1,
|
||||
keep_on_cpu,
|
||||
@@ -162,7 +162,7 @@ fn build_video_item(
|
||||
data.insert(
|
||||
key,
|
||||
MmFieldElem {
|
||||
data: Some(value.try_into()?),
|
||||
data: Some(value),
|
||||
field,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -236,9 +236,10 @@ fn has_content_item_loop(root: &Stmt<'_>) -> bool {
|
||||
|
||||
loops.into_iter().any(|loop_ast| {
|
||||
matches!(loop_ast.target, Expr::Var(_))
|
||||
&& message_varnames
|
||||
.iter()
|
||||
.any(|varname| is_var_or_elems_access(&loop_ast.iter, varname, Some("content")))
|
||||
&& (is_var_access(&loop_ast.iter, "content")
|
||||
|| message_varnames.iter().any(|varname| {
|
||||
is_var_or_elems_access(&loop_ast.iter, varname, Some("content"))
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -315,6 +316,16 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_openai_template_with_content_parameter_loop() {
|
||||
assert_eq!(
|
||||
detect(
|
||||
"{% macro render(content) %}{% for item in content %}{{ item }}{% endfor %}{% endmacro %}{% for message in messages %}{{ render(message.content) }}{% endfor %}"
|
||||
),
|
||||
ChatTemplateContentFormat::OpenAi
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_openai_template_with_messages_alias() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1309,6 +1309,26 @@ mod tests {
|
||||
.assert_eq(&rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qwen35_template_auto_detects_openai_multimodal_content() {
|
||||
let mut request = image_request();
|
||||
request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
|
||||
|
||||
let rendered = render_mm(
|
||||
QWEN3_5_0_8B_TEMPLATE,
|
||||
&request,
|
||||
ChatTemplateContentFormatOption::Auto,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
expect![[r#"
|
||||
Text(
|
||||
"<|im_start|>user\na<|vision_start|><|image_pad|><|vision_end|>b<|im_end|>\n",
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&rendered.prompt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qwen35_template_renders_closed_empty_reasoning_span_when_thinking_disabled() {
|
||||
let mut request = sample_request(vec![ChatMessage::text(ChatRole::User, "hello")]);
|
||||
|
||||
@@ -29,6 +29,7 @@ tokio-util.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace = true
|
||||
vllm-bench.workspace = true
|
||||
vllm-chat.workspace = true
|
||||
vllm-engine-core-client.workspace = true
|
||||
vllm-managed-engine.workspace = true
|
||||
|
||||
+11
-1
@@ -79,13 +79,23 @@ impl Cli {
|
||||
}
|
||||
|
||||
/// Supported top-level CLI commands.
|
||||
#[derive(Debug, Subcommand, PartialEq, Eq)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Run the Rust OpenAI frontend as a Python-supervised worker.
|
||||
Frontend(FrontendArgs),
|
||||
/// Launch a managed Python headless engine, then run the Rust OpenAI
|
||||
/// frontend.
|
||||
Serve(ServeArgs),
|
||||
/// Run vLLM benchmarks.
|
||||
#[command(subcommand)]
|
||||
Bench(BenchCommand),
|
||||
}
|
||||
|
||||
/// Supported benchmark commands.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum BenchCommand {
|
||||
/// Benchmark online serving throughput.
|
||||
Serve(vllm_bench::BenchServeArgs),
|
||||
}
|
||||
|
||||
/// A JSON-encoded list of strings, matching Python's `json.loads` CLI type for
|
||||
|
||||
@@ -5,7 +5,27 @@ use expect_test::expect;
|
||||
use vllm_engine_core_client::TransportMode;
|
||||
use vllm_server::{Config, HttpListenerMode, ParserSelection, RendererSelection};
|
||||
|
||||
use super::{Cli, Command};
|
||||
use super::{BenchCommand, Cli, Command};
|
||||
|
||||
#[test]
|
||||
fn bench_serve_args_parse_without_managed_engine_repartition() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"vllm-rs",
|
||||
"bench",
|
||||
"serve",
|
||||
"--backend",
|
||||
"openai-chat",
|
||||
"--request-rate",
|
||||
"inf",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let Command::Bench(BenchCommand::Serve(args)) = cli.command else {
|
||||
panic!("expected bench serve args");
|
||||
};
|
||||
assert_eq!(args.backend, vllm_bench::BackendKind::OpenaiChat);
|
||||
assert!(args.request_rate.is_infinite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_args_forward_python_flags_with_separator() {
|
||||
|
||||
@@ -12,7 +12,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::{info, warn};
|
||||
use vllm_managed_engine::ManagedEngineHandle;
|
||||
|
||||
use crate::cli::{Cli, Command};
|
||||
use crate::cli::{BenchCommand, Cli, Command};
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
@@ -100,6 +100,10 @@ fn main() -> Result<()> {
|
||||
async fn async_main(cli: Cli) -> Result<()> {
|
||||
match cli.command {
|
||||
Command::Frontend(args) => vllm_server::serve(args.into_config(), shutdown_signal()).await,
|
||||
Command::Bench(BenchCommand::Serve(bench_args)) => {
|
||||
vllm_bench::prepare_process();
|
||||
vllm_bench::run(bench_args).await
|
||||
}
|
||||
Command::Serve(args) => {
|
||||
let handshake_port = args.managed_engine.resolve_handshake_port()?;
|
||||
|
||||
|
||||
@@ -55,52 +55,57 @@ pub struct WireNdArray {
|
||||
|
||||
impl WireNdArray {
|
||||
/// Build a float32 tensor/ndarray backed by native-endian raw-view bytes.
|
||||
pub fn from_f32(shape: Vec<usize>, data: Vec<f32>) -> Result<Self, String> {
|
||||
pub fn from_f32(shape: Vec<usize>, data: impl AsRef<[f32]>) -> Result<Self, String> {
|
||||
let data = data.as_ref();
|
||||
validate_element_count(&shape, data.len())?;
|
||||
Ok(Self {
|
||||
dtype: "float32".to_string(),
|
||||
shape,
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<f32, u8>(&data)),
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<f32, u8>(data)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a float16 tensor/ndarray backed by native-endian raw-view bytes.
|
||||
pub fn from_f16(shape: Vec<usize>, data: Vec<f16>) -> Result<Self, String> {
|
||||
pub fn from_f16(shape: Vec<usize>, data: impl AsRef<[f16]>) -> Result<Self, String> {
|
||||
let data = data.as_ref();
|
||||
validate_element_count(&shape, data.len())?;
|
||||
Ok(Self {
|
||||
dtype: "float16".to_string(),
|
||||
shape,
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<f16, u8>(&data)),
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<f16, u8>(data)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a bfloat16 tensor/ndarray backed by native-endian raw-view bytes.
|
||||
pub fn from_bf16(shape: Vec<usize>, data: Vec<bf16>) -> Result<Self, String> {
|
||||
pub fn from_bf16(shape: Vec<usize>, data: impl AsRef<[bf16]>) -> Result<Self, String> {
|
||||
let data = data.as_ref();
|
||||
validate_element_count(&shape, data.len())?;
|
||||
Ok(Self {
|
||||
dtype: "bfloat16".to_string(),
|
||||
shape,
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<bf16, u8>(&data)),
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<bf16, u8>(data)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build an int64 tensor/ndarray backed by native-endian raw-view bytes.
|
||||
pub fn from_i64(shape: Vec<usize>, data: Vec<i64>) -> Result<Self, String> {
|
||||
pub fn from_i64(shape: Vec<usize>, data: impl AsRef<[i64]>) -> Result<Self, String> {
|
||||
let data = data.as_ref();
|
||||
validate_element_count(&shape, data.len())?;
|
||||
Ok(Self {
|
||||
dtype: "int64".to_string(),
|
||||
shape,
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<i64, u8>(&data)),
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<i64, u8>(data)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a uint32 tensor/ndarray backed by native-endian raw-view bytes.
|
||||
pub fn from_u32(shape: Vec<usize>, data: Vec<u32>) -> Result<Self, String> {
|
||||
pub fn from_u32(shape: Vec<usize>, data: impl AsRef<[u32]>) -> Result<Self, String> {
|
||||
let data = data.as_ref();
|
||||
validate_element_count(&shape, data.len())?;
|
||||
Ok(Self {
|
||||
dtype: "uint32".to_string(),
|
||||
shape,
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<u32, u8>(&data)),
|
||||
data: WireArrayData::RawView(pod_collect_to_vec::<u32, u8>(data)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -238,13 +238,18 @@ fn collect_generate(
|
||||
None
|
||||
};
|
||||
let prompt_logprobs = if include_prompt_logprobs {
|
||||
let prompt_logprobs = collected.prompt_logprobs.as_ref().ok_or_else(|| {
|
||||
ApiError::server_error(
|
||||
"raw generate response requested prompt_logprobs but generation returned none"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
Some(raw_prompt_logprobs_to_maps(prompt_logprobs))
|
||||
match collected.prompt_logprobs.as_ref() {
|
||||
Some(prompt_logprobs) => Some(raw_prompt_logprobs_to_maps(prompt_logprobs)),
|
||||
// A single-token prompt has no scored positions; same mapping
|
||||
// as /v1/completions.
|
||||
None if collected.prompt_token_ids.len() == 1 => Some(vec![None]),
|
||||
None => {
|
||||
return Err(ApiError::server_error(
|
||||
"raw generate response requested prompt_logprobs but generation returned none"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -472,4 +477,48 @@ mod tests {
|
||||
Some(2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_generate_maps_prompt_logprobs_for_single_token_prompt() {
|
||||
let output_without_payload = |prompt_token_ids: Vec<u32>| CollectedGenerateOutput {
|
||||
request_id: "raw-1".to_string(),
|
||||
prompt_logprobs: None,
|
||||
token_ids: vec![3],
|
||||
logprobs: None,
|
||||
finish_reason: FinishReason::stop_eos(),
|
||||
usage: vllm_llm::TokenUsage {
|
||||
prompt_token_count: prompt_token_ids.len(),
|
||||
output_token_count: 1,
|
||||
cached_token_count: 0,
|
||||
},
|
||||
kv_transfer_params: None,
|
||||
ec_transfer_params: None,
|
||||
prompt_token_ids,
|
||||
};
|
||||
|
||||
let response = collect_generate(
|
||||
output_without_payload(vec![9707]),
|
||||
"raw-1".to_string(),
|
||||
ApiServerOptions::default(),
|
||||
ResponseOptions {
|
||||
include_prompt_logprobs: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("single-token prompt without payload maps to [None]");
|
||||
let prompt_logprobs = response.prompt_logprobs.expect("prompt logprobs present");
|
||||
assert_eq!(prompt_logprobs.len(), 1);
|
||||
assert!(prompt_logprobs[0].is_none());
|
||||
|
||||
collect_generate(
|
||||
output_without_payload(vec![9707, 11]),
|
||||
"raw-2".to_string(),
|
||||
ApiServerOptions::default(),
|
||||
ResponseOptions {
|
||||
include_prompt_logprobs: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect_err("multi-token prompt without payload is an engine failure");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::routes::openai::chat_completions::types::{
|
||||
ChatMessageDelta,
|
||||
};
|
||||
use crate::routes::openai::utils::logprobs::{
|
||||
decoded_logprobs_to_openai_chat, decoded_prompt_logprobs_to_maps,
|
||||
decoded_logprobs_to_openai_chat, prompt_logprobs_to_maps,
|
||||
};
|
||||
use crate::routes::openai::utils::types::{
|
||||
ChatLogProbs, FunctionCallDelta, FunctionCallResponse, ToolCall, ToolCallDelta, Usage,
|
||||
@@ -181,14 +181,11 @@ async fn collect_chat_completion(
|
||||
None
|
||||
};
|
||||
let prompt_logprobs = if include_prompt_logprobs {
|
||||
Some(decoded_prompt_logprobs_to_maps(
|
||||
prompt_logprobs.as_ref().ok_or_else(|| {
|
||||
server_error!(
|
||||
"chat response requested prompt_logprobs but generation returned none"
|
||||
)
|
||||
})?,
|
||||
Some(prompt_logprobs_to_maps(
|
||||
prompt_logprobs.as_ref(),
|
||||
&prompt_token_ids,
|
||||
return_tokens_as_token_ids,
|
||||
))
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ mod convert;
|
||||
mod types;
|
||||
mod validate;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::result::Result;
|
||||
use std::sync::Arc;
|
||||
@@ -29,8 +28,8 @@ use vllm_text::{
|
||||
|
||||
use self::convert::{ResponseOptions, prepare_completion_request};
|
||||
use super::utils::logprobs::{
|
||||
collected_logprobs_to_openai, decoded_logprobs_to_openai, decoded_prompt_logprobs_to_maps,
|
||||
decoded_prompt_logprobs_to_openai, text_len,
|
||||
collected_logprobs_to_openai, decoded_logprobs_to_openai, decoded_prompt_logprobs_to_openai,
|
||||
prompt_logprobs_to_maps, text_len,
|
||||
};
|
||||
use super::utils::types::Usage;
|
||||
use crate::config::ApiServerOptions;
|
||||
@@ -505,27 +504,6 @@ fn prompt_only_logprobs_to_openai(
|
||||
))
|
||||
}
|
||||
|
||||
fn prompt_logprobs_to_maps(
|
||||
prompt_logprobs: Option<&DecodedPromptLogprobs>,
|
||||
prompt_token_ids: &[u32],
|
||||
return_tokens_as_token_ids: bool,
|
||||
) -> Result<Vec<Option<HashMap<String, f32>>>, ApiError> {
|
||||
if let Some(prompt_logprobs) = prompt_logprobs {
|
||||
return Ok(decoded_prompt_logprobs_to_maps(
|
||||
prompt_logprobs,
|
||||
return_tokens_as_token_ids,
|
||||
));
|
||||
}
|
||||
|
||||
if let [_token_id] = prompt_token_ids {
|
||||
return Ok(vec![None]);
|
||||
}
|
||||
|
||||
Err(server_error!(
|
||||
"completion response requested prompt_logprobs but generation returned none"
|
||||
))
|
||||
}
|
||||
|
||||
fn usage_chunk(
|
||||
request_id: &str,
|
||||
response_model: &str,
|
||||
|
||||
@@ -100,20 +100,31 @@ pub fn decoded_prompt_logprobs_to_openai(
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert decoded prompt logprobs into the vLLM-style prompt-logprobs response
|
||||
/// shape.
|
||||
pub fn decoded_prompt_logprobs_to_maps(
|
||||
prompt_logprobs: &DecodedPromptLogprobs,
|
||||
/// Map decoded prompt logprobs into vLLM-style per-position maps, treating a
|
||||
/// missing single-token payload as `[None]`.
|
||||
pub fn prompt_logprobs_to_maps(
|
||||
prompt_logprobs: Option<&DecodedPromptLogprobs>,
|
||||
prompt_token_ids: &[u32],
|
||||
return_tokens_as_token_ids: bool,
|
||||
) -> Vec<Option<HashMap<String, f32>>> {
|
||||
std::iter::once(None)
|
||||
.chain(prompt_logprobs.scored_positions.iter().map(|position| {
|
||||
Some(position_top_logprobs_map(
|
||||
position,
|
||||
return_tokens_as_token_ids,
|
||||
))
|
||||
}))
|
||||
.collect()
|
||||
) -> Result<Vec<Option<HashMap<String, f32>>>, ApiError> {
|
||||
if let Some(prompt_logprobs) = prompt_logprobs {
|
||||
return Ok(std::iter::once(None)
|
||||
.chain(prompt_logprobs.scored_positions.iter().map(|position| {
|
||||
Some(position_top_logprobs_map(
|
||||
position,
|
||||
return_tokens_as_token_ids,
|
||||
))
|
||||
}))
|
||||
.collect());
|
||||
}
|
||||
|
||||
if let [_token_id] = prompt_token_ids {
|
||||
return Ok(vec![None]);
|
||||
}
|
||||
|
||||
Err(server_error!(
|
||||
"prompt_logprobs were requested but generation returned none"
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert decoded token-position logprobs into the OpenAI chat `logprobs`
|
||||
@@ -275,7 +286,13 @@ pub fn clamp_logprob(logprob: f32) -> f32 {
|
||||
mod tests {
|
||||
use vllm_text::{DecodedLogprobs, DecodedPositionLogprobs, DecodedTokenLogprob};
|
||||
|
||||
use super::decoded_logprobs_to_openai_chat;
|
||||
use super::{decoded_logprobs_to_openai_chat, prompt_logprobs_to_maps};
|
||||
|
||||
#[test]
|
||||
fn prompt_logprobs_maps_reject_missing_multi_token_payload() {
|
||||
prompt_logprobs_to_maps(None, &[9707, 11], false)
|
||||
.expect_err("multi-token prompt without payload is an engine failure");
|
||||
}
|
||||
|
||||
fn sample_logprobs() -> DecodedLogprobs {
|
||||
DecodedLogprobs {
|
||||
|
||||
@@ -515,3 +515,15 @@ def test_structured_outputs_structural_tag_invalid(structural_tag):
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
structured_outputs={"structural_tag": structural_tag},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_name", ["prompt_logprobs", "top_logprobs"])
|
||||
def test_non_numeric_logprobs_rejected(field_name):
|
||||
"""A non-numeric logprobs value must be a clean 400 validation error, not a
|
||||
TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
|
||||
with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
|
||||
ChatCompletionRequest(
|
||||
model=MODEL_NAME,
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
**{field_name: "2"},
|
||||
)
|
||||
|
||||
@@ -610,3 +610,16 @@ class TestCompletionPromptListLimit:
|
||||
max_tokens=1,
|
||||
)
|
||||
assert len(request.prompt_embeds) == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_name", ["prompt_logprobs", "logprobs"])
|
||||
def test_non_numeric_logprobs_rejected(field_name):
|
||||
"""A non-numeric logprobs value must be a clean 400 validation error, not a
|
||||
TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
|
||||
with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
|
||||
CompletionRequest(
|
||||
model=MODEL_NAME,
|
||||
prompt="Test prompt",
|
||||
max_tokens=10,
|
||||
**{field_name: "2"},
|
||||
)
|
||||
|
||||
@@ -132,6 +132,21 @@ class TestResponsesRequestSamplingParams:
|
||||
assert sampling_params.structured_outputs is not None
|
||||
assert sampling_params.structured_outputs.grammar == "root ::= 'hello'"
|
||||
|
||||
def test_text_format_json_object_enables_structured_outputs(self):
|
||||
"""text.format json_object enables structured outputs for sampling."""
|
||||
request = ResponsesRequest(
|
||||
model="test-model",
|
||||
input="test input",
|
||||
text=ResponseTextConfig.model_validate({"format": {"type": "json_object"}}),
|
||||
)
|
||||
|
||||
sampling_params = request.to_sampling_params(default_max_tokens=1000)
|
||||
|
||||
assert sampling_params.structured_outputs is not None
|
||||
assert sampling_params.structured_outputs.json_object is True
|
||||
assert sampling_params.structured_outputs.json is None
|
||||
assert request.structured_outputs is None
|
||||
|
||||
def test_structured_outputs_and_json_schema_conflict(self):
|
||||
"""Test that specifying both structured_outputs and json_schema raises."""
|
||||
structured_outputs = StructuredOutputsParams(grammar="root ::= 'hello'")
|
||||
|
||||
@@ -58,6 +58,7 @@ def _make_builder():
|
||||
max_num_batched_tokens + 1, dtype=torch.int32, device="cpu"
|
||||
)
|
||||
builder._num_attention_heads = 16
|
||||
builder._num_compute_units = current_platform.num_compute_units()
|
||||
builder._mla_work_meta_data = torch.empty(1, dtype=torch.int32, device="cpu")
|
||||
builder._mla_work_indptr = torch.empty(1, dtype=torch.int32, device="cpu")
|
||||
builder._mla_work_info_set = torch.empty(1, dtype=torch.int32, device="cpu")
|
||||
@@ -116,6 +117,7 @@ def test_sparse_persistent_metadata_syncs_only_after_recompute(monkeypatch):
|
||||
|
||||
assert events == ["metadata", "sync"]
|
||||
assert fake_get_mla_metadata_v1_mock.call_count == 1
|
||||
assert fake_get_mla_metadata_v1_mock.call_args.kwargs["max_split_per_batch"] == 1
|
||||
|
||||
events.clear()
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ def test_causal_conv1d_torch_two_call_split(total_tokens: int, split: int) -> No
|
||||
match the single-call result.
|
||||
"""
|
||||
from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
|
||||
causal_conv1d_torch,
|
||||
causal_conv1d_fn_cpu as causal_conv1d_torch,
|
||||
)
|
||||
|
||||
x, weight, bias = _conv_inputs(total_tokens)
|
||||
|
||||
@@ -18,8 +18,12 @@ from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
|
||||
DEVICE = current_platform.device_type
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (current_platform.is_cuda_alike() or current_platform.is_xpu()),
|
||||
reason="causal_conv1d Triton kernels require CUDA-alike or XPU",
|
||||
not (
|
||||
current_platform.is_cuda_alike()
|
||||
or current_platform.is_xpu()
|
||||
or current_platform.is_cpu()
|
||||
),
|
||||
reason="causal_conv1d Triton kernels require CUDA-alike, XPU, or CPU",
|
||||
)
|
||||
|
||||
|
||||
@@ -284,7 +288,8 @@ def test_causal_conv1d_varlen(
|
||||
batch, with_padding, dim, seqlen, width, has_bias, silu_activation, itype
|
||||
):
|
||||
device = DEVICE
|
||||
torch.accelerator.empty_cache()
|
||||
if not current_platform.is_cpu():
|
||||
torch.accelerator.empty_cache()
|
||||
rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (3e-3, 5e-3)
|
||||
if itype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
|
||||
@@ -20,8 +20,12 @@ from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
|
||||
DEVICE = current_platform.device_type
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (current_platform.is_cuda_alike() or current_platform.is_xpu()),
|
||||
reason="mamba_ssm kernels require CUDA-alike or XPU",
|
||||
not (
|
||||
current_platform.is_cuda_alike()
|
||||
or current_platform.is_xpu()
|
||||
or current_platform.is_cpu()
|
||||
),
|
||||
reason="mamba_ssm kernels require CUDA-alike, XPU, or CPU",
|
||||
)
|
||||
|
||||
# selective_scan_fn is backed by the CUDA-only `ops.selective_scan_fwd` C++ op,
|
||||
@@ -342,12 +346,23 @@ def test_selective_scan(
|
||||
@pytest.mark.parametrize("has_z", [False, True])
|
||||
@pytest.mark.parametrize("dstate", [16, 64])
|
||||
@pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
|
||||
@pytest.mark.skipif(
|
||||
current_platform.is_cpu(),
|
||||
reason=(
|
||||
"CPU kernel for selective_state_update only supports "
|
||||
"Mamba 2 (scalar A/dt), not Mamba 1."
|
||||
),
|
||||
)
|
||||
def test_selective_state_update(dim, dstate, has_z, itype):
|
||||
device = DEVICE
|
||||
rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
|
||||
if itype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
if current_platform.is_rocm() or current_platform.is_xpu():
|
||||
if (
|
||||
current_platform.is_rocm()
|
||||
or current_platform.is_xpu()
|
||||
or current_platform.is_device_capability_family(90)
|
||||
):
|
||||
atol *= 2
|
||||
# set seed
|
||||
set_random_seed(0)
|
||||
@@ -432,6 +447,13 @@ def test_selective_state_update_stochastic_rounding(dim, dstate, has_z, philox_r
|
||||
@pytest.mark.parametrize("dstate", [16, 64])
|
||||
@pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
|
||||
@pytest.mark.parametrize("max_seq_len", [1, 2, 4])
|
||||
@pytest.mark.skipif(
|
||||
current_platform.is_cpu(),
|
||||
reason=(
|
||||
"CPU kernel for selective_state_update only supports "
|
||||
"Mamba 2 (scalar A/dt), not Mamba 1."
|
||||
),
|
||||
)
|
||||
def test_selective_state_update_varlen(dim, dstate, has_z, itype, max_seq_len):
|
||||
device = DEVICE
|
||||
rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
|
||||
@@ -693,6 +715,13 @@ def test_selective_scan_varlen(
|
||||
@pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
|
||||
# tests correctness in case subset of the sequences are padded
|
||||
@pytest.mark.parametrize("with_padding", [True, False])
|
||||
@pytest.mark.skipif(
|
||||
current_platform.is_cpu(),
|
||||
reason=(
|
||||
"CPU kernel for selective_state_update only supports "
|
||||
"Mamba 2 (scalar A/dt), not Mamba 1."
|
||||
),
|
||||
)
|
||||
def test_selective_state_update_with_batch_indices(
|
||||
with_padding, dim, dstate, has_z, itype
|
||||
):
|
||||
@@ -785,6 +814,13 @@ def test_selective_state_update_with_batch_indices(
|
||||
@pytest.mark.parametrize("ngroups", [1, 4])
|
||||
@pytest.mark.parametrize("dstate", [16, 64])
|
||||
@pytest.mark.parametrize("dim", [2048, 4096])
|
||||
@pytest.mark.skipif(
|
||||
current_platform.is_cpu(),
|
||||
reason=(
|
||||
"CPU kernel for selective_state_update only supports "
|
||||
"Mamba 2 (scalar A/dt), not Mamba 1."
|
||||
),
|
||||
)
|
||||
def test_selective_state_update_with_heads_with_batch_indices(
|
||||
dim, dstate, ngroups, has_z, tie_hdim, itype
|
||||
):
|
||||
@@ -858,6 +894,13 @@ def test_selective_state_update_with_heads_with_batch_indices(
|
||||
@pytest.mark.parametrize("dstate", [16, 64])
|
||||
@pytest.mark.parametrize("dim", [2048, 4096])
|
||||
@pytest.mark.parametrize("max_seq_len", [2, 4])
|
||||
@pytest.mark.skipif(
|
||||
current_platform.is_cpu(),
|
||||
reason=(
|
||||
"CPU kernel for selective_state_update only supports "
|
||||
"Mamba 2 (scalar A/dt), not Mamba 1."
|
||||
),
|
||||
)
|
||||
def test_selective_state_update_with_num_accepted_tokens(
|
||||
dim, dstate, has_z, itype, max_seq_len
|
||||
):
|
||||
@@ -984,6 +1027,13 @@ def test_selective_state_update_with_num_accepted_tokens(
|
||||
@pytest.mark.parametrize("dstate", [16, 64])
|
||||
@pytest.mark.parametrize("dim", [2048, 4096])
|
||||
@pytest.mark.parametrize("max_seq_len", [2, 4])
|
||||
@pytest.mark.skipif(
|
||||
current_platform.is_cpu(),
|
||||
reason=(
|
||||
"CPU kernel for selective_state_update only supports "
|
||||
"Mamba 2 (scalar A/dt), not Mamba 1."
|
||||
),
|
||||
)
|
||||
def test_selective_state_update_varlen_with_num_accepted(
|
||||
dim, dstate, has_z, itype, max_seq_len
|
||||
):
|
||||
|
||||
@@ -188,6 +188,16 @@ def test_models(
|
||||
|
||||
prompt_embeds.append(embed.squeeze(0))
|
||||
|
||||
vllm_kwargs = {}
|
||||
if (
|
||||
model == "bigscience/bloom-560m"
|
||||
and current_platform.is_device_capability_family(90)
|
||||
):
|
||||
# On SM90, the metadata builder otherwise selects FA3 AOT scheduling
|
||||
# before Bloom's ALiBi layers fall back to FA2. Pinning FA2 keeps the
|
||||
# builder and layer consistent and preserves the L4 test path.
|
||||
vllm_kwargs["attention_config"] = {"flash_attn_version": 2}
|
||||
|
||||
with vllm_runner(
|
||||
model,
|
||||
tokenizer_name=model_info.tokenizer or model,
|
||||
@@ -200,6 +210,7 @@ def test_models(
|
||||
max_num_seqs=1 if current_platform.is_rocm() else 2,
|
||||
enable_prompt_embeds=use_prompt_embeds,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2]},
|
||||
**vllm_kwargs,
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.generate_greedy_logprobs(
|
||||
example_prompts, max_tokens, num_logprobs
|
||||
|
||||
@@ -384,8 +384,13 @@ def test_fp32_cache_state(
|
||||
example_prompts, max_tokens, num_logprobs
|
||||
)
|
||||
|
||||
# Leave enough headroom for repeated engine initialization on a
|
||||
# 32.5 GiB MIG.
|
||||
with vllm_runner(
|
||||
model, max_num_seqs=MAX_NUM_SEQS, **{cache_dtype_param: "float32"}
|
||||
model,
|
||||
max_num_seqs=MAX_NUM_SEQS,
|
||||
gpu_memory_utilization=0.9,
|
||||
**{cache_dtype_param: "float32"},
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.generate_greedy_logprobs(
|
||||
example_prompts, max_tokens, num_logprobs
|
||||
|
||||
@@ -67,6 +67,8 @@ def test_models(
|
||||
|
||||
if kv_cache_dtype == "fp8_e5m2" and current_platform.is_rocm():
|
||||
pytest.skip(f"{kv_cache_dtype} is currently not supported on ROCm/HIP.")
|
||||
if kv_cache_dtype == "fp8_e5m2" and current_platform.is_cuda():
|
||||
pytest.skip(f"{kv_cache_dtype} is not supported by FLASH_ATTN on CUDA.")
|
||||
|
||||
if not (
|
||||
current_platform.is_xpu()
|
||||
|
||||
@@ -93,6 +93,9 @@ def test_online_quantization(
|
||||
use_rocm_aiter: bool,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
if kv_cache_dtype == "fp8" and current_platform.is_device_capability_family(90):
|
||||
pytest.skip("FA3 currently rejects FP8 KV cache output dtype on SM90")
|
||||
|
||||
if use_rocm_aiter:
|
||||
monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
|
||||
|
||||
|
||||
@@ -717,7 +717,10 @@ def test_triton_unified_attention_per_token_head_scale(
|
||||
|
||||
# Coarser quantization → wider tolerance.
|
||||
if is_int4:
|
||||
atol, rtol = 0.5, 0.5
|
||||
# Hopper's attention reduction order can move a few BF16 elements by
|
||||
# just over 1.0 after INT4 quantization.
|
||||
atol = 1.1 if current_platform.is_device_capability_family(90) else 0.5
|
||||
rtol = 0.5
|
||||
else:
|
||||
atol, rtol = 5e-2, 5e-2
|
||||
torch.testing.assert_close(output_q, output_ref, atol=atol, rtol=rtol)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from tests.models.utils import EmbedModelInfo
|
||||
from vllm import PoolingParams
|
||||
from vllm.config import ModelConfig, PoolerConfig
|
||||
from vllm.entrypoints.pooling.classify.protocol import ClassificationRequest
|
||||
from vllm.entrypoints.pooling.embed.protocol import EmbeddingRequest
|
||||
from vllm.entrypoints.pooling.pooling.protocol import PoolingRequest
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
|
||||
EMBEDDING_MODELS = [
|
||||
EmbedModelInfo("intfloat/multilingual-e5-small", is_matryoshka=False),
|
||||
@@ -27,6 +33,38 @@ class MockModelConfig:
|
||||
pooler_config: PoolerConfig
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("parameter", "value", "message"),
|
||||
[
|
||||
(
|
||||
"normalize",
|
||||
False,
|
||||
"Parameter `normalize` was removed; use `use_activation` instead.",
|
||||
),
|
||||
("task", "score", "`score` task was removed; use `classify` instead."),
|
||||
(
|
||||
"task",
|
||||
"encode",
|
||||
"`encode` task was removed; use `token_embed` or `token_classify` instead.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_removed_pooling_parameters(parameter: str, value: Any, message: str):
|
||||
data = {"input": "hello", parameter: value}
|
||||
for request_type in (EmbeddingRequest, ClassificationRequest, PoolingRequest):
|
||||
with pytest.raises(ValidationError, match=message) as exc_info:
|
||||
TypeAdapter(request_type).validate_python(data)
|
||||
assert len(exc_info.value.errors()) == 1
|
||||
|
||||
with pytest.raises(ValidationError, match=message) as exc_info:
|
||||
TypeAdapter(PoolerConfig).validate_python({parameter: value})
|
||||
assert len(exc_info.value.errors()) == 1
|
||||
|
||||
if parameter == "task":
|
||||
with pytest.raises(VLLMValidationError, match=message):
|
||||
PoolingParams(task=value)
|
||||
|
||||
|
||||
def test_embed():
|
||||
task = "embed"
|
||||
model_config = MockModelConfig(pooler_config=PoolerConfig(seq_pooling_type="CLS"))
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
from transformers import AutoTokenizer, PythonBackend, TokenizersBackend
|
||||
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.tokenizers.detokenizer_utils import convert_ids_list_to_tokens
|
||||
from vllm.tokenizers.mistral import MistralTokenizer
|
||||
from vllm.v1.engine import EngineCoreRequest
|
||||
from vllm.v1.engine.detokenizer import (
|
||||
@@ -239,3 +240,122 @@ def test_oov_decode(tokenizer, fast):
|
||||
|
||||
assert decoded_text == ""
|
||||
assert out_ids == [len(tokenizer)]
|
||||
|
||||
|
||||
# ---------- convert_ids_list_to_tokens collision tests ----------
|
||||
|
||||
|
||||
class _MockBackend:
|
||||
"""Fake backend_tokenizer that exposes pre_tokenizer config."""
|
||||
|
||||
def __init__(self, pre_tokenizer_type, replacement=None):
|
||||
import json
|
||||
|
||||
pre: dict = {"type": pre_tokenizer_type}
|
||||
if replacement is not None:
|
||||
pre["replacement"] = replacement
|
||||
self._config = json.dumps({"pre_tokenizer": pre})
|
||||
|
||||
def to_str(self):
|
||||
return self._config
|
||||
|
||||
|
||||
class _MockTokenizer:
|
||||
"""Minimal tokenizer mock for testing convert_ids_list_to_tokens."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_tokens: dict[int, str],
|
||||
decoded_tokens: dict[int, str],
|
||||
pre_tokenizer_type: str = "Metaspace",
|
||||
replacement: str | None = "▁",
|
||||
):
|
||||
self._raw = raw_tokens
|
||||
self._decoded = decoded_tokens
|
||||
self.backend_tokenizer = _MockBackend(pre_tokenizer_type, replacement)
|
||||
|
||||
def convert_ids_to_tokens(
|
||||
self, ids: list[int], skip_special_tokens: bool = False
|
||||
) -> list[str]:
|
||||
return [self._raw[tid] for tid in ids]
|
||||
|
||||
def decode(self, ids: list[int], skip_special_tokens: bool = False) -> str:
|
||||
return "".join(self._decoded[tid] for tid in ids)
|
||||
|
||||
|
||||
def test_sentencepiece_leading_space_preserved():
|
||||
"""▁true and true must produce distinct strings."""
|
||||
tok = _MockTokenizer(
|
||||
raw_tokens={0: "▁true", 1: "true", 2: "▁false", 3: "false"},
|
||||
decoded_tokens={0: "true", 1: "true", 2: "false", 3: "false"},
|
||||
)
|
||||
result = convert_ids_list_to_tokens(tok, [0, 1, 2, 3])
|
||||
assert result == [" true", "true", " false", "false"]
|
||||
|
||||
# No dict collision when used as top_logprobs keys
|
||||
logprobs = dict(zip(result, [-0.1, -0.2, -0.3, -0.4]))
|
||||
assert len(logprobs) == 4
|
||||
|
||||
|
||||
def test_whitespace_run_tokens_stay_distinct():
|
||||
"""▁, ▁▁, ▁▁▁ must produce different-length space strings."""
|
||||
tok = _MockTokenizer(
|
||||
raw_tokens={0: "▁", 1: "▁▁", 2: "▁▁▁"},
|
||||
decoded_tokens={0: "", 1: " ", 2: " "},
|
||||
)
|
||||
result = convert_ids_list_to_tokens(tok, [0, 1, 2])
|
||||
assert result == [" ", " ", " "]
|
||||
|
||||
|
||||
def test_bpe_leading_space_already_preserved():
|
||||
"""GPT-2 BPE: Ġtrue already decodes to ' true', no fix needed."""
|
||||
tok = _MockTokenizer(
|
||||
raw_tokens={0: "Ġtrue", 1: "true"},
|
||||
decoded_tokens={0: " true", 1: "true"},
|
||||
pre_tokenizer_type="ByteLevel",
|
||||
replacement=None,
|
||||
)
|
||||
result = convert_ids_list_to_tokens(tok, [0, 1])
|
||||
assert result == [" true", "true"]
|
||||
|
||||
|
||||
def test_logprobs_count_stable_across_k():
|
||||
"""logprobs=4 and logprobs=10 must return 4 and 10 entries."""
|
||||
tok = _MockTokenizer(
|
||||
raw_tokens={
|
||||
0: "▁true",
|
||||
1: "a",
|
||||
2: "b",
|
||||
3: "c",
|
||||
4: "true",
|
||||
5: "d",
|
||||
6: "e",
|
||||
7: "f",
|
||||
8: "g",
|
||||
9: "h",
|
||||
},
|
||||
decoded_tokens={
|
||||
0: "true",
|
||||
1: "a",
|
||||
2: "b",
|
||||
3: "c",
|
||||
4: "true",
|
||||
5: "d",
|
||||
6: "e",
|
||||
7: "f",
|
||||
8: "g",
|
||||
9: "h",
|
||||
},
|
||||
)
|
||||
ids = list(range(10))
|
||||
lps = [-0.1 * (i + 1) for i in range(10)]
|
||||
|
||||
tokens4 = convert_ids_list_to_tokens(tok, ids[:4])
|
||||
top4 = dict(zip(tokens4, lps[:4]))
|
||||
|
||||
tokens10 = convert_ids_list_to_tokens(tok, ids)
|
||||
top10 = dict(zip(tokens10, lps))
|
||||
|
||||
assert len(top4) == 4
|
||||
assert len(top10) == 10
|
||||
assert top4[" true"] == top10[" true"]
|
||||
|
||||
+14
-1
@@ -108,6 +108,7 @@ if current_platform.is_rocm():
|
||||
elif current_platform.is_cuda():
|
||||
from vllm.third_party.pynvml import (
|
||||
nvmlDeviceGetHandleByIndex,
|
||||
nvmlDeviceGetHandleByUUID,
|
||||
nvmlDeviceGetMemoryInfo,
|
||||
nvmlInit,
|
||||
nvmlShutdown,
|
||||
@@ -1521,6 +1522,18 @@ def get_physical_device_indices(devices: list[int]):
|
||||
return [index_mapping[i] for i in devices if i in index_mapping]
|
||||
|
||||
|
||||
def get_nvml_device_handle(device: int):
|
||||
visible_devices = os.environ.get("NVIDIA_VISIBLE_DEVICES")
|
||||
if visible_devices is not None:
|
||||
identifiers = visible_devices.split(",")
|
||||
if device < len(identifiers):
|
||||
identifier = identifiers[device]
|
||||
if identifier.startswith(("GPU-", "MIG-")):
|
||||
return nvmlDeviceGetHandleByUUID(identifier)
|
||||
|
||||
return nvmlDeviceGetHandleByIndex(device)
|
||||
|
||||
|
||||
@_nvml()
|
||||
def record_gpu_memory_usage_stats(
|
||||
*,
|
||||
@@ -1534,7 +1547,7 @@ def record_gpu_memory_usage_stats(
|
||||
gb_used = mem_info["vram_used"] / 2**10
|
||||
gb_total = mem_info["vram_total"] / 2**10
|
||||
else:
|
||||
dev_handle = nvmlDeviceGetHandleByIndex(device)
|
||||
dev_handle = get_nvml_device_handle(device)
|
||||
mem_info = nvmlDeviceGetMemoryInfo(dev_handle)
|
||||
gb_used = mem_info.used / 2**30
|
||||
gb_total = mem_info.total / 2**30
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.utils.extensible_tensor import ExtensibleTensor
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
|
||||
|
||||
def test_extensible_tensor_grows_without_moving() -> None:
|
||||
buffer = ExtensibleTensor(4096, device="cuda")
|
||||
try:
|
||||
base_ptr = buffer.base_ptr
|
||||
first_view = buffer.resize_(1024)
|
||||
assert first_view.data_ptr() == base_ptr
|
||||
first_view.fill_(7)
|
||||
|
||||
second_view = buffer.resize_(2048)
|
||||
assert second_view.data_ptr() == base_ptr
|
||||
assert torch.equal(second_view[:1024], torch.full_like(second_view[:1024], 7))
|
||||
|
||||
second_view[1024:].fill_(3)
|
||||
assert torch.equal(buffer.tensor, second_view)
|
||||
|
||||
full_view = buffer.full_view()
|
||||
assert full_view.data_ptr() == base_ptr
|
||||
assert full_view.numel() == 4096
|
||||
finally:
|
||||
buffer.free()
|
||||
|
||||
|
||||
def test_extensible_tensor_rejects_shrink_and_overflow() -> None:
|
||||
buffer = ExtensibleTensor(1024, device="cuda")
|
||||
try:
|
||||
buffer.resize_(512)
|
||||
with pytest.raises(ValueError, match="grow-only"):
|
||||
buffer.resize_(256)
|
||||
with pytest.raises(ValueError, match="exceeds the segment capacity"):
|
||||
buffer.resize_(1025)
|
||||
finally:
|
||||
buffer.free()
|
||||
|
||||
|
||||
def test_segments_grow_in_lockstep_and_zero_new() -> None:
|
||||
"""Each segment's committed prefix grows in lockstep.
|
||||
|
||||
Data written to a segment's committed prefix survives a grow; the newly
|
||||
committed range of each segment is zeroed with `zero_new=True` while old
|
||||
bytes are preserved.
|
||||
"""
|
||||
et = ExtensibleTensor(max_num_bytes=8192, device="cuda", num_segments=2)
|
||||
try:
|
||||
assert et.num_segments == 2
|
||||
assert et.segment_capacity_bytes == 4096
|
||||
|
||||
et.resize_per_segment_(256, zero_new=True)
|
||||
assert et.bytes_per_segment == 256
|
||||
assert et.num_bytes == 512
|
||||
fv = et.full_view()
|
||||
assert fv.shape == (8192,)
|
||||
# Committed prefixes start zeroed.
|
||||
assert torch.count_nonzero(fv[:256]) == 0
|
||||
assert torch.count_nonzero(fv[4096 : 4096 + 256]) == 0
|
||||
|
||||
pattern_a = torch.arange(256, device="cuda", dtype=torch.uint8)
|
||||
pattern_b = 255 - pattern_a
|
||||
fv[:256].copy_(pattern_a)
|
||||
fv[4096 : 4096 + 256].copy_(pattern_b)
|
||||
|
||||
et.resize_per_segment_(1024, zero_new=True)
|
||||
fv2 = et.full_view()
|
||||
assert fv2.data_ptr() == fv.data_ptr()
|
||||
# Old bytes of both segments preserved; freshly committed ranges zeroed.
|
||||
assert torch.equal(fv2[:256], pattern_a)
|
||||
assert torch.equal(fv2[4096 : 4096 + 256], pattern_b)
|
||||
assert torch.count_nonzero(fv2[256:1024]) == 0
|
||||
assert torch.count_nonzero(fv2[4096 + 256 : 4096 + 1024]) == 0
|
||||
finally:
|
||||
et.free()
|
||||
|
||||
|
||||
def test_segments_at_granularity_scale() -> None:
|
||||
"""Segments spanning multiple mapping granules commit correctly.
|
||||
|
||||
Uses a segment capacity that is not a multiple of the allocation
|
||||
granularity, so a granule straddles the segment boundary and is shared by
|
||||
the first commit of one segment and a later commit of the other -- it must
|
||||
be mapped exactly once.
|
||||
"""
|
||||
probe = ExtensibleTensor(max_num_bytes=1, device="cuda")
|
||||
granularity = probe.capacity_bytes
|
||||
probe.free()
|
||||
# Two segments of 1.5 granules each; the middle granule straddles the
|
||||
# boundary.
|
||||
max_num_bytes = 3 * granularity
|
||||
et = ExtensibleTensor(max_num_bytes=max_num_bytes, device="cuda", num_segments=2)
|
||||
try:
|
||||
seg = et.segment_capacity_bytes
|
||||
assert seg == max_num_bytes // 2
|
||||
|
||||
step = granularity // 2
|
||||
et.resize_per_segment_(step, zero_new=True)
|
||||
fv = et.full_view()
|
||||
fv[:step].fill_(1)
|
||||
fv[seg : seg + step].fill_(2)
|
||||
|
||||
# Grow to the full segment capacity: previously mapped granules
|
||||
# (including the boundary-straddling one) are reused, new ones are
|
||||
# committed and zeroed.
|
||||
et.resize_per_segment_(seg, zero_new=True)
|
||||
fv2 = et.full_view()
|
||||
assert torch.all(fv2[:step] == 1)
|
||||
assert torch.all(fv2[seg : seg + step] == 2)
|
||||
assert torch.count_nonzero(fv2[step:seg]) == 0
|
||||
assert torch.count_nonzero(fv2[seg + step :]) == 0
|
||||
finally:
|
||||
et.free()
|
||||
|
||||
|
||||
def test_multi_segment_invalid_usage_raises() -> None:
|
||||
"""Prefix-view APIs and invalid segment configs raise for multi-segment
|
||||
buffers."""
|
||||
with pytest.raises(ValueError):
|
||||
ExtensibleTensor(max_num_bytes=100, device="cuda", num_segments=3)
|
||||
|
||||
et = ExtensibleTensor(max_num_bytes=8192, device="cuda", num_segments=2)
|
||||
try:
|
||||
with pytest.raises(ValueError):
|
||||
_ = et.tensor
|
||||
with pytest.raises(ValueError):
|
||||
et.resize_(256)
|
||||
|
||||
et.resize_per_segment_(256)
|
||||
with pytest.raises(ValueError):
|
||||
et.resize_per_segment_(128) # shrink
|
||||
with pytest.raises(ValueError):
|
||||
et.resize_per_segment_(et.segment_capacity_bytes + 1) # over capacity
|
||||
finally:
|
||||
et.free()
|
||||
@@ -149,6 +149,30 @@ def test_has_cache_restores_from_freeable():
|
||||
assert manager.num_freeable_slots == 6
|
||||
|
||||
|
||||
def test_make_profiling_reservation():
|
||||
assert (
|
||||
EncoderCacheManager.make_profiling_reservation(
|
||||
cache_size=0,
|
||||
embed_size=8,
|
||||
dtype=torch.float16,
|
||||
device="cpu",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
reservation = EncoderCacheManager.make_profiling_reservation(
|
||||
cache_size=7,
|
||||
embed_size=8,
|
||||
dtype=torch.float16,
|
||||
device="cpu",
|
||||
)
|
||||
|
||||
assert reservation is not None
|
||||
assert reservation.shape == (7, 8)
|
||||
assert reservation.dtype == torch.float16
|
||||
assert reservation.device.type == "cpu"
|
||||
|
||||
|
||||
def test_get_freed_mm_hashes_clears_freed_list():
|
||||
manager = EncoderCacheManager(cache_size=10)
|
||||
req1 = MockRequest("reqA", ["a"], [5])
|
||||
|
||||
@@ -49,6 +49,18 @@ def test_prefix_caching_from_cli():
|
||||
args = parser.parse_args(["--prefix-caching-hash-algo", "invalid"])
|
||||
|
||||
|
||||
def test_extensible_kv_cache_from_cli():
|
||||
parser = EngineArgs.add_cli_args(FlexibleArgumentParser())
|
||||
|
||||
args = parser.parse_args([])
|
||||
engine_args = EngineArgs.from_cli_args(args=args)
|
||||
assert not engine_args.enable_extensible_kv_cache
|
||||
|
||||
args = parser.parse_args(["--enable-extensible-kv-cache"])
|
||||
engine_args = EngineArgs.from_cli_args(args=args)
|
||||
assert engine_args.enable_extensible_kv_cache
|
||||
|
||||
|
||||
@pytest.mark.skipif(_xxhash is None, reason="xxhash not installed")
|
||||
def test_prefix_caching_xxhash_from_cli():
|
||||
parser = EngineArgs.add_cli_args(FlexibleArgumentParser())
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""GPU integration tests for the extensible KV cache allocation paths.
|
||||
|
||||
Drives `GPUModelRunner._allocate_kv_cache_tensors` / `_reshape_kv_cache_tensors`
|
||||
/ `extend_kv_cache` directly with fake attention backends, covering the buffer
|
||||
layouts the extensible flow supports: block-major (one committed prefix),
|
||||
K/V-split (one prefix per half), Mamba (block-major per layer), and hybrid
|
||||
attention + Mamba (attention re-strided to block-major). Buffer sizes exceed
|
||||
the CUDA VMM allocation granularity so touching a block that the commit logic
|
||||
missed would fault instead of silently passing.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.attention.backend import AttentionBackend
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheTensor,
|
||||
MambaSpec,
|
||||
)
|
||||
from vllm.v1.worker.gpu.attn_utils import (
|
||||
_allocate_extensible_kv_cache,
|
||||
_kv_cache_num_segments_by_layer,
|
||||
_reshape_kv_cache,
|
||||
narrow_kv_caches_to_num_blocks,
|
||||
)
|
||||
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
|
||||
from vllm.v1.worker.gpu_worker import Worker
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
NUM_BLOCKS = 256
|
||||
|
||||
|
||||
class _SplitKVBackend(AttentionBackend):
|
||||
"""Fake backend with a K/V-split layout, like FlashAttention."""
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (2, num_blocks, block_size, num_kv_heads, head_size)
|
||||
|
||||
|
||||
class _BlockMajorBackend(AttentionBackend):
|
||||
"""Fake backend with a num-blocks-first layout, like FlashInfer."""
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, 2, block_size, num_kv_heads, head_size)
|
||||
|
||||
|
||||
class _StrideOrderBackend(AttentionBackend):
|
||||
"""Fake backend whose stride order makes a kv-first shape block-major."""
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (2, num_blocks, block_size, num_kv_heads, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
assert not include_num_layers_dimension
|
||||
return (1, 0, 2, 3, 4)
|
||||
|
||||
|
||||
def _full_attention_spec() -> FullAttentionSpec:
|
||||
# page_size_bytes = 2 (K+V) * 16 * 8 * 128 * 2 bytes = 64 KiB; 256 blocks
|
||||
# = 16 MiB, several VMM granules per buffer.
|
||||
return FullAttentionSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=8,
|
||||
head_size=128,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
|
||||
|
||||
def _mamba_spec() -> MambaSpec:
|
||||
# page_size_bytes = (8*128 + 16*64) * 4 bytes = 8 KiB per block per layer.
|
||||
return MambaSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
shapes=((8, 128), (16, 64)),
|
||||
dtypes=(torch.float32, torch.float32),
|
||||
)
|
||||
|
||||
|
||||
def _make_runner(kv_cache_config: KVCacheConfig, attn_groups) -> GPUModelRunner:
|
||||
runner = object.__new__(GPUModelRunner)
|
||||
runner.device = torch.device("cuda:0")
|
||||
runner.kv_cache_config = kv_cache_config
|
||||
runner.attn_groups = attn_groups
|
||||
runner.runner_only_attn_layers = set()
|
||||
runner.cache_config = SimpleNamespace(cache_dtype="auto")
|
||||
return runner
|
||||
|
||||
|
||||
def _attention_config(spec: FullAttentionSpec, backend) -> tuple[KVCacheConfig, list]:
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=NUM_BLOCKS * spec.page_size_bytes, shared_by=["layer.0"])
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=["layer.0"], kv_cache_spec=spec)],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=backend,
|
||||
layer_names=["layer.0"],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
]
|
||||
return kv_cache_config, attn_groups
|
||||
|
||||
|
||||
def _free_buffers(runner: GPUModelRunner) -> None:
|
||||
buffers = getattr(runner, "extensible_kv_buffers", None)
|
||||
if buffers is not None:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_kv_cache_num_segments_by_layer() -> None:
|
||||
"""Segment counts follow the physical layout of each layer's backend."""
|
||||
spec = _full_attention_spec()
|
||||
for backend, expected in (
|
||||
(_SplitKVBackend, 2),
|
||||
(_BlockMajorBackend, 1),
|
||||
# kv-first logical shape but block-major physical order -> 1 segment.
|
||||
(_StrideOrderBackend, 1),
|
||||
):
|
||||
kv_cache_config, attn_groups = _attention_config(spec, backend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
assert runner._kv_cache_num_segments_by_layer() == {"layer.0": expected}
|
||||
|
||||
|
||||
def test_extensible_split_layout_grows_both_halves() -> None:
|
||||
"""A K/V-split layer keeps its natural layout and both halves grow in
|
||||
lockstep."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
assert kv_cache.shape == (2, NUM_BLOCKS, BLOCK_SIZE, 8, 128)
|
||||
[(buffer, bytes_per_block_per_segment)] = runner.extensible_kv_buffers.buffers
|
||||
assert buffer.num_segments == 2
|
||||
assert bytes_per_block_per_segment == spec.page_size_bytes // 2
|
||||
|
||||
# Only block 0 is committed -- in each half.
|
||||
kv_cache[0, 0].fill_(1) # K, block 0
|
||||
kv_cache[1, 0].fill_(2) # V, block 0
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
# Old data survives the grow; new blocks are usable in both halves and
|
||||
# zeroed.
|
||||
assert torch.all(kv_cache[0, 0] == 1)
|
||||
assert torch.all(kv_cache[1, 0] == 2)
|
||||
kv_cache[0, NUM_BLOCKS - 1].fill_(3)
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(4)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0, NUM_BLOCKS - 1] == 3)
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.count_nonzero(kv_cache[:, 1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
def test_extensible_block_major_layout() -> None:
|
||||
"""A layer whose physical layout is block-major uses a single segment."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _BlockMajorBackend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
assert kv_cache.shape == (NUM_BLOCKS, 2, BLOCK_SIZE, 8, 128)
|
||||
[(buffer, bytes_per_block_per_segment)] = runner.extensible_kv_buffers.buffers
|
||||
assert buffer.num_segments == 1
|
||||
assert bytes_per_block_per_segment == spec.page_size_bytes
|
||||
|
||||
kv_cache[0].fill_(1)
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
kv_cache[NUM_BLOCKS - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0] == 1)
|
||||
assert torch.all(kv_cache[NUM_BLOCKS - 1] == 2)
|
||||
assert torch.count_nonzero(kv_cache[1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
def test_legacy_split_layout_commits_everything() -> None:
|
||||
"""Without `extensible`, the full buffer is committed up front."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(kv_cache_config, extensible=False)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
kv_cache[0, NUM_BLOCKS - 1].fill_(1)
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0, NUM_BLOCKS - 1] == 1)
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 2)
|
||||
with pytest.raises(RuntimeError, match="extensible"):
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
|
||||
|
||||
def test_extensible_mamba_grows_per_layer() -> None:
|
||||
"""Mamba per-layer buffers are block-major and grow with the KV cache."""
|
||||
spec = _mamba_spec()
|
||||
num_blocks = 512
|
||||
layer_names = ["mamba.0", "mamba.1"]
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=num_blocks * spec.page_size_bytes, shared_by=[name])
|
||||
for name in layer_names
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=layer_names, kv_cache_spec=spec)],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=layer_names,
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
]
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
assert set(kv_caches) == set(layer_names)
|
||||
assert len(runner.extensible_kv_buffers.buffers) == len(layer_names)
|
||||
for buffer, bytes_per_block_per_segment in runner.extensible_kv_buffers.buffers:
|
||||
assert buffer.num_segments == 1
|
||||
assert bytes_per_block_per_segment == spec.page_size_bytes
|
||||
|
||||
# Write block 0 of every state of every layer (the committed
|
||||
# prefixes), then grow.
|
||||
for name in layer_names:
|
||||
for state_tensor in kv_caches[name]:
|
||||
state_tensor[0].fill_(1)
|
||||
torch.accelerator.synchronize()
|
||||
runner.extend_kv_cache(num_blocks)
|
||||
for name in layer_names:
|
||||
for state_tensor in kv_caches[name]:
|
||||
state_tensor[num_blocks - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
for name in layer_names:
|
||||
for state_tensor in kv_caches[name]:
|
||||
assert torch.all(state_tensor[0] == 1)
|
||||
assert torch.all(state_tensor[num_blocks - 1] == 2)
|
||||
assert torch.count_nonzero(state_tensor[1 : num_blocks - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
def test_extensible_hybrid_attention_mamba() -> None:
|
||||
"""In hybrid models the attention cache is re-strided to block-major, so
|
||||
its buffer must use a single segment."""
|
||||
attn_spec = _full_attention_spec()
|
||||
mamba_spec = _mamba_spec()
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * attn_spec.page_size_bytes, shared_by=["attn.0"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * mamba_spec.page_size_bytes, shared_by=["mamba.0"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(layer_names=["attn.0"], kv_cache_spec=attn_spec),
|
||||
KVCacheGroupSpec(layer_names=["mamba.0"], kv_cache_spec=mamba_spec),
|
||||
],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_SplitKVBackend,
|
||||
layer_names=["attn.0"],
|
||||
kv_cache_spec=attn_spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
],
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=["mamba.0"],
|
||||
kv_cache_spec=mamba_spec,
|
||||
kv_cache_group_id=1,
|
||||
)
|
||||
],
|
||||
]
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
# The K/V-split attention layer is forced to one segment by the hybrid
|
||||
# block-major re-stride.
|
||||
assert runner._kv_cache_num_segments_by_layer() == {"attn.0": 1, "mamba.0": 1}
|
||||
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(
|
||||
raw_tensors, [BLOCK_SIZE, BLOCK_SIZE]
|
||||
)
|
||||
attn_cache = kv_caches["attn.0"]
|
||||
# `_update_hybrid_attention_mamba_layout` re-strides to interleave K/V
|
||||
# per block: block b spans one contiguous page.
|
||||
hidden_size = attn_cache.shape[2:].numel()
|
||||
assert attn_cache.stride()[:2] == (hidden_size, 2 * hidden_size)
|
||||
|
||||
attn_cache[0, 0].fill_(1) # K, block 0
|
||||
attn_cache[1, 0].fill_(2) # V, block 0
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[0].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
attn_cache[0, NUM_BLOCKS - 1].fill_(4)
|
||||
attn_cache[1, NUM_BLOCKS - 1].fill_(5)
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[NUM_BLOCKS - 1].fill_(6)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(attn_cache[0, 0] == 1)
|
||||
assert torch.all(attn_cache[1, 0] == 2)
|
||||
assert torch.all(attn_cache[0, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.all(attn_cache[1, NUM_BLOCKS - 1] == 5)
|
||||
assert torch.count_nonzero(attn_cache[:, 1 : NUM_BLOCKS - 1]) == 0
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
assert torch.all(state_tensor[0] == 3)
|
||||
assert torch.all(state_tensor[NUM_BLOCKS - 1] == 6)
|
||||
assert torch.count_nonzero(state_tensor[1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# V2 model runner (vllm.v1.worker.gpu) extensible allocation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _v2_allocate(kv_cache_config, attn_groups, kernel_block_sizes):
|
||||
flat_groups = [g for groups in attn_groups for g in groups]
|
||||
raw_tensors, buffers = _allocate_extensible_kv_cache(
|
||||
kv_cache_config,
|
||||
{},
|
||||
torch.device("cuda:0"),
|
||||
flat_groups,
|
||||
kernel_block_sizes,
|
||||
"auto",
|
||||
)
|
||||
kv_caches = _reshape_kv_cache(
|
||||
attn_groups=flat_groups,
|
||||
kv_cache_raw_tensors=raw_tensors,
|
||||
cache_dtype="auto",
|
||||
kernel_block_sizes=kernel_block_sizes,
|
||||
shared_kv_cache_layers={},
|
||||
kv_cache_config=kv_cache_config,
|
||||
)
|
||||
return kv_caches, buffers
|
||||
|
||||
|
||||
def test_v2_num_segments_by_layer() -> None:
|
||||
"""V2 segment counts follow the layer's physical layout, and hybrid
|
||||
models force block-major (one segment)."""
|
||||
spec = _full_attention_spec()
|
||||
for backend, expected in (
|
||||
(_SplitKVBackend, 2),
|
||||
(_BlockMajorBackend, 1),
|
||||
(_StrideOrderBackend, 1),
|
||||
):
|
||||
_, attn_groups = _attention_config(spec, backend)
|
||||
flat_groups = [g for groups in attn_groups for g in groups]
|
||||
assert _kv_cache_num_segments_by_layer(
|
||||
flat_groups, [BLOCK_SIZE], "auto", has_mamba=False
|
||||
) == {"layer.0": expected}
|
||||
assert _kv_cache_num_segments_by_layer(
|
||||
flat_groups, [BLOCK_SIZE], "auto", has_mamba=True
|
||||
) == {"layer.0": 1}
|
||||
|
||||
|
||||
def test_v2_extensible_split_layout_grows_incrementally() -> None:
|
||||
"""A K/V-split layer grows both halves in lockstep through the staged
|
||||
commits the V2 flow performs (init -> warmup prefix -> final size)."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
assert kv_cache.shape == (2, NUM_BLOCKS, BLOCK_SIZE, 8, 128)
|
||||
assert buffers.num_blocks_committed == 1
|
||||
|
||||
kv_cache[0, 0].fill_(1) # K, block 0
|
||||
kv_cache[1, 0].fill_(2) # V, block 0
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
# Warmup-style prefix commit, then the final post-warmup commit.
|
||||
buffers.commit(8)
|
||||
kv_cache[0, 7].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
# Shrink requests are ignored.
|
||||
buffers.commit(1)
|
||||
assert buffers.num_blocks_committed == NUM_BLOCKS
|
||||
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(4)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0, 0] == 1)
|
||||
assert torch.all(kv_cache[1, 0] == 2)
|
||||
assert torch.all(kv_cache[0, 7] == 3)
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.count_nonzero(kv_cache[:, 1:7]) == 0
|
||||
assert torch.count_nonzero(kv_cache[:, 8 : NUM_BLOCKS - 1]) == 0
|
||||
assert buffers.physical_bytes >= NUM_BLOCKS * spec.page_size_bytes
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_hybrid_attention_mamba() -> None:
|
||||
"""V2 hybrid models re-stride attention to block-major; both the
|
||||
attention and Mamba buffers grow as single-segment prefixes."""
|
||||
attn_spec = _full_attention_spec()
|
||||
mamba_spec = _mamba_spec()
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * attn_spec.page_size_bytes, shared_by=["attn.0"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * mamba_spec.page_size_bytes, shared_by=["mamba.0"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(layer_names=["attn.0"], kv_cache_spec=attn_spec),
|
||||
KVCacheGroupSpec(layer_names=["mamba.0"], kv_cache_spec=mamba_spec),
|
||||
],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_SplitKVBackend,
|
||||
layer_names=["attn.0"],
|
||||
kv_cache_spec=attn_spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
],
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=["mamba.0"],
|
||||
kv_cache_spec=mamba_spec,
|
||||
kv_cache_group_id=1,
|
||||
)
|
||||
],
|
||||
]
|
||||
kv_caches, buffers = _v2_allocate(
|
||||
kv_cache_config, attn_groups, [BLOCK_SIZE, BLOCK_SIZE]
|
||||
)
|
||||
try:
|
||||
attn_cache = kv_caches["attn.0"]
|
||||
# Re-strided to interleave K/V per block: block b spans one page.
|
||||
hidden_size = attn_cache.shape[2:].numel()
|
||||
assert attn_cache.stride()[:2] == (hidden_size, 2 * hidden_size)
|
||||
|
||||
attn_cache[0, 0].fill_(1)
|
||||
attn_cache[1, 0].fill_(2)
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[0].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
attn_cache[0, NUM_BLOCKS - 1].fill_(4)
|
||||
attn_cache[1, NUM_BLOCKS - 1].fill_(5)
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[NUM_BLOCKS - 1].fill_(6)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(attn_cache[0, 0] == 1)
|
||||
assert torch.all(attn_cache[1, 0] == 2)
|
||||
assert torch.all(attn_cache[0, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.all(attn_cache[1, NUM_BLOCKS - 1] == 5)
|
||||
assert torch.count_nonzero(attn_cache[:, 1 : NUM_BLOCKS - 1]) == 0
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
assert torch.all(state_tensor[0] == 3)
|
||||
assert torch.all(state_tensor[NUM_BLOCKS - 1] == 6)
|
||||
assert torch.count_nonzero(state_tensor[1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_packed_layout() -> None:
|
||||
"""A packed (block_stride) layout uses one shared block-major buffer;
|
||||
per-layer pages within a block stay isolated across commits."""
|
||||
spec = _full_attention_spec()
|
||||
page_bytes = spec.page_size_bytes
|
||||
block_stride = 2 * page_bytes # two layers packed per block
|
||||
layer_names = ["packed.0", "packed.1"]
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * block_stride,
|
||||
shared_by=[name],
|
||||
offset=i * page_bytes,
|
||||
block_stride=block_stride,
|
||||
)
|
||||
for i, name in enumerate(layer_names)
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=layer_names, kv_cache_spec=spec)],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=layer_names,
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
]
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
assert len(buffers.buffers) == 1
|
||||
[(buffer, bytes_per_block)] = buffers.buffers
|
||||
assert buffer.num_segments == 1
|
||||
assert bytes_per_block == block_stride
|
||||
|
||||
cache0, cache1 = kv_caches["packed.0"], kv_caches["packed.1"]
|
||||
assert cache0.shape == (NUM_BLOCKS, 2, BLOCK_SIZE, 8, 128)
|
||||
|
||||
cache0[0].fill_(1)
|
||||
cache1[0].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
cache0[NUM_BLOCKS - 1].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(cache0[0] == 1)
|
||||
assert torch.all(cache1[0] == 2)
|
||||
assert torch.all(cache0[NUM_BLOCKS - 1] == 3)
|
||||
# The other layer's page of the same block is untouched, and all
|
||||
# middle blocks were zeroed on commit.
|
||||
assert torch.count_nonzero(cache1[1:]) == 0
|
||||
assert torch.count_nonzero(cache0[1 : NUM_BLOCKS - 1]) == 0
|
||||
|
||||
committed = NUM_BLOCKS // 2
|
||||
narrowed = narrow_kv_caches_to_num_blocks(
|
||||
kv_caches,
|
||||
[g for groups in attn_groups for g in groups],
|
||||
[BLOCK_SIZE],
|
||||
"auto",
|
||||
committed,
|
||||
kv_cache_config,
|
||||
)
|
||||
narrowed0 = narrowed["packed.0"]
|
||||
narrowed1 = narrowed["packed.1"]
|
||||
assert narrowed0.untyped_storage().data_ptr() == buffer.base_ptr
|
||||
assert (
|
||||
narrowed0.untyped_storage().data_ptr()
|
||||
== narrowed1.untyped_storage().data_ptr()
|
||||
)
|
||||
assert narrowed0.untyped_storage().nbytes() == committed * block_stride
|
||||
assert narrowed0.stride() == cache0.stride()
|
||||
assert narrowed1.stride() == cache1.stride()
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_release_and_recommit() -> None:
|
||||
"""Sleep/wake cycle: release_physical discards data but keeps VA and
|
||||
views valid; recommit restores the committed size with zeroed pages."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
base_ptr = buffers.buffers[0][0].base_ptr
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
kv_cache.fill_(7)
|
||||
torch.accelerator.synchronize()
|
||||
assert buffers.physical_bytes > 0
|
||||
|
||||
buffers.release_physical()
|
||||
assert buffers.physical_bytes == 0
|
||||
assert buffers.num_blocks_committed == 0
|
||||
|
||||
buffers.recommit()
|
||||
assert buffers.num_blocks_committed == NUM_BLOCKS
|
||||
assert buffers.buffers[0][0].base_ptr == base_ptr
|
||||
torch.accelerator.synchronize()
|
||||
# Data was discarded; fresh pages are zeroed and writable through
|
||||
# the original views.
|
||||
assert torch.count_nonzero(kv_cache) == 0
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(9)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 9)
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_connector_sleep_fails_before_remapping() -> None:
|
||||
"""Connector registrations must not survive physical-page replacement."""
|
||||
worker = object.__new__(Worker)
|
||||
worker.model_runner = SimpleNamespace(extensible_kv_buffers=object())
|
||||
worker.vllm_config = SimpleNamespace(kv_transfer_config=object())
|
||||
|
||||
with pytest.raises(RuntimeError, match="invalidates.*memory registration"):
|
||||
worker.sleep()
|
||||
|
||||
|
||||
def test_v2_narrow_kv_caches_to_num_blocks() -> None:
|
||||
"""Connector-registration views are trimmed to the committed block count
|
||||
along each layout's block dim, keeping base pointers and strides (so the
|
||||
K and V segment prefixes are addressed exactly)."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
committed = 16
|
||||
buffers.commit(committed)
|
||||
narrowed = narrow_kv_caches_to_num_blocks(
|
||||
kv_caches,
|
||||
[g for groups in attn_groups for g in groups],
|
||||
[BLOCK_SIZE],
|
||||
"auto",
|
||||
committed,
|
||||
kv_cache_config,
|
||||
)
|
||||
full = kv_caches["layer.0"]
|
||||
trimmed = narrowed["layer.0"]
|
||||
assert trimmed.shape == (2, committed, BLOCK_SIZE, 8, 128)
|
||||
assert trimmed.stride() == full.stride()
|
||||
# K prefix starts at the buffer base; V prefix at the segment offset.
|
||||
assert trimmed[0].data_ptr() == full[0].data_ptr()
|
||||
assert trimmed[1].data_ptr() == full[1].data_ptr()
|
||||
# The narrowed views cover only committed memory.
|
||||
trimmed[0, committed - 1].fill_(1)
|
||||
trimmed[1, committed - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(full[0, committed - 1] == 1)
|
||||
assert torch.all(full[1, committed - 1] == 2)
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_defragment_on_commit() -> None:
|
||||
"""commit(defragment=True) re-maps each segment prefix as ONE physical
|
||||
chunk (required for KV-transfer registration), discarding prior data."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
# Staged commits spanning multiple VMM granules -> multiple physical
|
||||
# chunks per segment.
|
||||
buffers.commit(8)
|
||||
buffers.commit(NUM_BLOCKS // 2)
|
||||
kv_cache[0, 0].fill_(1)
|
||||
torch.accelerator.synchronize()
|
||||
[(buffer, _)] = buffers.buffers
|
||||
assert len(buffer._buffer._handles) > 2
|
||||
|
||||
buffers.commit(NUM_BLOCKS, defragment=True)
|
||||
# One chunk per segment; data discarded (zeroed); views still work.
|
||||
assert len(buffer._buffer._handles) == 2
|
||||
assert buffers.num_blocks_committed == NUM_BLOCKS
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.count_nonzero(kv_cache) == 0
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 3)
|
||||
finally:
|
||||
buffers.free()
|
||||
@@ -9,6 +9,7 @@ session is active. These tests verify that delegation and the session guard.
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.config import VllmConfig, get_current_vllm_config
|
||||
from vllm.v1.worker.gpu_worker import Worker
|
||||
|
||||
|
||||
@@ -21,29 +22,55 @@ class _RecordingEngine:
|
||||
self.finished = False
|
||||
self.reset_count = 0
|
||||
self.update_calls: list[dict] = []
|
||||
self.seen_configs: list[VllmConfig] = []
|
||||
|
||||
def _record_config(self) -> None:
|
||||
self.seen_configs.append(get_current_vllm_config())
|
||||
|
||||
def start_weight_update(self) -> None:
|
||||
self._record_config()
|
||||
self.started = True
|
||||
|
||||
def update_weights(self, update_info: dict) -> None:
|
||||
self._record_config()
|
||||
self.update_calls.append(update_info)
|
||||
if self.raise_on_update:
|
||||
raise ValueError("boom")
|
||||
|
||||
def finish_weight_update(self) -> None:
|
||||
self._record_config()
|
||||
self.finished = True
|
||||
|
||||
def reset_weight_update_target(self) -> None:
|
||||
self.reset_count += 1
|
||||
|
||||
|
||||
class _RecordingModelRunner:
|
||||
def __init__(self) -> None:
|
||||
self.seen_config: VllmConfig | None = None
|
||||
|
||||
def reload_weights(self) -> None:
|
||||
self.seen_config = get_current_vllm_config()
|
||||
|
||||
|
||||
def _make_worker(engine: _RecordingEngine | None) -> Worker:
|
||||
worker = object.__new__(Worker)
|
||||
worker.vllm_config = VllmConfig()
|
||||
worker.weight_transfer_engine = engine
|
||||
worker._weight_update_active = False
|
||||
return worker
|
||||
|
||||
|
||||
def test_reload_weights_sets_current_config():
|
||||
worker = _make_worker(None)
|
||||
model_runner = _RecordingModelRunner()
|
||||
worker.model_runner = model_runner # type: ignore[assignment]
|
||||
|
||||
Worker.reload_weights(worker)
|
||||
|
||||
assert model_runner.seen_config is worker.vllm_config
|
||||
|
||||
|
||||
def test_start_update_finish_delegates_to_engine():
|
||||
engine = _RecordingEngine()
|
||||
worker = _make_worker(engine)
|
||||
@@ -60,6 +87,7 @@ def test_start_update_finish_delegates_to_engine():
|
||||
assert engine.finished is True
|
||||
assert engine.reset_count == 1
|
||||
assert worker._weight_update_active is False
|
||||
assert engine.seen_configs == [worker.vllm_config] * 3
|
||||
|
||||
|
||||
def test_double_start_raises():
|
||||
|
||||
@@ -2070,6 +2070,93 @@ def selective_scan_fwd(
|
||||
)
|
||||
|
||||
|
||||
def causal_conv1d_update_cpu_vec(
|
||||
x: torch.Tensor,
|
||||
conv_state: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
activation: str | None = None,
|
||||
conv_state_indices: torch.Tensor | None = None,
|
||||
query_start_loc: torch.Tensor | None = None,
|
||||
pad_slot_id: int = 0,
|
||||
) -> torch.Tensor:
|
||||
return torch.ops._C.causal_conv1d_update_cpu_vec(
|
||||
x,
|
||||
conv_state,
|
||||
weight,
|
||||
bias,
|
||||
activation,
|
||||
conv_state_indices,
|
||||
query_start_loc,
|
||||
pad_slot_id,
|
||||
)
|
||||
|
||||
|
||||
def selective_state_update_cpu(
|
||||
state: torch.Tensor,
|
||||
x: torch.Tensor,
|
||||
dt: torch.Tensor,
|
||||
A: torch.Tensor,
|
||||
B: torch.Tensor,
|
||||
C: torch.Tensor,
|
||||
D: torch.Tensor | None,
|
||||
z: torch.Tensor | None,
|
||||
dt_bias: torch.Tensor | None,
|
||||
dt_softplus: bool,
|
||||
state_batch_indices: torch.Tensor | None,
|
||||
dst_state_batch_indices: torch.Tensor | None,
|
||||
null_block_id: int,
|
||||
out: torch.Tensor,
|
||||
num_accepted_tokens: torch.Tensor | None,
|
||||
cu_seqlens: torch.Tensor | None,
|
||||
):
|
||||
torch.ops._C.selective_state_update_cpu(
|
||||
state,
|
||||
x,
|
||||
dt,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
z,
|
||||
dt_bias,
|
||||
dt_softplus,
|
||||
state_batch_indices,
|
||||
dst_state_batch_indices,
|
||||
null_block_id,
|
||||
out,
|
||||
num_accepted_tokens,
|
||||
cu_seqlens,
|
||||
)
|
||||
|
||||
|
||||
def mamba_chunk_scan_fwd_cpu(
|
||||
out: torch.Tensor,
|
||||
final_states: torch.Tensor,
|
||||
x: torch.Tensor,
|
||||
dt: torch.Tensor,
|
||||
A: torch.Tensor,
|
||||
B: torch.Tensor,
|
||||
C: torch.Tensor,
|
||||
D: torch.Tensor | None,
|
||||
z: torch.Tensor | None,
|
||||
cu_seqlens: torch.Tensor,
|
||||
) -> None:
|
||||
"""Prefill SSM scan kernel. out and final_states are written in-place."""
|
||||
torch.ops._C.mamba_chunk_scan_fwd_cpu(
|
||||
out,
|
||||
final_states,
|
||||
x,
|
||||
dt,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
z,
|
||||
cu_seqlens,
|
||||
)
|
||||
|
||||
|
||||
# ROCm skinny gemms
|
||||
def LLMM1(a: torch.Tensor, b: torch.Tensor, rows_per_block: int) -> torch.Tensor:
|
||||
return torch.ops._rocm_C.LLMM1(a, b, rows_per_block)
|
||||
|
||||
@@ -219,6 +219,63 @@ def _xpu_ops_deepseek_scaling_rope_fake(
|
||||
return query, key
|
||||
|
||||
|
||||
def _xpu_fp8_bmm_impl(
|
||||
a: torch.Tensor,
|
||||
b: torch.Tensor,
|
||||
out_dtype: torch.dtype,
|
||||
a_scale: torch.Tensor,
|
||||
b_scale: torch.Tensor,
|
||||
bias: torch.Tensor | None,
|
||||
) -> torch.Tensor:
|
||||
"""XPU FP8 batched GEMM implementation for ``torch.ops.vllm.xpu_fp8_bmm``.
|
||||
|
||||
Computes batched matrix multiplication over the leading group dimension:
|
||||
``[G, M, K] @ [G, K, N] -> [G, M, N]``.
|
||||
|
||||
Args:
|
||||
a: FP8 activation tensor with shape ``[G, M, K]``.
|
||||
Does not need to be contiguous.
|
||||
b: FP8 weight tensor with shape ``[G, K, N]``.
|
||||
Does not need to be contiguous.
|
||||
out_dtype: Output dtype accepted by the kernel (typically
|
||||
``torch.bfloat16`` for the DeepSeek-V4 O-proj path).
|
||||
a_scale: Activation scale tensor for ``a``.
|
||||
In current DeepSeek-V4 XPU usage it is block-scaled with shape
|
||||
``[G, M, K/bs]`` (``bs`` is the quant block size, e.g. 128).
|
||||
Must be contiguous.
|
||||
b_scale: Weight scale tensor for ``b``.
|
||||
In current DeepSeek-V4 XPU usage it is block-scaled with shape
|
||||
``[G, K/bs, N/bs]`` (``bs`` is the quant block size, e.g. 128).
|
||||
Must be contiguous.
|
||||
bias: Optional bias tensor. Pass ``None`` when no bias is required.
|
||||
|
||||
Returns:
|
||||
Output tensor with shape ``[G, M, N]`` and dtype ``out_dtype``.
|
||||
|
||||
Notes:
|
||||
This implementation centralizes access to
|
||||
``torch.ops._xpu_C.fp8_bmm``. Both scales must be contiguous, while
|
||||
``a`` and ``b`` may be non-contiguous views.
|
||||
"""
|
||||
return torch.ops._xpu_C.fp8_bmm(a, b, out_dtype, a_scale, b_scale, bias)
|
||||
|
||||
|
||||
def _xpu_fp8_bmm_fake(
|
||||
a: torch.Tensor,
|
||||
b: torch.Tensor,
|
||||
out_dtype: torch.dtype,
|
||||
a_scale: torch.Tensor,
|
||||
b_scale: torch.Tensor,
|
||||
bias: torch.Tensor | None,
|
||||
) -> torch.Tensor:
|
||||
# [G, M, K] @ [G, K, N] => [G, M, N]
|
||||
return torch.empty(
|
||||
(a.shape[0], a.shape[1], b.shape[2]),
|
||||
dtype=out_dtype,
|
||||
device=a.device,
|
||||
)
|
||||
|
||||
|
||||
def _xpu_fp8_mqa_logits_impl(
|
||||
q: torch.Tensor,
|
||||
k_quant: torch.Tensor,
|
||||
@@ -1053,6 +1110,12 @@ class xpu_ops:
|
||||
fake_impl=_xpu_mxfp4_quantize_fake,
|
||||
)
|
||||
|
||||
direct_register_custom_op(
|
||||
op_name="xpu_fp8_bmm",
|
||||
op_func=_xpu_fp8_bmm_impl,
|
||||
fake_impl=_xpu_fp8_bmm_fake,
|
||||
)
|
||||
|
||||
direct_register_custom_op(
|
||||
op_name="xpu_fp8_mqa_logits",
|
||||
op_func=_xpu_fp8_mqa_logits_impl,
|
||||
|
||||
@@ -177,6 +177,18 @@ class CacheConfig:
|
||||
gpu_memory_utilization. Note that kv_cache_memory_bytes
|
||||
(when not-None) ignores gpu_memory_utilization"""
|
||||
|
||||
enable_extensible_kv_cache: bool = False
|
||||
"""Use driver virtual memory to reserve the KV cache address range up
|
||||
front, run warmup and CUDA graph capture with only a small block prefix
|
||||
physically committed, and commit the final size afterwards.
|
||||
|
||||
This makes automatic KV sizing account for the memory that warmup and
|
||||
CUDA graph capture actually consume (including worst-case activation
|
||||
working sets, e.g. with speculative decoding), and avoids warmup-time
|
||||
OOMs. Requires driver VMM support (CUDA or ROCm; falls back to standard
|
||||
allocation with a warning where unavailable, e.g. WSL2).
|
||||
"""
|
||||
|
||||
kv_offloading_size: float | None = None
|
||||
"""Size of the KV cache offloading buffer in GiB. When TP > 1, this is
|
||||
the total buffer size summed across all TP ranks. By default, this is set
|
||||
@@ -222,6 +234,8 @@ class CacheConfig:
|
||||
"kv_cache_max_concurrency",
|
||||
# WIP feature toggle not impacting compiled graph shape
|
||||
"kv_sharing_fast_prefill",
|
||||
# Runtime memory allocation strategy, not graph structure.
|
||||
"enable_extensible_kv_cache",
|
||||
}
|
||||
|
||||
from vllm.config.utils import get_hash_factors, hash_factors
|
||||
|
||||
@@ -27,6 +27,7 @@ class MambaBackendEnum(Enum, metaclass=_MambaBackendEnumMeta):
|
||||
|
||||
TRITON = "triton"
|
||||
FLASHINFER = "flashinfer"
|
||||
CPU = "cpu"
|
||||
|
||||
|
||||
@config
|
||||
|
||||
+17
-1
@@ -3,9 +3,12 @@
|
||||
|
||||
from typing import Any, Literal, get_args
|
||||
|
||||
from pydantic import model_validator
|
||||
from pydantic_core import ArgsKwargs
|
||||
|
||||
from vllm.config.utils import config
|
||||
from vllm.logger import init_logger
|
||||
from vllm.tasks import PoolingTask
|
||||
from vllm.tasks import PoolingTask, check_removed_pooling_task
|
||||
from vllm.utils.hashing import safe_hash
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -112,6 +115,19 @@ class PoolerConfig:
|
||||
`math-shepherd-mistral-7b-prm` model.
|
||||
"""
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def reject_removed_parameters(cls, data):
|
||||
values = data.kwargs if isinstance(data, ArgsKwargs) else data
|
||||
if not isinstance(values, dict):
|
||||
return data
|
||||
if "normalize" in values:
|
||||
raise ValueError(
|
||||
"Parameter `normalize` was removed; use `use_activation` instead."
|
||||
)
|
||||
check_removed_pooling_task(values.get("task"))
|
||||
return data
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.logit_sigma is not None and self.logit_sigma == 0:
|
||||
raise ValueError("logit_sigma cannot be 0 (division by zero)")
|
||||
|
||||
@@ -10,8 +10,8 @@ from cutlass.cutlass_dsl import dsl_user_op
|
||||
|
||||
NVVM_CTA_GROUP_MAP = [
|
||||
None,
|
||||
nvvm.Tcgen05GroupKind.CTA_1,
|
||||
nvvm.Tcgen05GroupKind.CTA_2,
|
||||
nvvm.CTAGroupKind.CTA_1,
|
||||
nvvm.CTAGroupKind.CTA_2,
|
||||
]
|
||||
LDST_MAP = {
|
||||
"32x32b": (nvvm.Tcgen05LdStShape.SHAPE_32X32B, 1),
|
||||
@@ -136,7 +136,7 @@ def commit(mbar, cta_mask=None, cta_group: int = 1, *, loc=None, ip=None):
|
||||
group = NVVM_CTA_GROUP_MAP[cta_group]
|
||||
if cutlass.const_expr(cta_mask is not None):
|
||||
with cute.arch.elect_one():
|
||||
nvvm.tcgen05_commit_arrive(
|
||||
nvvm.tcgen05_commit(
|
||||
mbar_llvm,
|
||||
multicast_mask=cta_mask.ir_value(loc=loc, ip=ip),
|
||||
group=group,
|
||||
@@ -145,7 +145,7 @@ def commit(mbar, cta_mask=None, cta_group: int = 1, *, loc=None, ip=None):
|
||||
)
|
||||
else:
|
||||
with cute.arch.elect_one():
|
||||
nvvm.tcgen05_commit_arrive(mbar_llvm, group=group, loc=loc, ip=ip)
|
||||
nvvm.tcgen05_commit(mbar_llvm, group=group, loc=loc, ip=ip)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
|
||||
@@ -255,6 +255,14 @@ class KVConnectorBase_V1(ABC):
|
||||
|
||||
Args:
|
||||
kv_caches: dictionary of layer names, kv cache
|
||||
|
||||
Note:
|
||||
The views' shapes/strides/numel are the authoritative source of
|
||||
the KV cache geometry; do not derive block sizes or extents from
|
||||
`untyped_storage().nbytes()`. With the extensible KV cache, the
|
||||
underlying storage spans the reserved virtual-address capacity,
|
||||
of which only each view's per-segment block prefix is physically
|
||||
committed (and safe to access or register).
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
@@ -1935,13 +1935,8 @@ class NixlBaseConnectorWorker:
|
||||
indices = torch.tensor(block_ids, device=self.device_type, dtype=torch.long)
|
||||
|
||||
for _, cache_or_caches in self.device_kv_caches.items():
|
||||
blocks_to_update = cache_or_caches.index_select(1, indices)
|
||||
current_platform.pack_kv_cache(
|
||||
key=blocks_to_update[0],
|
||||
value=blocks_to_update[1],
|
||||
key_cache=cache_or_caches[0],
|
||||
value_cache=cache_or_caches[1],
|
||||
block_ids=block_ids,
|
||||
kv_cache=cache_or_caches,
|
||||
indices=indices,
|
||||
)
|
||||
|
||||
|
||||
@@ -400,13 +400,10 @@ class GroupCoordinator:
|
||||
self.rank = torch.distributed.get_rank()
|
||||
self.local_rank = local_rank
|
||||
self.device_index: int
|
||||
if _WORLD is not None:
|
||||
self.device_index = _WORLD.device_index
|
||||
else:
|
||||
assert local_rank >= 0, (
|
||||
"local_rank must be provided when creating the world group"
|
||||
)
|
||||
self.device_index = local_rank
|
||||
assert local_rank >= 0, (
|
||||
"local_rank must be provided when creating the world group"
|
||||
)
|
||||
self.device_index = local_rank
|
||||
|
||||
self_device_group = None
|
||||
self_cpu_group = None
|
||||
|
||||
@@ -525,6 +525,7 @@ class EngineArgs:
|
||||
offload_params: set[str] = get_field(PrefetchOffloadConfig, "offload_params")
|
||||
gpu_memory_utilization: float = CacheConfig.gpu_memory_utilization
|
||||
kv_cache_memory_bytes: int | None = CacheConfig.kv_cache_memory_bytes
|
||||
enable_extensible_kv_cache: bool = CacheConfig.enable_extensible_kv_cache
|
||||
max_num_batched_tokens: int | None = None
|
||||
max_num_scheduled_tokens: int | None = None
|
||||
max_num_partial_prefills: int = SchedulerConfig.max_num_partial_prefills
|
||||
@@ -1165,6 +1166,10 @@ class EngineArgs:
|
||||
cache_group.add_argument(
|
||||
"--kv-cache-memory-bytes", **cache_kwargs["kv_cache_memory_bytes"]
|
||||
)
|
||||
cache_group.add_argument(
|
||||
"--enable-extensible-kv-cache",
|
||||
**cache_kwargs["enable_extensible_kv_cache"],
|
||||
)
|
||||
cache_group.add_argument("--kv-cache-dtype", **cache_kwargs["cache_dtype"])
|
||||
cache_group.add_argument(
|
||||
"--num-gpu-blocks-override", **cache_kwargs["num_gpu_blocks_override"]
|
||||
@@ -1905,6 +1910,7 @@ class EngineArgs:
|
||||
block_size=self.block_size, # type: ignore[arg-type]
|
||||
gpu_memory_utilization=self.gpu_memory_utilization,
|
||||
kv_cache_memory_bytes=self.kv_cache_memory_bytes,
|
||||
enable_extensible_kv_cache=self.enable_extensible_kv_cache,
|
||||
cache_dtype=resolved_cache_dtype, # type: ignore[arg-type]
|
||||
is_attention_free=model_config.is_attention_free,
|
||||
num_gpu_blocks_override=self.num_gpu_blocks_override,
|
||||
|
||||
@@ -1,11 +1,68 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from vllm.benchmarks.serve import add_cli_args, main
|
||||
from vllm.benchmarks.serve import add_cli_args
|
||||
from vllm.benchmarks.serve import main as python_main
|
||||
from vllm.entrypoints.cli.benchmark.base import BenchmarkSubcommandBase
|
||||
from vllm.logger import init_logger
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
logger = init_logger(__name__)
|
||||
_RUST_CLI_PATH = Path(__file__).resolve().parents[3] / "vllm-rs"
|
||||
_RUST_SUPPORTED_DATASETS = frozenset(
|
||||
{
|
||||
"custom",
|
||||
"hf",
|
||||
"prefix_repetition",
|
||||
"random",
|
||||
"random-mm",
|
||||
"random-rerank",
|
||||
"sharegpt",
|
||||
"sonnet",
|
||||
"speed_bench",
|
||||
}
|
||||
)
|
||||
_RUST_SUPPORTED_BACKENDS = frozenset(
|
||||
{
|
||||
"openai",
|
||||
"openai-chat",
|
||||
"openai-embeddings",
|
||||
"openai-embeddings-chat",
|
||||
"vllm",
|
||||
"vllm-pooling",
|
||||
"vllm-rerank",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _rust_unsupported_reason(args: argparse.Namespace) -> str | None:
|
||||
if args.dataset_name not in _RUST_SUPPORTED_DATASETS:
|
||||
return f"dataset {args.dataset_name!r} is not supported by the Rust benchmark"
|
||||
if args.backend not in _RUST_SUPPORTED_BACKENDS:
|
||||
return f"backend {args.backend!r} is not supported by the Rust benchmark"
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_exec_rust_bench(args: argparse.Namespace) -> None:
|
||||
if reason := _rust_unsupported_reason(args):
|
||||
logger.info("Using Python benchmark: %s.", reason)
|
||||
return
|
||||
|
||||
if not _RUST_CLI_PATH.is_file():
|
||||
logger.warning(
|
||||
"Rust benchmark binary not found at %s; falling back to Python.",
|
||||
_RUST_CLI_PATH,
|
||||
)
|
||||
return
|
||||
|
||||
rust_cli = str(_RUST_CLI_PATH)
|
||||
logger.info("Delegating `vllm bench serve` to Rust binary at %s.", rust_cli)
|
||||
os.execv(rust_cli, [rust_cli, "bench", "serve", *sys.argv[3:]])
|
||||
|
||||
|
||||
class BenchmarkServingSubcommand(BenchmarkSubcommandBase):
|
||||
"""The `serve` subcommand for `vllm bench`."""
|
||||
@@ -19,4 +76,5 @@ class BenchmarkServingSubcommand(BenchmarkSubcommandBase):
|
||||
|
||||
@staticmethod
|
||||
def cmd(args: argparse.Namespace) -> None:
|
||||
main(args)
|
||||
_maybe_exec_rust_bench(args)
|
||||
python_main(args)
|
||||
|
||||
@@ -119,6 +119,11 @@ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin):
|
||||
compared with using gpu_memory_utilization. Note that
|
||||
kv_cache_memory_bytes (when not-None) ignores
|
||||
gpu_memory_utilization
|
||||
enable_extensible_kv_cache: Use CUDA virtual memory to reserve the KV
|
||||
cache address range before CUDA graph capture and commit the final
|
||||
cache size after capture. Supported by V1 CUDA workers for all
|
||||
attention backends (block-major and K/V-split KV cache layouts)
|
||||
and for Mamba / linear-attention models.
|
||||
cpu_offload_gb: The size (GiB) of CPU memory to use for offloading
|
||||
the model weights. This virtually increases the GPU memory space
|
||||
you can use to hold the model weights, at the cost of CPU-GPU data
|
||||
@@ -211,6 +216,7 @@ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin):
|
||||
profiler_config: dict[str, Any] | ProfilerConfig | None = None,
|
||||
attention_config: dict[str, Any] | AttentionConfig | None = None,
|
||||
kv_cache_memory_bytes: int | None = None,
|
||||
enable_extensible_kv_cache: bool = False,
|
||||
compilation_config: int | dict[str, Any] | CompilationConfig | None = None,
|
||||
quantization_config: dict[str, Any] | QuantizationConfigArgs | None = None,
|
||||
logits_processors: list[str | type[LogitsProcessor]] | None = None,
|
||||
@@ -309,6 +315,7 @@ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin):
|
||||
seed=seed,
|
||||
gpu_memory_utilization=gpu_memory_utilization,
|
||||
kv_cache_memory_bytes=kv_cache_memory_bytes,
|
||||
enable_extensible_kv_cache=enable_extensible_kv_cache,
|
||||
cpu_offload_gb=cpu_offload_gb,
|
||||
offload_group_size=offload_group_size,
|
||||
offload_num_in_group=offload_num_in_group,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
# Adapted from
|
||||
# https://github.com/lm-sys/FastChat/blob/168ccc29d3f7edc50823016105c024fe2282732a/fastchat/protocol/openai_api_protocol.py
|
||||
import json
|
||||
import time
|
||||
from typing import Annotated, Any, ClassVar, Literal
|
||||
|
||||
@@ -14,7 +13,6 @@ from openai.types.chat.chat_completion_message import Annotation as OpenAIAnnota
|
||||
from pydantic import Field, PrivateAttr, model_serializer, model_validator
|
||||
|
||||
from vllm.config import ModelConfig
|
||||
from vllm.config.utils import replace
|
||||
from vllm.entrypoints.chat_utils import (
|
||||
ChatCompletionMessageParam,
|
||||
ChatTemplateContentFormatOption,
|
||||
@@ -24,13 +22,12 @@ from vllm.entrypoints.openai.engine.protocol import (
|
||||
DeltaMessage,
|
||||
FunctionCall,
|
||||
FunctionDefinition,
|
||||
LegacyStructuralTagResponseFormat,
|
||||
OpenAIBaseModel,
|
||||
PerRequestTimingMetrics,
|
||||
StreamOptions,
|
||||
StructuralTagResponseFormat,
|
||||
ToolCall,
|
||||
UsageInfo,
|
||||
structured_outputs_from_response_format,
|
||||
validate_structural_tag_response_format,
|
||||
validate_structured_outputs_structural_tag,
|
||||
)
|
||||
@@ -607,6 +604,13 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
||||
include_stop_str_in_output=self.include_stop_str_in_output,
|
||||
)
|
||||
|
||||
def extract_structured_outputs(self) -> StructuredOutputsParams | None:
|
||||
"""Normalize request constraints into ``StructuredOutputsParams``."""
|
||||
return structured_outputs_from_response_format(
|
||||
self.structured_outputs,
|
||||
self.response_format,
|
||||
)
|
||||
|
||||
def to_sampling_params(
|
||||
self,
|
||||
max_tokens: int,
|
||||
@@ -651,38 +655,6 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
||||
if prompt_logprobs is None and self.echo:
|
||||
prompt_logprobs = self.top_logprobs
|
||||
|
||||
response_format = self.response_format
|
||||
if response_format is not None:
|
||||
structured_outputs_kwargs = dict[str, Any]()
|
||||
|
||||
# Set structured output params for response format
|
||||
if response_format.type == "json_object":
|
||||
structured_outputs_kwargs["json_object"] = True
|
||||
elif response_format.type == "json_schema":
|
||||
json_schema = response_format.json_schema
|
||||
assert json_schema is not None
|
||||
structured_outputs_kwargs["json"] = json_schema.json_schema
|
||||
elif response_format.type == "structural_tag":
|
||||
structural_tag = response_format
|
||||
assert structural_tag is not None and isinstance(
|
||||
structural_tag,
|
||||
(
|
||||
LegacyStructuralTagResponseFormat,
|
||||
StructuralTagResponseFormat,
|
||||
),
|
||||
)
|
||||
s_tag_obj = structural_tag.model_dump(by_alias=True)
|
||||
structured_outputs_kwargs["structural_tag"] = json.dumps(s_tag_obj)
|
||||
|
||||
# If structured outputs wasn't already enabled,
|
||||
# we must enable it for these features to work
|
||||
if len(structured_outputs_kwargs) > 0:
|
||||
self.structured_outputs = (
|
||||
StructuredOutputsParams(**structured_outputs_kwargs)
|
||||
if self.structured_outputs is None
|
||||
else replace(self.structured_outputs, **structured_outputs_kwargs)
|
||||
)
|
||||
|
||||
extra_args: dict[str, Any] = self.vllm_xargs if self.vllm_xargs else {}
|
||||
if self.kv_transfer_params:
|
||||
# Pass in kv_transfer_params via extra_args
|
||||
@@ -718,7 +690,7 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
||||
output_kind=RequestOutputKind.DELTA
|
||||
if self.stream
|
||||
else RequestOutputKind.FINAL_ONLY,
|
||||
structured_outputs=self.structured_outputs,
|
||||
structured_outputs=self.extract_structured_outputs(),
|
||||
logit_bias=self.logit_bias,
|
||||
bad_words=self.bad_words,
|
||||
thinking_token_budget=self.thinking_token_budget,
|
||||
@@ -785,6 +757,18 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
||||
parameter="logprob_token_ids",
|
||||
)
|
||||
|
||||
# These fields are integers, but `mode="before"` runs on the raw
|
||||
# request data, so a non-numeric value (e.g. a JSON string) would
|
||||
# reach the comparisons below and raise TypeError -> HTTP 500. Reject
|
||||
# it here so the client gets a clean 400 instead.
|
||||
for field_name in ("prompt_logprobs", "top_logprobs"):
|
||||
field_value = data.get(field_name)
|
||||
if field_value is not None and not isinstance(field_value, (int, float)):
|
||||
raise VLLMValidationError(
|
||||
f"`{field_name}` must be an integer.",
|
||||
parameter=field_name,
|
||||
value=field_value,
|
||||
)
|
||||
if (prompt_logprobs := data.get("prompt_logprobs")) is not None:
|
||||
if data.get("stream") and (prompt_logprobs > 0 or prompt_logprobs == -1):
|
||||
raise VLLMValidationError(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
# Adapted from
|
||||
# https://github.com/lm-sys/FastChat/blob/168ccc29d3f7edc50823016105c024fe2282732a/fastchat/protocol/openai_api_protocol.py
|
||||
import json
|
||||
import time
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
@@ -11,15 +10,13 @@ from pydantic import Field, model_validator
|
||||
|
||||
import vllm.envs as envs
|
||||
from vllm.config import ModelConfig
|
||||
from vllm.config.utils import replace
|
||||
from vllm.entrypoints.openai.engine.protocol import (
|
||||
AnyResponseFormat,
|
||||
LegacyStructuralTagResponseFormat,
|
||||
OpenAIBaseModel,
|
||||
PerRequestTimingMetrics,
|
||||
StreamOptions,
|
||||
StructuralTagResponseFormat,
|
||||
UsageInfo,
|
||||
structured_outputs_from_response_format,
|
||||
validate_structural_tag_response_format,
|
||||
validate_structured_outputs_structural_tag,
|
||||
)
|
||||
@@ -281,6 +278,13 @@ class CompletionRequest(OpenAIBaseModel):
|
||||
include_stop_str_in_output=self.include_stop_str_in_output,
|
||||
)
|
||||
|
||||
def extract_structured_outputs(self) -> StructuredOutputsParams | None:
|
||||
"""Normalize request constraints into ``StructuredOutputsParams``."""
|
||||
return structured_outputs_from_response_format(
|
||||
self.structured_outputs,
|
||||
self.response_format,
|
||||
)
|
||||
|
||||
def to_sampling_params(
|
||||
self,
|
||||
max_tokens: int,
|
||||
@@ -330,38 +334,6 @@ class CompletionRequest(OpenAIBaseModel):
|
||||
|
||||
echo_without_generation = self.echo and self.max_tokens == 0
|
||||
|
||||
response_format = self.response_format
|
||||
if response_format is not None:
|
||||
structured_outputs_kwargs = dict[str, Any]()
|
||||
|
||||
# Set structured output params for response format
|
||||
if response_format.type == "json_object":
|
||||
structured_outputs_kwargs["json_object"] = True
|
||||
elif response_format.type == "json_schema":
|
||||
json_schema = response_format.json_schema
|
||||
assert json_schema is not None
|
||||
structured_outputs_kwargs["json"] = json_schema.json_schema
|
||||
elif response_format.type == "structural_tag":
|
||||
structural_tag = response_format
|
||||
assert isinstance(
|
||||
structural_tag,
|
||||
(
|
||||
LegacyStructuralTagResponseFormat,
|
||||
StructuralTagResponseFormat,
|
||||
),
|
||||
)
|
||||
s_tag_obj = structural_tag.model_dump(by_alias=True)
|
||||
structured_outputs_kwargs["structural_tag"] = json.dumps(s_tag_obj)
|
||||
|
||||
# If structured outputs wasn't already enabled,
|
||||
# we must enable it for these features to work
|
||||
if len(structured_outputs_kwargs) > 0:
|
||||
self.structured_outputs = (
|
||||
StructuredOutputsParams(**structured_outputs_kwargs)
|
||||
if self.structured_outputs is None
|
||||
else replace(self.structured_outputs, **structured_outputs_kwargs)
|
||||
)
|
||||
|
||||
extra_args: dict[str, Any] = self.vllm_xargs if self.vllm_xargs else {}
|
||||
if self.kv_transfer_params:
|
||||
# Pass in kv_transfer_params via extra_args
|
||||
@@ -393,7 +365,7 @@ class CompletionRequest(OpenAIBaseModel):
|
||||
output_kind=RequestOutputKind.DELTA
|
||||
if self.stream
|
||||
else RequestOutputKind.FINAL_ONLY,
|
||||
structured_outputs=self.structured_outputs,
|
||||
structured_outputs=self.extract_structured_outputs(),
|
||||
logit_bias=self.logit_bias,
|
||||
allowed_token_ids=self.allowed_token_ids,
|
||||
bad_words=self.bad_words,
|
||||
@@ -496,6 +468,18 @@ class CompletionRequest(OpenAIBaseModel):
|
||||
parameter="logprob_token_ids",
|
||||
)
|
||||
|
||||
# These fields are integers, but `mode="before"` runs on the raw
|
||||
# request data, so a non-numeric value (e.g. a JSON string) would
|
||||
# reach the comparisons below and raise TypeError -> HTTP 500. Reject
|
||||
# it here so the client gets a clean 400 instead.
|
||||
for field_name in ("prompt_logprobs", "logprobs"):
|
||||
field_value = data.get(field_name)
|
||||
if field_value is not None and not isinstance(field_value, (int, float)):
|
||||
raise VLLMValidationError(
|
||||
f"`{field_name}` must be an integer.",
|
||||
parameter=field_name,
|
||||
value=field_value,
|
||||
)
|
||||
if (prompt_logprobs := data.get("prompt_logprobs")) is not None:
|
||||
if data.get("stream") and (prompt_logprobs > 0 or prompt_logprobs == -1):
|
||||
raise VLLMValidationError(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
# Adapted from
|
||||
# https://github.com/lm-sys/FastChat/blob/168ccc29d3f7edc50823016105c024fe2282732a/fastchat/protocol/openai_api_protocol.py
|
||||
import json
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from typing import Any, ClassVar, Literal, TypeAlias
|
||||
@@ -16,9 +17,11 @@ from pydantic import (
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from vllm.config.utils import replace
|
||||
from vllm.entrypoints.chat_utils import make_tool_call_id
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
from vllm.logger import init_logger
|
||||
from vllm.sampling_params import StructuredOutputsParams
|
||||
from vllm.utils import random_uuid
|
||||
from vllm.utils.import_utils import resolve_obj_by_qualname
|
||||
|
||||
@@ -173,6 +176,39 @@ AnyResponseFormat: TypeAlias = (
|
||||
)
|
||||
|
||||
|
||||
def structured_outputs_from_response_format(
|
||||
structured_outputs: StructuredOutputsParams | None,
|
||||
response_format: AnyResponseFormat | None,
|
||||
) -> StructuredOutputsParams | None:
|
||||
"""Apply ``response_format`` overrides to ``structured_outputs``."""
|
||||
if response_format is None or response_format.type == "text":
|
||||
return structured_outputs
|
||||
|
||||
overrides: dict[str, Any]
|
||||
if response_format.type == "json_object":
|
||||
overrides = {"json_object": True}
|
||||
elif response_format.type == "json_schema":
|
||||
json_schema = response_format.json_schema
|
||||
assert json_schema is not None
|
||||
overrides = {"json": json_schema.json_schema}
|
||||
else:
|
||||
assert isinstance(
|
||||
response_format,
|
||||
(
|
||||
LegacyStructuralTagResponseFormat,
|
||||
StructuralTagResponseFormat,
|
||||
),
|
||||
)
|
||||
overrides = {
|
||||
"structural_tag": json.dumps(response_format.model_dump(by_alias=True))
|
||||
}
|
||||
|
||||
if structured_outputs is None:
|
||||
return StructuredOutputsParams(**overrides)
|
||||
|
||||
return replace(structured_outputs, **overrides)
|
||||
|
||||
|
||||
def validate_structural_tag_response_format(
|
||||
response_format: AnyStructuralTagResponseFormat | dict[str, Any],
|
||||
) -> None:
|
||||
@@ -181,8 +217,6 @@ def validate_structural_tag_response_format(
|
||||
Engine-side validation reports malformed structural tags as generation
|
||||
failures. OpenAI request parsing should classify them as bad requests.
|
||||
"""
|
||||
import json
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
if isinstance(response_format, dict):
|
||||
|
||||
@@ -354,6 +354,31 @@ class ResponsesRequest(OpenAIBaseModel):
|
||||
"top_k": 0,
|
||||
}
|
||||
|
||||
def extract_structured_outputs(self) -> StructuredOutputsParams | None:
|
||||
"""Normalize request constraints into ``StructuredOutputsParams``."""
|
||||
if self.text is None or self.text.format is None:
|
||||
return self.structured_outputs
|
||||
|
||||
if self.structured_outputs is not None:
|
||||
raise VLLMValidationError(
|
||||
"Cannot specify both structured_outputs and text.format",
|
||||
parameter="structured_outputs",
|
||||
)
|
||||
|
||||
response_format = self.text.format
|
||||
if response_format.type == "json_object":
|
||||
return StructuredOutputsParams(json_object=True)
|
||||
if (
|
||||
response_format.type == "json_schema"
|
||||
and response_format.schema_ is not None
|
||||
):
|
||||
return StructuredOutputsParams(
|
||||
json=response_format.schema_ # type: ignore[call-arg]
|
||||
# --follow-imports skip hides the class definition but also hides
|
||||
# multiple third party conflicts, so best of both evils
|
||||
)
|
||||
return None
|
||||
|
||||
def to_sampling_params(
|
||||
self,
|
||||
default_max_tokens: int,
|
||||
@@ -387,27 +412,6 @@ class ResponsesRequest(OpenAIBaseModel):
|
||||
if (frequency_penalty := self.frequency_penalty) is None:
|
||||
frequency_penalty = default_sampling_params.get("frequency_penalty", 0.0)
|
||||
|
||||
# Structured output
|
||||
structured_outputs = self.structured_outputs
|
||||
|
||||
# Also check text.format for OpenAI-style json_schema
|
||||
if self.text is not None and self.text.format is not None:
|
||||
if structured_outputs is not None:
|
||||
raise VLLMValidationError(
|
||||
"Cannot specify both structured_outputs and text.format",
|
||||
parameter="structured_outputs",
|
||||
)
|
||||
response_format = self.text.format
|
||||
if (
|
||||
response_format.type == "json_schema"
|
||||
and response_format.schema_ is not None
|
||||
):
|
||||
structured_outputs = StructuredOutputsParams(
|
||||
json=response_format.schema_ # type: ignore[call-arg]
|
||||
# --follow-imports skip hides the class definition but also hides
|
||||
# multiple third party conflicts, so best of both evils
|
||||
)
|
||||
|
||||
stop = self.stop if self.stop else []
|
||||
if isinstance(stop, str):
|
||||
stop = [stop]
|
||||
@@ -433,7 +437,7 @@ class ResponsesRequest(OpenAIBaseModel):
|
||||
output_kind=(
|
||||
RequestOutputKind.DELTA if self.stream else RequestOutputKind.FINAL_ONLY
|
||||
),
|
||||
structured_outputs=structured_outputs,
|
||||
structured_outputs=self.extract_structured_outputs(),
|
||||
logit_bias=self.logit_bias,
|
||||
extra_args=extra_args,
|
||||
skip_clone=True, # Created fresh per request, safe to skip clone
|
||||
|
||||
@@ -14,10 +14,23 @@ from vllm.entrypoints.chat_utils import (
|
||||
from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
|
||||
from vllm.tasks import check_removed_pooling_task
|
||||
from vllm.utils import random_uuid
|
||||
from vllm.utils.serial_utils import EmbedDType, EncodingFormat, Endianness
|
||||
|
||||
|
||||
def reject_removed_pooling_parameters(data):
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
if "normalize" in data:
|
||||
raise VLLMValidationError(
|
||||
"Parameter `normalize` was removed; use `use_activation` instead.",
|
||||
parameter="normalize",
|
||||
)
|
||||
check_removed_pooling_task(data.get("task"))
|
||||
return data
|
||||
|
||||
|
||||
class PoolingBasicRequestMixin(OpenAIBaseModel):
|
||||
# --8<-- [start:pooling-common-params]
|
||||
model: str | None = None
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import time
|
||||
from typing import TypeAlias
|
||||
from typing import Annotated, TypeAlias
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import BeforeValidator, Field
|
||||
|
||||
from vllm import PoolingParams
|
||||
from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel, UsageInfo
|
||||
@@ -17,6 +17,7 @@ from ..base.protocol import (
|
||||
CompletionRequestMixin,
|
||||
FixedMaxLenTokenizeParamsMixin,
|
||||
PoolingBasicRequestMixin,
|
||||
reject_removed_pooling_parameters,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -48,9 +49,10 @@ class ClassificationChatRequest(
|
||||
)
|
||||
|
||||
|
||||
ClassificationRequest: TypeAlias = (
|
||||
ClassificationCompletionRequest | ClassificationChatRequest
|
||||
)
|
||||
ClassificationRequest: TypeAlias = Annotated[
|
||||
ClassificationCompletionRequest | ClassificationChatRequest,
|
||||
BeforeValidator(reject_removed_pooling_parameters),
|
||||
]
|
||||
|
||||
|
||||
class ClassificationData(OpenAIBaseModel):
|
||||
|
||||
@@ -13,7 +13,7 @@ from collections.abc import Sequence
|
||||
from typing import Annotated, Any, Literal, TypeAlias
|
||||
|
||||
import pybase64 as base64
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic import BaseModel, BeforeValidator, Field, model_validator
|
||||
|
||||
from vllm import PoolingParams
|
||||
from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
|
||||
@@ -27,6 +27,7 @@ from ..base.protocol import (
|
||||
EmbeddingTokenizeParamsMixin,
|
||||
EmbedRequestMixin,
|
||||
PoolingBasicRequestMixin,
|
||||
reject_removed_pooling_parameters,
|
||||
)
|
||||
|
||||
|
||||
@@ -154,13 +155,14 @@ class EmbeddingBatchChatInputRequest(EmbeddingBatchChatRequest):
|
||||
return normalized
|
||||
|
||||
|
||||
EmbeddingRequest: TypeAlias = (
|
||||
EmbeddingRequest: TypeAlias = Annotated[
|
||||
EmbeddingCompletionRequest
|
||||
| EmbeddingChatRequest
|
||||
| EmbeddingBatchChatRequest
|
||||
| EmbeddingChatInputRequest
|
||||
| EmbeddingBatchChatInputRequest
|
||||
)
|
||||
| EmbeddingBatchChatInputRequest,
|
||||
BeforeValidator(reject_removed_pooling_parameters),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import time
|
||||
from typing import Generic, TypeAlias, TypeVar
|
||||
from typing import Annotated, Generic, TypeAlias, TypeVar
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import BeforeValidator, Field
|
||||
|
||||
from vllm import PoolingParams
|
||||
from vllm.config import ModelConfig
|
||||
@@ -20,6 +20,7 @@ from ..base.protocol import (
|
||||
EncodingRequestMixin,
|
||||
FixedMaxLenTokenizeParamsMixin,
|
||||
PoolingBasicRequestMixin,
|
||||
reject_removed_pooling_parameters,
|
||||
)
|
||||
|
||||
|
||||
@@ -92,9 +93,10 @@ class IOProcessorResponse(OpenAIBaseModel, Generic[T]):
|
||||
"""
|
||||
|
||||
|
||||
PoolingRequest: TypeAlias = (
|
||||
PoolingCompletionRequest | PoolingChatRequest | IOProcessorRequest
|
||||
)
|
||||
PoolingRequest: TypeAlias = Annotated[
|
||||
PoolingCompletionRequest | PoolingChatRequest | IOProcessorRequest,
|
||||
BeforeValidator(reject_removed_pooling_parameters),
|
||||
]
|
||||
|
||||
|
||||
class PoolingResponseData(OpenAIBaseModel):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from typing import cast
|
||||
|
||||
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
||||
from typing_extensions import assert_never
|
||||
|
||||
@@ -52,7 +54,7 @@ class ServingPooling(PoolingBaseServing):
|
||||
self.json_response_cls = get_json_response_cls()
|
||||
|
||||
def get_io_processor(self, request: AnyPoolingRequest) -> PoolingIOProcessor:
|
||||
assert isinstance(request, PoolingRequest)
|
||||
request = cast(PoolingRequest, request)
|
||||
pooling_task = self._verify_pooling_task(request)
|
||||
return self.io_processors[pooling_task]
|
||||
|
||||
|
||||
@@ -197,6 +197,37 @@ class XPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
|
||||
return False, "XPUFp8BlockScaledMM only support on XPU"
|
||||
return True, None
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module):
|
||||
super().process_weights_after_loading(layer)
|
||||
scale_attr = (
|
||||
"weight_scale_inv" if hasattr(layer, "weight_scale_inv") else "weight_scale"
|
||||
)
|
||||
scale = getattr(layer, scale_attr)
|
||||
# Transpose scale from checkpoint layout [N/128, K/128] to
|
||||
# oneDNN expected layout [K/128, N/128] at load time (one-time cost).
|
||||
scale_t = scale.data.t().contiguous()
|
||||
replace_parameter(layer, scale_attr, scale_t)
|
||||
|
||||
# For BMM layers (e.g. wo_a), precompute 3D scale and weight:
|
||||
# [K/bs, N/bs] -> [batch, K/bs, N_per_batch/bs]
|
||||
if getattr(layer, "is_bmm", False):
|
||||
batch = layer.bmm_batch_size
|
||||
k_blocks = scale_t.shape[0]
|
||||
n_per_batch_blocks = scale_t.shape[1] // batch
|
||||
layer.bmm_scale = (
|
||||
scale_t.reshape(k_blocks, batch, n_per_batch_blocks)
|
||||
.permute(1, 0, 2)
|
||||
.contiguous()
|
||||
)
|
||||
# Precompute [G, K, N] weight for fp8_bmm.
|
||||
# Original weight is [N_total, K] where N_total = G * N_per_group.
|
||||
w = layer.weight.data
|
||||
N_total, K = w.shape
|
||||
N_per_group = N_total // batch
|
||||
layer.bmm_weight = w.reshape(batch, N_per_group, K).permute(
|
||||
0, 2, 1
|
||||
) # [G, K, N]
|
||||
|
||||
def apply_block_scaled_mm(
|
||||
self,
|
||||
A: torch.Tensor,
|
||||
@@ -205,12 +236,12 @@ class XPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
|
||||
Bs: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
# Weight is [N, K]. Use .t() to create a [K, N] view without copying.
|
||||
# Bs is [N/128, K/128] — transpose to [K/128, N/128] for oneDNN.
|
||||
# Bs is already [K/128, N/128] from process_weights_after_loading.
|
||||
return torch.ops._xpu_C.fp8_gemm(
|
||||
A,
|
||||
B.t(),
|
||||
self.config.out_dtype,
|
||||
As,
|
||||
Bs.t().contiguous(),
|
||||
Bs,
|
||||
torch.Tensor(),
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
kFp8StaticTensorSym,
|
||||
kInt4Static,
|
||||
kInt4Static32,
|
||||
kMxfp4Dynamic,
|
||||
kMxfp4Static,
|
||||
kMxfp8Dynamic,
|
||||
kMxfp8Static,
|
||||
@@ -64,10 +65,16 @@ class XPUExperts(mk.FusedMoEExpertsModular):
|
||||
)
|
||||
self.gemm1_clamp_limit = quant_config.gemm1_clamp_limit
|
||||
self.fused_moe_impl: XpuFusedMoe | None = None
|
||||
is_xe2_or_xe3 = torch.ops._xpu_C.is_xe2_arch() or torch.ops._xpu_C.is_xe3_arch()
|
||||
if not is_xe2_or_xe3:
|
||||
raise NotImplementedError(
|
||||
"XPUExperts is only supported on Intel Xe2/Xe3 GPUs"
|
||||
)
|
||||
self._expects_unquantized_inputs = is_xe2_or_xe3
|
||||
|
||||
@property
|
||||
def expects_unquantized_inputs(self) -> bool:
|
||||
return True
|
||||
return self._expects_unquantized_inputs
|
||||
|
||||
@staticmethod
|
||||
def activation_format() -> mk.FusedMoEActivationFormat:
|
||||
@@ -172,6 +179,7 @@ class XPUExperts(mk.FusedMoEExpertsModular):
|
||||
hidden_states=hidden_states,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
a1q_scale=a1q_scale,
|
||||
)
|
||||
|
||||
|
||||
@@ -309,6 +317,24 @@ class XPUExpertsMxFp4(XPUExperts):
|
||||
num_dispatchers,
|
||||
)
|
||||
|
||||
def workspace_shapes(
|
||||
self,
|
||||
M: int,
|
||||
N: int,
|
||||
K: int,
|
||||
topk: int,
|
||||
global_num_experts: int,
|
||||
local_num_experts: int,
|
||||
expert_tokens_meta: mk.ExpertTokensMetadata | None,
|
||||
activation: MoEActivation,
|
||||
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
|
||||
# K = a1q.size(-1). When activations are pre-quantized packed mxfp4,
|
||||
# K is the packed hidden_size (= logical / 2); the kernel output is at
|
||||
# logical hidden_size (2 * K). When unquantized (bf16), K is already
|
||||
# the logical size.
|
||||
logical_K = K if self.expects_unquantized_inputs else 2 * K
|
||||
return (0,), (0,), (M, logical_K)
|
||||
|
||||
@staticmethod
|
||||
def _supports_quant_scheme(
|
||||
weight_key: QuantKey | None,
|
||||
@@ -316,5 +342,6 @@ class XPUExpertsMxFp4(XPUExperts):
|
||||
) -> bool:
|
||||
SUPPORTED_W_A = [
|
||||
(kMxfp4Static, None),
|
||||
(kMxfp4Static, kMxfp4Dynamic),
|
||||
]
|
||||
return (weight_key, activation_key) in SUPPORTED_W_A
|
||||
|
||||
@@ -71,7 +71,7 @@ class TopKWeightAndReduceNoOP(mk.TopKWeightAndReduce):
|
||||
assert output.size() == fused_expert_output.size(), (
|
||||
"output shape is expected to match the fused_expert_output shape. "
|
||||
f"But got output={output.size()}, "
|
||||
f"used_expert_output={fused_expert_output.size()}"
|
||||
f"fused_expert_output={fused_expert_output.size()}"
|
||||
)
|
||||
output.copy_(fused_expert_output, non_blocking=True)
|
||||
return output
|
||||
|
||||
@@ -17,12 +17,14 @@ from vllm.model_executor.layers.quantization.utils.int8_utils import (
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
|
||||
quant_dequant_mxfp4,
|
||||
xpu_mxfp4_quantize,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.mxfp6_utils import (
|
||||
quant_dequant_mxfp6,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
|
||||
mxfp8_e4m3_quantize,
|
||||
xpu_mxfp8_quantize,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
|
||||
ref_nvfp4_quant_dequant,
|
||||
@@ -195,6 +197,8 @@ def _mxfp4_quantize(
|
||||
per_act_token_quant: bool,
|
||||
block_shape: list[int] | None = None,
|
||||
) -> tuple[torch.Tensor, None]:
|
||||
if current_platform.is_xpu():
|
||||
return xpu_mxfp4_quantize(A)
|
||||
assert block_shape is None
|
||||
# TODO: native mxfp4 is currently not integrated in vllm,
|
||||
# so simulating even on devices supporting this data type natively.
|
||||
@@ -223,6 +227,8 @@ def _mxfp8_e4m3_quantize(
|
||||
is_sf_swizzled_layout: bool = False,
|
||||
mx_alignment: int = 0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
if current_platform.is_xpu():
|
||||
return xpu_mxfp8_quantize(A)
|
||||
assert A_scale is None
|
||||
assert not per_act_token_quant
|
||||
assert block_shape is None or block_shape == [1, 32]
|
||||
@@ -309,7 +315,7 @@ def moe_kernel_quantize_input(
|
||||
A = ref_nvfp4_quant_dequant(A, A_scale, block_size=16)
|
||||
return A, None
|
||||
elif quant_dtype == "mxfp4":
|
||||
if not quantization_emulation:
|
||||
if not current_platform.is_xpu() and not quantization_emulation:
|
||||
raise NotImplementedError(
|
||||
"moe_kernel_quantize_input should not be used for native"
|
||||
" quant_dtype='mxfp4' MOE. Please open an issue."
|
||||
@@ -318,7 +324,7 @@ def moe_kernel_quantize_input(
|
||||
elif quant_dtype == "mxfp8":
|
||||
# TODO: `quant_dtype == "mxfp8"` is ambiguous,
|
||||
# should be fp8_e4m3. OCP MX also defines `fp8_e5m2`.
|
||||
if quantization_emulation:
|
||||
if not current_platform.is_xpu() and quantization_emulation:
|
||||
raise NotImplementedError(
|
||||
"moe_kernel_quantize_input does not support quant_dtype='mxfp8' MOE "
|
||||
"quantization emulation. Please open an issue."
|
||||
|
||||
@@ -1237,3 +1237,15 @@ def causal_conv1d_update(
|
||||
if unsqueeze:
|
||||
out = out.squeeze(-1)
|
||||
return out.to(original_x_dtype)
|
||||
|
||||
|
||||
from vllm.platforms import current_platform # noqa: E402
|
||||
|
||||
if current_platform.is_cpu():
|
||||
from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
|
||||
causal_conv1d_fn_cpu,
|
||||
causal_conv1d_update_cpu,
|
||||
)
|
||||
|
||||
causal_conv1d_fn = causal_conv1d_fn_cpu # type: ignore
|
||||
causal_conv1d_update = causal_conv1d_update_cpu # type: ignore
|
||||
|
||||
@@ -6,18 +6,31 @@ from __future__ import annotations
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from vllm._custom_ops import causal_conv1d_update_cpu_vec
|
||||
from vllm.v1.attention.backends.utils import NULL_BLOCK_ID, PAD_SLOT_ID
|
||||
|
||||
# for prefill
|
||||
def causal_conv1d_torch(
|
||||
|
||||
def causal_conv1d_fn_cpu(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
bias: torch.Tensor | None,
|
||||
conv_states: torch.Tensor,
|
||||
query_start_loc: torch.Tensor,
|
||||
cache_indices: torch.Tensor,
|
||||
has_initial_state: torch.Tensor,
|
||||
cache_indices: torch.Tensor | None = None,
|
||||
has_initial_state: torch.Tensor | None = None,
|
||||
activation: str | None = "silu",
|
||||
pad_slot_id: int = PAD_SLOT_ID,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
"""CPU implementation for causal_conv1d_fwd."""
|
||||
if isinstance(activation, bool) and activation:
|
||||
activation = "silu"
|
||||
elif isinstance(activation, bool):
|
||||
activation = None
|
||||
|
||||
original_x_dtype = x.dtype
|
||||
x = x.to(conv_states.dtype)
|
||||
|
||||
out = torch.empty_like(x)
|
||||
state_len = weight.shape[1] - 1
|
||||
assert activation in {None, "silu", "swish"}
|
||||
@@ -27,11 +40,21 @@ def causal_conv1d_torch(
|
||||
for idx in range(query_start_loc.shape[0] - 1)
|
||||
]
|
||||
weight = weight.unsqueeze(1)
|
||||
|
||||
for seq_idx, (bos, eos) in enumerate(seq_begin_end_idx):
|
||||
slot = int(cache_indices[seq_idx].item())
|
||||
if bos == eos:
|
||||
continue
|
||||
|
||||
slot = (
|
||||
int(cache_indices[seq_idx].item()) if cache_indices is not None else seq_idx
|
||||
)
|
||||
|
||||
if slot == pad_slot_id:
|
||||
continue
|
||||
|
||||
seq_x = x[:, bos:eos].unsqueeze(0)
|
||||
if bool(has_initial_state[seq_idx].item()):
|
||||
|
||||
if has_initial_state is not None and bool(has_initial_state[seq_idx].item()):
|
||||
initial_state = conv_states[slot, :, :state_len].unsqueeze(0)
|
||||
else:
|
||||
initial_state = torch.zeros(
|
||||
@@ -51,16 +74,48 @@ def causal_conv1d_torch(
|
||||
groups=weight.shape[0],
|
||||
)
|
||||
seq_out = seq_out[..., -seq_x.shape[-1] :].to(dtype=x.dtype)
|
||||
|
||||
if activation in ("silu", "swish"):
|
||||
seq_out = F.silu(seq_out)
|
||||
|
||||
out[:, bos:eos] = seq_out.squeeze(0)
|
||||
conv_states[slot, :, :state_len].copy_(conv_input[..., -state_len:].squeeze(0))
|
||||
|
||||
return out
|
||||
return out.to(original_x_dtype)
|
||||
|
||||
|
||||
def causal_conv1d_update_cpu(
|
||||
x: torch.Tensor,
|
||||
conv_state: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
activation: bool | str | None = None,
|
||||
conv_state_indices: torch.Tensor | None = None,
|
||||
query_start_loc: torch.Tensor | None = None,
|
||||
pad_slot_id: int | None = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
"""CPU implementation for causal_conv1d_update."""
|
||||
if isinstance(activation, bool):
|
||||
activation = "silu" if activation else None
|
||||
|
||||
if pad_slot_id is None:
|
||||
pad_slot_id = kwargs.get("null_block_id", NULL_BLOCK_ID)
|
||||
if pad_slot_id is None:
|
||||
pad_slot_id = NULL_BLOCK_ID
|
||||
|
||||
return causal_conv1d_update_cpu_vec(
|
||||
x,
|
||||
conv_state,
|
||||
weight,
|
||||
bias,
|
||||
activation,
|
||||
conv_state_indices,
|
||||
query_start_loc,
|
||||
pad_slot_id,
|
||||
)
|
||||
|
||||
|
||||
# for decode
|
||||
def causal_conv1d_update_torch(
|
||||
x: torch.Tensor,
|
||||
conv_state: torch.Tensor,
|
||||
@@ -68,6 +123,11 @@ def causal_conv1d_update_torch(
|
||||
bias: torch.Tensor | None = None,
|
||||
activation: str | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Pure PyTorch fallback for causal_conv1d_update.
|
||||
Currently used as a fallback for Arm (aarch64) to leverage
|
||||
oneDNN/ACL F.conv1d kernels for batched decoding.
|
||||
"""
|
||||
assert activation in {None, "silu", "swish"}
|
||||
|
||||
_, dim, seq_len = x.shape
|
||||
|
||||
@@ -10,9 +10,13 @@ import vllm._custom_ops as ops
|
||||
from vllm.forward_context import ForwardContext, get_forward_context
|
||||
from vllm.model_executor.layers.mamba.mamba_utils import is_conv_state_dim_first
|
||||
from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
|
||||
causal_conv1d_torch,
|
||||
causal_conv1d_fn_cpu as causal_conv1d_torch,
|
||||
)
|
||||
from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
|
||||
causal_conv1d_update_cpu,
|
||||
causal_conv1d_update_torch,
|
||||
)
|
||||
from vllm.platforms import CpuArchEnum, current_platform
|
||||
from vllm.utils.torch_utils import (
|
||||
LayerNameType,
|
||||
_resolve_layer_name,
|
||||
@@ -140,21 +144,30 @@ def _cpu_gdn_attention_nonspec(
|
||||
conv_states=conv_state,
|
||||
weight=layer.conv1d.weight,
|
||||
bias=layer.conv1d.bias,
|
||||
silu_activation=layer.activation == "silu",
|
||||
silu_activation=(layer.activation == "silu"),
|
||||
conv_state_indices=decode_state_indices,
|
||||
is_vnni=True,
|
||||
)
|
||||
else:
|
||||
decode_conv_state = conv_state[decode_state_indices].contiguous()
|
||||
decode_mixed_qkv = causal_conv1d_update_torch(
|
||||
# [B, dim] -> [B, dim, 1]
|
||||
x=decode_mixed_qkv.unsqueeze(-1),
|
||||
conv_state=decode_conv_state,
|
||||
weight=conv_weights,
|
||||
bias=layer.conv1d.bias,
|
||||
activation=layer.activation,
|
||||
).squeeze(-1)
|
||||
conv_state[decode_state_indices] = decode_conv_state
|
||||
if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
|
||||
decode_conv_state = conv_state[decode_state_indices].contiguous()
|
||||
decode_mixed_qkv = causal_conv1d_update_torch(
|
||||
x=decode_mixed_qkv.unsqueeze(-1),
|
||||
conv_state=decode_conv_state,
|
||||
weight=conv_weights,
|
||||
bias=layer.conv1d.bias,
|
||||
activation=layer.activation,
|
||||
).squeeze(-1)
|
||||
conv_state[decode_state_indices] = decode_conv_state
|
||||
else:
|
||||
decode_mixed_qkv = causal_conv1d_update_cpu(
|
||||
x=decode_mixed_qkv,
|
||||
conv_state=conv_state,
|
||||
weight=conv_weights,
|
||||
bias=layer.conv1d.bias,
|
||||
activation=layer.activation,
|
||||
conv_state_indices=decode_state_indices,
|
||||
)
|
||||
|
||||
query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
|
||||
|
||||
@@ -495,17 +508,26 @@ def _spec_aware_nonspec(
|
||||
decode_a = a[:num_decode_tokens]
|
||||
decode_state_indices = state_indices_tensor[:num_decodes]
|
||||
# Only the first ``width-1`` columns hold the real conv state.
|
||||
decode_conv_state = conv_buf[decode_state_indices][
|
||||
:, :, : width - 1
|
||||
].contiguous()
|
||||
decode_mixed_qkv = causal_conv1d_update_torch(
|
||||
x=decode_mixed_qkv.unsqueeze(-1),
|
||||
conv_state=decode_conv_state,
|
||||
weight=conv_weights,
|
||||
bias=layer.conv1d.bias,
|
||||
activation=layer.activation,
|
||||
).squeeze(-1)
|
||||
conv_buf[decode_state_indices, :, : width - 1] = decode_conv_state
|
||||
if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
|
||||
conv_state_view = conv_buf[:, :, : width - 1]
|
||||
decode_conv_state = conv_state_view[decode_state_indices].contiguous()
|
||||
decode_mixed_qkv = causal_conv1d_update_torch(
|
||||
x=decode_mixed_qkv.unsqueeze(-1),
|
||||
conv_state=decode_conv_state,
|
||||
weight=conv_weights,
|
||||
bias=layer.conv1d.bias,
|
||||
activation=layer.activation,
|
||||
).squeeze(-1)
|
||||
conv_state_view[decode_state_indices] = decode_conv_state
|
||||
else:
|
||||
decode_mixed_qkv = causal_conv1d_update_cpu(
|
||||
x=decode_mixed_qkv,
|
||||
conv_state=conv_buf[:, :, : width - 1],
|
||||
weight=conv_weights,
|
||||
bias=layer.conv1d.bias,
|
||||
activation=layer.activation,
|
||||
conv_state_indices=decode_state_indices,
|
||||
)
|
||||
|
||||
query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
|
||||
# rearrange_mixed_qkv can return views whose last dim is not
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import torch
|
||||
|
||||
import vllm._custom_ops as ops
|
||||
from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
|
||||
|
||||
|
||||
def _mamba_chunk_scan_combined_fwd_cpu(
|
||||
x,
|
||||
dt,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
chunk_size,
|
||||
out,
|
||||
D=None,
|
||||
z=None,
|
||||
dt_bias=None,
|
||||
initial_states=None,
|
||||
return_intermediate_states=False,
|
||||
seq_idx=None,
|
||||
cu_seqlens=None,
|
||||
cu_chunk_seqlens=None,
|
||||
last_chunk_indices=None,
|
||||
dt_softplus=False,
|
||||
dt_limit=(0.0, float("inf")),
|
||||
state_dtype=None,
|
||||
**kwargs,
|
||||
):
|
||||
seqlen, nheads, headdim = x.shape
|
||||
_, ngroups, dstate = B.shape
|
||||
|
||||
assert cu_seqlens is not None
|
||||
batch = cu_seqlens.size(0) - 1
|
||||
|
||||
dt_f = dt.float()
|
||||
if dt_bias is not None:
|
||||
dt_f = dt_f + dt_bias.float().unsqueeze(0)
|
||||
if dt_softplus:
|
||||
dt_f = torch.nn.functional.softplus(dt_f)
|
||||
if dt_limit[0] > 0.0 or dt_limit[1] < float("inf"):
|
||||
dt_f = dt_f.clamp(min=dt_limit[0], max=dt_limit[1])
|
||||
|
||||
all_states = torch.zeros(
|
||||
batch, nheads, headdim, dstate, dtype=torch.float32, device=x.device
|
||||
)
|
||||
if initial_states is not None:
|
||||
all_states.copy_(initial_states.float())
|
||||
|
||||
assert out.is_contiguous(), (
|
||||
"_mamba_chunk_scan_combined_fwd_cpu: `out` must be "
|
||||
"pre-allocated as a contiguous tensor"
|
||||
)
|
||||
|
||||
D_1d = None
|
||||
if D is not None:
|
||||
d = D.float()
|
||||
while d.dim() > 1 and d.stride(-1) == 0:
|
||||
d = d.squeeze(-1)
|
||||
D_1d = d.contiguous()
|
||||
|
||||
ops.mamba_chunk_scan_fwd_cpu(
|
||||
out,
|
||||
all_states,
|
||||
x,
|
||||
dt_f,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D_1d,
|
||||
z,
|
||||
cu_seqlens.to(torch.int32),
|
||||
)
|
||||
|
||||
out_dtype = state_dtype if state_dtype is not None else x.dtype
|
||||
all_states = all_states.to(out_dtype)
|
||||
|
||||
return all_states
|
||||
|
||||
|
||||
def selective_state_update(
|
||||
state,
|
||||
x,
|
||||
dt,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D=None,
|
||||
z=None,
|
||||
dt_bias=None,
|
||||
dt_softplus=False,
|
||||
state_batch_indices=None,
|
||||
dst_state_batch_indices=None,
|
||||
null_block_id=NULL_BLOCK_ID,
|
||||
out=None,
|
||||
num_accepted_tokens=None,
|
||||
cu_seqlens=None,
|
||||
is_blackwell=False,
|
||||
enable_stochastic_rounding=False,
|
||||
cache_philox_rounds=0,
|
||||
):
|
||||
"""CPU implementation for selective_state_update."""
|
||||
# Ensure out tensor exists
|
||||
if out is None:
|
||||
out = torch.empty_like(x if x.dim() == 2 else x)
|
||||
|
||||
_state = state.unsqueeze(1) if state.dim() == 3 else state
|
||||
_x = x.unsqueeze(1) if x.dim() == 2 else x
|
||||
_dt = dt.unsqueeze(1) if dt.dim() == 2 else dt
|
||||
_A = A.unsqueeze(0) if A.dim() == 2 else A
|
||||
_B = B.unsqueeze(1) if B.dim() == 2 else B
|
||||
_C = C.unsqueeze(1) if C.dim() == 2 else C
|
||||
_D = D.unsqueeze(0) if (D is not None and D.dim() == 1) else D
|
||||
_z = z.unsqueeze(1) if (z is not None and z.dim() == 2) else z
|
||||
_dt_bias = (
|
||||
dt_bias.unsqueeze(0)
|
||||
if (dt_bias is not None and dt_bias.dim() == 1)
|
||||
else dt_bias
|
||||
)
|
||||
_out = out.unsqueeze(1) if out.dim() == 2 else out
|
||||
|
||||
_sbi = state_batch_indices
|
||||
_dsbi = dst_state_batch_indices
|
||||
ops.selective_state_update_cpu(
|
||||
_state,
|
||||
_x,
|
||||
_dt,
|
||||
_A,
|
||||
_B,
|
||||
_C,
|
||||
_D,
|
||||
_z,
|
||||
_dt_bias,
|
||||
dt_softplus,
|
||||
_sbi,
|
||||
_dsbi,
|
||||
null_block_id,
|
||||
_out,
|
||||
num_accepted_tokens,
|
||||
cu_seqlens,
|
||||
)
|
||||
return _out.squeeze(1) if out.dim() == 2 else _out
|
||||
@@ -845,3 +845,13 @@ def selective_scan_fn(
|
||||
return delta # output written inplace to delta
|
||||
else:
|
||||
return z # output written inplace to z
|
||||
|
||||
|
||||
from vllm.platforms import current_platform # noqa: E402
|
||||
|
||||
if current_platform.is_cpu():
|
||||
from vllm.model_executor.layers.mamba.ops.cpu.mamba_ssm import (
|
||||
selective_state_update as selective_state_update_cpu,
|
||||
)
|
||||
|
||||
selective_state_update = selective_state_update_cpu # type: ignore
|
||||
|
||||
@@ -225,3 +225,11 @@ def mamba_chunk_scan_combined_varlen(
|
||||
)
|
||||
|
||||
return varlen_states
|
||||
|
||||
|
||||
from vllm.platforms import current_platform # noqa: E402
|
||||
|
||||
if current_platform.is_cpu():
|
||||
import vllm.model_executor.layers.mamba.ops.cpu.mamba_ssm as cpu_mamba_ssm
|
||||
|
||||
_mamba_chunk_scan_combined_fwd = cpu_mamba_ssm._mamba_chunk_scan_combined_fwd_cpu # type: ignore
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
Dispatch module for Mamba selective state update (SSU) backends.
|
||||
|
||||
Provides a unified `selective_state_update` function that dispatches to
|
||||
either the Triton or FlashInfer backend based on the configured
|
||||
`MambaBackendEnum`. Follows SGLang's dispatch pattern adapted for vLLM.
|
||||
the Triton, FlashInfer, or CPU backend based on the configured
|
||||
`MambaBackendEnum`. On CPU-only platforms (PowerPC, x86 without CUDA)
|
||||
the backend defaults to 'cpu'.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -182,9 +183,75 @@ class FlashInferSSUBackend(MambaSSUBackend):
|
||||
)
|
||||
|
||||
|
||||
class CPUSSUBackend(MambaSSUBackend):
|
||||
"""CPU SSU backend using the compiled C++ VSX/scalar kernel.
|
||||
|
||||
On CPU-only platforms (PowerPC, x86 without CUDA) this dispatches to
|
||||
the vectorized C++ kernel registered as ``torch.ops._C.selective_state_update_cpu``.
|
||||
That kernel uses vec_op SIMD intrinsics (VSX on ppc64le, AVX2 on x86,
|
||||
scalar fallback elsewhere) and is parallelised with OpenMP across heads.
|
||||
|
||||
Falls back to the pure-PyTorch implementation only if the C++ op is
|
||||
unavailable (e.g. a CPU-less build).
|
||||
"""
|
||||
|
||||
def __init__(self, mamba_config: MambaConfig):
|
||||
super().__init__(mamba_config)
|
||||
from vllm import _custom_ops as ops
|
||||
|
||||
self._cpp_kernel = ops.selective_state_update_cpu
|
||||
logger.info("CPUSSUBackend: using compiled C++ selective_state_update kernel.")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "cpu"
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
state: torch.Tensor,
|
||||
x: torch.Tensor,
|
||||
dt: torch.Tensor,
|
||||
A: torch.Tensor,
|
||||
B: torch.Tensor,
|
||||
C: torch.Tensor,
|
||||
D: torch.Tensor,
|
||||
dt_bias: torch.Tensor,
|
||||
z: torch.Tensor | None = None,
|
||||
dt_softplus: bool = False,
|
||||
state_batch_indices: torch.Tensor | None = None,
|
||||
dst_state_batch_indices: torch.Tensor | None = None,
|
||||
null_block_id: int = NULL_BLOCK_ID,
|
||||
out: torch.Tensor | None = None,
|
||||
num_accepted_tokens: torch.Tensor | None = None,
|
||||
cu_seqlens: torch.Tensor | None = None,
|
||||
is_blackwell: bool = False,
|
||||
) -> None:
|
||||
# C++ kernel: state shape expected as (nstates, nheads, dim, dstate)
|
||||
# The kernel writes in-place into `out` and updates `state`.
|
||||
self._cpp_kernel(
|
||||
state,
|
||||
x,
|
||||
dt,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
z,
|
||||
dt_bias,
|
||||
dt_softplus,
|
||||
state_batch_indices,
|
||||
dst_state_batch_indices,
|
||||
null_block_id,
|
||||
out,
|
||||
num_accepted_tokens,
|
||||
cu_seqlens,
|
||||
)
|
||||
|
||||
|
||||
_BACKEND_REGISTRY: dict[MambaBackendEnum, type[MambaSSUBackend]] = {
|
||||
MambaBackendEnum.TRITON: TritonSSUBackend,
|
||||
MambaBackendEnum.FLASHINFER: FlashInferSSUBackend,
|
||||
MambaBackendEnum.CPU: CPUSSUBackend,
|
||||
}
|
||||
|
||||
_mamba_ssu_backend: MambaSSUBackend | None = None
|
||||
@@ -210,6 +277,20 @@ def initialize_mamba_ssu_backend(
|
||||
global _mamba_ssu_backend
|
||||
|
||||
backend = mamba_config.backend
|
||||
|
||||
# On CPU-only platforms (PowerPC, x86 without CUDA) Triton JIT is
|
||||
# unstable or unavailable. Silently fall back to the CPU
|
||||
# backend unless the user explicitly chose something other than "triton".
|
||||
if backend == MambaBackendEnum.TRITON:
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
if current_platform.is_cpu():
|
||||
logger.info(
|
||||
"CPU platform detected: overriding Mamba SSU backend "
|
||||
"from 'triton' to 'cpu'."
|
||||
)
|
||||
backend = MambaBackendEnum.CPU
|
||||
|
||||
if backend not in _BACKEND_REGISTRY:
|
||||
raise ValueError(
|
||||
f"Unknown Mamba SSU backend: {backend}. "
|
||||
|
||||
@@ -94,9 +94,13 @@ class ShortConv(MambaBase, CustomOp):
|
||||
# Reference torch causal conv1d; runs on all CPU platforms. AMX kernels
|
||||
# for causal conv can be plugged in here later.
|
||||
from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
|
||||
causal_conv1d_torch,
|
||||
causal_conv1d_fn_cpu as causal_conv1d_torch,
|
||||
)
|
||||
from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
|
||||
causal_conv1d_update_cpu,
|
||||
causal_conv1d_update_torch,
|
||||
)
|
||||
from vllm.platforms import CpuArchEnum, current_platform
|
||||
|
||||
forward_context = get_forward_context()
|
||||
attn_metadata_raw = forward_context.attn_metadata
|
||||
@@ -164,17 +168,26 @@ class ShortConv(MambaBase, CustomOp):
|
||||
if has_decode:
|
||||
assert attn_metadata.state_indices_tensor_d is not None
|
||||
state_indices_d = attn_metadata.state_indices_tensor_d.flatten()
|
||||
Bx_d = (B_d * x_d).unsqueeze(-1) # (num_decodes, dim, 1)
|
||||
# Advanced indexing returns a copy; update in-place then scatter back
|
||||
gathered = conv_state[state_indices_d] # (num_decodes, dim, state_len)
|
||||
out_d = causal_conv1d_update_torch(
|
||||
Bx_d,
|
||||
gathered,
|
||||
conv_weights,
|
||||
self.conv.bias,
|
||||
activation=None,
|
||||
).squeeze(-1) # (num_decodes, dim)
|
||||
conv_state[state_indices_d] = gathered
|
||||
Bx_d = B_d * x_d # (num_decodes, dim)
|
||||
if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
|
||||
conv_state_view = conv_state[state_indices_d].contiguous()
|
||||
out_d = causal_conv1d_update_torch(
|
||||
Bx_d.unsqueeze(-1),
|
||||
conv_state_view,
|
||||
conv_weights,
|
||||
self.conv.bias,
|
||||
activation=None,
|
||||
).squeeze(-1)
|
||||
conv_state[state_indices_d] = conv_state_view
|
||||
else:
|
||||
out_d = causal_conv1d_update_cpu(
|
||||
Bx_d,
|
||||
conv_state,
|
||||
conv_weights,
|
||||
self.conv.bias,
|
||||
activation=None,
|
||||
conv_state_indices=state_indices_d,
|
||||
)
|
||||
conv_output_list.insert(0, C_d * out_d)
|
||||
|
||||
hidden_states_out = torch.vstack(conv_output_list)
|
||||
|
||||
@@ -234,10 +234,14 @@ def dispatch_cpu_unquantized_gemm(
|
||||
layer.cpu_linear = torch.nn.functional.linear
|
||||
return
|
||||
|
||||
# Skip CPU GEMM dispatch for non-2D weights (e.g. MoE 3D expert weights).
|
||||
# These layers are handled by their own specialized methods.
|
||||
if layer.weight.ndim != 2:
|
||||
# this is not a linear layer
|
||||
# For now it should be a causal_conv1d op
|
||||
if torch.cpu._is_amx_tile_supported():
|
||||
# For now it should be a causal_conv1d op or MoE 3D expert weights
|
||||
if torch.cpu._is_amx_tile_supported() and hasattr(
|
||||
ops, "causal_conv1d_weight_pack"
|
||||
):
|
||||
# prepack conv weight
|
||||
unpacked = (
|
||||
layer.weight.view(
|
||||
|
||||
@@ -174,6 +174,12 @@ def _warm_zero_kv_blocks_with_runner_zeroer(runner: object) -> bool:
|
||||
if not callable(zero_block_ids):
|
||||
return False
|
||||
|
||||
# With the extensible KV cache (V2), only a prefix of the blocks is
|
||||
# physically committed; make sure the blocks zeroed below are backed.
|
||||
ensure_kv_cache_blocks = getattr(runner, "ensure_kv_cache_blocks", None)
|
||||
if callable(ensure_kv_cache_blocks):
|
||||
ensure_kv_cache_blocks(max(_ZERO_KV_N_BLOCKS))
|
||||
|
||||
for n_blocks in _ZERO_KV_N_BLOCKS:
|
||||
zero_block_ids(list(range(n_blocks)))
|
||||
return True
|
||||
|
||||
@@ -257,7 +257,6 @@ def _fused_inv_rope_fp8_quant_kernel_impl(
|
||||
)
|
||||
grid = (tma_aligned_T, n_groups * heads_per_group)
|
||||
use_gdc = current_platform.is_arch_support_pdl()
|
||||
pdl_kwargs = {"launch_pdl": True} if use_gdc else {}
|
||||
_fused_inv_rope_fp8_quant_per_head[grid](
|
||||
o,
|
||||
positions,
|
||||
@@ -281,8 +280,8 @@ def _fused_inv_rope_fp8_quant_kernel_impl(
|
||||
HALF_ROPE=half_rope,
|
||||
TMA_ALIGNED_SCALES=tma_aligned_scales,
|
||||
USE_GDC=use_gdc,
|
||||
launch_pdl=use_gdc,
|
||||
num_stages=1,
|
||||
**pdl_kwargs,
|
||||
num_warps=1,
|
||||
)
|
||||
return fp8_buf, scale_buf
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user