forked from Karylab-cklius/vllm
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c8a5eaa5 | ||
|
|
c4547482ca | ||
|
|
91ef0afcb2 | ||
|
|
97cd2c41ad | ||
|
|
c001535038 | ||
|
|
de6bc297df | ||
|
|
0012818287 | ||
|
|
73cd7e25ae | ||
|
|
964c6eb485 | ||
|
|
d0e6514bf8 | ||
|
|
c7aa186d67 | ||
|
|
f653761252 | ||
|
|
4a8ae26e53 | ||
|
|
1333864408 | ||
|
|
01b9b5af67 | ||
|
|
8c57b6e7bc | ||
|
|
79246b5ea6 | ||
|
|
48954de237 | ||
|
|
c6235ed180 |
@@ -309,6 +309,7 @@ steps:
|
||||
depends_on: ~
|
||||
|
||||
- label: "Build release image - x86_64 - CPU"
|
||||
key: build-cpu-release-image-x86
|
||||
depends_on:
|
||||
- block-cpu-release-image-build
|
||||
- input-release-version
|
||||
@@ -327,7 +328,8 @@ steps:
|
||||
depends_on: ~
|
||||
|
||||
- label: "Build release image - arm64 - CPU"
|
||||
depends_on:
|
||||
key: build-cpu-release-image-arm64
|
||||
depends_on:
|
||||
- block-arm64-cpu-release-image-build
|
||||
- input-release-version
|
||||
agents:
|
||||
@@ -436,6 +438,41 @@ steps:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
DOCKERHUB_USERNAME: "vllmbot"
|
||||
|
||||
- block: "Publish release images to DockerHub"
|
||||
key: block-publish-release-images
|
||||
depends_on:
|
||||
- create-multi-arch-manifest
|
||||
- create-multi-arch-manifest-cuda-12-9
|
||||
- create-multi-arch-manifest-ubuntu2404
|
||||
- create-multi-arch-manifest-cuda-12-9-ubuntu2404
|
||||
- build-rocm-release-image
|
||||
- input-release-version
|
||||
# Wait for CPU builds if their block steps were unblocked, so publish
|
||||
# doesn't race the in-progress CPU build. allow_failure lets publish
|
||||
# proceed when the operator legitimately leaves the CPU block steps
|
||||
# unblocked or the CPU build fails.
|
||||
- step: build-cpu-release-image-x86
|
||||
allow_failure: true
|
||||
- step: build-cpu-release-image-arm64
|
||||
allow_failure: true
|
||||
if: build.env("NIGHTLY") != "1"
|
||||
|
||||
- label: "Publish release images to DockerHub"
|
||||
depends_on:
|
||||
- block-publish-release-images
|
||||
key: publish-release-images-dockerhub
|
||||
agents:
|
||||
queue: small_cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/publish-release-images.sh"
|
||||
plugins:
|
||||
- docker-login#v3.0.0:
|
||||
username: vllmbot
|
||||
password-env: DOCKERHUB_TOKEN
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
DOCKERHUB_USERNAME: "vllmbot"
|
||||
|
||||
- group: "Publish wheels"
|
||||
key: "publish-wheels"
|
||||
steps:
|
||||
|
||||
@@ -8,8 +8,6 @@ if [ -z "${RELEASE_VERSION}" ]; then
|
||||
RELEASE_VERSION="1.0.0.dev"
|
||||
fi
|
||||
|
||||
ROCM_BASE_CACHE_KEY=$(.buildkite/scripts/cache-rocm-base-wheels.sh key)
|
||||
|
||||
buildkite-agent annotate --style 'info' --context 'release-workflow' << EOF
|
||||
To download the wheel (by commit):
|
||||
\`\`\`
|
||||
@@ -25,95 +23,5 @@ aws s3 cp s3://vllm-wheels/${BUILDKITE_COMMIT}/vllm-${RELEASE_VERSION}+cpu-cp38-
|
||||
aws s3 cp s3://vllm-wheels/${BUILDKITE_COMMIT}/vllm-${RELEASE_VERSION}+cpu-cp38-abi3-manylinux_2_35_aarch64.whl .
|
||||
\`\`\`
|
||||
|
||||
|
||||
To download and upload the image:
|
||||
|
||||
\`\`\`
|
||||
# Download images:
|
||||
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-x86_64
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-aarch64
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-x86_64-cu129
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-aarch64-cu129
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${ROCM_BASE_CACHE_KEY}-rocm-base
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-rocm
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-cpu-release-repo:v${RELEASE_VERSION}
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-arm64-cpu-release-repo:v${RELEASE_VERSION}
|
||||
|
||||
# Tag and push images:
|
||||
|
||||
## CUDA
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-x86_64 vllm/vllm-openai:x86_64
|
||||
docker tag vllm/vllm-openai:x86_64 vllm/vllm-openai:latest-x86_64
|
||||
docker tag vllm/vllm-openai:x86_64 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64
|
||||
docker push vllm/vllm-openai:latest-x86_64
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-x86_64
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-x86_64-cu129 vllm/vllm-openai:x86_64-cu129
|
||||
docker tag vllm/vllm-openai:x86_64-cu129 vllm/vllm-openai:latest-x86_64-cu129
|
||||
docker tag vllm/vllm-openai:x86_64-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129
|
||||
docker push vllm/vllm-openai:latest-x86_64-cu129
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-aarch64 vllm/vllm-openai:aarch64
|
||||
docker tag vllm/vllm-openai:aarch64 vllm/vllm-openai:latest-aarch64
|
||||
docker tag vllm/vllm-openai:aarch64 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64
|
||||
docker push vllm/vllm-openai:latest-aarch64
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-aarch64
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-aarch64-cu129 vllm/vllm-openai:aarch64-cu129
|
||||
docker tag vllm/vllm-openai:aarch64-cu129 vllm/vllm-openai:latest-aarch64-cu129
|
||||
docker tag vllm/vllm-openai:aarch64-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129
|
||||
docker push vllm/vllm-openai:latest-aarch64-cu129
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129
|
||||
|
||||
## ROCm
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${BUILDKITE_COMMIT}-rocm vllm/vllm-openai-rocm:${BUILDKITE_COMMIT}
|
||||
docker tag vllm/vllm-openai-rocm:${BUILDKITE_COMMIT} vllm/vllm-openai-rocm:latest
|
||||
docker tag vllm/vllm-openai-rocm:${BUILDKITE_COMMIT} vllm/vllm-openai-rocm:v${RELEASE_VERSION}
|
||||
docker push vllm/vllm-openai-rocm:latest
|
||||
docker push vllm/vllm-openai-rocm:v${RELEASE_VERSION}
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${ROCM_BASE_CACHE_KEY}-rocm-base vllm/vllm-openai-rocm:${BUILDKITE_COMMIT}-base
|
||||
docker tag vllm/vllm-openai-rocm:${BUILDKITE_COMMIT}-base vllm/vllm-openai-rocm:latest-base
|
||||
docker tag vllm/vllm-openai-rocm:${BUILDKITE_COMMIT}-base vllm/vllm-openai-rocm:v${RELEASE_VERSION}-base
|
||||
docker push vllm/vllm-openai-rocm:latest-base
|
||||
docker push vllm/vllm-openai-rocm:v${RELEASE_VERSION}-base
|
||||
|
||||
## CPU
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-cpu-release-repo:v${RELEASE_VERSION} vllm/vllm-openai-cpu:x86_64
|
||||
docker tag vllm/vllm-openai-cpu:x86_64 vllm/vllm-openai-cpu:latest-x86_64
|
||||
docker tag vllm/vllm-openai-cpu:x86_64 vllm/vllm-openai-cpu:v${RELEASE_VERSION}-x86_64
|
||||
docker push vllm/vllm-openai-cpu:latest-x86_64
|
||||
docker push vllm/vllm-openai-cpu:v${RELEASE_VERSION}-x86_64
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-arm64-cpu-release-repo:v${RELEASE_VERSION} vllm/vllm-openai-cpu:arm64
|
||||
docker tag vllm/vllm-openai-cpu:arm64 vllm/vllm-openai-cpu:latest-arm64
|
||||
docker tag vllm/vllm-openai-cpu:arm64 vllm/vllm-openai-cpu:v${RELEASE_VERSION}-arm64
|
||||
docker push vllm/vllm-openai-cpu:latest-arm64
|
||||
docker push vllm/vllm-openai-cpu:v${RELEASE_VERSION}-arm64
|
||||
|
||||
# Create multi-arch manifest:
|
||||
|
||||
docker manifest rm vllm/vllm-openai:latest
|
||||
docker manifest create vllm/vllm-openai:latest vllm/vllm-openai:latest-x86_64 vllm/vllm-openai:latest-aarch64
|
||||
docker manifest create vllm/vllm-openai:v${RELEASE_VERSION} vllm/vllm-openai:v${RELEASE_VERSION}-x86_64 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64
|
||||
docker manifest push vllm/vllm-openai:latest
|
||||
docker manifest push vllm/vllm-openai:v${RELEASE_VERSION}
|
||||
|
||||
docker manifest rm vllm/vllm-openai:latest-cu129
|
||||
docker manifest create vllm/vllm-openai:latest-cu129 vllm/vllm-openai:latest-x86_64-cu129 vllm/vllm-openai:latest-aarch64-cu129
|
||||
docker manifest create vllm/vllm-openai:v${RELEASE_VERSION}-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129
|
||||
docker manifest push vllm/vllm-openai:latest-cu129
|
||||
docker manifest push vllm/vllm-openai:v${RELEASE_VERSION}-cu129
|
||||
|
||||
docker manifest rm vllm/vllm-openai-cpu:latest || true
|
||||
docker manifest create vllm/vllm-openai-cpu:latest vllm/vllm-openai-cpu:latest-x86_64 vllm/vllm-openai-cpu:latest-arm64
|
||||
docker manifest create vllm/vllm-openai-cpu:v${RELEASE_VERSION} vllm/vllm-openai-cpu:v${RELEASE_VERSION}-x86_64 vllm/vllm-openai-cpu:v${RELEASE_VERSION}-arm64
|
||||
docker manifest push vllm/vllm-openai-cpu:latest
|
||||
docker manifest push vllm/vllm-openai-cpu:v${RELEASE_VERSION}
|
||||
\`\`\`
|
||||
Docker images are published automatically by the "Publish release images to DockerHub" pipeline step.
|
||||
EOF
|
||||
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
#
|
||||
# Publish release Docker images from ECR to DockerHub.
|
||||
# Pulls per-arch images, tags with latest and versioned tags, pushes them,
|
||||
# then creates and pushes multi-arch manifests.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RELEASE_VERSION=$(buildkite-agent meta-data get release-version --default "" | sed 's/^v//')
|
||||
if [ -z "${RELEASE_VERSION}" ]; then
|
||||
echo "ERROR: release-version metadata not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMMIT="$BUILDKITE_COMMIT"
|
||||
ROCM_BASE_CACHE_KEY=$(.buildkite/scripts/cache-rocm-base-wheels.sh key)
|
||||
|
||||
echo "========================================"
|
||||
echo "Publishing release images v${RELEASE_VERSION}"
|
||||
echo " Commit: ${COMMIT}"
|
||||
echo " ROCm base cache key: ${ROCM_BASE_CACHE_KEY}"
|
||||
echo "========================================"
|
||||
|
||||
# Login to ECR to pull staging images
|
||||
aws ecr-public get-login-password --region us-east-1 | \
|
||||
docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7
|
||||
|
||||
# ---- CUDA (default: 13.0) ----
|
||||
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64 vllm/vllm-openai:latest-x86_64
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64
|
||||
docker push vllm/vllm-openai:latest-x86_64
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-x86_64
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64 vllm/vllm-openai:latest-aarch64
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64
|
||||
docker push vllm/vllm-openai:latest-aarch64
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-aarch64
|
||||
|
||||
docker manifest rm vllm/vllm-openai:latest || true
|
||||
docker manifest rm vllm/vllm-openai:v${RELEASE_VERSION} || true
|
||||
docker manifest create vllm/vllm-openai:latest vllm/vllm-openai:latest-x86_64 vllm/vllm-openai:latest-aarch64
|
||||
docker manifest create vllm/vllm-openai:v${RELEASE_VERSION} vllm/vllm-openai:v${RELEASE_VERSION}-x86_64 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64
|
||||
docker manifest push vllm/vllm-openai:latest
|
||||
docker manifest push vllm/vllm-openai:v${RELEASE_VERSION}
|
||||
|
||||
# ---- CUDA 12.9 ----
|
||||
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-cu129
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-cu129
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-cu129 vllm/vllm-openai:latest-x86_64-cu129
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129
|
||||
docker push vllm/vllm-openai:latest-x86_64-cu129
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-cu129 vllm/vllm-openai:latest-aarch64-cu129
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129
|
||||
docker push vllm/vllm-openai:latest-aarch64-cu129
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129
|
||||
|
||||
docker manifest rm vllm/vllm-openai:latest-cu129 || true
|
||||
docker manifest rm vllm/vllm-openai:v${RELEASE_VERSION}-cu129 || true
|
||||
docker manifest create vllm/vllm-openai:latest-cu129 vllm/vllm-openai:latest-x86_64-cu129 vllm/vllm-openai:latest-aarch64-cu129
|
||||
docker manifest create vllm/vllm-openai:v${RELEASE_VERSION}-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129
|
||||
docker manifest push vllm/vllm-openai:latest-cu129
|
||||
docker manifest push vllm/vllm-openai:v${RELEASE_VERSION}-cu129
|
||||
|
||||
# ---- Ubuntu 24.04 (CUDA 13.0) ----
|
||||
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-ubuntu2404
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-ubuntu2404
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-ubuntu2404 vllm/vllm-openai:latest-x86_64-ubuntu2404
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-ubuntu2404
|
||||
docker push vllm/vllm-openai:latest-x86_64-ubuntu2404
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-ubuntu2404
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-ubuntu2404 vllm/vllm-openai:latest-aarch64-ubuntu2404
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-ubuntu2404
|
||||
docker push vllm/vllm-openai:latest-aarch64-ubuntu2404
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-ubuntu2404
|
||||
|
||||
docker manifest rm vllm/vllm-openai:latest-ubuntu2404 || true
|
||||
docker manifest rm vllm/vllm-openai:v${RELEASE_VERSION}-ubuntu2404 || true
|
||||
docker manifest create vllm/vllm-openai:latest-ubuntu2404 vllm/vllm-openai:latest-x86_64-ubuntu2404 vllm/vllm-openai:latest-aarch64-ubuntu2404
|
||||
docker manifest create vllm/vllm-openai:v${RELEASE_VERSION}-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-ubuntu2404
|
||||
docker manifest push vllm/vllm-openai:latest-ubuntu2404
|
||||
docker manifest push vllm/vllm-openai:v${RELEASE_VERSION}-ubuntu2404
|
||||
|
||||
# ---- Ubuntu 24.04 (CUDA 12.9) ----
|
||||
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-cu129-ubuntu2404
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-cu129-ubuntu2404
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-cu129-ubuntu2404 vllm/vllm-openai:latest-x86_64-cu129-ubuntu2404
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64-cu129-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129-ubuntu2404
|
||||
docker push vllm/vllm-openai:latest-x86_64-cu129-ubuntu2404
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129-ubuntu2404
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-cu129-ubuntu2404 vllm/vllm-openai:latest-aarch64-cu129-ubuntu2404
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-aarch64-cu129-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129-ubuntu2404
|
||||
docker push vllm/vllm-openai:latest-aarch64-cu129-ubuntu2404
|
||||
docker push vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129-ubuntu2404
|
||||
|
||||
docker manifest rm vllm/vllm-openai:latest-cu129-ubuntu2404 || true
|
||||
docker manifest rm vllm/vllm-openai:v${RELEASE_VERSION}-cu129-ubuntu2404 || true
|
||||
docker manifest create vllm/vllm-openai:latest-cu129-ubuntu2404 vllm/vllm-openai:latest-x86_64-cu129-ubuntu2404 vllm/vllm-openai:latest-aarch64-cu129-ubuntu2404
|
||||
docker manifest create vllm/vllm-openai:v${RELEASE_VERSION}-cu129-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-cu129-ubuntu2404 vllm/vllm-openai:v${RELEASE_VERSION}-aarch64-cu129-ubuntu2404
|
||||
docker manifest push vllm/vllm-openai:latest-cu129-ubuntu2404
|
||||
docker manifest push vllm/vllm-openai:v${RELEASE_VERSION}-cu129-ubuntu2404
|
||||
|
||||
# ---- ROCm ----
|
||||
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-rocm
|
||||
docker pull public.ecr.aws/q9t5s3a7/vllm-release-repo:${ROCM_BASE_CACHE_KEY}-rocm-base
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-rocm vllm/vllm-openai-rocm:latest
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-rocm vllm/vllm-openai-rocm:v${RELEASE_VERSION}
|
||||
docker push vllm/vllm-openai-rocm:latest
|
||||
docker push vllm/vllm-openai-rocm:v${RELEASE_VERSION}
|
||||
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${ROCM_BASE_CACHE_KEY}-rocm-base vllm/vllm-openai-rocm:latest-base
|
||||
docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:${ROCM_BASE_CACHE_KEY}-rocm-base vllm/vllm-openai-rocm:v${RELEASE_VERSION}-base
|
||||
docker push vllm/vllm-openai-rocm:latest-base
|
||||
docker push vllm/vllm-openai-rocm:v${RELEASE_VERSION}-base
|
||||
|
||||
# ---- CPU ----
|
||||
# CPU images are behind separate block steps and may not have been built.
|
||||
# All-or-nothing: inspect both arches first, then either publish everything
|
||||
# (per-arch + multi-arch manifest) or skip everything. Publishing only one
|
||||
# arch would leave `:latest-x86_64` pointing at the new release while the
|
||||
# `:latest` multi-arch manifest still resolves to the previous release.
|
||||
|
||||
CPU_X86_TAG=public.ecr.aws/q9t5s3a7/vllm-cpu-release-repo:v${RELEASE_VERSION}
|
||||
CPU_ARM_TAG=public.ecr.aws/q9t5s3a7/vllm-arm64-cpu-release-repo:v${RELEASE_VERSION}
|
||||
|
||||
CPU_X86_AVAILABLE=false
|
||||
CPU_ARM_AVAILABLE=false
|
||||
docker manifest inspect "${CPU_X86_TAG}" >/dev/null 2>&1 && CPU_X86_AVAILABLE=true
|
||||
docker manifest inspect "${CPU_ARM_TAG}" >/dev/null 2>&1 && CPU_ARM_AVAILABLE=true
|
||||
|
||||
if [ "$CPU_X86_AVAILABLE" = "true" ] && [ "$CPU_ARM_AVAILABLE" = "true" ]; then
|
||||
docker pull "${CPU_X86_TAG}"
|
||||
docker tag "${CPU_X86_TAG}" vllm/vllm-openai-cpu:latest-x86_64
|
||||
docker tag "${CPU_X86_TAG}" vllm/vllm-openai-cpu:v${RELEASE_VERSION}-x86_64
|
||||
docker push vllm/vllm-openai-cpu:latest-x86_64
|
||||
docker push vllm/vllm-openai-cpu:v${RELEASE_VERSION}-x86_64
|
||||
|
||||
docker pull "${CPU_ARM_TAG}"
|
||||
docker tag "${CPU_ARM_TAG}" vllm/vllm-openai-cpu:latest-arm64
|
||||
docker tag "${CPU_ARM_TAG}" vllm/vllm-openai-cpu:v${RELEASE_VERSION}-arm64
|
||||
docker push vllm/vllm-openai-cpu:latest-arm64
|
||||
docker push vllm/vllm-openai-cpu:v${RELEASE_VERSION}-arm64
|
||||
|
||||
docker manifest rm vllm/vllm-openai-cpu:latest || true
|
||||
docker manifest rm vllm/vllm-openai-cpu:v${RELEASE_VERSION} || true
|
||||
docker manifest create vllm/vllm-openai-cpu:latest vllm/vllm-openai-cpu:latest-x86_64 vllm/vllm-openai-cpu:latest-arm64
|
||||
docker manifest create vllm/vllm-openai-cpu:v${RELEASE_VERSION} vllm/vllm-openai-cpu:v${RELEASE_VERSION}-x86_64 vllm/vllm-openai-cpu:v${RELEASE_VERSION}-arm64
|
||||
docker manifest push vllm/vllm-openai-cpu:latest
|
||||
docker manifest push vllm/vllm-openai-cpu:v${RELEASE_VERSION}
|
||||
elif [ "$CPU_X86_AVAILABLE" = "false" ] && [ "$CPU_ARM_AVAILABLE" = "false" ]; then
|
||||
echo "WARNING: Neither CPU image found in ECR, skipping CPU publish (ensure block-cpu-release-image-build and block-arm64-cpu-release-image-build were unblocked and the builds finished pushing)"
|
||||
else
|
||||
# Partial state: one arch built, the other did not. Fail loudly rather than
|
||||
# ship a Docker Hub state where `:latest-${arch}` and `:latest` (multi-arch)
|
||||
# disagree on which release they point at.
|
||||
echo "ERROR: Partial CPU build detected (x86_64=${CPU_X86_AVAILABLE}, arm64=${CPU_ARM_AVAILABLE})."
|
||||
echo " Refusing to publish to avoid split-tag drift between per-arch and multi-arch tags."
|
||||
echo " Re-run the missing CPU build and retry, or manually publish if a single-arch release is intended."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Successfully published release images for v${RELEASE_VERSION}"
|
||||
@@ -51,6 +51,7 @@ vllm serve "$MODEL" \
|
||||
--offload-num-in-group 2 \
|
||||
--offload-prefetch-step 1 \
|
||||
--offload-params w13_weight w2_weight \
|
||||
--generation-config vllm \
|
||||
--port "$PORT" \
|
||||
${EXTRA_ARGS+"${EXTRA_ARGS[@]}"} &
|
||||
SERVER_PID=$!
|
||||
|
||||
@@ -39,10 +39,11 @@ fi
|
||||
|
||||
set -x # avoid printing secrets above
|
||||
|
||||
# install twine from pypi
|
||||
# install twine and sdist build prerequisites from pypi
|
||||
python3 -m venv /tmp/vllm-release-env
|
||||
source /tmp/vllm-release-env/bin/activate
|
||||
pip install twine
|
||||
pip install -r requirements/build/cuda.txt
|
||||
python3 -m twine --version
|
||||
|
||||
# copy release wheels to local directory
|
||||
|
||||
@@ -17,7 +17,7 @@ steps:
|
||||
- label: V1 attention (B200)
|
||||
key: v1-attention-b200
|
||||
timeout_in_minutes: 30
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
source_file_dependencies:
|
||||
- vllm/config/attention.py
|
||||
- vllm/model_executor/layers/attention
|
||||
|
||||
@@ -14,7 +14,7 @@ steps:
|
||||
|
||||
- label: Attention Benchmarks Smoke Test (B200)
|
||||
key: attention-benchmarks-smoke-test-b200
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
num_gpus: 2
|
||||
optional: true
|
||||
working_dir: "/vllm-workspace/"
|
||||
|
||||
@@ -43,7 +43,7 @@ steps:
|
||||
key: asynctp-correctness-tests-b200
|
||||
timeout_in_minutes: 50
|
||||
working_dir: "/vllm-workspace/"
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
optional: true
|
||||
num_devices: 2
|
||||
commands:
|
||||
@@ -68,7 +68,7 @@ steps:
|
||||
key: fusion-and-compile-unit-tests-2xb200
|
||||
timeout_in_minutes: 20
|
||||
working_dir: "/vllm-workspace/"
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
source_file_dependencies:
|
||||
- csrc/quantization/fp4/
|
||||
- vllm/model_executor/layers/quantization/
|
||||
@@ -137,7 +137,7 @@ steps:
|
||||
key: fusion-e2e-config-sweep-b200
|
||||
timeout_in_minutes: 30
|
||||
working_dir: "/vllm-workspace/"
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
num_devices: 1
|
||||
optional: true
|
||||
commands:
|
||||
@@ -209,7 +209,7 @@ steps:
|
||||
key: fusion-e2e-tp2-b200
|
||||
timeout_in_minutes: 20
|
||||
working_dir: "/vllm-workspace/"
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
num_devices: 2
|
||||
source_file_dependencies:
|
||||
- csrc/quantization/
|
||||
|
||||
@@ -212,7 +212,7 @@ steps:
|
||||
|
||||
- label: Distributed Tests (2 GPUs)(B200)
|
||||
key: distributed-tests-2-gpus-b200
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
optional: true
|
||||
working_dir: "/vllm-workspace/"
|
||||
num_devices: 2
|
||||
|
||||
@@ -25,7 +25,7 @@ steps:
|
||||
- label: Qwen3-30B-A3B-FP8-block Accuracy (B200)
|
||||
key: qwen3-30b-a3b-fp8-block-accuracy-b200
|
||||
timeout_in_minutes: 60
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
optional: true
|
||||
num_devices: 2
|
||||
working_dir: "/vllm-workspace"
|
||||
|
||||
@@ -125,7 +125,7 @@ steps:
|
||||
key: kernels-b200
|
||||
timeout_in_minutes: 30
|
||||
working_dir: "/vllm-workspace/"
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
# optional: true
|
||||
source_file_dependencies:
|
||||
- csrc/quantization/fp4/
|
||||
@@ -212,7 +212,7 @@ steps:
|
||||
- label: Kernels Fp4 MoE Test (B200)
|
||||
key: kernels-fp4-moe-test-b200
|
||||
timeout_in_minutes: 60
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
num_devices: 1
|
||||
optional: true
|
||||
commands:
|
||||
|
||||
@@ -51,7 +51,7 @@ steps:
|
||||
- label: LM Eval Qwen3.5 Models (B200)
|
||||
key: lm-eval-qwen3-5-models-b200
|
||||
timeout_in_minutes: 120
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
optional: true
|
||||
num_devices: 2
|
||||
source_file_dependencies:
|
||||
@@ -84,7 +84,7 @@ steps:
|
||||
|
||||
- label: MoE Refactor Integration Test (B200 - TEMPORARY)
|
||||
key: moe-refactor-integration-test-b200-temporary
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
optional: true
|
||||
num_devices: 2
|
||||
commands:
|
||||
|
||||
@@ -224,7 +224,7 @@ steps:
|
||||
- label: Batch Invariance (B200)
|
||||
key: batch-invariance-b200
|
||||
timeout_in_minutes: 30
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
source_file_dependencies:
|
||||
- vllm/v1/attention
|
||||
- vllm/model_executor/layers
|
||||
|
||||
@@ -25,7 +25,7 @@ steps:
|
||||
key: quantized-moe-test-b200
|
||||
timeout_in_minutes: 60
|
||||
working_dir: "/vllm-workspace/"
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
source_file_dependencies:
|
||||
- tests/quantization/test_blackwell_moe.py
|
||||
- vllm/model_executor/models/deepseek_v2.py
|
||||
|
||||
@@ -75,7 +75,7 @@ steps:
|
||||
- label: Spec Decode Draft Model Nightly B200
|
||||
key: spec-decode-draft-model-nightly-b200
|
||||
timeout_in_minutes: 30
|
||||
device: b200
|
||||
device: b200-k8s
|
||||
optional: true
|
||||
source_file_dependencies:
|
||||
- vllm/v1/spec_decode/
|
||||
|
||||
@@ -203,6 +203,7 @@ hardware and configuration.
|
||||
| `FLASH_ATTN`‡ | FlashAttention varlen (FA2/FA3/FA4) | fp16, bf16 | Any | FA4 on SM100+, FA3 on SM90, FA2 otherwise |
|
||||
| `TRTLLM_RAGGED` | TensorRT-LLM ragged attention | fp16, bf16 | 10.x | DeepSeek R1 dims only |
|
||||
| `FLASHINFER` | FlashInfer CUTLASS backend | fp16, bf16 | 10.x | DeepSeek R1 dims only |
|
||||
| `TOKENSPEED_MLA` | | fp16, bf16 | 10.x | DeepSeek R1 dims only |
|
||||
|
||||
> **‡** TRT-LLM Ragged is the default on Blackwell (SM100).
|
||||
> On other GPUs, FlashAttention is used as the default.
|
||||
@@ -223,5 +224,6 @@ MLA decode backends are selected using the standard
|
||||
| `ROCM_AITER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %1 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
|
||||
| `ROCM_AITER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 1, 64 | Any | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
|
||||
| `ROCM_AITER_TRITON_MLA` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
|
||||
| `TOKENSPEED_MLA` | fp16, bf16 | `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
|
||||
| `TRITON_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
|
||||
| `XPU_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16` | Any | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | Any |
|
||||
|
||||
@@ -49,7 +49,7 @@ ijson # Required for mistral streaming tool parser
|
||||
setproctitle # Used to set process names for better debugging and monitoring
|
||||
openai-harmony >= 0.0.3 # Required for gpt-oss
|
||||
anthropic >= 0.71.0
|
||||
model-hosting-container-standards >= 0.1.13, < 1.0.0
|
||||
model-hosting-container-standards >= 0.1.14, < 1.0.0
|
||||
mcp
|
||||
opentelemetry-sdk >= 1.27.0
|
||||
opentelemetry-api >= 1.27.0
|
||||
|
||||
@@ -23,3 +23,6 @@ fastsafetensors >= 0.2.2
|
||||
# QuACK and Cutlass DSL for FA4 (cute-DSL implementation)
|
||||
nvidia-cutlass-dsl>=4.4.2
|
||||
quack-kernels>=0.3.3
|
||||
|
||||
# Tokenspeed_MLA for faster mla with spec decode
|
||||
tokenspeed-mla==0.1.1
|
||||
@@ -0,0 +1,128 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Parity: tokenspeed_mla_decode vs flashinfer trtllm_batch_decode_with_kv_cache_mla."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
if not current_platform.has_device_capability(100):
|
||||
pytest.skip(
|
||||
reason="tokenspeed_mla / TRT-LLM MLA decode require Blackwell (SM100+).",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
try:
|
||||
from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
|
||||
except ImportError:
|
||||
pytest.skip(reason="flashinfer not installed", allow_module_level=True)
|
||||
|
||||
try:
|
||||
from tokenspeed_mla import get_num_sm, tokenspeed_mla_decode
|
||||
except ImportError:
|
||||
pytest.skip(reason="tokenspeed_mla not installed", allow_module_level=True)
|
||||
|
||||
|
||||
FLASHINFER_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
|
||||
_TS_MAX_Q_LEN = 8
|
||||
|
||||
|
||||
def _ts_workspace(device, num_heads, kv_lora_rank):
|
||||
needed = get_num_sm(device) * num_heads * _TS_MAX_Q_LEN * (kv_lora_rank + 1) * 4
|
||||
return torch.empty(needed, dtype=torch.int8, device=device)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bs", [1, 2, 4, 16])
|
||||
@pytest.mark.parametrize("block_size", [32, 64])
|
||||
@pytest.mark.parametrize("q_len_per_request", [1, 2, 4])
|
||||
def test_tokenspeed_vs_trtllm_decode(bs: int, block_size: int, q_len_per_request: int):
|
||||
"""Match tokenspeed_mla_decode against TRT-LLM batch decode MLA.
|
||||
|
||||
Both kernels consume the same FP8 KV cache, paged block table, and
|
||||
seq_lens. The only structural difference is rank: TRT-LLM expects 4D
|
||||
(`unsqueeze(1)` for the kv-head dim) while tokenspeed expects 3D. We
|
||||
pass each kernel its preferred shape from the same underlying tensor.
|
||||
"""
|
||||
torch.set_default_device("cuda")
|
||||
torch.manual_seed(42)
|
||||
|
||||
# Deepseek R1 dims — both kernels are R1-shape-specialized.
|
||||
num_heads = 128
|
||||
kv_lora_rank = 512
|
||||
qk_nope_head_dim = 128
|
||||
qk_rope_head_dim = 64
|
||||
qk_head_dim = kv_lora_rank + qk_rope_head_dim
|
||||
scale = (qk_nope_head_dim + qk_rope_head_dim) ** -0.5
|
||||
|
||||
MAX_SEQ_LEN = 1024
|
||||
|
||||
seq_lens = [torch.randint(2, MAX_SEQ_LEN, (1,)).item() for _ in range(bs)]
|
||||
seq_lens[-1] = MAX_SEQ_LEN
|
||||
max_seq_len = max(seq_lens)
|
||||
seq_lens_tensor = torch.tensor(seq_lens, dtype=torch.int32)
|
||||
|
||||
blocks_per_seq = (seq_lens_tensor + block_size - 1) // block_size
|
||||
max_num_blocks_per_seq = max(blocks_per_seq.max().item(), 4)
|
||||
total_blocks_needed = sum(blocks_per_seq).item()
|
||||
all_block_ids = torch.randperm(total_blocks_needed, dtype=torch.int32)
|
||||
|
||||
block_tables = torch.zeros((bs, max_num_blocks_per_seq), dtype=torch.int32)
|
||||
block_id = 0
|
||||
for i in range(bs):
|
||||
n = blocks_per_seq[i].item()
|
||||
block_tables[i, :n] = all_block_ids[block_id : block_id + n]
|
||||
block_id += n
|
||||
|
||||
# KV cache: build in BF16 then cast once to FP8 so both kernels see the
|
||||
# exact same quantized values. Shape (num_blocks, block_size, qk_head_dim).
|
||||
kv_cache_bf16 = torch.randn(
|
||||
block_tables.numel(), block_size, qk_head_dim, dtype=torch.bfloat16
|
||||
)
|
||||
kv_cache = kv_cache_bf16.to(torch.float8_e4m3fn)
|
||||
|
||||
# Query: (bs, q_len_per_request, num_heads, qk_head_dim) — same layout as
|
||||
# FlashInferMLAImpl.forward_mqa. Cast to FP8 to match KV.
|
||||
q = torch.randn(
|
||||
bs, q_len_per_request, num_heads, qk_head_dim, dtype=torch.bfloat16
|
||||
).to(torch.float8_e4m3fn)
|
||||
|
||||
# --- TRT-LLM reference ---
|
||||
fi_workspace = torch.zeros(FLASHINFER_WORKSPACE_BUFFER_SIZE, dtype=torch.uint8)
|
||||
out_ref = trtllm_batch_decode_with_kv_cache_mla(
|
||||
query=q,
|
||||
kv_cache=kv_cache.unsqueeze(1),
|
||||
workspace_buffer=fi_workspace,
|
||||
qk_nope_head_dim=qk_nope_head_dim,
|
||||
kv_lora_rank=kv_lora_rank,
|
||||
qk_rope_head_dim=qk_rope_head_dim,
|
||||
block_tables=block_tables,
|
||||
seq_lens=seq_lens_tensor,
|
||||
max_seq_len=max_seq_len,
|
||||
bmm1_scale=scale,
|
||||
)
|
||||
|
||||
# --- TokenSpeed candidate ---
|
||||
ts_workspace = _ts_workspace(q.device, num_heads, kv_lora_rank)
|
||||
out_ts = tokenspeed_mla_decode(
|
||||
query=q,
|
||||
kv_cache=kv_cache,
|
||||
workspace_buffer=ts_workspace,
|
||||
kv_lora_rank=kv_lora_rank,
|
||||
qk_rope_head_dim=qk_rope_head_dim,
|
||||
block_tables=block_tables,
|
||||
seq_lens=seq_lens_tensor,
|
||||
max_seq_len=max_seq_len,
|
||||
softmax_scale=scale,
|
||||
)
|
||||
|
||||
# Both kernels output v_head_dim=kv_lora_rank=512 per head.
|
||||
# Output dtypes can differ; compare in float32.
|
||||
out_ref_f = out_ref.to(torch.float32)
|
||||
out_ts_f = out_ts.to(torch.float32)
|
||||
assert out_ref_f.shape == out_ts_f.shape, (
|
||||
f"shape mismatch: trtllm={tuple(out_ref_f.shape)} "
|
||||
f"tokenspeed={tuple(out_ts_f.shape)}"
|
||||
)
|
||||
|
||||
torch.testing.assert_close(out_ts_f, out_ref_f, atol=2e-2, rtol=2e-2)
|
||||
@@ -0,0 +1,249 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Numeric accuracy parity: tokenspeed_mla_prefill vs trtllm_ragged_attention_deepseek.
|
||||
|
||||
Two cases mirror what the vLLM MLA prefill backend does in production:
|
||||
- `test_prefill_no_context`: causal Q==KV ragged batch (run_prefill_new_tokens).
|
||||
- `test_prefill_with_context`: non-causal Q ragged + KV ragged with
|
||||
per-request kv_len > q_len (run_prefill_context_chunk).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
if not current_platform.has_device_capability(100):
|
||||
pytest.skip(
|
||||
reason="tokenspeed_mla / TRT-LLM ragged require Blackwell (SM100+).",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
try:
|
||||
from flashinfer.prefill import trtllm_ragged_attention_deepseek
|
||||
except ImportError:
|
||||
pytest.skip(reason="flashinfer not installed", allow_module_level=True)
|
||||
|
||||
try:
|
||||
from tokenspeed_mla import tokenspeed_mla_prefill, warmup_compile_prefill
|
||||
except ImportError:
|
||||
pytest.skip(reason="tokenspeed_mla not installed", allow_module_level=True)
|
||||
|
||||
|
||||
FLASHINFER_WORKSPACE_BUFFER_SIZE = 384 * 1024 * 1024
|
||||
|
||||
|
||||
# Deepseek R1 dimensions — both kernels are shape-specialized for these.
|
||||
NUM_HEADS = 128
|
||||
KV_LORA_RANK = 512
|
||||
QK_NOPE_HEAD_DIM = 128
|
||||
QK_ROPE_HEAD_DIM = 64
|
||||
V_HEAD_DIM = 128
|
||||
QK_HEAD_DIM = QK_NOPE_HEAD_DIM + QK_ROPE_HEAD_DIM # 192
|
||||
SCALE = QK_HEAD_DIM**-0.5
|
||||
|
||||
|
||||
def _make_q_kv(
|
||||
seq_lens: list[int],
|
||||
kv_lens: list[int],
|
||||
dtype: torch.dtype,
|
||||
):
|
||||
"""Build ragged Q (qk_head_dim) and K (qk_head_dim) / V (v_head_dim)."""
|
||||
total_q = sum(seq_lens)
|
||||
total_kv = sum(kv_lens)
|
||||
|
||||
q = torch.randn(total_q, NUM_HEADS, QK_HEAD_DIM, dtype=torch.bfloat16).to(dtype)
|
||||
k = torch.randn(total_kv, NUM_HEADS, QK_HEAD_DIM, dtype=torch.bfloat16).to(dtype)
|
||||
v = torch.randn(total_kv, NUM_HEADS, V_HEAD_DIM, dtype=torch.bfloat16).to(dtype)
|
||||
return q, k, v
|
||||
|
||||
|
||||
def _cumsum_int32(lens: list[int]) -> torch.Tensor:
|
||||
out = torch.zeros(len(lens) + 1, dtype=torch.int32)
|
||||
out[1:] = torch.tensor(lens, dtype=torch.int32).cumsum(0)
|
||||
return out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float8_e4m3fn])
|
||||
@pytest.mark.parametrize("bs", [1, 4, 16])
|
||||
@pytest.mark.parametrize("max_q_len", [64, 256, 1024])
|
||||
def test_prefill_no_context(dtype: torch.dtype, bs: int, max_q_len: int):
|
||||
"""Causal Q==KV ragged: matches the run_prefill_new_tokens code path."""
|
||||
torch.set_default_device("cuda")
|
||||
torch.manual_seed(0)
|
||||
|
||||
if dtype == torch.float8_e4m3fn:
|
||||
warmup_compile_prefill(
|
||||
q_dtype=torch.float8_e4m3fn,
|
||||
d_qk=QK_HEAD_DIM,
|
||||
d_v=V_HEAD_DIM,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
seq_lens = [int(torch.randint(2, max_q_len + 1, (1,)).item()) for _ in range(bs)]
|
||||
seq_lens[-1] = max_q_len # pin the last so max_q_len is hit
|
||||
|
||||
seq_lens_tensor = torch.tensor(seq_lens, dtype=torch.int32)
|
||||
cum_seq_lens = _cumsum_int32(seq_lens)
|
||||
|
||||
q, k, v = _make_q_kv(seq_lens, seq_lens, dtype)
|
||||
|
||||
# --- TRT-LLM reference ---
|
||||
workspace = torch.zeros(FLASHINFER_WORKSPACE_BUFFER_SIZE, dtype=torch.uint8)
|
||||
out_ref = torch.empty(q.shape[0], q.shape[1], v.shape[2], dtype=torch.bfloat16)
|
||||
ref_ret = trtllm_ragged_attention_deepseek(
|
||||
query=q,
|
||||
key=k,
|
||||
value=v,
|
||||
workspace_buffer=workspace,
|
||||
seq_lens=seq_lens_tensor,
|
||||
max_q_len=max_q_len,
|
||||
max_kv_len=max_q_len,
|
||||
bmm1_scale=SCALE,
|
||||
bmm2_scale=1.0,
|
||||
o_sf_scale=1.0,
|
||||
batch_size=bs,
|
||||
window_left=-1,
|
||||
cum_seq_lens_q=cum_seq_lens,
|
||||
cum_seq_lens_kv=cum_seq_lens,
|
||||
enable_pdl=False,
|
||||
is_causal=True,
|
||||
return_lse=False,
|
||||
out=out_ref,
|
||||
)
|
||||
out_ref = ref_ret if not isinstance(ref_ret, tuple) else ref_ret[0]
|
||||
|
||||
# --- TokenSpeed candidate ---
|
||||
out_ts = tokenspeed_mla_prefill(
|
||||
query=q,
|
||||
key=k,
|
||||
value=v,
|
||||
seq_lens=seq_lens_tensor,
|
||||
cum_seq_lens=cum_seq_lens,
|
||||
max_seq_len=max_q_len,
|
||||
batch_size=bs,
|
||||
softmax_scale=SCALE,
|
||||
is_causal=True,
|
||||
return_lse=False,
|
||||
enable_pdl=False,
|
||||
)
|
||||
if isinstance(out_ts, tuple):
|
||||
out_ts = out_ts[0]
|
||||
|
||||
out_ref_f = out_ref.to(torch.float32)
|
||||
out_ts_f = out_ts.to(torch.float32)
|
||||
assert out_ref_f.shape == out_ts_f.shape, (
|
||||
f"shape mismatch: trtllm={tuple(out_ref_f.shape)} "
|
||||
f"tokenspeed={tuple(out_ts_f.shape)}"
|
||||
)
|
||||
|
||||
if dtype == torch.float8_e4m3fn:
|
||||
atol, rtol = 5e-2, 5e-2
|
||||
else:
|
||||
atol, rtol = 1e-2, 1e-2
|
||||
torch.testing.assert_close(out_ts_f, out_ref_f, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float8_e4m3fn])
|
||||
@pytest.mark.parametrize("bs", [1, 4, 16])
|
||||
def test_prefill_with_context(dtype: torch.dtype, bs: int):
|
||||
"""Non-causal Q ragged + KV ragged: run_prefill_context_chunk path.
|
||||
|
||||
Per-request KV length is independent of (and >=) Q length, mimicking the
|
||||
chunked-context call site where KV is the cache chunk and Q is the new tokens.
|
||||
"""
|
||||
torch.set_default_device("cuda")
|
||||
torch.manual_seed(1)
|
||||
|
||||
if dtype == torch.float8_e4m3fn:
|
||||
warmup_compile_prefill(
|
||||
q_dtype=torch.float8_e4m3fn,
|
||||
d_qk=QK_HEAD_DIM,
|
||||
d_v=V_HEAD_DIM,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
q_lens = [int(torch.randint(16, 257, (1,)).item()) for _ in range(bs)]
|
||||
kv_lens = [q_lens[i] + int(torch.randint(0, 1025, (1,)).item()) for i in range(bs)]
|
||||
|
||||
kv_lens_t = torch.tensor(kv_lens, dtype=torch.int32)
|
||||
cum_q = _cumsum_int32(q_lens)
|
||||
cum_kv = _cumsum_int32(kv_lens)
|
||||
max_q_len = max(q_lens)
|
||||
max_kv_len = max(kv_lens)
|
||||
|
||||
q, k, v = _make_q_kv(q_lens, kv_lens, dtype)
|
||||
|
||||
# --- TRT-LLM reference ---
|
||||
workspace = torch.zeros(FLASHINFER_WORKSPACE_BUFFER_SIZE, dtype=torch.uint8)
|
||||
out_ref = torch.empty(q.shape[0], q.shape[1], v.shape[2], dtype=torch.bfloat16)
|
||||
ref_ret = trtllm_ragged_attention_deepseek(
|
||||
query=q,
|
||||
key=k,
|
||||
value=v,
|
||||
workspace_buffer=workspace,
|
||||
seq_lens=kv_lens_t,
|
||||
max_q_len=max_q_len,
|
||||
max_kv_len=max_kv_len,
|
||||
bmm1_scale=SCALE,
|
||||
bmm2_scale=1.0,
|
||||
o_sf_scale=1.0,
|
||||
batch_size=bs,
|
||||
window_left=-1,
|
||||
cum_seq_lens_q=cum_q,
|
||||
cum_seq_lens_kv=cum_kv,
|
||||
enable_pdl=False,
|
||||
is_causal=False,
|
||||
return_lse=True,
|
||||
out=out_ref,
|
||||
)
|
||||
out_ref, lse_ref = ref_ret[0], ref_ret[1]
|
||||
|
||||
# --- TokenSpeed candidate ---
|
||||
ts_ret = tokenspeed_mla_prefill(
|
||||
query=q,
|
||||
key=k,
|
||||
value=v,
|
||||
seq_lens=kv_lens_t,
|
||||
cum_seq_lens=cum_kv,
|
||||
max_seq_len=max_kv_len,
|
||||
batch_size=bs,
|
||||
softmax_scale=SCALE,
|
||||
is_causal=False,
|
||||
return_lse=True,
|
||||
cum_seq_lens_q=cum_q,
|
||||
max_seq_len_q=max_q_len,
|
||||
enable_pdl=False,
|
||||
)
|
||||
out_ts, lse_ts = ts_ret[0], ts_ret[1]
|
||||
|
||||
if dtype == torch.float8_e4m3fn:
|
||||
atol, rtol = 5e-2, 5e-2
|
||||
else:
|
||||
atol, rtol = 1e-2, 1e-2
|
||||
torch.testing.assert_close(
|
||||
out_ts.to(torch.float32),
|
||||
out_ref.to(torch.float32),
|
||||
atol=atol,
|
||||
rtol=rtol,
|
||||
)
|
||||
|
||||
# LSE: trtllm returns (q_len, num_heads). Tokenspeed convention should
|
||||
# match shape-by-shape — if it doesn't, the LSE transpose contract that
|
||||
# merge_attn_states relies on is broken and this assert surfaces it.
|
||||
assert lse_ref.shape == lse_ts.shape, (
|
||||
f"LSE shape mismatch: trtllm={tuple(lse_ref.shape)} "
|
||||
f"tokenspeed={tuple(lse_ts.shape)}"
|
||||
)
|
||||
# Log-base normalization: trtllm returns LSE in log2, tokenspeed and
|
||||
# vLLM's merge_attn_states (triton_merge_attn_states.py:138) both use
|
||||
# natural-log. Convert trtllm's log2 LSE to natural log before
|
||||
# comparison, otherwise we'd be comparing different bases (factor ln 2).
|
||||
import math
|
||||
|
||||
torch.testing.assert_close(
|
||||
lse_ts.to(torch.float32),
|
||||
lse_ref.to(torch.float32) * math.log(2),
|
||||
atol=5e-3,
|
||||
rtol=5e-3,
|
||||
)
|
||||
@@ -590,6 +590,33 @@ def _test_extract_tool_calls_streaming(
|
||||
]
|
||||
assert_tool_calls(actual_tool_calls, expected_tool_calls)
|
||||
|
||||
if expected_tool_calls:
|
||||
assert len(tool_parser.streamed_args_for_tool) == len(expected_tool_calls)
|
||||
assert len(tool_parser.prev_tool_call_arr) == len(expected_tool_calls)
|
||||
for i in range(len(expected_tool_calls)):
|
||||
assert (
|
||||
tool_parser.prev_tool_call_arr[i]["arguments"]
|
||||
== tool_parser.streamed_args_for_tool[i]
|
||||
)
|
||||
assert tool_parser.streamed_args_for_tool[i] == function_args_strs[i]
|
||||
assert (
|
||||
tool_parser.prev_tool_call_arr[i]["name"]
|
||||
== expected_tool_calls[i].function.name
|
||||
)
|
||||
|
||||
# Simulate the serving layer's unstreamed-args check
|
||||
index = len(tool_parser.prev_tool_call_arr) - 1
|
||||
args = tool_parser.prev_tool_call_arr[index].get("arguments", {})
|
||||
expected_call = (
|
||||
args if isinstance(args, str) else json.dumps(args, ensure_ascii=False)
|
||||
)
|
||||
actual_call = tool_parser.streamed_args_for_tool[index]
|
||||
remaining_call = expected_call.replace(actual_call, "", 1)
|
||||
assert remaining_call == ""
|
||||
else:
|
||||
assert len(tool_parser.streamed_args_for_tool) == 0
|
||||
assert len(tool_parser.prev_tool_call_arr) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
ids=[
|
||||
@@ -855,6 +882,8 @@ def test_extract_tool_calls_streaming_v11_no_tools(
|
||||
previous_text = current_text
|
||||
|
||||
assert collected_content == model_output
|
||||
assert len(mistral_tool_parser.streamed_args_for_tool) == 0
|
||||
assert len(mistral_tool_parser.prev_tool_call_arr) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -22,7 +22,6 @@ from vllm.config.vllm import set_current_vllm_config
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
QueryLenSupport,
|
||||
_DecodeConcatQuantFP8,
|
||||
get_mla_prefill_scale,
|
||||
)
|
||||
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
|
||||
@@ -31,6 +30,7 @@ from vllm.utils.math_utils import cdiv
|
||||
from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE
|
||||
from vllm.v1.attention.backend import CommonAttentionMetadata
|
||||
from vllm.v1.attention.backends.fa_utils import flash_attn_supports_mla
|
||||
from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
from vllm.v1.attention.ops.flashmla import is_flashmla_dense_supported
|
||||
from vllm.v1.kv_cache_interface import MLAAttentionSpec
|
||||
@@ -622,6 +622,19 @@ def run_attention_backend(
|
||||
k_scale=k_scale,
|
||||
)
|
||||
|
||||
# Attach prefill backend (normally created by MLAAttention.__init__)
|
||||
prefill_scale = (qk_nope_head_dim + qk_rope_head_dim) ** -0.5
|
||||
prefill_backend_cls = get_mla_prefill_backend(vllm_config)
|
||||
mock_layer.prefill_backend = prefill_backend_cls(
|
||||
num_heads=num_heads,
|
||||
scale=prefill_scale,
|
||||
kv_lora_rank=kv_lora_rank,
|
||||
qk_nope_head_dim=qk_nope_head_dim,
|
||||
qk_rope_head_dim=qk_rope_head_dim,
|
||||
v_head_dim=v_head_dim,
|
||||
vllm_config=vllm_config,
|
||||
)
|
||||
|
||||
# Populate static_forward_context with mock attention layers
|
||||
for layer_name in layer_names:
|
||||
vllm_config.compilation_config.static_forward_context[layer_name] = (
|
||||
@@ -787,7 +800,8 @@ def test_backend_correctness(
|
||||
f"MLA dimensions don't match: {total_head_size} != {head_size}"
|
||||
)
|
||||
decode_scale = 1.0 / (total_head_size**0.5)
|
||||
prefill_scale = get_mla_prefill_scale(vllm_config.model_config)
|
||||
qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
|
||||
prefill_scale = qk_head_dim**-0.5
|
||||
|
||||
# 2. Generate data and compute SDPA reference output for MLA
|
||||
all_q_vllm, all_kv_c_vllm, all_k_pe_vllm = [], [], []
|
||||
|
||||
@@ -2,17 +2,12 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for MLA prefill backend selector."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.config import AttentionConfig, ModelConfig, VllmConfig
|
||||
from vllm.model_executor.layers.attention.mla_attention import get_mla_prefill_scale
|
||||
from vllm.model_executor.layers.rotary_embedding.deepseek_scaling_rope import (
|
||||
yarn_get_mscale,
|
||||
)
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
|
||||
from vllm.v1.attention.backends.mla.prefill.selector import (
|
||||
@@ -58,62 +53,6 @@ def _make_vllm_config(
|
||||
return mock_vllm_config
|
||||
|
||||
|
||||
class TestMLAPrefillScale:
|
||||
"""Tests for the MLA prefill softmax scale."""
|
||||
|
||||
def test_uses_qk_head_dim_for_deepseek_v2_style_mla(self):
|
||||
model_config = SimpleNamespace(
|
||||
hf_text_config=SimpleNamespace(
|
||||
q_lora_rank=None,
|
||||
kv_lora_rank=512,
|
||||
qk_nope_head_dim=128,
|
||||
qk_rope_head_dim=64,
|
||||
v_head_dim=128,
|
||||
rope_parameters={"rope_type": "default"},
|
||||
)
|
||||
)
|
||||
|
||||
assert get_mla_prefill_scale(model_config) == pytest.approx(192**-0.5)
|
||||
|
||||
def test_applies_deepseek_yarn_mscale(self):
|
||||
model_config = SimpleNamespace(
|
||||
hf_text_config=SimpleNamespace(
|
||||
q_lora_rank=None,
|
||||
kv_lora_rank=512,
|
||||
qk_nope_head_dim=128,
|
||||
qk_rope_head_dim=64,
|
||||
v_head_dim=128,
|
||||
rope_parameters={
|
||||
"rope_type": "yarn",
|
||||
"factor": 40,
|
||||
"mscale_all_dim": 0.707,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
mscale = yarn_get_mscale(40, 0.707)
|
||||
assert get_mla_prefill_scale(model_config) == pytest.approx(
|
||||
192**-0.5 * mscale * mscale
|
||||
)
|
||||
|
||||
def test_deepseek_v4_style_mla_does_not_apply_yarn_mscale(self):
|
||||
model_config = SimpleNamespace(
|
||||
hf_text_config=SimpleNamespace(
|
||||
compress_ratios=[4],
|
||||
q_lora_rank=1536,
|
||||
head_dim=128,
|
||||
qk_rope_head_dim=64,
|
||||
rope_parameters={
|
||||
"rope_type": "yarn",
|
||||
"factor": 40,
|
||||
"mscale_all_dim": 0.707,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert get_mla_prefill_scale(model_config) == pytest.approx(128**-0.5)
|
||||
|
||||
|
||||
class TestGetMLAPrefillBackend:
|
||||
"""Tests for get_mla_prefill_backend (public API)."""
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
|
||||
from tests.utils import get_attn_backend_list_based_on_platform
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.config import ModelConfig, ParallelConfig, SpeculativeConfig
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.sampling_params import StructuredOutputsParams
|
||||
|
||||
@@ -77,3 +78,23 @@ def test_eagle_max_len(
|
||||
"is longer than the eagle max length"
|
||||
)
|
||||
assert o.outputs[0].text == "a b c d e " * 15
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spec_max_model_len", [80, 150])
|
||||
def test_mtp_speculative_config_max_model_len(spec_max_model_len: int):
|
||||
"""Regression test for #41456: max_model_len in speculative config
|
||||
should be respected for the draft model."""
|
||||
model_config = ModelConfig(
|
||||
model="XiaomiMiMo/MiMo-7B-Base",
|
||||
runner="generate",
|
||||
max_model_len=200,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
spec_config = SpeculativeConfig(
|
||||
target_model_config=model_config,
|
||||
target_parallel_config=ParallelConfig(),
|
||||
method="mtp",
|
||||
num_speculative_tokens=1,
|
||||
max_model_len=spec_max_model_len,
|
||||
)
|
||||
assert spec_config.draft_model_config.max_model_len == spec_max_model_len
|
||||
|
||||
@@ -50,7 +50,6 @@ MTPModelTypes = Literal[
|
||||
"pangu_ultra_moe_mtp",
|
||||
"step3p5_mtp",
|
||||
"hy_v3_mtp",
|
||||
"gemma4_mtp",
|
||||
]
|
||||
NgramGPUTypes = Literal["ngram_gpu"]
|
||||
DFlashModelTypes = Literal["dflash"]
|
||||
@@ -492,17 +491,6 @@ class SpeculativeConfig:
|
||||
{"n_predict": n_predict, "architectures": ["HYV3MTPModel"]}
|
||||
)
|
||||
|
||||
if hf_config.model_type == "gemma4_assistant":
|
||||
hf_config.model_type = "gemma4_mtp"
|
||||
text_config = getattr(hf_config, "text_config", hf_config)
|
||||
# The assistant runs all decoder layers in a single forward
|
||||
# call to produce one draft token, so n_predict=1.
|
||||
# num_kv_shared_layers must be 0: cross-model KV sharing is
|
||||
# set up by the proposer after model construction.
|
||||
if hasattr(text_config, "num_kv_shared_layers"):
|
||||
text_config.num_kv_shared_layers = 0
|
||||
hf_config.update({"n_predict": 1, "architectures": ["Gemma4MTPModel"]})
|
||||
|
||||
return hf_config
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -638,6 +626,7 @@ class SpeculativeConfig:
|
||||
revision=self.revision,
|
||||
code_revision=self.code_revision,
|
||||
tokenizer_revision=self.target_model_config.tokenizer_revision,
|
||||
max_model_len=self.max_model_len, # type: ignore[arg-type]
|
||||
spec_target_max_model_len=self.target_model_config.max_model_len,
|
||||
quantization=self.quantization,
|
||||
enforce_eager=self.target_model_config.enforce_eager,
|
||||
@@ -849,10 +838,17 @@ class SpeculativeConfig:
|
||||
|
||||
return speculative_max_model_len
|
||||
|
||||
return min(
|
||||
result = min(
|
||||
draft_max_model_len,
|
||||
target_max_model_len,
|
||||
)
|
||||
if result != draft_max_model_len:
|
||||
logger.info(
|
||||
"Overriding draft model max model len from %d to %d",
|
||||
draft_max_model_len,
|
||||
result,
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _verify_and_get_draft_tp(
|
||||
@@ -1044,14 +1040,6 @@ class SpeculativeConfig:
|
||||
slots_per_req += 1
|
||||
return slots_per_req
|
||||
|
||||
def use_gemma4_mtp(self) -> bool:
|
||||
return (
|
||||
self.method == "mtp"
|
||||
and self.draft_model_config is not None
|
||||
and getattr(self.draft_model_config.hf_config, "model_type", None)
|
||||
== "gemma4_mtp"
|
||||
)
|
||||
|
||||
def use_eagle(self) -> bool:
|
||||
return self.method in ("eagle", "eagle3", "mtp", "dflash")
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ from openai.types.responses import (
|
||||
ResponseOutputItem,
|
||||
ResponseOutputItemAddedEvent,
|
||||
ResponseOutputItemDoneEvent,
|
||||
ResponseOutputMessage,
|
||||
ResponsePrompt,
|
||||
ResponseReasoningItem,
|
||||
ResponseReasoningTextDeltaEvent,
|
||||
ResponseReasoningTextDoneEvent,
|
||||
ResponseStatus,
|
||||
@@ -451,18 +453,21 @@ class ResponsesRequest(OpenAIBaseModel):
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def function_call_parsing(cls, data):
|
||||
"""Parse function_call dictionaries into ResponseFunctionToolCall objects.
|
||||
This ensures Pydantic can properly resolve union types in the input field.
|
||||
Function calls provided as dicts are converted to ResponseFunctionToolCall
|
||||
objects before validation, while invalid structures are left for Pydantic
|
||||
to reject with appropriate error messages.
|
||||
"""
|
||||
def input_item_parsing(cls, data):
|
||||
"""Parse input items that are missing required fields or that Pydantic
|
||||
cannot disambiguate in a Union of TypedDict / BaseModel types.
|
||||
|
||||
Specifically handles:
|
||||
- function_call -> ResponseFunctionToolCall
|
||||
- reasoning -> ResponseReasoningItem (auto-generates id)
|
||||
- message(role=assistant) -> ResponseOutputMessage (auto-generates
|
||||
id/status and annotations)
|
||||
|
||||
Invalid structures are left for Pydantic to reject.
|
||||
"""
|
||||
input_data = data.get("input")
|
||||
|
||||
# Early return for None, strings, or bytes
|
||||
# (strings are iterable but shouldn't be processed)
|
||||
if input_data is None or isinstance(input_data, (str, bytes)):
|
||||
return data
|
||||
|
||||
@@ -476,16 +481,61 @@ class ResponsesRequest(OpenAIBaseModel):
|
||||
|
||||
processed_input = []
|
||||
for item in input_data:
|
||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||
if not isinstance(item, dict):
|
||||
processed_input.append(item)
|
||||
continue
|
||||
|
||||
item_type = item.get("type")
|
||||
|
||||
if item_type == "function_call":
|
||||
try:
|
||||
processed_input.append(ResponseFunctionToolCall(**item))
|
||||
except ValidationError:
|
||||
# Let Pydantic handle validation for malformed function calls
|
||||
logger.debug(
|
||||
"Failed to parse function_call to ResponseFunctionToolCall, "
|
||||
"leaving for Pydantic validation"
|
||||
)
|
||||
processed_input.append(item)
|
||||
|
||||
elif item_type == "reasoning":
|
||||
if "id" not in item:
|
||||
item = {**item, "id": f"rs_{random_uuid()}"}
|
||||
try:
|
||||
processed_input.append(ResponseReasoningItem(**item))
|
||||
except ValidationError:
|
||||
logger.debug(
|
||||
"Failed to parse reasoning to ResponseReasoningItem, "
|
||||
"leaving for Pydantic validation"
|
||||
)
|
||||
processed_input.append(item)
|
||||
|
||||
elif item_type == "message" and item.get("role") == "assistant":
|
||||
item = dict(item)
|
||||
if "id" not in item:
|
||||
item["id"] = f"msg_{random_uuid()}"
|
||||
if "status" not in item:
|
||||
item["status"] = "completed"
|
||||
# ResponseOutputText requires annotations
|
||||
if isinstance(item.get("content"), list):
|
||||
new_content = []
|
||||
for c in item["content"]:
|
||||
if (
|
||||
isinstance(c, dict)
|
||||
and c.get("type") == "output_text"
|
||||
and "annotations" not in c
|
||||
):
|
||||
c = {**c, "annotations": []}
|
||||
new_content.append(c)
|
||||
item["content"] = new_content
|
||||
try:
|
||||
processed_input.append(ResponseOutputMessage(**item))
|
||||
except ValidationError:
|
||||
logger.debug(
|
||||
"Failed to parse assistant message to ResponseOutputMessage, "
|
||||
"leaving for Pydantic validation"
|
||||
)
|
||||
processed_input.append(item)
|
||||
|
||||
else:
|
||||
processed_input.append(item)
|
||||
|
||||
|
||||
@@ -238,9 +238,6 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
kFp8StaticTensorSym,
|
||||
kNvfp4Dynamic,
|
||||
)
|
||||
from vllm.model_executor.layers.rotary_embedding.deepseek_scaling_rope import (
|
||||
yarn_get_mscale,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.flashinfer import has_flashinfer
|
||||
from vllm.utils.math_utils import cdiv, round_down
|
||||
@@ -262,7 +259,10 @@ from vllm.v1.attention.backend import (
|
||||
MLAAttentionImpl,
|
||||
SparseMLAAttentionImpl,
|
||||
)
|
||||
from vllm.v1.attention.backends.mla.prefill import MLAPrefillBackend
|
||||
from vllm.v1.attention.backends.mla.prefill import (
|
||||
MLAPrefillBackend,
|
||||
get_mla_prefill_backend,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
get_dcp_local_seq_lens,
|
||||
split_decodes_and_prefills,
|
||||
@@ -454,20 +454,32 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
self.q_pad_num_heads = getattr(self.impl, "q_pad_num_heads", None)
|
||||
self.use_direct_call = not current_platform.opaque_attention_op()
|
||||
|
||||
compilation_config = get_current_vllm_config().compilation_config
|
||||
vllm_config = get_current_vllm_config()
|
||||
compilation_config = vllm_config.compilation_config
|
||||
if prefix in compilation_config.static_forward_context:
|
||||
raise ValueError(f"Duplicate layer name: {prefix}")
|
||||
compilation_config.static_forward_context[prefix] = self
|
||||
|
||||
prefill_backend_cls = get_mla_prefill_backend(vllm_config)
|
||||
self.prefill_backend = prefill_backend_cls(
|
||||
num_heads=self.num_heads,
|
||||
scale=self.scale,
|
||||
kv_lora_rank=self.kv_lora_rank,
|
||||
qk_nope_head_dim=self.qk_nope_head_dim,
|
||||
qk_rope_head_dim=self.qk_rope_head_dim,
|
||||
v_head_dim=self.v_head_dim,
|
||||
vllm_config=vllm_config,
|
||||
)
|
||||
|
||||
self.kv_cache = torch.tensor([])
|
||||
|
||||
self.use_sparse = use_sparse
|
||||
|
||||
vllm_config = get_current_vllm_config_or_none()
|
||||
_vllm_config = get_current_vllm_config_or_none()
|
||||
self.dcp_a2a = (
|
||||
vllm_config is not None
|
||||
and vllm_config.parallel_config.decode_context_parallel_size > 1
|
||||
and vllm_config.parallel_config.dcp_comm_backend == "a2a"
|
||||
_vllm_config is not None
|
||||
and _vllm_config.parallel_config.decode_context_parallel_size > 1
|
||||
and _vllm_config.parallel_config.dcp_comm_backend == "a2a"
|
||||
)
|
||||
|
||||
# Initialize q/k/v range constants.
|
||||
@@ -1330,35 +1342,6 @@ def get_mla_dims(model_config: ModelConfig) -> MLADims:
|
||||
)
|
||||
|
||||
|
||||
def get_mla_prefill_scale(model_config: ModelConfig) -> float:
|
||||
hf_text_config = model_config.hf_text_config
|
||||
mla_dims = get_mla_dims(model_config)
|
||||
qk_head_dim = mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim
|
||||
scale = qk_head_dim**-0.5
|
||||
|
||||
# Deepseek V4 disables YaRN mscale for attention; Deepseek V2/V3 applies
|
||||
# the same mscale correction when constructing the MLA attention module.
|
||||
if hasattr(hf_text_config, "compress_ratios"):
|
||||
return scale
|
||||
|
||||
rope_parameters = getattr(hf_text_config, "rope_parameters", None)
|
||||
if rope_parameters is None:
|
||||
rope_parameters = getattr(hf_text_config, "rope_scaling", None)
|
||||
|
||||
if rope_parameters is None:
|
||||
return scale
|
||||
|
||||
rope_type = rope_parameters.get("rope_type", rope_parameters.get("type"))
|
||||
apply_yarn_scaling = rope_parameters.get("apply_yarn_scaling", True)
|
||||
if rope_type != "default" and apply_yarn_scaling:
|
||||
mscale_all_dim = rope_parameters.get("mscale_all_dim", False)
|
||||
scaling_factor = rope_parameters["factor"]
|
||||
mscale = yarn_get_mscale(float(scaling_factor), float(mscale_all_dim))
|
||||
scale *= mscale * mscale
|
||||
|
||||
return scale
|
||||
|
||||
|
||||
@functools.cache
|
||||
def backend_supports_prefill_query_quantization() -> bool:
|
||||
"""Check if the selected MLA prefill backend supports query quantization.
|
||||
@@ -1384,6 +1367,7 @@ def backend_supports_prefill_query_quantization() -> bool:
|
||||
return backend_cls.get_name() in (
|
||||
"FLASHINFER",
|
||||
"TRTLLM_RAGGED",
|
||||
"TOKENSPEED_MLA",
|
||||
)
|
||||
|
||||
|
||||
@@ -1554,20 +1538,9 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
|
||||
device=device,
|
||||
)
|
||||
|
||||
from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
|
||||
|
||||
prefill_backend_cls = get_mla_prefill_backend(vllm_config)
|
||||
self._prefill_backend = prefill_backend_cls(
|
||||
num_heads=self.num_heads,
|
||||
scale=get_mla_prefill_scale(self.model_config),
|
||||
kv_lora_rank=self.mla_dims.kv_lora_rank,
|
||||
qk_nope_head_dim=self.mla_dims.qk_nope_head_dim,
|
||||
qk_rope_head_dim=self.mla_dims.qk_rope_head_dim,
|
||||
v_head_dim=self.mla_dims.v_head_dim,
|
||||
vllm_config=vllm_config,
|
||||
device=device,
|
||||
layer_names=layer_names,
|
||||
)
|
||||
self._prefill_backend = self.compilation_config.static_forward_context[
|
||||
layer_names[0]
|
||||
].prefill_backend
|
||||
|
||||
supports_spec_decode = self.query_len_support != QueryLenSupport.SINGLE_ONLY
|
||||
self._init_reorder_batch_threshold(
|
||||
|
||||
@@ -140,6 +140,8 @@ def _fwd_kernel_ep_scatter_2(
|
||||
offset_in_s = tl.arange(0, SCALE_HIDDEN_SIZE_PAD)
|
||||
mask_s = offset_in_s < SCALE_HIDDEN_SIZE
|
||||
|
||||
output_tensor_stride0 = output_tensor_stride0.to(tl.int64)
|
||||
|
||||
for token_id in range(start_token_id, total_token_num, grid_num):
|
||||
to_copy = tl.load(recv_x + token_id * recv_x_stride0 + offset_in, mask=mask)
|
||||
to_copy_s = tl.load(
|
||||
@@ -154,12 +156,13 @@ def _fwd_kernel_ep_scatter_2(
|
||||
|
||||
if expert_id >= 0:
|
||||
dest_token_index = tl.atomic_add(expert_start_loc + expert_id, 1)
|
||||
dest_token_index_i64 = dest_token_index.to(tl.int64)
|
||||
tl.store(
|
||||
output_index + token_id * output_index_stride0 + topk_index,
|
||||
dest_token_index,
|
||||
)
|
||||
output_tensor_ptr = (
|
||||
output_tensor + dest_token_index * output_tensor_stride0
|
||||
output_tensor + dest_token_index_i64 * output_tensor_stride0
|
||||
)
|
||||
output_tensor_scale_ptr = (
|
||||
output_tensor_scale + dest_token_index * output_tensor_scale_stride0
|
||||
|
||||
@@ -1,602 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Inference-only Gemma4 MTP (Multi-Token Prediction) model.
|
||||
|
||||
The Gemma4 assistant model is a lightweight decoder that shares KV cache
|
||||
with the target (backbone) model. All assistant decoder layers are
|
||||
KV-shared: they only have Q projections (no K/V projections or norms),
|
||||
and read K/V from the target model's cache at runtime.
|
||||
|
||||
Checkpoint layout (``gemma4_assistant``)::
|
||||
|
||||
model.embed_tokens.* -- token embeddings
|
||||
model.layers.{i}.* -- decoder layers (Q-only attention + MLP)
|
||||
model.norm.* -- final RMSNorm
|
||||
pre_projection.* -- Linear(2 * backbone_hidden_size, hidden_size)
|
||||
post_projection.* -- Linear(hidden_size, backbone_hidden_size)
|
||||
lm_head.* -- language model head (tied to embed_tokens)
|
||||
masked_embedding.centroids.* -- centroid projection (when use_ordered_embeddings)
|
||||
masked_embedding.token_ordering -- token-to-centroid mapping buffer
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm.compilation.decorators import support_torch_compile
|
||||
from vllm.config import CacheConfig, VllmConfig
|
||||
from vllm.distributed import (
|
||||
get_tensor_model_parallel_world_size,
|
||||
tensor_model_parallel_all_gather,
|
||||
)
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.attention import Attention
|
||||
from vllm.model_executor.layers.layernorm import RMSNorm
|
||||
from vllm.model_executor.layers.linear import (
|
||||
ColumnParallelLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from vllm.model_executor.layers.logits_processor import LogitsProcessor
|
||||
from vllm.model_executor.layers.quantization import QuantizationConfig
|
||||
from vllm.model_executor.layers.rotary_embedding import get_rope
|
||||
from vllm.model_executor.layers.vocab_parallel_embedding import (
|
||||
ParallelLMHead,
|
||||
VocabParallelEmbedding,
|
||||
)
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.sequence import IntermediateTensors
|
||||
|
||||
from .gemma4 import Gemma4MLP, _get_text_config
|
||||
from .utils import (
|
||||
AutoWeightsLoader,
|
||||
WeightsMapper,
|
||||
extract_layer_index,
|
||||
maybe_prefix,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class Gemma4MTPMaskedEmbedder(nn.Module):
|
||||
"""Sparse logit computation via centroid-based vocabulary masking.
|
||||
|
||||
Instead of computing logits against the full vocabulary, projects
|
||||
hidden states to centroid scores, selects top-K centroids, and
|
||||
computes logits only for the ~top_k * (vocab_size / num_centroids)
|
||||
tokens belonging to those centroids.
|
||||
"""
|
||||
|
||||
token_ordering: torch.Tensor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
vocab_size: int,
|
||||
num_centroids: int,
|
||||
centroid_intermediate_top_k: int,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.vocab_size = vocab_size
|
||||
self.num_centroids = num_centroids
|
||||
self.centroid_intermediate_top_k = centroid_intermediate_top_k
|
||||
self.vocab_size_per_centroid = vocab_size // num_centroids
|
||||
self.num_selected = centroid_intermediate_top_k * self.vocab_size_per_centroid
|
||||
|
||||
self.centroids = nn.Linear(hidden_size, num_centroids, bias=False)
|
||||
self.register_buffer(
|
||||
"token_ordering",
|
||||
torch.empty(vocab_size, dtype=torch.long),
|
||||
)
|
||||
|
||||
def _select_and_score(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
lm_head_weight: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Centroid selection + sparse dot product.
|
||||
|
||||
Returns:
|
||||
logits: (num_tokens, num_selected) sparse logits.
|
||||
indices: (num_tokens, num_selected) corresponding vocab indices.
|
||||
"""
|
||||
num_tokens = hidden_states.shape[0]
|
||||
_, top_k_indices = torch.topk(
|
||||
self.centroids(hidden_states),
|
||||
k=self.centroid_intermediate_top_k,
|
||||
dim=-1,
|
||||
)
|
||||
clusters = self.token_ordering.view(
|
||||
self.num_centroids,
|
||||
self.vocab_size_per_centroid,
|
||||
)
|
||||
selected = clusters[top_k_indices]
|
||||
embeddings = lm_head_weight[selected.reshape(-1)].view(
|
||||
num_tokens,
|
||||
self.num_selected,
|
||||
self.hidden_size,
|
||||
)
|
||||
logits = torch.einsum("td,tsd->ts", hidden_states, embeddings)
|
||||
return logits, selected.view(num_tokens, -1)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
lm_head_weight: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Full-vocab logits with non-selected positions masked to -inf."""
|
||||
logits, indices = self._select_and_score(hidden_states, lm_head_weight)
|
||||
output = torch.full(
|
||||
(hidden_states.shape[0], self.vocab_size),
|
||||
fill_value=torch.finfo(hidden_states.dtype).min,
|
||||
dtype=hidden_states.dtype,
|
||||
device=hidden_states.device,
|
||||
)
|
||||
return output.scatter_(-1, indices, logits)
|
||||
|
||||
def get_top_tokens(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
lm_head_weight: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Sparse argmax — returns vocab token IDs without full-vocab tensor."""
|
||||
logits, indices = self._select_and_score(hidden_states, lm_head_weight)
|
||||
return indices.gather(-1, logits.argmax(-1, keepdim=True)).squeeze(-1)
|
||||
|
||||
|
||||
class Gemma4MTPAttention(nn.Module):
|
||||
"""Q-only attention for Gemma4 MTP layers.
|
||||
|
||||
K/V come from the target model's KV cache via
|
||||
``kv_sharing_target_layer_name`` (set by the proposer after
|
||||
model construction).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config,
|
||||
hidden_size: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
head_dim: int,
|
||||
max_position_embeddings: int,
|
||||
cache_config: CacheConfig | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
attn_logits_soft_cap: float | None = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.hidden_size = hidden_size
|
||||
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
self.total_num_heads = num_heads
|
||||
self.num_heads = self.total_num_heads // tp_size
|
||||
self.total_num_kv_heads = num_kv_heads
|
||||
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
|
||||
self.head_dim = head_dim
|
||||
self.q_size = self.num_heads * self.head_dim
|
||||
self.scaling = 1.0
|
||||
|
||||
self.q_proj = ColumnParallelLinear(
|
||||
hidden_size,
|
||||
self.total_num_heads * self.head_dim,
|
||||
bias=config.attention_bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.q_proj",
|
||||
)
|
||||
self.o_proj = RowParallelLinear(
|
||||
self.total_num_heads * self.head_dim,
|
||||
hidden_size,
|
||||
bias=config.attention_bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.o_proj",
|
||||
)
|
||||
self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
|
||||
layer_idx = extract_layer_index(prefix)
|
||||
layer_type = config.layer_types[layer_idx]
|
||||
self.is_sliding = layer_type == "sliding_attention"
|
||||
sliding_window = config.sliding_window if self.is_sliding else None
|
||||
|
||||
if layer_type in config.rope_parameters:
|
||||
rope_parameters = dict(config.rope_parameters[layer_type])
|
||||
else:
|
||||
rope_parameters = dict(config.rope_parameters.copy())
|
||||
if self.is_sliding:
|
||||
rope_parameters["rope_theta"] = getattr(
|
||||
config, "rope_local_base_freq", 10000.0
|
||||
)
|
||||
|
||||
self.rotary_emb = get_rope(
|
||||
self.head_dim,
|
||||
max_position=max_position_embeddings,
|
||||
rope_parameters=rope_parameters,
|
||||
is_neox_style=True,
|
||||
)
|
||||
|
||||
# kv_sharing_target_layer_name is set after model construction
|
||||
# by Gemma4Proposer._setup_gemma4_kv_sharing().
|
||||
self.is_kv_shared_layer = True
|
||||
self.attn = Attention(
|
||||
self.num_heads,
|
||||
self.head_dim,
|
||||
self.scaling,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
logits_soft_cap=attn_logits_soft_cap,
|
||||
per_layer_sliding_window=sliding_window,
|
||||
prefix=f"{prefix}.attn",
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
q, _ = self.q_proj(hidden_states)
|
||||
|
||||
q = q.unflatten(-1, (self.num_heads, self.head_dim))
|
||||
q = self.q_norm(q)
|
||||
q = q.flatten(-2, -1)
|
||||
|
||||
q, _ = self.rotary_emb(positions, q, None)
|
||||
|
||||
# Attention reads K/V from the target's cache via KV sharing;
|
||||
# these dummy tensors are never consumed but required by the API.
|
||||
num_tokens = q.shape[0]
|
||||
kv_dummy = torch.empty(
|
||||
num_tokens,
|
||||
self.num_kv_heads * self.head_dim,
|
||||
dtype=q.dtype,
|
||||
device=q.device,
|
||||
)
|
||||
attn_output = self.attn(q, kv_dummy, kv_dummy)
|
||||
output, _ = self.o_proj(attn_output)
|
||||
return output
|
||||
|
||||
|
||||
class Gemma4MTPDecoderLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config,
|
||||
cache_config: CacheConfig | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = config.hidden_size
|
||||
|
||||
layer_idx = extract_layer_index(prefix)
|
||||
layer_type = config.layer_types[layer_idx]
|
||||
is_full_attention = layer_type == "full_attention"
|
||||
head_dim = (
|
||||
getattr(config, "global_head_dim", config.head_dim)
|
||||
if is_full_attention
|
||||
else config.head_dim
|
||||
)
|
||||
|
||||
self.self_attn = Gemma4MTPAttention(
|
||||
config=config,
|
||||
hidden_size=self.hidden_size,
|
||||
num_heads=config.num_attention_heads,
|
||||
num_kv_heads=config.num_key_value_heads,
|
||||
head_dim=head_dim,
|
||||
max_position_embeddings=config.max_position_embeddings,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
attn_logits_soft_cap=getattr(config, "attn_logit_softcapping", None),
|
||||
prefix=f"{prefix}.self_attn",
|
||||
)
|
||||
|
||||
self.mlp = Gemma4MLP(
|
||||
hidden_size=self.hidden_size,
|
||||
intermediate_size=config.intermediate_size,
|
||||
hidden_activation=config.hidden_activation,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.mlp",
|
||||
)
|
||||
|
||||
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.pre_feedforward_layernorm = RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm = RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
self.register_buffer("layer_scalar", torch.ones(1))
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
residual: torch.Tensor | None,
|
||||
**kwargs,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
residual = hidden_states
|
||||
hidden_states = self.input_layernorm(residual)
|
||||
|
||||
hidden_states = self.self_attn(
|
||||
positions=positions,
|
||||
hidden_states=hidden_states,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
hidden_states = self.post_attention_layernorm(hidden_states)
|
||||
hidden_states = hidden_states + residual
|
||||
residual = hidden_states
|
||||
|
||||
hidden_states = self.pre_feedforward_layernorm(hidden_states)
|
||||
hidden_states = self.mlp(hidden_states)
|
||||
|
||||
hidden_states = self.post_feedforward_layernorm(hidden_states)
|
||||
hidden_states = hidden_states + residual
|
||||
|
||||
hidden_states = hidden_states * self.layer_scalar
|
||||
return hidden_states, None
|
||||
|
||||
|
||||
class Gemma4MultiTokenPredictor(nn.Module):
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
|
||||
config = vllm_config.speculative_config.draft_model_config.hf_config
|
||||
text_config = _get_text_config(config)
|
||||
self.config = text_config
|
||||
|
||||
self.hidden_size = text_config.hidden_size
|
||||
self.backbone_hidden_size = getattr(
|
||||
config, "backbone_hidden_size", self.hidden_size
|
||||
)
|
||||
self.vocab_size = text_config.vocab_size
|
||||
self.num_mtp_layers = text_config.num_hidden_layers
|
||||
|
||||
self.embed_tokens = VocabParallelEmbedding(
|
||||
self.vocab_size,
|
||||
self.hidden_size,
|
||||
)
|
||||
|
||||
self.pre_projection = ColumnParallelLinear(
|
||||
2 * self.backbone_hidden_size,
|
||||
self.hidden_size,
|
||||
bias=False,
|
||||
gather_output=True,
|
||||
prefix=f"{prefix}.pre_projection",
|
||||
)
|
||||
|
||||
self.post_projection = RowParallelLinear(
|
||||
self.hidden_size,
|
||||
self.backbone_hidden_size,
|
||||
bias=False,
|
||||
input_is_parallel=False,
|
||||
prefix=f"{prefix}.post_projection",
|
||||
)
|
||||
|
||||
self.layers = nn.ModuleList(
|
||||
Gemma4MTPDecoderLayer(
|
||||
text_config,
|
||||
cache_config=vllm_config.cache_config,
|
||||
quant_config=vllm_config.quant_config,
|
||||
prefix=f"{prefix}.layers.{idx}",
|
||||
)
|
||||
for idx in range(self.num_mtp_layers)
|
||||
)
|
||||
|
||||
self.norm = RMSNorm(self.hidden_size, eps=text_config.rms_norm_eps)
|
||||
|
||||
# After embedding sharing, embed_tokens is replaced with the
|
||||
# target model's backbone-dim embedding. Scale by
|
||||
# sqrt(backbone_hidden_size) to match the target's convention.
|
||||
self.register_buffer(
|
||||
"normalizer",
|
||||
torch.tensor(self.backbone_hidden_size**0.5),
|
||||
persistent=False,
|
||||
)
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.embed_tokens(input_ids) * self.normalizer
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
stacked_params_mapping = [
|
||||
("gate_up_proj", "gate_proj", 0),
|
||||
("gate_up_proj", "up_proj", 1),
|
||||
]
|
||||
|
||||
params_dict = dict(self.named_parameters())
|
||||
params_dict.update(dict(self.named_buffers()))
|
||||
loaded_params: set[str] = set()
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
if "rotary_emb.inv_freq" in name:
|
||||
continue
|
||||
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if weight_name not in name:
|
||||
continue
|
||||
name = name.replace(weight_name, param_name)
|
||||
if name.endswith(".bias") and name not in params_dict:
|
||||
continue
|
||||
if name not in params_dict:
|
||||
continue
|
||||
param = params_dict[name]
|
||||
weight_loader = param.weight_loader
|
||||
weight_loader(param, loaded_weight, shard_id)
|
||||
break
|
||||
else:
|
||||
if name.endswith(".bias") and name not in params_dict:
|
||||
continue
|
||||
if name not in params_dict:
|
||||
continue
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(name)
|
||||
|
||||
return loaded_params
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
spec_step_idx: int = 0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Returns (draft_hidden_states, backbone_hidden_states).
|
||||
|
||||
draft_hidden_states: draft-dim, used by compute_logits via lm_head.
|
||||
backbone_hidden_states: backbone-dim, stored in the proposer's
|
||||
hidden-state buffer and fed back as input to the next step.
|
||||
"""
|
||||
if inputs_embeds is None:
|
||||
inputs_embeds = self.embed_input_ids(input_ids)
|
||||
|
||||
combined = torch.cat([inputs_embeds, hidden_states], dim=-1)
|
||||
hidden_states, _ = self.pre_projection(combined)
|
||||
|
||||
residual = None
|
||||
for layer in self.layers:
|
||||
hidden_states, residual = layer(
|
||||
positions=positions,
|
||||
hidden_states=hidden_states,
|
||||
residual=residual,
|
||||
)
|
||||
|
||||
draft_hidden_states = self.norm(hidden_states)
|
||||
|
||||
backbone_hidden_states, _ = self.post_projection(draft_hidden_states)
|
||||
return draft_hidden_states, backbone_hidden_states
|
||||
|
||||
|
||||
@support_torch_compile
|
||||
class Gemma4MTP(nn.Module):
|
||||
"""Gemma4 Multi-Token Prediction model for speculative decoding.
|
||||
|
||||
forward() returns (draft_hidden_states, backbone_hidden_states).
|
||||
The proposer uses draft_hidden_states for compute_logits (via
|
||||
the draft-dim lm_head) and backbone_hidden_states for the
|
||||
hidden-state feedback buffer.
|
||||
"""
|
||||
|
||||
has_own_lm_head = True
|
||||
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
orig_to_new_prefix={
|
||||
"pre_projection.": "model.pre_projection.",
|
||||
"post_projection.": "model.post_projection.",
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
config = vllm_config.speculative_config.draft_model_config.hf_config
|
||||
text_config = _get_text_config(config)
|
||||
self.config = config
|
||||
|
||||
self.model = Gemma4MultiTokenPredictor(
|
||||
vllm_config=vllm_config,
|
||||
prefix=maybe_prefix(prefix, "model"),
|
||||
)
|
||||
|
||||
# lm_head operates in draft-dim. Tied to embed_tokens at init
|
||||
# so load_weights populates both from a single checkpoint entry.
|
||||
# After embedding sharing, lm_head.weight still references the
|
||||
# original draft-dim tensor.
|
||||
self.lm_head = ParallelLMHead(
|
||||
text_config.vocab_size,
|
||||
text_config.hidden_size,
|
||||
prefix=maybe_prefix(prefix, "lm_head"),
|
||||
)
|
||||
if getattr(config, "tie_word_embeddings", True):
|
||||
self.lm_head.weight = self.model.embed_tokens.weight
|
||||
|
||||
self.logits_processor = LogitsProcessor(
|
||||
text_config.vocab_size,
|
||||
soft_cap=getattr(text_config, "final_logit_softcapping", None),
|
||||
)
|
||||
|
||||
if getattr(config, "use_ordered_embeddings", False):
|
||||
num_centroids = getattr(config, "num_centroids", 2048)
|
||||
top_k = getattr(config, "centroid_intermediate_top_k", 32)
|
||||
self.masked_embedding = Gemma4MTPMaskedEmbedder(
|
||||
hidden_size=text_config.hidden_size,
|
||||
vocab_size=text_config.vocab_size,
|
||||
num_centroids=num_centroids,
|
||||
centroid_intermediate_top_k=top_k,
|
||||
)
|
||||
logger.info(
|
||||
"Gemma4 MTP: centroids masking enabled "
|
||||
"(num_centroids=%d, top_k=%d, active_tokens=%d/%d).",
|
||||
num_centroids,
|
||||
top_k,
|
||||
top_k * (text_config.vocab_size // num_centroids),
|
||||
text_config.vocab_size,
|
||||
)
|
||||
else:
|
||||
self.masked_embedding = None
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.embed_input_ids(input_ids)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor | None,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
spec_step_idx: int = 0,
|
||||
**kwargs: object,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return self.model(
|
||||
input_ids,
|
||||
positions,
|
||||
hidden_states,
|
||||
intermediate_tensors,
|
||||
inputs_embeds,
|
||||
spec_step_idx,
|
||||
)
|
||||
|
||||
def _get_full_lm_head_weight(self) -> torch.Tensor:
|
||||
lm_head_weight = self.lm_head.weight
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
if tp_size > 1:
|
||||
lm_head_weight = tensor_model_parallel_all_gather(
|
||||
lm_head_weight,
|
||||
dim=0,
|
||||
)
|
||||
return lm_head_weight[: self.masked_embedding.vocab_size]
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
spec_step_idx: int = 0,
|
||||
) -> torch.Tensor | None:
|
||||
if self.masked_embedding is not None:
|
||||
return self.masked_embedding(
|
||||
hidden_states,
|
||||
self._get_full_lm_head_weight(),
|
||||
)
|
||||
return self.logits_processor(self.lm_head, hidden_states)
|
||||
|
||||
def get_top_tokens(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Sparse argmax via centroids masking. Returns token IDs directly."""
|
||||
return self.masked_embedding.get_top_tokens(
|
||||
hidden_states,
|
||||
self._get_full_lm_head_weight(),
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
||||
@@ -601,7 +601,6 @@ _SPECULATIVE_DECODING_MODELS = {
|
||||
"EagleDeepSeekMTPModel": ("deepseek_eagle", "EagleDeepseekV3ForCausalLM"),
|
||||
"DeepSeekMTPModel": ("deepseek_mtp", "DeepSeekMTP"),
|
||||
"DeepSeekV4MTPModel": ("deepseek_v4_mtp", "DeepSeekV4MTP"),
|
||||
"Gemma4MTPModel": ("gemma4_mtp", "Gemma4MTP"),
|
||||
"ErnieMTPModel": ("ernie_mtp", "ErnieMTP"),
|
||||
"ExaoneMoeMTP": ("exaone_moe_mtp", "ExaoneMoeMTP"),
|
||||
"Exaone4_5_MTP": ("exaone4_5_mtp", "Exaone4_5_MTP"),
|
||||
|
||||
@@ -623,13 +623,6 @@ class MistralToolParser(ToolParser):
|
||||
if len(delta_tool_calls) > 0:
|
||||
delta.tool_calls = delta_tool_calls
|
||||
|
||||
# HACK: serving_chat.py inspects the internal state of tool parsers
|
||||
# when determining its final streaming delta, automatically
|
||||
# adding autocompleted JSON.
|
||||
# These two lines avoid that nonsense while ensuring finish_reason
|
||||
# is set to tool_calls when at least one tool is called.
|
||||
if delta_tool_calls and not self.prev_tool_call_arr:
|
||||
self.prev_tool_call_arr = [{"arguments": {}}]
|
||||
return delta
|
||||
|
||||
def _generate_delta_tool_call(self, delta_text: str) -> list[DeltaToolCall]:
|
||||
@@ -642,6 +635,8 @@ class MistralToolParser(ToolParser):
|
||||
StreamingState.PARSING_ARGUMENTS,
|
||||
] and delta_text.startswith(self.bot_token):
|
||||
self.current_tool_id += 1
|
||||
self.streamed_args_for_tool.append("")
|
||||
self.prev_tool_call_arr.append({})
|
||||
self.streaming_state = StreamingState.PARSING_NAME
|
||||
delta_text = delta_text.replace(self.bot_token, "", 1)
|
||||
if self.streaming_state == StreamingState.PARSING_NAME:
|
||||
@@ -655,6 +650,9 @@ class MistralToolParser(ToolParser):
|
||||
self.current_tool_name += delta_function_name
|
||||
# HF tokenizers may include [ARGS] in the text
|
||||
self.current_tool_name = self.current_tool_name.replace("[ARGS]", "")
|
||||
self.prev_tool_call_arr[self.current_tool_id]["name"] = (
|
||||
self.current_tool_name
|
||||
)
|
||||
delta_text = delta_text[len(delta_function_name) :]
|
||||
self.streaming_state = StreamingState.PARSING_ARGUMENTS
|
||||
else:
|
||||
@@ -671,6 +669,10 @@ class MistralToolParser(ToolParser):
|
||||
self.streaming_state = StreamingState.TOOL_COMPLETE
|
||||
else:
|
||||
delta_arguments = delta_text
|
||||
self.streamed_args_for_tool[self.current_tool_id] += delta_arguments
|
||||
self.prev_tool_call_arr[self.current_tool_id]["arguments"] = (
|
||||
self.streamed_args_for_tool[self.current_tool_id]
|
||||
)
|
||||
ret = []
|
||||
if self.current_tool_name or delta_arguments:
|
||||
ret += [
|
||||
@@ -820,9 +822,12 @@ class MistralToolParser(ToolParser):
|
||||
if self.current_tool_mistral_id is not None:
|
||||
current_tool_call.id = self.current_tool_mistral_id
|
||||
self.current_tool_mistral_id = None
|
||||
self._track_streamed_args_pre_v11(current_tool_call)
|
||||
delta_tool_calls.append(current_tool_call)
|
||||
current_tool_call_modified = False
|
||||
self.current_tool_id += 1
|
||||
self.streamed_args_for_tool.append("")
|
||||
self.prev_tool_call_arr.append({})
|
||||
self.current_tool_mistral_id = MistralToolCall.generate_random_id()
|
||||
current_tool_call = DeltaToolCall(
|
||||
index=self.current_tool_id,
|
||||
@@ -835,6 +840,9 @@ class MistralToolParser(ToolParser):
|
||||
# we have the complete tool name
|
||||
current_tool_call_modified = True
|
||||
current_tool_call.function.name = self.current_tool_name
|
||||
self.prev_tool_call_arr[self.current_tool_id]["name"] = (
|
||||
self.current_tool_name
|
||||
)
|
||||
self.current_tool_name = None
|
||||
if self.streaming_state == StreamingState.PARSING_NAME_COMPLETED:
|
||||
self.streaming_state = StreamingState.WAITING_FOR_TOOL_KEY
|
||||
@@ -860,16 +868,9 @@ class MistralToolParser(ToolParser):
|
||||
if self.current_tool_mistral_id is not None:
|
||||
current_tool_call.id = self.current_tool_mistral_id
|
||||
self.current_tool_mistral_id = None
|
||||
self._track_streamed_args_pre_v11(current_tool_call)
|
||||
delta_tool_calls.append(current_tool_call)
|
||||
|
||||
# HACK: serving_chat.py inspects the internal state of tool parsers
|
||||
# when determining it's final streaming delta, automatically
|
||||
# adding autocompleted JSON.
|
||||
# These two lines avoid that nonsense while ensuring finish_reason
|
||||
# is set to tool_calls when at least one tool is called.
|
||||
if delta_tool_calls and not self.prev_tool_call_arr:
|
||||
self.prev_tool_call_arr = [{"arguments": {}}]
|
||||
|
||||
if content or len(delta_tool_calls) > 0:
|
||||
delta_message = DeltaMessage()
|
||||
if content:
|
||||
@@ -883,6 +884,16 @@ class MistralToolParser(ToolParser):
|
||||
else:
|
||||
return None
|
||||
|
||||
def _track_streamed_args_pre_v11(self, tool_call: DeltaToolCall) -> None:
|
||||
r"""Accumulate `tool_call` arguments into the streaming state."""
|
||||
if tool_call.function is not None and tool_call.function.arguments is not None:
|
||||
self.streamed_args_for_tool[self.current_tool_id] += (
|
||||
tool_call.function.arguments
|
||||
)
|
||||
self.prev_tool_call_arr[self.current_tool_id]["arguments"] = (
|
||||
self.streamed_args_for_tool[self.current_tool_id]
|
||||
)
|
||||
|
||||
def _split_delta(
|
||||
self,
|
||||
delta_text: str,
|
||||
|
||||
@@ -512,17 +512,6 @@ class LongCatFlashMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
return getattr(self.hf_text_config, "num_nextn_predict_layers", 1)
|
||||
|
||||
|
||||
class Gemma4MTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
def get_hidden_size(self) -> int:
|
||||
# The speculator buffer must match the backbone (target) model's
|
||||
# hidden dimension, not the draft model's smaller dimension.
|
||||
return getattr(self.hf_config, "backbone_hidden_size",
|
||||
super().get_hidden_size())
|
||||
|
||||
def get_num_hidden_layers(self) -> int:
|
||||
return getattr(self.hf_text_config, "num_hidden_layers", 0)
|
||||
|
||||
|
||||
class Gemma4ModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
def is_mm_prefix_lm(self) -> bool:
|
||||
return (
|
||||
@@ -552,7 +541,6 @@ MODEL_ARCH_CONFIG_CONVERTORS = {
|
||||
"falcon": FalconModelArchConfigConvertor,
|
||||
"gemma4": Gemma4ModelArchConfigConvertor,
|
||||
"gemma4_text": Gemma4ModelArchConfigConvertor,
|
||||
"gemma4_mtp": Gemma4MTPModelArchConfigConvertor,
|
||||
"RefinedWeb": FalconModelArchConfigConvertor,
|
||||
"RefinedWebModel": FalconModelArchConfigConvertor,
|
||||
"nemotron-nas": NemotronNasModelArchConfigConvertor,
|
||||
|
||||
@@ -81,8 +81,6 @@ class MLAPrefillBackend(ABC):
|
||||
qk_rope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
vllm_config: "VllmConfig",
|
||||
device: torch.device,
|
||||
layer_names: list[str] | None = None,
|
||||
) -> None:
|
||||
self.num_heads = num_heads
|
||||
self.scale = scale
|
||||
@@ -91,8 +89,6 @@ class MLAPrefillBackend(ABC):
|
||||
self.qk_rope_head_dim = qk_rope_head_dim
|
||||
self.v_head_dim = v_head_dim
|
||||
self.vllm_config = vllm_config
|
||||
self.device = device
|
||||
self.layer_names = layer_names
|
||||
|
||||
def prepare_metadata( # noqa: B027
|
||||
self,
|
||||
|
||||
@@ -44,8 +44,6 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
|
||||
qk_rope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
vllm_config: "VllmConfig",
|
||||
device: torch.device,
|
||||
layer_names: list[str] | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
num_heads=num_heads,
|
||||
@@ -55,8 +53,6 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
|
||||
qk_rope_head_dim=qk_rope_head_dim,
|
||||
v_head_dim=v_head_dim,
|
||||
vllm_config=vllm_config,
|
||||
device=device,
|
||||
layer_names=layer_names,
|
||||
)
|
||||
|
||||
# Handle the differences between the flash_attn_varlen from
|
||||
|
||||
@@ -9,6 +9,7 @@ import torch
|
||||
import vllm.envs as envs
|
||||
from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
PerLayerParameters,
|
||||
get_per_layer_parameters,
|
||||
infer_global_hyperparameters,
|
||||
)
|
||||
@@ -62,8 +63,6 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
qk_rope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
vllm_config: "VllmConfig",
|
||||
device: torch.device,
|
||||
layer_names: list[str] | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
num_heads=num_heads,
|
||||
@@ -73,25 +72,11 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
qk_rope_head_dim=qk_rope_head_dim,
|
||||
v_head_dim=v_head_dim,
|
||||
vllm_config=vllm_config,
|
||||
device=device,
|
||||
layer_names=layer_names,
|
||||
)
|
||||
|
||||
self._prefill_main: BatchPrefillWithRaggedKVCacheWrapper | None = None
|
||||
self._prefill_chunks: list[BatchPrefillWithRaggedKVCacheWrapper] = []
|
||||
if layer_names is None:
|
||||
raise ValueError(
|
||||
"FlashInferPrefillBackend requires layer_names to "
|
||||
"initialize global hyperparameters."
|
||||
)
|
||||
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLACommonImpl,
|
||||
)
|
||||
|
||||
self._global_hyperparameters = infer_global_hyperparameters(
|
||||
get_per_layer_parameters(vllm_config, layer_names, MLACommonImpl) # type: ignore[type-abstract]
|
||||
)
|
||||
self._global_hyperparameters: PerLayerParameters | None = None
|
||||
|
||||
def _ensure_chunks(
|
||||
self,
|
||||
@@ -106,10 +91,36 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
)
|
||||
)
|
||||
|
||||
def _resolve_global_hyperparameters(self) -> PerLayerParameters:
|
||||
if self._global_hyperparameters is not None:
|
||||
return self._global_hyperparameters
|
||||
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLAAttention,
|
||||
MLACommonImpl,
|
||||
)
|
||||
|
||||
forward_context = self.vllm_config.compilation_config.static_forward_context
|
||||
layer_names = [
|
||||
name
|
||||
for name, layer in forward_context.items()
|
||||
if isinstance(layer, MLAAttention)
|
||||
]
|
||||
|
||||
self._global_hyperparameters = infer_global_hyperparameters(
|
||||
get_per_layer_parameters(
|
||||
self.vllm_config,
|
||||
layer_names,
|
||||
MLACommonImpl, # type: ignore[type-abstract]
|
||||
)
|
||||
)
|
||||
return self._global_hyperparameters
|
||||
|
||||
def prepare_metadata(
|
||||
self,
|
||||
prefill_metadata: "MLACommonPrefillMetadata",
|
||||
) -> None:
|
||||
global_hyperparameters = self._resolve_global_hyperparameters()
|
||||
qo_indptr = prefill_metadata.query_start_loc
|
||||
has_context = prefill_metadata.chunked_context is not None
|
||||
(workspace_buffer,) = current_workspace_manager().get_simultaneous(
|
||||
@@ -144,9 +155,9 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
head_dim_qk=head_dim_qk,
|
||||
head_dim_vo=head_dim_vo,
|
||||
causal=True,
|
||||
sm_scale=self._global_hyperparameters.sm_scale,
|
||||
window_left=self._global_hyperparameters.window_left,
|
||||
logits_soft_cap=self._global_hyperparameters.logits_soft_cap,
|
||||
sm_scale=global_hyperparameters.sm_scale,
|
||||
window_left=global_hyperparameters.window_left,
|
||||
logits_soft_cap=global_hyperparameters.logits_soft_cap,
|
||||
q_data_type=prefill_metadata.q_data_type,
|
||||
o_data_type=prefill_metadata.output_dtype,
|
||||
)
|
||||
@@ -165,9 +176,9 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
head_dim_qk=head_dim_qk,
|
||||
head_dim_vo=head_dim_vo,
|
||||
causal=False,
|
||||
sm_scale=self._global_hyperparameters.sm_scale,
|
||||
window_left=self._global_hyperparameters.window_left,
|
||||
logits_soft_cap=self._global_hyperparameters.logits_soft_cap,
|
||||
sm_scale=global_hyperparameters.sm_scale,
|
||||
window_left=global_hyperparameters.window_left,
|
||||
logits_soft_cap=global_hyperparameters.logits_soft_cap,
|
||||
q_data_type=prefill_metadata.q_data_type,
|
||||
o_data_type=prefill_metadata.output_dtype,
|
||||
)
|
||||
|
||||
@@ -43,6 +43,10 @@ class MLAPrefillBackendEnum(Enum, metaclass=_MLAPrefillBackendEnumMeta):
|
||||
"vllm.v1.attention.backends.mla.prefill.trtllm_ragged."
|
||||
"TrtllmRaggedPrefillBackend"
|
||||
)
|
||||
TOKENSPEED_MLA = (
|
||||
"vllm.v1.attention.backends.mla.prefill.tokenspeed_mla."
|
||||
"TokenspeedMLAPrefillBackend"
|
||||
)
|
||||
|
||||
def get_path(self) -> str:
|
||||
"""Get the fully qualified class path for this backend."""
|
||||
|
||||
@@ -67,6 +67,7 @@ def _get_mla_prefill_backend_priorities(
|
||||
MLAPrefillBackendEnum.FLASH_ATTN,
|
||||
MLAPrefillBackendEnum.TRTLLM_RAGGED,
|
||||
MLAPrefillBackendEnum.FLASHINFER,
|
||||
MLAPrefillBackendEnum.TOKENSPEED_MLA,
|
||||
]
|
||||
else: # Hopper (SM90) and older
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""TokenSpeed CuTe DSL backend for MLA prefill."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLACommonPrefillMetadata,
|
||||
)
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
|
||||
|
||||
class TokenspeedMLAPrefillBackend(MLAPrefillBackend):
|
||||
"""TokenSpeed CuTe DSL backend for MLA prefill."""
|
||||
|
||||
requires_r1_mla_dimensions = True
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "TOKENSPEED_MLA"
|
||||
|
||||
@classmethod
|
||||
def supports_compute_capability(cls, device_capability: "DeviceCapability") -> bool:
|
||||
return device_capability.major == 10
|
||||
|
||||
_INSTALL_HINT = (
|
||||
"tokenspeed_mla package is not installed. "
|
||||
"Install it with: `uv pip install tokenspeed-mla`"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_available(cls) -> bool:
|
||||
try:
|
||||
from tokenspeed_mla import (
|
||||
tokenspeed_mla_prefill, # noqa: F401
|
||||
)
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def validate_configuration(
|
||||
cls,
|
||||
device_capability,
|
||||
selector_config,
|
||||
) -> list[str]:
|
||||
# Replace the generic "required dependencies not available" message
|
||||
# from the base class with a specific install hint so users know
|
||||
# exactly which package to install when they explicitly select this
|
||||
# backend without having tokenspeed_mla installed.
|
||||
reasons = super().validate_configuration(device_capability, selector_config)
|
||||
return [
|
||||
cls._INSTALL_HINT if r == "required dependencies not available" else r
|
||||
for r in reasons
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
scale: float,
|
||||
kv_lora_rank: int,
|
||||
qk_nope_head_dim: int,
|
||||
qk_rope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
vllm_config: "VllmConfig",
|
||||
) -> None:
|
||||
super().__init__(
|
||||
num_heads=num_heads,
|
||||
scale=scale,
|
||||
kv_lora_rank=kv_lora_rank,
|
||||
qk_nope_head_dim=qk_nope_head_dim,
|
||||
qk_rope_head_dim=qk_rope_head_dim,
|
||||
v_head_dim=v_head_dim,
|
||||
vllm_config=vllm_config,
|
||||
)
|
||||
|
||||
# Pre-JIT BF16 and FP8 prefill kernels. Idempotent — also called from
|
||||
# TokenspeedMLAImpl.__init__; second call is a no-op.
|
||||
from tokenspeed_mla import warmup_compile_prefill
|
||||
|
||||
for q_dtype in (torch.bfloat16, torch.float8_e4m3fn):
|
||||
warmup_compile_prefill(
|
||||
q_dtype=q_dtype,
|
||||
d_qk=qk_nope_head_dim + qk_rope_head_dim,
|
||||
d_v=v_head_dim,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
def prepare_metadata(
|
||||
self,
|
||||
prefill_metadata: "MLACommonPrefillMetadata",
|
||||
) -> None:
|
||||
super().prepare_metadata(prefill_metadata)
|
||||
# Kernel signature requires `seq_lens` but the implementation never reads
|
||||
# it (per-batch lengths are derived from `cum_seq_lens` diffs); compute
|
||||
# for parity with trtllm_ragged. cuda-graph padding in
|
||||
# `query_start_loc` is saturated to `total_num_tokens`
|
||||
# (gpu_model_runner.py:1905), so trailing diffs are 0 and padded batches
|
||||
# are kernel no-ops — same reason trtllm passes the padded length as
|
||||
# batch_size directly.
|
||||
self._query_seq_lens = (
|
||||
prefill_metadata.query_start_loc[1:] - prefill_metadata.query_start_loc[:-1]
|
||||
)
|
||||
|
||||
def run_prefill_new_tokens(
|
||||
self,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
return_softmax_lse: bool,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
||||
from tokenspeed_mla import tokenspeed_mla_prefill
|
||||
|
||||
# `v` arrives as the second half of `kv_nope.split(...)` in
|
||||
# mla_attention.forward_mha — a non-contiguous view of `kv_nope` along
|
||||
# dim=-1. The kernel does `v.reshape(1, total_kv, h_k, 1, d_v)` which
|
||||
# would silently copy on a non-contiguous tensor; force contiguity here
|
||||
# so the copy (if any) happens once outside the kernel call.
|
||||
v = v.contiguous()
|
||||
|
||||
ret = tokenspeed_mla_prefill(
|
||||
query=q,
|
||||
key=k,
|
||||
value=v,
|
||||
seq_lens=self._query_seq_lens,
|
||||
cum_seq_lens=self._prefill_metadata.query_start_loc,
|
||||
max_seq_len=self._prefill_metadata.max_query_len,
|
||||
batch_size=self._query_seq_lens.shape[0],
|
||||
softmax_scale=self.scale,
|
||||
is_causal=True,
|
||||
return_lse=return_softmax_lse,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
if isinstance(ret, tuple):
|
||||
# Convert from (q_len, num_heads) to (num_heads, q_len)
|
||||
return ret[0], ret[1].transpose(0, 1).contiguous()
|
||||
return ret
|
||||
|
||||
def run_prefill_context_chunk(
|
||||
self,
|
||||
chunk_idx: int,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
from tokenspeed_mla import tokenspeed_mla_prefill
|
||||
|
||||
assert self._prefill_metadata.chunked_context is not None
|
||||
chunked = self._prefill_metadata.chunked_context
|
||||
|
||||
# See note in run_prefill_new_tokens — `v` is a split-view of `kv_nope`
|
||||
# in `_compute_prefill_context` and arrives non-contiguous.
|
||||
v = v.contiguous()
|
||||
|
||||
attn_out, lse = tokenspeed_mla_prefill(
|
||||
query=q,
|
||||
key=k,
|
||||
value=v,
|
||||
seq_lens=chunked.seq_lens[chunk_idx],
|
||||
cum_seq_lens=chunked.cu_seq_lens[chunk_idx],
|
||||
max_seq_len=chunked.max_seq_lens[chunk_idx],
|
||||
batch_size=chunked.seq_lens[chunk_idx].shape[0],
|
||||
softmax_scale=self.scale,
|
||||
is_causal=False,
|
||||
return_lse=True,
|
||||
cum_seq_lens_q=self._prefill_metadata.query_start_loc,
|
||||
max_seq_len_q=self._prefill_metadata.max_query_len,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
# Convert from (q_len, num_heads) to (num_heads, q_len)
|
||||
return attn_out, lse.transpose(0, 1).contiguous()
|
||||
@@ -51,8 +51,6 @@ class TrtllmRaggedPrefillBackend(MLAPrefillBackend):
|
||||
qk_rope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
vllm_config: "VllmConfig",
|
||||
device: torch.device,
|
||||
layer_names: list[str] | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
num_heads=num_heads,
|
||||
@@ -62,8 +60,6 @@ class TrtllmRaggedPrefillBackend(MLAPrefillBackend):
|
||||
qk_rope_head_dim=qk_rope_head_dim,
|
||||
v_head_dim=v_head_dim,
|
||||
vllm_config=vllm_config,
|
||||
device=device,
|
||||
layer_names=layer_names,
|
||||
)
|
||||
|
||||
def _get_workspace_buffer(self) -> torch.Tensor:
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""TokenSpeed CuTe DSL MLA decode backend (Blackwell, FP8 KV cache only)."""
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.config.cache import CacheDType
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLACommonBackend,
|
||||
MLACommonImpl,
|
||||
MLACommonMetadata,
|
||||
MLACommonMetadataBuilder,
|
||||
QueryLenSupport,
|
||||
)
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
from vllm.utils.torch_utils import is_quantized_kv_cache
|
||||
from vllm.v1.attention.backend import (
|
||||
AttentionCGSupport,
|
||||
AttentionLayer,
|
||||
AttentionType,
|
||||
MultipleOf,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import KVCacheLayoutType
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Workspace upper bound for tokenspeed_mla_decode (per-device, lazy):
|
||||
# num_sms * num_heads * MAX_Q_LEN * (kv_lora_rank + 1) * sizeof(float32)
|
||||
# Matches the kernel's `get_workspace_size` formula. MAX_Q_LEN=8 covers up to
|
||||
# EAGLE3 / MTP-2 spec decoding query lengths; larger q_len fails the kernel's
|
||||
# own buffer check.
|
||||
_TOKENSPEED_MAX_Q_LEN = 8
|
||||
|
||||
_g_workspace: dict[torch.device, torch.Tensor] = {}
|
||||
|
||||
|
||||
def _get_workspace(
|
||||
device: torch.device, num_heads: int, kv_lora_rank: int
|
||||
) -> torch.Tensor:
|
||||
from tokenspeed_mla import get_num_sm
|
||||
|
||||
needed = (
|
||||
get_num_sm(device) * num_heads * _TOKENSPEED_MAX_Q_LEN * (kv_lora_rank + 1) * 4
|
||||
)
|
||||
existing = _g_workspace.get(device)
|
||||
if existing is None or existing.numel() < needed:
|
||||
_g_workspace[device] = torch.empty(needed, dtype=torch.int8, device=device)
|
||||
return _g_workspace[device]
|
||||
|
||||
|
||||
class TokenspeedMLAMetadataBuilder(MLACommonMetadataBuilder[MLACommonMetadata]):
|
||||
_cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
|
||||
query_len_support: ClassVar[QueryLenSupport] = QueryLenSupport.UNIFORM
|
||||
|
||||
|
||||
class TokenspeedMLABackend(MLACommonBackend):
|
||||
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
|
||||
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
|
||||
"fp8",
|
||||
"fp8_e4m3",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
||||
return [32, 64]
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "TOKENSPEED_MLA"
|
||||
|
||||
@staticmethod
|
||||
def get_impl_cls() -> type["TokenspeedMLAImpl"]:
|
||||
return TokenspeedMLAImpl
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type["TokenspeedMLAMetadataBuilder"]:
|
||||
return TokenspeedMLAMetadataBuilder
|
||||
|
||||
@classmethod
|
||||
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
|
||||
return capability.major == 10
|
||||
|
||||
@classmethod
|
||||
def supports_combination(
|
||||
cls,
|
||||
head_size: int,
|
||||
dtype: torch.dtype,
|
||||
kv_cache_dtype: CacheDType | None,
|
||||
block_size: int | None,
|
||||
use_mla: bool,
|
||||
has_sink: bool,
|
||||
use_sparse: bool,
|
||||
device_capability: DeviceCapability,
|
||||
) -> str | None:
|
||||
# Surface a clear install hint up front rather than letting a raw
|
||||
# ModuleNotFoundError fire deep inside `forward_mqa` at first request.
|
||||
try:
|
||||
import tokenspeed_mla # noqa: F401
|
||||
except ImportError:
|
||||
return (
|
||||
"tokenspeed_mla package is not installed. "
|
||||
"Install it with: `uv pip install tokenspeed-mla`"
|
||||
)
|
||||
|
||||
# tokenspeed_mla CuTe DSL kernel is shape-specialized for DeepSeek R1
|
||||
# MLA dimensions (qk_nope=128, qk_rope=64, v=128). Reject anything else.
|
||||
from vllm.config import get_current_vllm_config
|
||||
|
||||
vllm_config = get_current_vllm_config()
|
||||
if vllm_config.model_config is not None:
|
||||
hf_text_config = vllm_config.model_config.hf_text_config
|
||||
qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 0)
|
||||
qk_rope_head_dim = getattr(hf_text_config, "qk_rope_head_dim", 0)
|
||||
v_head_dim = getattr(hf_text_config, "v_head_dim", 0)
|
||||
if qk_nope_head_dim != 128 or qk_rope_head_dim != 64 or v_head_dim != 128:
|
||||
return (
|
||||
"tokenspeed_mla requires DeepSeek R1 MLA dimensions "
|
||||
"(qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128), "
|
||||
f"got ({qk_nope_head_dim}, {qk_rope_head_dim}, {v_head_dim})"
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
|
||||
return "HND"
|
||||
|
||||
|
||||
class TokenspeedMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
head_size: int,
|
||||
scale: float,
|
||||
num_kv_heads: int,
|
||||
alibi_slopes: list[float] | None,
|
||||
sliding_window: int | None,
|
||||
kv_cache_dtype: str,
|
||||
logits_soft_cap: float | None,
|
||||
attn_type: str,
|
||||
kv_sharing_target_layer_name: str | None,
|
||||
# MLA Specific Arguments
|
||||
**mla_args,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
num_heads,
|
||||
head_size,
|
||||
scale,
|
||||
num_kv_heads,
|
||||
alibi_slopes,
|
||||
sliding_window,
|
||||
kv_cache_dtype,
|
||||
logits_soft_cap,
|
||||
attn_type,
|
||||
kv_sharing_target_layer_name,
|
||||
**mla_args,
|
||||
)
|
||||
|
||||
unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
|
||||
if any(unsupported_features):
|
||||
raise NotImplementedError(
|
||||
"TokenspeedMLAImpl does not support one of the following: "
|
||||
"alibi_slopes, sliding_window, logits_soft_cap"
|
||||
)
|
||||
|
||||
if attn_type != AttentionType.DECODER:
|
||||
raise NotImplementedError(
|
||||
"Encoder self-attention and "
|
||||
"encoder/decoder cross-attention "
|
||||
"are not implemented for "
|
||||
"TokenspeedMLAImpl"
|
||||
)
|
||||
|
||||
if not is_quantized_kv_cache(self.kv_cache_dtype):
|
||||
raise NotImplementedError(
|
||||
"TokenspeedMLAImpl requires an FP8 KV cache "
|
||||
"(--kv-cache-dtype fp8 or fp8_e4m3); "
|
||||
f"got kv_cache_dtype={self.kv_cache_dtype!r}."
|
||||
)
|
||||
|
||||
# Allocate (or fetch the cached) workspace lazily on first forward —
|
||||
# __init__ runs before the device is necessarily set on the worker;
|
||||
# we know it for sure at forward time when we see the input tensor.
|
||||
self._workspace_buffer: torch.Tensor | None = None
|
||||
self.softmax_scale: float | None = None
|
||||
self.output_scale: float | None = None
|
||||
|
||||
# Pre-JIT BF16 and FP8 prefill kernels here too — decode impl always
|
||||
# runs when tokenspeed is selected, prefill backend may not (user can
|
||||
# pair with flash_attn / trtllm). Idempotent.
|
||||
from tokenspeed_mla import warmup_compile_prefill
|
||||
|
||||
for q_dtype in (torch.bfloat16, torch.float8_e4m3fn):
|
||||
warmup_compile_prefill(
|
||||
q_dtype=q_dtype,
|
||||
d_qk=self.qk_nope_head_dim + self.qk_rope_head_dim,
|
||||
d_v=self.v_head_dim,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
def forward_mqa(
|
||||
self,
|
||||
q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
|
||||
kv_c_and_k_pe_cache: torch.Tensor,
|
||||
attn_metadata: MLACommonMetadata,
|
||||
layer: AttentionLayer,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
from tokenspeed_mla import tokenspeed_mla_decode
|
||||
|
||||
assert kv_c_and_k_pe_cache.numel() > 0
|
||||
assert attn_metadata.decode is not None
|
||||
|
||||
if isinstance(q, tuple):
|
||||
q_nope, q_pe = q
|
||||
q = torch.cat([q_nope, q_pe], dim=-1)
|
||||
|
||||
# supports_quant_query_input=True (set in MLACommonImpl) tells the
|
||||
# pipeline to concat+FP8-quantize Q upstream via _decode_concat_quant_fp8_op.
|
||||
# The kernel is shape-specialized for FP8 Q + FP8 KV, so anything else
|
||||
# here means the upstream quant didn't run and the kernel will produce
|
||||
# garbage.
|
||||
assert q.dtype == torch.float8_e4m3fn, (
|
||||
f"TokenspeedMLAImpl expected FP8 query (supports_quant_query_input=True), "
|
||||
f"got {q.dtype}. Pipeline isinstance(q, tuple)={isinstance(q, tuple)}, "
|
||||
f"q_scale={layer._q_scale_float}, k_scale={layer._k_scale_float}."
|
||||
)
|
||||
|
||||
# tokenspeed_mla_decode expects query shape
|
||||
# (num_decodes, q_len_per_request, num_heads, head_dim).
|
||||
if attn_metadata.num_decode_tokens % attn_metadata.num_decodes != 0:
|
||||
logger.warning_once(
|
||||
"""TokenspeedMLAImpl got a query of uneven length.
|
||||
This usually indicates an issue in batch reordering
|
||||
or incorrect setup in dummy_run."""
|
||||
)
|
||||
q = q.unsqueeze(1)
|
||||
else:
|
||||
q = q.view(attn_metadata.num_decodes, -1, q.shape[-2], q.shape[-1])
|
||||
|
||||
if self.softmax_scale is None:
|
||||
# FP8 KV cache is mandatory for this backend, so q_scale/k_scale
|
||||
# always apply. softmax_scale is bmm1; output_scale is bmm2 — both
|
||||
# required to recover the correct attention output from the FP8
|
||||
# KV cache (V is stored as V_real/k_scale).
|
||||
self.softmax_scale = (
|
||||
self.scale * layer._q_scale_float * layer._k_scale_float
|
||||
)
|
||||
self.output_scale = layer._k_scale_float
|
||||
|
||||
if self._workspace_buffer is None:
|
||||
self._workspace_buffer = _get_workspace(
|
||||
q.device, self.num_heads, self.kv_lora_rank
|
||||
)
|
||||
|
||||
# vLLM kv_c_and_k_pe_cache is already (num_blocks, block_size, head_size).
|
||||
# tokenspeed_mla_decode wants 3D — pass as-is (no unsqueeze, unlike trtllm).
|
||||
o = tokenspeed_mla_decode(
|
||||
query=q,
|
||||
kv_cache=kv_c_and_k_pe_cache,
|
||||
workspace_buffer=self._workspace_buffer,
|
||||
kv_lora_rank=self.kv_lora_rank,
|
||||
qk_rope_head_dim=self.qk_rope_head_dim,
|
||||
block_tables=attn_metadata.decode.block_table,
|
||||
seq_lens=attn_metadata.decode.seq_lens,
|
||||
max_seq_len=attn_metadata.max_seq_len,
|
||||
softmax_scale=self.softmax_scale,
|
||||
output_scale=self.output_scale,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
# Flatten the output for consistent shape
|
||||
o = o.view(-1, o.shape[-2], o.shape[-1])
|
||||
|
||||
# tokenspeed_mla_decode does not return LSE.
|
||||
return o, None
|
||||
@@ -63,6 +63,9 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
|
||||
FLASHINFER_MLA = (
|
||||
"vllm.v1.attention.backends.mla.flashinfer_mla.FlashInferMLABackend"
|
||||
)
|
||||
TOKENSPEED_MLA = (
|
||||
"vllm.v1.attention.backends.mla.tokenspeed_mla.TokenspeedMLABackend"
|
||||
)
|
||||
FLASHINFER_MLA_SPARSE = (
|
||||
"vllm.v1.attention.backends.mla.flashinfer_mla_sparse."
|
||||
"FlashInferMLASparseBackend"
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Gemma4 MTP (Multi-Token Prediction) proposer for speculative decoding.
|
||||
|
||||
The Gemma4 assistant model runs all decoder layers per draft step
|
||||
(producing one token), and all its attention layers share KV cache
|
||||
with the target model via cross-model KV sharing.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm.config import VllmConfig, get_layers_from_vllm_config, replace
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
|
||||
from vllm.v1.attention.backend import CommonAttentionMetadata
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
KVCacheConfig,
|
||||
KVCacheSpec,
|
||||
UniformTypeKVCacheSpecs,
|
||||
)
|
||||
from vllm.v1.spec_decode.llm_base_proposer import SpecDecodeBaseProposer
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class Gemma4Proposer(SpecDecodeBaseProposer):
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
device: torch.device,
|
||||
runner=None,
|
||||
):
|
||||
super().__init__(
|
||||
vllm_config,
|
||||
device,
|
||||
pass_hidden_states_to_model=True,
|
||||
runner=runner,
|
||||
)
|
||||
# All draft steps predict from the same position (the last
|
||||
# target-model position), so positions and seq_lens must not
|
||||
# advance between steps.
|
||||
self.constant_draft_positions = True
|
||||
|
||||
# Per-group block tables for multi-group KV cache models.
|
||||
# Populated by gpu_model_runner during _prepare_inputs.
|
||||
self._per_group_block_tables: dict[int, torch.Tensor] = {}
|
||||
|
||||
# Centroids CUDA graphs — populated in load_model if centroids
|
||||
# masking is active. _centroids_sizes is pre-sorted for fast
|
||||
# lookup in _greedy_sample.
|
||||
self._centroids_sizes: list[int] = []
|
||||
self._centroids_graphs: dict[int, torch.cuda.CUDAGraph] = {}
|
||||
self._centroids_inputs: dict[int, torch.Tensor] = {}
|
||||
self._centroids_outputs: dict[int, torch.Tensor] = {}
|
||||
|
||||
def set_per_group_block_table(self, gid: int, block_table: torch.Tensor) -> None:
|
||||
self._per_group_block_tables[gid] = block_table
|
||||
|
||||
def model_returns_tuple(self) -> bool:
|
||||
# forward() returns (draft_hidden_states, backbone_hidden_states).
|
||||
# The proposer uses draft_hidden_states for compute_logits and
|
||||
# backbone_hidden_states for the hidden-state feedback buffer.
|
||||
return True
|
||||
|
||||
def build_per_group_and_layer_attn_metadata(
|
||||
self,
|
||||
common_attn_metadata: CommonAttentionMetadata,
|
||||
draft_index: int = 0,
|
||||
) -> tuple[list[object], dict[str, object]]:
|
||||
"""Build attention metadata using the correct block table per group.
|
||||
|
||||
Gemma4 has multiple KV cache groups (sliding vs full attention)
|
||||
with different block tables. The base class receives a single
|
||||
common_attn_metadata whose block_table belongs to one group.
|
||||
We swap in the correct block table for each draft attention group.
|
||||
"""
|
||||
per_group_attn_metadata: list[object] = []
|
||||
per_layer_attn_metadata: dict[str, object] = {}
|
||||
for attn_group in self.draft_attn_groups:
|
||||
gid = attn_group.kv_cache_group_id
|
||||
if gid in self._per_group_block_tables:
|
||||
cm = copy(common_attn_metadata)
|
||||
cm.block_table_tensor = self._per_group_block_tables[gid]
|
||||
else:
|
||||
cm = common_attn_metadata
|
||||
attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
|
||||
common_attn_metadata=cm, draft_index=draft_index
|
||||
)
|
||||
per_group_attn_metadata.append(attn_metadata)
|
||||
for layer_name in attn_group.layer_names:
|
||||
per_layer_attn_metadata[layer_name] = attn_metadata
|
||||
return per_group_attn_metadata, per_layer_attn_metadata
|
||||
|
||||
def _greedy_sample(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
if self._centroids_sizes:
|
||||
T = hidden_states.shape[0]
|
||||
for size in self._centroids_sizes:
|
||||
if size >= T:
|
||||
self._centroids_inputs[size][:T].copy_(hidden_states)
|
||||
self._centroids_graphs[size].replay()
|
||||
return self._centroids_outputs[size][:T].clone()
|
||||
return self.model.get_top_tokens(hidden_states)
|
||||
return super()._greedy_sample(hidden_states)
|
||||
|
||||
def _setup_centroids_cuda_graphs(self) -> None:
|
||||
"""Capture CUDA graphs for centroids get_top_tokens at key sizes."""
|
||||
masked_emb = self.model.masked_embedding
|
||||
lm_head_weight = self.model._get_full_lm_head_weight()
|
||||
|
||||
for size in [1, 2, 4, 8, 16, 32, 64]:
|
||||
static_input = torch.zeros(
|
||||
size,
|
||||
masked_emb.hidden_size,
|
||||
dtype=self.dtype,
|
||||
device=self.device,
|
||||
)
|
||||
for _ in range(3):
|
||||
masked_emb.get_top_tokens(static_input, lm_head_weight)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
g = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(g):
|
||||
static_output = masked_emb.get_top_tokens(
|
||||
static_input,
|
||||
lm_head_weight,
|
||||
)
|
||||
self._centroids_graphs[size] = g
|
||||
self._centroids_inputs[size] = static_input
|
||||
self._centroids_outputs[size] = static_output
|
||||
|
||||
self._centroids_sizes = sorted(self._centroids_graphs)
|
||||
logger.info(
|
||||
"Gemma4 MTP: captured centroids CUDA graphs for sizes %s.",
|
||||
self._centroids_sizes,
|
||||
)
|
||||
|
||||
def _create_draft_vllm_config(self) -> VllmConfig:
|
||||
"""Preserve the target's forced TRITON_ATTN backend for draft layers.
|
||||
|
||||
Gemma4 forces TRITON_ATTN due to heterogeneous head dimensions
|
||||
(head_dim=256 sliding, global_head_dim=512 full). The base class
|
||||
resets attention_config.backend to None for draft models, causing
|
||||
sliding layers to fall back to FLASH_ATTN which cannot handle
|
||||
KV-shared cache. Override to carry the target's backend through.
|
||||
"""
|
||||
base = super()._create_draft_vllm_config()
|
||||
target_backend = self.vllm_config.attention_config.backend
|
||||
if target_backend is not None:
|
||||
base = replace(
|
||||
base,
|
||||
attention_config=replace(
|
||||
base.attention_config,
|
||||
backend=target_backend,
|
||||
),
|
||||
)
|
||||
return base
|
||||
|
||||
def _maybe_share_lm_head(self, target_language_model: nn.Module) -> None:
|
||||
"""Gemma4 MTP always keeps its own draft-dim lm_head.
|
||||
|
||||
The draft model's lm_head operates in draft hidden_size (e.g. 256),
|
||||
which differs from the target's backbone hidden_size (e.g. 1536).
|
||||
Sharing would break compute_logits (and centroids masking when
|
||||
use_ordered_embeddings is enabled).
|
||||
"""
|
||||
logger.info(
|
||||
"Gemma4 MTP: keeping draft model's own lm_head (draft_dim != backbone_dim)."
|
||||
)
|
||||
|
||||
def load_model(self, target_model: nn.Module) -> None:
|
||||
target_attn_layer_names = set(
|
||||
get_layers_from_vllm_config(
|
||||
self.vllm_config,
|
||||
AttentionLayerBase,
|
||||
).keys()
|
||||
)
|
||||
|
||||
super().load_model(target_model)
|
||||
|
||||
self._setup_gemma4_kv_sharing(target_attn_layer_names)
|
||||
|
||||
if getattr(self.model, "masked_embedding", None) is not None:
|
||||
self._setup_centroids_cuda_graphs()
|
||||
|
||||
def validate_same_kv_cache_group(self, kv_cache_config: KVCacheConfig) -> None:
|
||||
"""Draft layers span multiple KV cache groups (sliding + full
|
||||
attention with different head dimensions), so skip the base
|
||||
class single-group assertion."""
|
||||
|
||||
def initialize_attn_backend(
|
||||
self,
|
||||
kv_cache_config: KVCacheConfig,
|
||||
kernel_block_sizes: list[int] | None = None,
|
||||
) -> None:
|
||||
"""Create separate AttentionGroup objects per KV cache spec
|
||||
so that each head-dim variant gets its own metadata builder."""
|
||||
all_attn_layers = get_layers_from_vllm_config(
|
||||
self.vllm_config,
|
||||
AttentionLayerBase,
|
||||
)
|
||||
|
||||
layer_to_gid: dict[str, int] = {}
|
||||
layer_to_spec: dict[str, KVCacheSpec] = {}
|
||||
for gid, group in enumerate(kv_cache_config.kv_cache_groups):
|
||||
group_spec = group.kv_cache_spec
|
||||
for ln in group.layer_names:
|
||||
layer_to_gid[ln] = gid
|
||||
if isinstance(group_spec, UniformTypeKVCacheSpecs):
|
||||
if ln in group_spec.kv_cache_specs:
|
||||
layer_to_spec[ln] = group_spec.kv_cache_specs[ln]
|
||||
else:
|
||||
tgt = getattr(
|
||||
all_attn_layers.get(ln),
|
||||
"kv_sharing_target_layer_name",
|
||||
None,
|
||||
)
|
||||
if tgt and tgt in group_spec.kv_cache_specs:
|
||||
layer_to_spec[ln] = group_spec.kv_cache_specs[tgt]
|
||||
else:
|
||||
layer_to_spec[ln] = group_spec
|
||||
else:
|
||||
layer_to_spec[ln] = group_spec
|
||||
|
||||
attention_groups: dict[tuple[str, KVCacheSpec], AttentionGroup] = {}
|
||||
for layer_name in self._draft_attn_layer_names:
|
||||
if layer_name not in layer_to_spec:
|
||||
continue
|
||||
attn_layer = all_attn_layers[layer_name]
|
||||
attn_backend = attn_layer.get_attn_backend()
|
||||
spec = layer_to_spec[layer_name]
|
||||
gid = layer_to_gid[layer_name]
|
||||
group_key = (attn_backend.full_cls_name(), spec)
|
||||
|
||||
if group_key not in attention_groups:
|
||||
kernel_block_size = (
|
||||
kernel_block_sizes[gid]
|
||||
if kernel_block_sizes is not None and gid < len(kernel_block_sizes)
|
||||
else None
|
||||
)
|
||||
attn_group = AttentionGroup(
|
||||
backend=attn_backend,
|
||||
layer_names=[layer_name],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=gid,
|
||||
)
|
||||
attn_group.create_metadata_builders(
|
||||
self.vllm_config,
|
||||
self.device,
|
||||
kernel_block_size=kernel_block_size,
|
||||
)
|
||||
attention_groups[group_key] = attn_group
|
||||
else:
|
||||
attention_groups[group_key].layer_names.append(layer_name)
|
||||
|
||||
self.draft_attn_groups = list(attention_groups.values())
|
||||
if self.draft_attn_groups:
|
||||
self.kv_cache_gid = self.draft_attn_groups[0].kv_cache_group_id
|
||||
self.block_size = (
|
||||
self.draft_attn_groups[0]
|
||||
.get_metadata_builder()
|
||||
.kv_cache_spec.block_size
|
||||
)
|
||||
else:
|
||||
self.kv_cache_gid = 0
|
||||
self.block_size = kv_cache_config.kv_cache_groups[
|
||||
0
|
||||
].kv_cache_spec.block_size
|
||||
logger.debug("Using block size %d for drafting layers", self.block_size)
|
||||
|
||||
def _setup_gemma4_kv_sharing(
|
||||
self,
|
||||
target_attn_layer_names: set[str],
|
||||
) -> None:
|
||||
"""Wire draft layers to share KV with the target model.
|
||||
|
||||
Each draft decoder layer is mapped to the last non-KV-shared
|
||||
target layer of the same attention type (sliding or full).
|
||||
"""
|
||||
draft_config = self.speculative_config.draft_model_config.hf_config
|
||||
draft_text_config = draft_config.get_text_config()
|
||||
target_config = self.vllm_config.model_config.hf_config
|
||||
target_text_config = target_config.get_text_config()
|
||||
target_layer_types = getattr(target_text_config, "layer_types", [])
|
||||
|
||||
if not (hasattr(self.model, "model") and hasattr(self.model.model, "layers")):
|
||||
return
|
||||
|
||||
target_num_kv_shared = getattr(target_text_config, "num_kv_shared_layers", 0)
|
||||
num_non_shared = len(target_layer_types) - target_num_kv_shared
|
||||
type_to_target_indices: dict[str, list[int]] = defaultdict(list)
|
||||
for idx, lt in enumerate(target_layer_types[:num_non_shared]):
|
||||
type_to_target_indices[lt].append(idx)
|
||||
|
||||
target_prefix = "model.layers"
|
||||
for name in target_attn_layer_names:
|
||||
if ".layers." in name:
|
||||
target_prefix = name.split(".layers.")[0] + ".layers"
|
||||
break
|
||||
|
||||
draft_layer_types = getattr(draft_text_config, "layer_types", [])
|
||||
for draft_idx, layer in enumerate(self.model.model.layers):
|
||||
if not hasattr(layer, "self_attn"):
|
||||
continue
|
||||
attn = getattr(layer.self_attn, "attn", None)
|
||||
if attn is None:
|
||||
continue
|
||||
|
||||
draft_layer_type = (
|
||||
draft_layer_types[draft_idx]
|
||||
if draft_idx < len(draft_layer_types)
|
||||
else "full_attention"
|
||||
)
|
||||
candidates = type_to_target_indices.get(draft_layer_type, [])
|
||||
if not candidates:
|
||||
logger.warning(
|
||||
"No target layer of type '%s' for draft layer %d",
|
||||
draft_layer_type,
|
||||
draft_idx,
|
||||
)
|
||||
continue
|
||||
|
||||
target_idx = candidates[-1]
|
||||
target_layer_name = f"{target_prefix}.{target_idx}.self_attn.attn"
|
||||
attn.kv_sharing_target_layer_name = target_layer_name
|
||||
logger.info(
|
||||
"Gemma4 MTP: draft layer %d (%s) -> %s",
|
||||
draft_idx,
|
||||
draft_layer_type,
|
||||
target_layer_name,
|
||||
)
|
||||
@@ -105,12 +105,6 @@ class SpecDecodeBaseProposer:
|
||||
)
|
||||
self.needs_extra_input_slots = self.net_num_new_slots_per_request > 0
|
||||
|
||||
# When True, all draft steps reuse the same position as the
|
||||
# first step instead of advancing by one each iteration.
|
||||
# Used by draft models with Q-only attention that share KV
|
||||
# with the target and always predict from the same position.
|
||||
self.constant_draft_positions: bool = False
|
||||
|
||||
self.parallel_drafting_token_id: int = 0
|
||||
self.parallel_drafting_hidden_state_tensor: torch.Tensor | None = None
|
||||
if self.parallel_drafting:
|
||||
@@ -394,9 +388,9 @@ class SpecDecodeBaseProposer:
|
||||
return {name: view for name in self._draft_attn_layer_names}
|
||||
|
||||
def initialize_cudagraph_keys(self, cudagraph_mode: CUDAGraphMode) -> None:
|
||||
"""Initialize cudagraph dispatcher keys for the drafter.
|
||||
"""Initialize cudagraph dispatcher keys for eagle.
|
||||
|
||||
Only supports PIECEWISE cudagraphs (via mixed_mode).
|
||||
Eagle only supports PIECEWISE cudagraphs (via mixed_mode).
|
||||
This should be called after adjust_cudagraph_sizes_for_spec_decode.
|
||||
"""
|
||||
if (
|
||||
@@ -505,12 +499,6 @@ class SpecDecodeBaseProposer:
|
||||
positions = self.positions[token_indices_to_sample]
|
||||
hidden_states = hidden_states[token_indices_to_sample]
|
||||
|
||||
if self.constant_draft_positions:
|
||||
# Write the sampling positions into the front of the
|
||||
# positions buffer so that subsequent loop iterations
|
||||
# (which read via _get_positions) use the correct values.
|
||||
self.positions[:batch_size] = positions
|
||||
|
||||
if any(isinstance(md, TreeAttentionMetadata) for md in per_group_attn_metadata):
|
||||
# Draft using tree attention - requires full logits for top-k
|
||||
logits = self.model.compute_logits(sample_hidden_states)
|
||||
@@ -568,25 +556,59 @@ class SpecDecodeBaseProposer:
|
||||
# cast to int32 is crucial when eagle model is compiled.
|
||||
# tensor.argmax() returns int64 by default.
|
||||
input_ids = draft_token_ids_list[-1].int()
|
||||
# Use fused kernel for slot mapping and metadata updates.
|
||||
# Write clamped positions directly into the positions buffer to
|
||||
# avoid an extra D2D copy for the common (non-mrope) case.
|
||||
positions_1d = positions[0] if self.uses_mrope else positions
|
||||
if self.uses_mrope:
|
||||
out_pos = self.mrope_positions[0, :batch_size]
|
||||
elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
|
||||
out_pos = self.xdrope_positions[0, :batch_size]
|
||||
else:
|
||||
out_pos = self.positions[:batch_size]
|
||||
eagle_step_update_slot_mapping_and_metadata(
|
||||
positions_1d=positions_1d,
|
||||
block_table_tensor=common_attn_metadata.block_table_tensor,
|
||||
seq_lens=common_attn_metadata.seq_lens,
|
||||
block_size=block_size,
|
||||
max_model_len=self.max_model_len,
|
||||
out_clamped_positions=out_pos,
|
||||
out_slot_mapping=self._slot_mapping_buffer[:input_batch_size],
|
||||
input_batch_size=input_batch_size,
|
||||
)
|
||||
common_attn_metadata.slot_mapping = self._slot_mapping_buffer[:batch_size]
|
||||
if self.uses_mrope:
|
||||
self.mrope_positions[1:, :batch_size] = self.mrope_positions[
|
||||
0, :batch_size
|
||||
]
|
||||
positions = self.mrope_positions[:, :batch_size]
|
||||
elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
|
||||
self.xdrope_positions[1:, :batch_size] = self.xdrope_positions[
|
||||
0, :batch_size
|
||||
]
|
||||
positions = self.xdrope_positions[0, :batch_size]
|
||||
else:
|
||||
positions = self.positions[:batch_size]
|
||||
# Increment the maximum sequence length. We increment max_seq_len
|
||||
# unconditionally even though some seq_lens may have been capped above,
|
||||
# as max_seq_len serves as an upper bound for sequence lengths.
|
||||
common_attn_metadata.max_seq_len = min(
|
||||
common_attn_metadata.max_seq_len + 1, self.max_model_len
|
||||
)
|
||||
|
||||
if not self.constant_draft_positions:
|
||||
positions = self._update_positions_dependent_metadata(
|
||||
positions,
|
||||
common_attn_metadata,
|
||||
batch_size,
|
||||
input_batch_size,
|
||||
block_size,
|
||||
)
|
||||
# Also update the CPU-side shadow; NOTE: this is hacky and should be
|
||||
# removed in when common_attn_metadata.seq_lens_cpu is deprecated.
|
||||
if common_attn_metadata._seq_lens_cpu is not None:
|
||||
common_attn_metadata._seq_lens_cpu += 1
|
||||
if common_attn_metadata._num_computed_tokens_cpu is not None:
|
||||
common_attn_metadata._num_computed_tokens_cpu += 1
|
||||
if common_attn_metadata.seq_lens_cpu_upper_bound is not None:
|
||||
common_attn_metadata.seq_lens_cpu_upper_bound += 1
|
||||
|
||||
# Rebuild attention metadata. When draft positions are constant
|
||||
# (e.g. Gemma4 MTP), common_attn_metadata is invariant across
|
||||
# loop iterations so we build once and reuse.
|
||||
if not self.constant_draft_positions or token_index == 0:
|
||||
_, per_layer_attn_metadata = (
|
||||
self.build_per_group_and_layer_attn_metadata(
|
||||
common_attn_metadata, draft_index=token_index + 1
|
||||
)
|
||||
)
|
||||
# Rebuild attention metadata
|
||||
_, per_layer_attn_metadata = self.build_per_group_and_layer_attn_metadata(
|
||||
common_attn_metadata, draft_index=token_index + 1
|
||||
)
|
||||
|
||||
# copy inputs to buffer for cudagraph
|
||||
self.input_ids[:batch_size] = input_ids
|
||||
@@ -632,58 +654,6 @@ class SpecDecodeBaseProposer:
|
||||
draft_token_ids = torch.stack(draft_token_ids_list, dim=1)
|
||||
return draft_token_ids
|
||||
|
||||
def _update_positions_dependent_metadata(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
common_attn_metadata,
|
||||
batch_size: int,
|
||||
input_batch_size: int,
|
||||
block_size: int,
|
||||
) -> torch.Tensor:
|
||||
"""Update positions, slot mappings, and sequence metadata for the
|
||||
next draft step. Returns the updated positions tensor."""
|
||||
positions_1d = positions[0] if self.uses_mrope else positions
|
||||
if self.uses_mrope:
|
||||
out_pos = self.mrope_positions[0, :batch_size]
|
||||
elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
|
||||
out_pos = self.xdrope_positions[0, :batch_size]
|
||||
else:
|
||||
out_pos = self.positions[:batch_size]
|
||||
eagle_step_update_slot_mapping_and_metadata(
|
||||
positions_1d=positions_1d,
|
||||
block_table_tensor=common_attn_metadata.block_table_tensor,
|
||||
seq_lens=common_attn_metadata.seq_lens,
|
||||
block_size=block_size,
|
||||
max_model_len=self.max_model_len,
|
||||
out_clamped_positions=out_pos,
|
||||
out_slot_mapping=self._slot_mapping_buffer[:input_batch_size],
|
||||
input_batch_size=input_batch_size,
|
||||
)
|
||||
common_attn_metadata.slot_mapping = self._slot_mapping_buffer[:batch_size]
|
||||
if self.uses_mrope:
|
||||
self.mrope_positions[1:, :batch_size] = self.mrope_positions[0, :batch_size]
|
||||
positions = self.mrope_positions[:, :batch_size]
|
||||
elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
|
||||
self.xdrope_positions[1:, :batch_size] = self.xdrope_positions[
|
||||
0, :batch_size
|
||||
]
|
||||
positions = self.xdrope_positions[0, :batch_size]
|
||||
else:
|
||||
positions = self.positions[:batch_size]
|
||||
common_attn_metadata.max_seq_len = min(
|
||||
common_attn_metadata.max_seq_len + 1,
|
||||
self.max_model_len,
|
||||
)
|
||||
|
||||
if common_attn_metadata._seq_lens_cpu is not None:
|
||||
common_attn_metadata._seq_lens_cpu += 1
|
||||
if common_attn_metadata._num_computed_tokens_cpu is not None:
|
||||
common_attn_metadata._num_computed_tokens_cpu += 1
|
||||
if common_attn_metadata.seq_lens_cpu_upper_bound is not None:
|
||||
common_attn_metadata.seq_lens_cpu_upper_bound += 1
|
||||
|
||||
return positions
|
||||
|
||||
def set_inputs_first_pass(
|
||||
self,
|
||||
target_token_ids: torch.Tensor,
|
||||
|
||||
@@ -169,7 +169,6 @@ from vllm.v1.spec_decode.dflash import DFlashProposer
|
||||
from vllm.v1.spec_decode.draft_model import DraftModelProposer
|
||||
from vllm.v1.spec_decode.eagle import EagleProposer
|
||||
from vllm.v1.spec_decode.extract_hidden_states import ExtractHiddenStatesProposer
|
||||
from vllm.v1.spec_decode.gemma4 import Gemma4Proposer
|
||||
from vllm.v1.spec_decode.medusa import MedusaProposer
|
||||
from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
|
||||
from vllm.v1.spec_decode.ngram_proposer_gpu import (
|
||||
@@ -525,7 +524,6 @@ class GPUModelRunner(
|
||||
| DraftModelProposer
|
||||
| MedusaProposer
|
||||
| ExtractHiddenStatesProposer
|
||||
| Gemma4Proposer
|
||||
)
|
||||
if self.speculative_config.method == "ngram":
|
||||
from vllm.v1.spec_decode.ngram_proposer import NgramProposer
|
||||
@@ -554,8 +552,6 @@ class GPUModelRunner(
|
||||
self._ngram_pinned_val_buf = torch.zeros(
|
||||
self.max_num_reqs, dtype=torch.int32, pin_memory=True
|
||||
)
|
||||
elif self.speculative_config.use_gemma4_mtp():
|
||||
self.drafter = Gemma4Proposer(self.vllm_config, self.device, self)
|
||||
elif self.speculative_config.use_dflash():
|
||||
self.drafter = DFlashProposer(self.vllm_config, self.device, self)
|
||||
self.use_aux_hidden_state_outputs = True
|
||||
@@ -2314,18 +2310,11 @@ class GPUModelRunner(
|
||||
cm.slot_mapping = slot_mappings[kv_cache_gid]
|
||||
|
||||
if self.speculative_config and spec_decode_common_attn_metadata is None:
|
||||
if isinstance(
|
||||
self.drafter, (EagleProposer, DFlashProposer, Gemma4Proposer)
|
||||
):
|
||||
if isinstance(self.drafter, (EagleProposer, DFlashProposer)):
|
||||
if self.drafter.kv_cache_gid == kv_cache_gid:
|
||||
spec_decode_common_attn_metadata = cm
|
||||
else:
|
||||
spec_decode_common_attn_metadata = cm
|
||||
# Capture per-group block tables for multi-group proposers.
|
||||
if self.speculative_config and isinstance(self.drafter, Gemma4Proposer):
|
||||
self.drafter.set_per_group_block_table(
|
||||
kv_cache_gid, cm.block_table_tensor
|
||||
)
|
||||
|
||||
for attn_gid in range(len(self.attn_groups[kv_cache_gid])):
|
||||
if ubatch_slices is not None:
|
||||
@@ -4287,8 +4276,7 @@ class GPUModelRunner(
|
||||
EagleProposer
|
||||
| DFlashProposer
|
||||
| DraftModelProposer
|
||||
| ExtractHiddenStatesProposer
|
||||
| Gemma4Proposer,
|
||||
| ExtractHiddenStatesProposer,
|
||||
)
|
||||
sampled_token_ids = sampler_output.sampled_token_ids
|
||||
if input_fits_in_drafter:
|
||||
@@ -4684,8 +4672,7 @@ class GPUModelRunner(
|
||||
or spec_config.uses_draft_model()
|
||||
):
|
||||
assert isinstance(
|
||||
self.drafter,
|
||||
EagleProposer | DFlashProposer | DraftModelProposer | Gemma4Proposer,
|
||||
self.drafter, EagleProposer | DFlashProposer | DraftModelProposer
|
||||
)
|
||||
|
||||
if spec_config.disable_padded_drafter_batch:
|
||||
@@ -5607,8 +5594,7 @@ class GPUModelRunner(
|
||||
EagleProposer
|
||||
| DFlashProposer
|
||||
| DraftModelProposer
|
||||
| ExtractHiddenStatesProposer
|
||||
| Gemma4Proposer,
|
||||
| ExtractHiddenStatesProposer,
|
||||
)
|
||||
assert self.speculative_config is not None
|
||||
# Eagle currently only supports PIECEWISE cudagraphs.
|
||||
@@ -6409,8 +6395,7 @@ class GPUModelRunner(
|
||||
or self.speculative_config.uses_draft_model()
|
||||
):
|
||||
assert isinstance(
|
||||
self.drafter,
|
||||
EagleProposer | DFlashProposer | DraftModelProposer | Gemma4Proposer,
|
||||
self.drafter, EagleProposer | DFlashProposer | DraftModelProposer
|
||||
)
|
||||
self.drafter.initialize_attn_backend(kv_cache_config, kernel_block_sizes)
|
||||
|
||||
@@ -6463,10 +6448,7 @@ class GPUModelRunner(
|
||||
):
|
||||
assert isinstance(
|
||||
self.drafter,
|
||||
EagleProposer
|
||||
| DFlashProposer
|
||||
| ExtractHiddenStatesProposer
|
||||
| Gemma4Proposer,
|
||||
EagleProposer | DFlashProposer | ExtractHiddenStatesProposer,
|
||||
)
|
||||
self.drafter.initialize_cudagraph_keys(cudagraph_mode)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user