forked from Karylab-cklius/vllm
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c8a5eaa5 | ||
|
|
c4547482ca | ||
|
|
91ef0afcb2 | ||
|
|
97cd2c41ad | ||
|
|
c001535038 | ||
|
|
de6bc297df | ||
|
|
0012818287 | ||
|
|
73cd7e25ae | ||
|
|
964c6eb485 | ||
|
|
d0e6514bf8 | ||
|
|
c7aa186d67 | ||
|
|
f653761252 | ||
|
|
4a8ae26e53 | ||
|
|
1333864408 | ||
|
|
01b9b5af67 | ||
|
|
8c57b6e7bc | ||
|
|
79246b5ea6 | ||
|
|
48954de237 | ||
|
|
c6235ed180 | ||
|
|
628c436301 | ||
|
|
2228fe6868 | ||
|
|
84bd8a3c1e | ||
|
|
b786ec8e74 | ||
|
|
20dcd984f9 | ||
|
|
6fca518157 | ||
|
|
98661fe012 | ||
|
|
b0765bee17 | ||
|
|
0a201b60cf | ||
|
|
8b9ea2f881 | ||
|
|
2ceea42958 | ||
|
|
bee126165f | ||
|
|
27cc676be3 | ||
|
|
4845aee6b7 | ||
|
|
0c620d2e08 | ||
|
|
6bb924bbf3 | ||
|
|
eaec7be446 | ||
|
|
f04fd1677b | ||
|
|
420b0a5c95 | ||
|
|
1e9500410a | ||
|
|
416f9cdede | ||
|
|
685bf811d6 | ||
|
|
e1e4646b06 | ||
|
|
4f2af1a7c0 | ||
|
|
577b9623e6 | ||
|
|
1cb0838721 |
@@ -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"
|
||||
|
||||
@@ -13,8 +13,9 @@ steps:
|
||||
- tests/test_config
|
||||
- tests/test_logger
|
||||
- tests/test_vllm_port
|
||||
- tests/test_jit_monitor.py
|
||||
commands:
|
||||
- pytest -v -s engine test_sequence.py test_config.py test_logger.py test_vllm_port.py
|
||||
- pytest -v -s engine test_sequence.py test_config.py test_logger.py test_vllm_port.py test_jit_monitor.py
|
||||
|
||||
- label: Engine (1 GPU)
|
||||
key: engine-1-gpu
|
||||
|
||||
@@ -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/
|
||||
|
||||
+6
-6
@@ -307,12 +307,12 @@ set(VLLM_EXT_SRC
|
||||
"csrc/quantization/activation_kernels.cu"
|
||||
"csrc/cuda_utils_kernels.cu"
|
||||
"csrc/custom_all_reduce.cu"
|
||||
"csrc/torch_bindings.cpp")
|
||||
"csrc/torch_bindings.cpp"
|
||||
"csrc/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu")
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
list(APPEND VLLM_EXT_SRC
|
||||
"csrc/minimax_reduce_rms_kernel.cu"
|
||||
"csrc/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu")
|
||||
"csrc/minimax_reduce_rms_kernel.cu")
|
||||
|
||||
SET(CUTLASS_ENABLE_HEADERS_ONLY ON CACHE BOOL "Enable only the header library")
|
||||
|
||||
@@ -1047,13 +1047,13 @@ endif()
|
||||
set(VLLM_MOE_EXT_SRC
|
||||
"csrc/moe/torch_bindings.cpp"
|
||||
"csrc/moe/moe_align_sum_kernels.cu"
|
||||
"csrc/moe/topk_softmax_kernels.cu")
|
||||
"csrc/moe/topk_softmax_kernels.cu"
|
||||
"csrc/moe/topk_softplus_sqrt_kernels.cu")
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
list(APPEND VLLM_MOE_EXT_SRC
|
||||
"csrc/moe/moe_wna16.cu"
|
||||
"csrc/moe/grouped_topk_kernels.cu"
|
||||
"csrc/moe/topk_softplus_sqrt_kernels.cu")
|
||||
"csrc/moe/grouped_topk_kernels.cu")
|
||||
endif()
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
|
||||
@@ -1473,6 +1473,12 @@ async def main() -> None:
|
||||
"(for example: --warmup-percentages=0%%,50%%)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
action="store_true",
|
||||
help="Trust remote code when loading the tokenizer.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logger.info(args)
|
||||
@@ -1515,7 +1521,9 @@ async def main() -> None:
|
||||
np.random.seed(args.seed)
|
||||
|
||||
logger.info("Loading tokenizer")
|
||||
tokenizer = AutoTokenizer.from_pretrained(args.model)
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
args.model, trust_remote_code=args.trust_remote_code
|
||||
)
|
||||
|
||||
await get_server_info(args.url)
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ torch::Tensor get_scheduler_metadata(
|
||||
isa = cpu_attention::ISA::NEON;
|
||||
} else if (isa_hint == "vxe") {
|
||||
isa = cpu_attention::ISA::VXE;
|
||||
} else if (isa_hint == "vsx") {
|
||||
isa = cpu_attention::ISA::VSX;
|
||||
} else {
|
||||
TORCH_CHECK(false, "Unsupported CPU attention ISA hint: " + isa_hint);
|
||||
}
|
||||
@@ -129,6 +131,8 @@ void cpu_attn_reshape_and_cache(
|
||||
return cpu_attention::ISA::NEON;
|
||||
} else if (isa == "vxe") {
|
||||
return cpu_attention::ISA::VXE;
|
||||
} else if (isa == "vsx") {
|
||||
return cpu_attention::ISA::VSX;
|
||||
} else {
|
||||
TORCH_CHECK(false, "Invalid ISA type: " + isa);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "cpu/utils.hpp"
|
||||
|
||||
namespace cpu_attention {
|
||||
enum class ISA { AMX, VEC, VEC16, NEON, VXE };
|
||||
enum class ISA { AMX, VEC, VEC16, NEON, VXE, VSX };
|
||||
|
||||
// Mirrors csrc/attention/dtype_fp8.cuh Fp8KVCacheDataType exactly.
|
||||
enum class Fp8KVCacheDataType {
|
||||
@@ -164,6 +164,9 @@ struct AttentionMetadata {
|
||||
case ISA::VXE:
|
||||
ss << "VXE, ";
|
||||
break;
|
||||
case ISA::VSX:
|
||||
ss << "VSX, ";
|
||||
break;
|
||||
}
|
||||
ss << "workitem_group_num: " << workitem_group_num
|
||||
<< ", reduction_item_num: " << reduction_item_num
|
||||
|
||||
@@ -27,8 +27,8 @@ FORCE_INLINE std::pair<vec_op::FP32Vec16, vec_op::FP32Vec16> load_b_pair_vec(
|
||||
return {vec_op::FP32Vec16(bf16_b_reg, 0), vec_op::FP32Vec16(bf16_b_reg, 1)};
|
||||
} else {
|
||||
using load_vec_t = typename VecTypeTrait<kv_cache_t>::vec_t;
|
||||
return {vec_op::FP32Vec16(load_vec_t(ptr)),
|
||||
vec_op::FP32Vec16(load_vec_t(ptr + 16))};
|
||||
return std::make_pair(vec_op::FP32Vec16(load_vec_t(ptr)),
|
||||
vec_op::FP32Vec16(load_vec_t(ptr + 16)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
#ifndef CPU_ATTN_VSX_HPP
|
||||
#define CPU_ATTN_VSX_HPP
|
||||
|
||||
#include "cpu_attn_impl.hpp"
|
||||
#include <altivec.h>
|
||||
#include <type_traits>
|
||||
|
||||
namespace cpu_attention {
|
||||
|
||||
namespace {
|
||||
|
||||
// ppc64le Vector = 16 bytes (128 bits)
|
||||
#define BLOCK_SIZE_ALIGNMENT 32
|
||||
#define HEAD_SIZE_ALIGNMENT 32
|
||||
#define MAX_Q_HEAD_NUM_PER_ITER 16
|
||||
|
||||
template <typename kv_cache_t>
|
||||
FORCE_INLINE void load_row8_B_as_f32(const kv_cache_t* p, __vector float& b0,
|
||||
__vector float& b1);
|
||||
|
||||
// [1] Float Specialization
|
||||
template <>
|
||||
FORCE_INLINE void load_row8_B_as_f32<float>(const float* p, __vector float& b0,
|
||||
__vector float& b1) {
|
||||
b0 = vec_xl(0, const_cast<float*>(p));
|
||||
b1 = vec_xl(0, const_cast<float*>(p + 4));
|
||||
}
|
||||
|
||||
// [2] BFloat16 Specialization (Little Endian ppc64le)
|
||||
// On ppc64le (LE): BF16 bits should land in the HIGH 16 bits of each float32.
|
||||
// Byte layout of float32 on LE: [byte0(LSB), byte1, byte2, byte3(MSB)]
|
||||
// We need BF16 in bytes2-3 (high half) with bytes0-1 zeroed.
|
||||
// vec_mergeh on LE interleaves elements 0..3: result_i = {a[i], b[i]}
|
||||
// So vec_mergeh(zeros_u16, raw_u16) gives for each uint16 pair:
|
||||
// uint16[2i] = zeros[i] -> low 16 bits of uint32 -> zeroed mantissa LSBs
|
||||
// uint16[2i+1] = raw[i] -> high 16 bits of uint32 -> BF16 bits
|
||||
// Cast to float32 gives exactly (bf16_bits << 16) per element.
|
||||
template <>
|
||||
FORCE_INLINE void load_row8_B_as_f32<c10::BFloat16>(const c10::BFloat16* p,
|
||||
__vector float& b0,
|
||||
__vector float& b1) {
|
||||
__vector unsigned short raw = vec_xl(
|
||||
0, reinterpret_cast<unsigned short*>(const_cast<c10::BFloat16*>(p)));
|
||||
__vector unsigned short zeros = vec_splat_u16(0);
|
||||
|
||||
// LE: zeros in low 16 bits, raw in high 16 bits → bf16 << 16 == float32
|
||||
b0 = (__vector float)vec_mergeh(zeros, raw);
|
||||
b1 = (__vector float)vec_mergel(zeros, raw);
|
||||
}
|
||||
|
||||
// Note: c10::Half (FP16) is not supported on PowerPC architecture
|
||||
|
||||
template <int32_t M, typename kv_cache_t>
|
||||
FORCE_INLINE void gemm_micro_ppc64le_Mx8_Ku4(
|
||||
const float* __restrict A, // [M x K]
|
||||
const kv_cache_t* __restrict B, // [K x 8]
|
||||
float* __restrict C, // [M x 8]
|
||||
int64_t lda, int64_t ldb, int64_t ldc, int32_t K, bool accumulate) {
|
||||
static_assert(1 <= M && M <= 8, "M must be in [1,8]");
|
||||
|
||||
#define ROWS_APPLY(OP) OP(0) OP(1) OP(2) OP(3) OP(4) OP(5) OP(6) OP(7)
|
||||
#define IF_M(i) if constexpr (M > (i))
|
||||
|
||||
// 1. Define A pointers
|
||||
#define DECL_A(i) const float* a##i = A + (i) * lda;
|
||||
ROWS_APPLY(DECL_A)
|
||||
#undef DECL_A
|
||||
|
||||
// 2. Define Accumulators (2 vectors covers 8 columns)
|
||||
#define DECL_ACC(i) __vector float acc##i##_0, acc##i##_1;
|
||||
ROWS_APPLY(DECL_ACC)
|
||||
#undef DECL_ACC
|
||||
|
||||
// 3. Initialize Accumulators (Load C or Zero)
|
||||
#define INIT_ACC(i) \
|
||||
IF_M(i) { \
|
||||
if (accumulate) { \
|
||||
acc##i##_0 = vec_xl(0, const_cast<float*>(C + (i) * ldc + 0)); \
|
||||
acc##i##_1 = vec_xl(0, const_cast<float*>(C + (i) * ldc + 4)); \
|
||||
} else { \
|
||||
acc##i##_0 = vec_splats(0.0f); \
|
||||
acc##i##_1 = vec_splats(0.0f); \
|
||||
} \
|
||||
}
|
||||
ROWS_APPLY(INIT_ACC)
|
||||
#undef INIT_ACC
|
||||
|
||||
int32_t k = 0;
|
||||
|
||||
for (; k + 3 < K; k += 4) {
|
||||
// Load 4 values of A for each Row M: A[k...k+3]
|
||||
#define LOAD_A4(i) \
|
||||
__vector float a##i##v; \
|
||||
IF_M(i) a##i##v = vec_xl(0, const_cast<float*>(a##i + k));
|
||||
ROWS_APPLY(LOAD_A4)
|
||||
#undef LOAD_A4
|
||||
|
||||
// FMA for specific lane L of A
|
||||
// ppc64le: vec_madd(b, vec_splat(a, lane), acc)
|
||||
#define FMAS_LANE(i, aiv, L) \
|
||||
IF_M(i) { \
|
||||
__vector float a_broad = vec_splat(aiv, L); \
|
||||
acc##i##_0 = vec_madd(b0, a_broad, acc##i##_0); \
|
||||
acc##i##_1 = vec_madd(b1, a_broad, acc##i##_1); \
|
||||
}
|
||||
|
||||
// Unroll K=0..3
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 0) * ldb, b0, b1);
|
||||
#define STEP_K0(i) FMAS_LANE(i, a##i##v, 0)
|
||||
ROWS_APPLY(STEP_K0)
|
||||
#undef STEP_K0
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 1) * ldb, b0, b1);
|
||||
#define STEP_K1(i) FMAS_LANE(i, a##i##v, 1)
|
||||
ROWS_APPLY(STEP_K1)
|
||||
#undef STEP_K1
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 2) * ldb, b0, b1);
|
||||
#define STEP_K2(i) FMAS_LANE(i, a##i##v, 2)
|
||||
ROWS_APPLY(STEP_K2)
|
||||
#undef STEP_K2
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 3) * ldb, b0, b1);
|
||||
#define STEP_K3(i) FMAS_LANE(i, a##i##v, 3)
|
||||
ROWS_APPLY(STEP_K3)
|
||||
#undef STEP_K3
|
||||
}
|
||||
#undef FMAS_LANE
|
||||
}
|
||||
|
||||
for (; k < K; ++k) {
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)k * ldb, b0, b1);
|
||||
#define TAIL_ROW(i) \
|
||||
IF_M(i) { \
|
||||
__vector float ai = vec_splats(*(a##i + k)); \
|
||||
acc##i##_0 = vec_madd(b0, ai, acc##i##_0); \
|
||||
acc##i##_1 = vec_madd(b1, ai, acc##i##_1); \
|
||||
}
|
||||
ROWS_APPLY(TAIL_ROW)
|
||||
#undef TAIL_ROW
|
||||
}
|
||||
|
||||
#define STORE_ROW(i) \
|
||||
IF_M(i) { \
|
||||
vec_xst(acc##i##_0, 0, C + (i) * ldc + 0); \
|
||||
vec_xst(acc##i##_1, 0, C + (i) * ldc + 4); \
|
||||
}
|
||||
ROWS_APPLY(STORE_ROW)
|
||||
#undef STORE_ROW
|
||||
|
||||
#undef ROWS_APPLY
|
||||
#undef IF_M
|
||||
}
|
||||
|
||||
template <int32_t N, typename kv_cache_t>
|
||||
FORCE_INLINE void gemm_macro_ppc64le_Mx8_Ku4(const float* __restrict A,
|
||||
const kv_cache_t* __restrict B,
|
||||
float* __restrict C, int32_t M,
|
||||
int32_t K, int64_t lda,
|
||||
int64_t ldb, int64_t ldc,
|
||||
bool accumulate) {
|
||||
static_assert(N % 8 == 0, "N must be a multiple of 8");
|
||||
for (int32_t m = 0; m < M;) {
|
||||
int32_t mb = (M - m >= 8) ? 8 : (M - m >= 4) ? 4 : (M - m >= 2) ? 2 : 1;
|
||||
const float* Ab = A + m * lda;
|
||||
float* Cb = C + m * ldc;
|
||||
|
||||
for (int32_t n = 0; n < N; n += 8) {
|
||||
const kv_cache_t* Bn = B + n;
|
||||
float* Cn = Cb + n;
|
||||
switch (mb) {
|
||||
case 8:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<8, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
case 4:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<4, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
case 2:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<2, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
default:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<1, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m += mb;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename kv_cache_t>
|
||||
class TileGemmPPC64 {
|
||||
public:
|
||||
template <AttentionGemmPhase phase, int32_t k_size>
|
||||
FORCE_INLINE static void gemm(const int32_t m_size,
|
||||
float* __restrict__ a_tile,
|
||||
kv_cache_t* __restrict__ b_tile,
|
||||
float* __restrict__ c_tile, const int64_t lda,
|
||||
const int64_t ldb, const int64_t ldc,
|
||||
const int32_t block_size,
|
||||
const int32_t dynamic_k_size,
|
||||
const bool accum_c) {
|
||||
if constexpr (phase == AttentionGemmPhase::QK) {
|
||||
gemm_macro_ppc64le_Mx8_Ku4<BLOCK_SIZE_ALIGNMENT, kv_cache_t>(
|
||||
a_tile, b_tile, c_tile, m_size, k_size, lda, ldb, ldc, accum_c);
|
||||
} else {
|
||||
gemm_macro_ppc64le_Mx8_Ku4<HEAD_SIZE_ALIGNMENT, kv_cache_t>(
|
||||
a_tile, b_tile, c_tile, m_size, dynamic_k_size, lda, ldb, ldc,
|
||||
accum_c);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
template <typename scalar_t, int64_t head_dim>
|
||||
class AttentionImpl<ISA::VSX, scalar_t, head_dim> {
|
||||
public:
|
||||
using query_t = scalar_t;
|
||||
using q_buffer_t = float;
|
||||
using kv_cache_t = scalar_t;
|
||||
using logits_buffer_t = float;
|
||||
using partial_output_buffer_t = float;
|
||||
using prob_buffer_t = float;
|
||||
|
||||
constexpr static int64_t BlockSizeAlignment = BLOCK_SIZE_ALIGNMENT;
|
||||
constexpr static int64_t HeadDimAlignment = HEAD_SIZE_ALIGNMENT;
|
||||
constexpr static int64_t MaxQHeadNumPerIteration = MAX_Q_HEAD_NUM_PER_ITER;
|
||||
constexpr static int64_t HeadDim = head_dim;
|
||||
constexpr static ISA ISAType = ISA::VSX;
|
||||
constexpr static bool scale_on_logits =
|
||||
false; // Scale is applied to Q during copy
|
||||
|
||||
public:
|
||||
AttentionImpl() {}
|
||||
|
||||
template <template <typename tile_gemm_t> typename attention>
|
||||
FORCE_INLINE void execute_attention(DEFINE_CPU_ATTENTION_PARAMS) {
|
||||
attention<TileGemmPPC64<kv_cache_t>> attention_iteration;
|
||||
attention_iteration(CPU_ATTENTION_PARAMS);
|
||||
}
|
||||
|
||||
// Strides for Memory Layout
|
||||
constexpr static int64_t k_cache_token_group_stride(
|
||||
const int32_t block_size) {
|
||||
return BlockSizeAlignment; // [head_dim, block_size] layout
|
||||
}
|
||||
|
||||
constexpr static int64_t v_cache_token_group_stride(
|
||||
const int32_t block_size) {
|
||||
return head_dim * BlockSizeAlignment;
|
||||
}
|
||||
|
||||
constexpr static int64_t v_cache_head_group_stride(const int32_t block_size) {
|
||||
return HeadDimAlignment;
|
||||
}
|
||||
|
||||
static void copy_q_heads_tile(scalar_t* __restrict__ src,
|
||||
float* __restrict__ q_buffer,
|
||||
const int32_t q_num,
|
||||
const int32_t q_heads_per_kv,
|
||||
const int64_t q_num_stride,
|
||||
const int64_t q_head_stride, float scale) {
|
||||
__vector float scale_vec = vec_splats(scale);
|
||||
constexpr bool is_bf16 = std::is_same<scalar_t, c10::BFloat16>::value;
|
||||
|
||||
for (int32_t i = 0; i < q_num; ++i) {
|
||||
for (int32_t h = 0; h < q_heads_per_kv; ++h) {
|
||||
scalar_t* curr_src = src + i * q_num_stride + h * q_head_stride;
|
||||
float* curr_dst =
|
||||
q_buffer + i * q_heads_per_kv * head_dim + h * head_dim;
|
||||
|
||||
int32_t d = 0;
|
||||
for (; d <= head_dim - 8; d += 8) {
|
||||
__vector float v0, v1;
|
||||
load_row8_B_as_f32<scalar_t>(curr_src + d, v0, v1);
|
||||
|
||||
v0 = vec_mul(v0, scale_vec);
|
||||
v1 = vec_mul(v1, scale_vec);
|
||||
|
||||
vec_xst(v0, 0, curr_dst + d);
|
||||
vec_xst(v1, 0, curr_dst + d + 4);
|
||||
}
|
||||
|
||||
for (; d < head_dim; ++d) {
|
||||
float val = static_cast<float>(curr_src[d]);
|
||||
curr_dst[d] = val * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void reshape_and_cache(
|
||||
const scalar_t* __restrict__ key, const scalar_t* __restrict__ value,
|
||||
scalar_t* __restrict__ key_cache, scalar_t* __restrict__ value_cache,
|
||||
const int64_t* __restrict__ slot_mapping, const int64_t token_num,
|
||||
const int64_t key_token_num_stride, const int64_t value_token_num_stride,
|
||||
const int64_t head_num, const int64_t key_head_num_stride,
|
||||
const int64_t value_head_num_stride, const int64_t num_blocks,
|
||||
const int64_t num_blocks_stride, const int64_t cache_head_num_stride,
|
||||
const int64_t block_size, const int64_t block_size_stride,
|
||||
const float k_inv = 0.0f, const float v_inv = 0.0f) {
|
||||
// k_inv and v_inv are unused on VSX: FP8 KV cache is not supported on
|
||||
// PowerPC. The parameters are present to match the common interface.
|
||||
#pragma omp parallel for collapse(2)
|
||||
for (int64_t token_idx = 0; token_idx < token_num; ++token_idx) {
|
||||
for (int64_t head_idx = 0; head_idx < head_num; ++head_idx) {
|
||||
const int64_t pos = slot_mapping[token_idx];
|
||||
if (pos < 0) continue;
|
||||
|
||||
const int64_t block_idx = pos / block_size;
|
||||
const int64_t block_offset = pos % block_size;
|
||||
|
||||
{
|
||||
const scalar_t* key_src = key + token_idx * key_token_num_stride +
|
||||
head_idx * key_head_num_stride;
|
||||
scalar_t* key_dst = key_cache + block_idx * num_blocks_stride +
|
||||
head_idx * cache_head_num_stride + block_offset;
|
||||
|
||||
for (int64_t i = 0, j = 0; i < head_dim; ++i, j += block_size) {
|
||||
key_dst[j] = key_src[i];
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const scalar_t* val_src = value + token_idx * value_token_num_stride +
|
||||
head_idx * value_head_num_stride;
|
||||
scalar_t* val_dst = value_cache + block_idx * num_blocks_stride +
|
||||
head_idx * cache_head_num_stride +
|
||||
block_offset * head_dim;
|
||||
|
||||
std::memcpy(val_dst, val_src, sizeof(scalar_t) * head_dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace cpu_attention
|
||||
|
||||
#undef BLOCK_SIZE_ALIGNMENT
|
||||
#undef HEAD_SIZE_ALIGNMENT
|
||||
#undef MAX_Q_HEAD_NUM_PER_ITER
|
||||
|
||||
#endif // CPU_ATTN_VSX_HPP
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
namespace vec_op {
|
||||
|
||||
// FP8 tag types for tag dispatch (see cpu_attn_vec.hpp)
|
||||
struct fp8_e4m3_tag {};
|
||||
struct fp8_e5m2_tag {};
|
||||
|
||||
// FIXME: FP16 is not fully supported in Torch-CPU
|
||||
#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \
|
||||
AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \
|
||||
|
||||
@@ -20,6 +20,7 @@ ISA_TYPES = {
|
||||
"VEC16": 2,
|
||||
"NEON": 3,
|
||||
"VXE": 4,
|
||||
"VSX": 5,
|
||||
}
|
||||
|
||||
# KV cache index: 0 = auto (same as scalar_t), 1 = fp8_e4m3, 2 = fp8_e5m2
|
||||
@@ -37,7 +38,7 @@ KV_CACHE_CPP_TYPES = {
|
||||
}
|
||||
|
||||
# ISAs supported for head_dims divisible by 32
|
||||
ISA_FOR_32 = ["AMX", "NEON", "VEC", "VEC16", "VXE"]
|
||||
ISA_FOR_32 = ["AMX", "NEON", "VEC", "VEC16", "VXE", "VSX"]
|
||||
|
||||
# ISAs supported for head_dims divisible by 16 only
|
||||
ISA_FOR_16 = ["VEC16"]
|
||||
@@ -148,6 +149,10 @@ def generate_header_file() -> str:
|
||||
#include "cpu_attn_vxe.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef __powerpc__
|
||||
#include "cpu_attn_vsx.hpp"
|
||||
#endif
|
||||
|
||||
"""
|
||||
|
||||
header += generate_helper_function()
|
||||
@@ -207,6 +212,11 @@ def generate_header_file() -> str:
|
||||
["VXE", "VEC", "VEC16"],
|
||||
fp8=False,
|
||||
)
|
||||
header += _macro_block(
|
||||
"#elif defined(__powerpc__)",
|
||||
["VSX", "VEC", "VEC16"],
|
||||
fp8=False,
|
||||
)
|
||||
header += _macro_block(
|
||||
"#elif defined(__AVX512F__)",
|
||||
["VEC", "VEC16"],
|
||||
@@ -223,7 +233,8 @@ def generate_header_file() -> str:
|
||||
fp8=False,
|
||||
)
|
||||
header += (
|
||||
"#endif /* CPU_CAPABILITY_AMXBF16 / __aarch64__ / __s390x__ */\n\n"
|
||||
"#endif /* CPU_CAPABILITY_AMXBF16 / __aarch64__ / "
|
||||
"__s390x__ / __powerpc__ */\n\n"
|
||||
"#endif // CPU_ATTN_DISPATCH_GENERATED_H\n"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ struct Counter {
|
||||
};
|
||||
|
||||
inline int64_t get_available_l2_size() {
|
||||
#if defined(__s390x__)
|
||||
#if defined(__s390x__) || defined(__powerpc__)
|
||||
static int64_t size = []() {
|
||||
uint32_t l2_cache_size = 0;
|
||||
auto caps = at::cpu::get_cpu_capabilities();
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include <cuda_fp8.h>
|
||||
#ifndef USE_ROCM
|
||||
#include <cuda_fp8.h>
|
||||
#else
|
||||
#include <hip/hip_fp8.h>
|
||||
#endif
|
||||
#include <cuda_runtime.h>
|
||||
#include <type_traits>
|
||||
|
||||
@@ -42,7 +46,23 @@
|
||||
#include "type_convert.cuh"
|
||||
|
||||
#ifndef FINAL_MASK
|
||||
#define FINAL_MASK 0xffffffffu
|
||||
#ifdef USE_ROCM
|
||||
#define FINAL_MASK 0xffffffffffffffffULL
|
||||
#else
|
||||
#define FINAL_MASK 0xffffffffu
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ROCM
|
||||
// ROCm-compatible FP8 conversion helpers
|
||||
__device__ __forceinline__ uint8_t rocm_cvt_float_to_fp8_e4m3(float val) {
|
||||
#if defined(HIP_FP8_TYPE_OCP)
|
||||
__hip_fp8_e4m3 fp8_val(val);
|
||||
#else
|
||||
__hip_fp8_e4m3_fnuz fp8_val(val);
|
||||
#endif
|
||||
return reinterpret_cast<uint8_t&>(fp8_val);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace vllm {
|
||||
@@ -314,9 +334,13 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel(
|
||||
for (int i = 0; i < kElemsPerLane; i++) {
|
||||
float scaled = elements[i] * inv_scale;
|
||||
scaled = fminf(fmaxf(scaled, -kFp8Max), kFp8Max);
|
||||
#ifndef USE_ROCM
|
||||
__nv_fp8_storage_t s =
|
||||
__nv_cvt_float_to_fp8(scaled, __NV_SATFINITE, __NV_E4M3);
|
||||
out_bytes[i] = static_cast<uint8_t>(s);
|
||||
#else
|
||||
out_bytes[i] = rocm_cvt_float_to_fp8_e4m3(scaled);
|
||||
#endif
|
||||
}
|
||||
// One 16-byte STG per lane.
|
||||
*reinterpret_cast<uint4*>(token_fp8_ptr + dim_base) =
|
||||
@@ -384,6 +408,7 @@ void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert(
|
||||
// PDL: enable programmatic stream serialization whenever the hardware
|
||||
// supports it (SM90+). On pre-Hopper GPUs the attribute is unavailable,
|
||||
// so leave numAttrs = 0 and launch as a regular kernel.
|
||||
#ifndef USE_ROCM
|
||||
static int const sm_version = getSMVersion();
|
||||
// Host-side guard: the device kernel body is compiled as a no-op for
|
||||
// bf16 on pre-Ampere (sm_70/sm_75) because _typeConvert<BFloat16> is
|
||||
@@ -410,6 +435,15 @@ void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert(
|
||||
q_inout, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache, eps,
|
||||
num_tokens_full, num_tokens_insert, num_heads_q, cache_block_size,
|
||||
kv_block_stride);
|
||||
#else
|
||||
// ROCm: use standard kernel launch syntax (no PDL/stream serialization)
|
||||
// clang-format off
|
||||
fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel<scalar_t_in>
|
||||
<<<grid, kBlockSize, 0, stream>>>(
|
||||
q_inout, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache,
|
||||
eps, num_tokens_full, num_tokens_insert, num_heads_q,
|
||||
cache_block_size, kv_block_stride);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace deepseek_v4_fused_ops
|
||||
|
||||
@@ -60,15 +60,6 @@ __device__ __forceinline__ float toFloat(T value) {
|
||||
}
|
||||
}
|
||||
|
||||
#define FINAL_MASK 0xffffffff
|
||||
template <typename T>
|
||||
__inline__ __device__ T warpReduceSum(T val) {
|
||||
#pragma unroll
|
||||
for (int mask = 16; mask > 0; mask >>= 1)
|
||||
val += __shfl_xor_sync(FINAL_MASK, val, mask, 32);
|
||||
return val;
|
||||
}
|
||||
|
||||
// ====================== TopK softplus_sqrt things
|
||||
// ===============================
|
||||
|
||||
@@ -272,8 +263,14 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
|
||||
}
|
||||
}
|
||||
// Compute per-thread scale (using warp reduction when renormalizing).
|
||||
// THREADS_PER_ROW-parameterized butterfly works for both warp sizes (32
|
||||
// on CUDA, 64 on ROCm CDNA) and any THREADS_PER_ROW the dispatch picks.
|
||||
if (renormalize) {
|
||||
selected_sum = warpReduceSum(selected_sum);
|
||||
#pragma unroll
|
||||
for (int mask = THREADS_PER_ROW / 2; mask > 0; mask /= 2) {
|
||||
selected_sum +=
|
||||
VLLM_SHFL_XOR_SYNC_WIDTH(selected_sum, mask, THREADS_PER_ROW);
|
||||
}
|
||||
}
|
||||
float scale = static_cast<float>(routed_scaling_factor);
|
||||
if (renormalize) {
|
||||
@@ -544,7 +541,6 @@ void topkGatingSoftplusSqrtKernelLauncher(
|
||||
const IndType* tid2eid, cudaStream_t stream) {
|
||||
static constexpr int WARPS_PER_TB = 4;
|
||||
static constexpr int BYTES_PER_LDG_POWER_OF_2 = 16;
|
||||
#ifndef USE_ROCM
|
||||
// for bfloat16 dtype, we need 4 bytes loading to make sure num_experts
|
||||
// elements can be loaded by a warp
|
||||
static constexpr int BYTES_PER_LDG_MULTIPLE_64 =
|
||||
@@ -552,6 +548,19 @@ void topkGatingSoftplusSqrtKernelLauncher(
|
||||
std::is_same_v<InputType, __half>)
|
||||
? 4
|
||||
: 8;
|
||||
// Narrower LDG (ELTS_PER_LDG=1) used by 192/320/448/576 on ROCm WARP_SIZE=64
|
||||
// where ELTS_PER_LDG=2 fails the EXPERTS%(ELTS_PER_LDG*WARP_SIZE)==0 check.
|
||||
// On CUDA WARP_SIZE=32 the wider LDG already aligns, so the alias collapses
|
||||
// back to BYTES_PER_LDG_MULTIPLE_64 — no behavioral change for CUDA.
|
||||
#ifdef USE_ROCM
|
||||
static constexpr int BYTES_PER_LDG_MULTIPLE_64_NARROW =
|
||||
(std::is_same_v<InputType, __nv_bfloat16> ||
|
||||
std::is_same_v<InputType, __half>)
|
||||
? 2
|
||||
: 4;
|
||||
#else
|
||||
static constexpr int BYTES_PER_LDG_MULTIPLE_64_NARROW =
|
||||
BYTES_PER_LDG_MULTIPLE_64;
|
||||
#endif
|
||||
switch (num_experts) {
|
||||
case 1:
|
||||
@@ -584,27 +593,29 @@ void topkGatingSoftplusSqrtKernelLauncher(
|
||||
case 512:
|
||||
LAUNCH_SOFTPLUS_SQRT(512, WARPS_PER_TB, BYTES_PER_LDG_POWER_OF_2);
|
||||
break;
|
||||
// (CUDA only) support multiples of 64 when num_experts is not power of 2.
|
||||
// ROCm uses WARP_SIZE 64 so 8 bytes loading won't fit for some of
|
||||
// num_experts, alternatively we can test 4 bytes loading and enable it in
|
||||
// future.
|
||||
#ifndef USE_ROCM
|
||||
// Multiples of 64 that are not powers of 2. The kernel requires
|
||||
// EXPERTS % (ELTS_PER_LDG * WARP_SIZE) == 0. With ELTS_PER_LDG=2
|
||||
// (BYTES_PER_LDG_MULTIPLE_64), this holds for all five values on CUDA
|
||||
// WARP_SIZE=32 but only for 384 on ROCm WARP_SIZE=64. The other four
|
||||
// use BYTES_PER_LDG_MULTIPLE_64_NARROW (ELTS_PER_LDG=1), which
|
||||
// satisfies the assertion for any multiple of 64 on either backend;
|
||||
// on CUDA the narrow alias collapses back to the wider load, so CUDA
|
||||
// behavior is unchanged.
|
||||
case 192:
|
||||
LAUNCH_SOFTPLUS_SQRT(192, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(192, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
case 320:
|
||||
LAUNCH_SOFTPLUS_SQRT(320, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(320, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
case 384:
|
||||
LAUNCH_SOFTPLUS_SQRT(384, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
break;
|
||||
case 448:
|
||||
LAUNCH_SOFTPLUS_SQRT(448, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(448, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
case 576:
|
||||
LAUNCH_SOFTPLUS_SQRT(576, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(576, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
#endif
|
||||
default: {
|
||||
TORCH_CHECK(false, "Unsupported expert number: ", num_experts);
|
||||
}
|
||||
|
||||
@@ -16,14 +16,13 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, m) {
|
||||
"bias) -> ()");
|
||||
m.impl("topk_sigmoid", torch::kCUDA, &topk_sigmoid);
|
||||
|
||||
#ifndef USE_ROCM
|
||||
m.def(
|
||||
"topk_softplus_sqrt(Tensor! topk_weights, Tensor! topk_indices, Tensor! "
|
||||
"token_expert_indices, Tensor gating_output, bool renormalize, float "
|
||||
"routed_scaling_factor, Tensor? "
|
||||
"bias, Tensor? input_ids, Tensor? tid2eid) -> ()");
|
||||
m.impl("topk_softplus_sqrt", torch::kCUDA, &topk_softplus_sqrt);
|
||||
#endif
|
||||
|
||||
// Calculate the result of moe by summing up the partial results
|
||||
// from all selected experts.
|
||||
m.def("moe_sum(Tensor input, Tensor! output) -> ()");
|
||||
|
||||
@@ -183,7 +183,6 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
"int forced_token_heads_per_warp=-1) -> ()");
|
||||
ops.impl("fused_qk_norm_rope", torch::kCUDA, &fused_qk_norm_rope);
|
||||
|
||||
#ifndef USE_ROCM
|
||||
// Horizontally-fused DeepseekV4-MLA: per-head RMSNorm + GPT-J RoPE for Q, and
|
||||
// GPT-J RoPE + UE8M0 FP8 quant + paged cache insert for KV, all in one
|
||||
// kernel launch.
|
||||
@@ -194,7 +193,6 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
"float eps, int cache_block_size) -> ()");
|
||||
ops.impl("fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert", torch::kCUDA,
|
||||
&fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert);
|
||||
#endif
|
||||
|
||||
// Apply repetition penalties to logits in-place
|
||||
ops.def(
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -614,6 +614,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
|
||||
| `Phi4MMForCausalLM` | Phi-4-multimodal | T + I<sup>+</sup> / T + A<sup>+</sup> / I<sup>+</sup> + A<sup>+</sup> | `microsoft/Phi-4-multimodal-instruct`, etc. | ✅︎ | ✅︎ |
|
||||
| `Phi4ForCausalLMV` | Phi-4-reasoning-vision | T + I<sup>+</sup> | `microsoft/Phi-4-reasoning-vision-15B`, etc. | | ✅︎ |
|
||||
| `PixtralForConditionalGeneration` | Ministral 3 (Mistral format), Mistral 3 (Mistral format), Mistral Large 3 (Mistral format), Pixtral (Mistral format) | T + I<sup>+</sup> | `mistralai/Ministral-3-3B-Instruct-2512`, `mistralai/Mistral-Small-3.1-24B-Instruct-2503`, `mistralai/Mistral-Large-3-675B-Instruct-2512` `mistralai/Pixtral-12B-2409` etc. | ✅︎ | ✅︎ |
|
||||
| `QianfanOCRForConditionalGeneration` | QianfanOCR | T + I<sup>E+</sup> | `baidu/Qianfan-OCR`, etc. | ✅︎ | ✅︎ |
|
||||
| `QwenVLForConditionalGeneration`<sup>^</sup> | Qwen-VL | T + I<sup>E+</sup> | `Qwen/Qwen-VL`, `Qwen/Qwen-VL-Chat`, etc. | ✅︎ | ✅︎ |
|
||||
| `Qwen2AudioForConditionalGeneration` | Qwen2-Audio | T + A<sup>+</sup> | `Qwen/Qwen2-Audio-7B-Instruct` | | ✅︎ |
|
||||
| `Qwen2VLForConditionalGeneration` | QVQ, Qwen2-VL | T + I<sup>E+</sup> + V<sup>E+</sup> | `Qwen/QVQ-72B-Preview`, `Qwen/Qwen2-VL-7B-Instruct`, `Qwen/Qwen2-VL-72B-Instruct`, etc. | ✅︎ | ✅︎ |
|
||||
|
||||
@@ -98,7 +98,7 @@ For larger scale deployments especially, it can make sense to handle the orchest
|
||||
|
||||
In this case, it's more convenient to treat each DP rank like a separate vLLM deployment, with its own endpoint, and have an external router balance HTTP requests between them, making use of appropriate real-time telemetry from each server for routing decisions.
|
||||
|
||||
This can already be done trivially for non-MoE models, since each deployed server is fully independent. No data parallel CLI options need to be used for this.
|
||||
This can already be done trivially for non-MoE models, since each deployed server is fully independent. In that case, launch independent vLLM instances without any `--data-parallel-*` arguments; external DP CLI options are only supported for MoE deployments.
|
||||
|
||||
We support an equivalent topology for MoE DP+EP which can be configured via the following CLI arguments.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -21,3 +21,6 @@ timm>=1.0.17
|
||||
# amd-quark: required for Quark quantization on ROCm
|
||||
# To be consistent with test_quark.py
|
||||
amd-quark>=0.8.99
|
||||
# tilelang has to be installed for mhc module to be
|
||||
# imported correctly.
|
||||
tilelang==0.1.9
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -28,6 +28,25 @@ HOPPER_MXFP4_BF16_AVAILABLE = (
|
||||
and has_flashinfer()
|
||||
)
|
||||
|
||||
# ROCm platform and dependencies
|
||||
ROCM_AVAILABLE = current_platform.is_rocm()
|
||||
ROCM_TRITON_KERNELS_AVAILABLE = False
|
||||
ROCM_AITER_AVAILABLE = False
|
||||
ROCM_GFX950 = False
|
||||
|
||||
if ROCM_AVAILABLE:
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
from vllm.platforms.rocm import on_gfx950
|
||||
from vllm.utils.import_utils import has_triton_kernels
|
||||
|
||||
ROCM_TRITON_KERNELS_AVAILABLE = has_triton_kernels()
|
||||
ROCM_GFX950 = on_gfx950()
|
||||
ROCM_AITER_AVAILABLE = rocm_aiter_ops.is_enabled()
|
||||
|
||||
if ROCM_AITER_AVAILABLE:
|
||||
from aiter.ops.triton.moe.quant_moe import upcast_from_mxfp
|
||||
from aiter.ops.triton.quant import dynamic_mxfp4_quant
|
||||
|
||||
if TRTLLM_GEN_MXFP4_AVAILABLE:
|
||||
from flashinfer import (
|
||||
fp4_quantize,
|
||||
@@ -111,6 +130,7 @@ def test_mxfp4_loading_and_execution_moe(vllm_runner, model_case: ModelCase):
|
||||
|
||||
def swiglu(x, alpha: float = 1.702, beta: float = 1.0, limit: float | None = None):
|
||||
# Note we add an extra bias of 1 to the linear layer
|
||||
# Uses chunked layout: first half is gate, second half is up
|
||||
x_glu, x_linear = torch.chunk(x, 2, dim=-1)
|
||||
if limit is not None:
|
||||
x_glu = x_glu.clamp(max=limit)
|
||||
@@ -119,6 +139,16 @@ def swiglu(x, alpha: float = 1.702, beta: float = 1.0, limit: float | None = Non
|
||||
return out_glu * (x_linear + beta)
|
||||
|
||||
|
||||
def swigluoai(x, alpha: float = 1.702, limit: float = 7.0):
|
||||
# OAI swiglu uses interleaved layout: gate/up alternating
|
||||
# See SwigluOAIAndMul in vllm/model_executor/layers/activation.py
|
||||
gate, up = x[..., ::2], x[..., 1::2]
|
||||
gate = gate.clamp(max=limit)
|
||||
up = up.clamp(min=-limit, max=limit)
|
||||
glu = gate * torch.sigmoid(gate * alpha)
|
||||
return (up + 1) * glu
|
||||
|
||||
|
||||
fp4_lookup_table = [0, 0.5, 1, 1.5, 2, 3, 4, 6, -0, -0.5, -1, -1.5, -2, -3, -4, -6]
|
||||
|
||||
|
||||
@@ -168,8 +198,20 @@ def reference_moe(
|
||||
beta,
|
||||
limit,
|
||||
act_type,
|
||||
is_gated,
|
||||
activation: str = "swiglu",
|
||||
use_interleaved_layout: bool = False,
|
||||
):
|
||||
"""
|
||||
Reference MoE implementation for accuracy testing.
|
||||
|
||||
Args:
|
||||
activation: One of "swiglu", "silu", "relu2". Controls the activation
|
||||
function used after the first MLP.
|
||||
use_interleaved_layout: If True, uses interleaved gate/up layout
|
||||
(gate=x[..., ::2], up=x[..., 1::2]) as used by SWIGLUOAI.
|
||||
If False, uses chunked layout (gate, up = chunk(x, 2)) as used
|
||||
by standard swiglu/silu.
|
||||
"""
|
||||
# renormalize routing
|
||||
experts = torch.topk(roouting_logits, k=topk, dim=-1, sorted=True)
|
||||
expert_weights = torch.nn.functional.softmax(experts.values, dim=1)
|
||||
@@ -179,12 +221,21 @@ def reference_moe(
|
||||
mlp1_weight = w13[expert_indices, ...]
|
||||
mlp1_bias = bias13[expert_indices, ...]
|
||||
t = torch.einsum("beck,bk->bec", mlp1_weight, t) + mlp1_bias
|
||||
if is_gated:
|
||||
t = swiglu(t, alpha=alpha, beta=beta, limit=limit)
|
||||
else:
|
||||
|
||||
# Apply activation
|
||||
if activation in ("swiglu", "silu"):
|
||||
if use_interleaved_layout:
|
||||
# SWIGLUOAI: interleaved gate/up layout
|
||||
t = swigluoai(t, alpha=alpha, limit=limit)
|
||||
else:
|
||||
# Standard swiglu/silu: chunked layout
|
||||
t = swiglu(t, alpha=alpha, beta=beta, limit=limit)
|
||||
elif activation == "relu2":
|
||||
# RELU2_NO_MUL: relu(x)^2
|
||||
t = torch.relu(t)
|
||||
t = t * t
|
||||
else:
|
||||
raise ValueError(f"Unknown activation: {activation}")
|
||||
|
||||
if act_type == "mxfp8":
|
||||
t_quantized, t_scale = mxfp8_quantize(
|
||||
@@ -585,7 +636,8 @@ def test_trtllm_gen_mxfp4_fused_moe(
|
||||
beta,
|
||||
limit,
|
||||
act_type,
|
||||
is_gated=True,
|
||||
activation="swiglu",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
ref_result[start_idx:end_idx].copy_(chunk_result)
|
||||
|
||||
@@ -722,7 +774,8 @@ def test_flashinfer_cutlass_mxfp4_fused_moe(
|
||||
beta,
|
||||
limit,
|
||||
"bf16",
|
||||
is_gated=True,
|
||||
activation="swiglu",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
|
||||
from vllm.utils.flashinfer import flashinfer_cutlass_fused_moe
|
||||
@@ -908,7 +961,8 @@ def test_flashinfer_cutlass_mxfp4_mxfp8_fused_moe(
|
||||
beta,
|
||||
limit,
|
||||
"mxfp8",
|
||||
is_gated=True,
|
||||
activation="swiglu",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
|
||||
# Prepare inputs for FlashInfer CUTLASS fused MoE
|
||||
@@ -1080,7 +1134,8 @@ def test_trtllm_gen_mxfp8_block_scale_moe(
|
||||
beta=0.0,
|
||||
limit=None,
|
||||
act_type="mxfp8",
|
||||
is_gated=is_gated,
|
||||
activation="swiglu" if is_gated else "relu2",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
|
||||
# Shuffle weights/scales with the same indexed layout used by TRTLLM kernels.
|
||||
@@ -1150,3 +1205,328 @@ def test_trtllm_gen_mxfp8_block_scale_moe(
|
||||
|
||||
# Block-scale MXFP8 kernels are approximate; require majority close.
|
||||
check_accuracy(ref, out, atol=0.1, rtol=0.85, percent=0.8)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ROCm Oracle-based kernel execution tests
|
||||
# -----------------------------------------------------------------------------
|
||||
# TODO: Further tighten the accuracy threshold.
|
||||
# - More accurate ref moe to include activation quantization
|
||||
# - Check aiter kernel accuracy. E.g., quant / dequant details.
|
||||
ROCM_BACKEND_CONFIGS = {
|
||||
"TRITON": {
|
||||
"activation": "SWIGLUOAI",
|
||||
"rtol": 0.3,
|
||||
"percent": 0.95,
|
||||
"requires_aiter": False,
|
||||
"requires_gfx950": False,
|
||||
},
|
||||
"TRITON_UNFUSED": {
|
||||
"activation": "SWIGLUOAI",
|
||||
"rtol": 0.3,
|
||||
"percent": 0.95,
|
||||
"requires_aiter": False,
|
||||
"requires_gfx950": False,
|
||||
},
|
||||
"AITER_MXFP4_BF16": {
|
||||
"activation": "SILU",
|
||||
"rtol": 1.0,
|
||||
"percent": 0.7,
|
||||
"requires_aiter": True,
|
||||
"requires_gfx950": True,
|
||||
},
|
||||
"AITER_MXFP4_FP8": {
|
||||
"activation": "SWIGLUOAI",
|
||||
"rtol": 0.5,
|
||||
"percent": 0.9,
|
||||
"requires_aiter": True,
|
||||
"requires_gfx950": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend_name", list(ROCM_BACKEND_CONFIGS.keys()))
|
||||
@pytest.mark.parametrize("topk", [4])
|
||||
@pytest.mark.parametrize("num_experts", [8])
|
||||
@pytest.mark.parametrize("num_tokens,hidden_size,intermediate_size", [(16, 256, 256)])
|
||||
@pytest.mark.skipif(
|
||||
not ROCM_AVAILABLE,
|
||||
reason="ROCm is required for this test",
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_rocm_mxfp4_moe_oracle(
|
||||
backend_name: str,
|
||||
topk: int,
|
||||
num_experts: int,
|
||||
num_tokens: int,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
):
|
||||
"""
|
||||
Test ROCm MXFP4 MoE using oracle functions.
|
||||
|
||||
This test validates that the oracle functions work end-to-end:
|
||||
- select_mxfp4_moe_backend() selects a valid backend
|
||||
- convert_to_mxfp4_moe_kernel_format() converts weights without error
|
||||
- make_mxfp4_moe_quant_config() builds a valid quant config
|
||||
- make_mxfp4_moe_kernel() creates a kernel that runs without error
|
||||
- The kernel output is within accuracy tolerance of reference
|
||||
"""
|
||||
config = ROCM_BACKEND_CONFIGS[backend_name]
|
||||
|
||||
# Check platform requirements
|
||||
if not ROCM_TRITON_KERNELS_AVAILABLE:
|
||||
pytest.skip("triton_kernels required for quantization")
|
||||
if config["requires_aiter"] and not ROCM_AITER_AVAILABLE:
|
||||
pytest.skip(f"Backend {backend_name} requires AITER")
|
||||
if config["requires_gfx950"] and not ROCM_GFX950:
|
||||
pytest.skip(f"Backend {backend_name} requires GFX950")
|
||||
|
||||
from vllm.config import VllmConfig, set_current_vllm_config
|
||||
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
||||
from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
|
||||
Mxfp4MoeBackend,
|
||||
backend_to_kernel_cls,
|
||||
convert_to_mxfp4_moe_kernel_format,
|
||||
make_mxfp4_moe_kernel,
|
||||
make_mxfp4_moe_quant_config,
|
||||
)
|
||||
from vllm.v1.worker.workspace import init_workspace_manager
|
||||
|
||||
# Initialize workspace manager (needed for modular kernels)
|
||||
init_workspace_manager(torch.accelerator.current_device_index())
|
||||
|
||||
# Map string to enum
|
||||
backend = Mxfp4MoeBackend[backend_name]
|
||||
|
||||
# Get experts class from oracle
|
||||
experts_cls_list = backend_to_kernel_cls(backend)
|
||||
if experts_cls_list is None or len(experts_cls_list) == 0:
|
||||
pytest.skip(f"Backend {backend_name} not available")
|
||||
|
||||
# Use first experts class
|
||||
experts_cls = experts_cls_list[0]
|
||||
|
||||
torch.manual_seed(42)
|
||||
dtype = torch.bfloat16
|
||||
device = "cuda:0"
|
||||
|
||||
# Create MoE config with Renormalize routing (required by monolithic kernels)
|
||||
from vllm.model_executor.layers.fused_moe import FusedMoEConfig
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
FusedMoEParallelConfig,
|
||||
RoutingMethodType,
|
||||
)
|
||||
|
||||
moe_config = FusedMoEConfig(
|
||||
num_experts=num_experts,
|
||||
experts_per_token=topk,
|
||||
hidden_dim=hidden_size,
|
||||
intermediate_size_per_partition=intermediate_size,
|
||||
num_local_experts=num_experts,
|
||||
num_logical_experts=num_experts,
|
||||
moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
|
||||
activation=MoEActivation[config["activation"]],
|
||||
in_dtype=dtype,
|
||||
device="cuda",
|
||||
routing_method=RoutingMethodType.Renormalize,
|
||||
)
|
||||
|
||||
# Create float weights in checkpoint format:
|
||||
# w13: [num_experts, 2*intermediate_size, hidden_size]
|
||||
# w2: [num_experts, hidden_size, intermediate_size]
|
||||
w13_float = torch.randn(
|
||||
num_experts, 2 * intermediate_size, hidden_size, dtype=dtype, device=device
|
||||
)
|
||||
w2_float = torch.randn(
|
||||
num_experts, hidden_size, intermediate_size, dtype=dtype, device=device
|
||||
)
|
||||
|
||||
# dynamic_mxfp4_quant expects 2D input, so reshape 3D weights
|
||||
# w13: [E, 2*I, H] -> [E*2*I, H] -> quantize -> [E, 2*I, H//2]
|
||||
# w2: [E, H, I] -> [E*H, I] -> quantize -> [E, H, I//2]
|
||||
w13_2d = w13_float.reshape(-1, hidden_size)
|
||||
w13_quant_2d, w13_scale_2d = dynamic_mxfp4_quant(w13_2d)
|
||||
w13_quant = w13_quant_2d.reshape(num_experts, 2 * intermediate_size, -1)
|
||||
w13_scale = w13_scale_2d.reshape(num_experts, 2 * intermediate_size, -1)
|
||||
|
||||
w2_2d = w2_float.reshape(-1, intermediate_size)
|
||||
w2_quant_2d, w2_scale_2d = dynamic_mxfp4_quant(w2_2d)
|
||||
w2_quant = w2_quant_2d.reshape(num_experts, hidden_size, -1)
|
||||
w2_scale = w2_scale_2d.reshape(num_experts, hidden_size, -1)
|
||||
|
||||
w13_bias = torch.randn(
|
||||
num_experts, 2 * intermediate_size, dtype=dtype, device=device
|
||||
)
|
||||
w2_bias = torch.randn(num_experts, hidden_size, dtype=dtype, device=device)
|
||||
|
||||
# Create static input scales for W4A8 backend (AITER_MXFP4_FP8)
|
||||
w13_input_scale: torch.Tensor | None = None
|
||||
w2_input_scale: torch.Tensor | None = None
|
||||
if backend_name == "AITER_MXFP4_FP8":
|
||||
# Static FP8 scales: one scale per expert
|
||||
w13_input_scale = torch.ones(num_experts, dtype=torch.float32, device=device)
|
||||
w2_input_scale = torch.ones(num_experts, dtype=torch.float32, device=device)
|
||||
|
||||
# Create mock layer for oracle functions
|
||||
class MockLayer:
|
||||
w13_weight: torch.Tensor
|
||||
w2_weight: torch.Tensor
|
||||
w13_weight_scale: torch.Tensor
|
||||
w2_weight_scale: torch.Tensor
|
||||
w13_input_scale: torch.Tensor | None
|
||||
w2_input_scale: torch.Tensor | None
|
||||
|
||||
layer = MockLayer()
|
||||
layer.w13_weight = w13_quant
|
||||
layer.w2_weight = w2_quant
|
||||
layer.w13_weight_scale = w13_scale
|
||||
layer.w2_weight_scale = w2_scale
|
||||
layer.w13_input_scale = w13_input_scale
|
||||
layer.w2_input_scale = w2_input_scale
|
||||
|
||||
# Convert weights using oracle
|
||||
w13_conv, w2_conv, w13_scale_conv, w2_scale_conv, w13_bias_conv, w2_bias_conv = (
|
||||
convert_to_mxfp4_moe_kernel_format(
|
||||
mxfp4_backend=backend,
|
||||
layer=layer, # type: ignore[arg-type]
|
||||
w13_weight=w13_quant,
|
||||
w2_weight=w2_quant,
|
||||
w13_weight_scale=w13_scale,
|
||||
w2_weight_scale=w2_scale,
|
||||
w13_bias=w13_bias,
|
||||
w2_bias=w2_bias,
|
||||
)
|
||||
)
|
||||
|
||||
# Build quant config using oracle
|
||||
quant_config = make_mxfp4_moe_quant_config(
|
||||
mxfp4_backend=backend,
|
||||
w1_scale=w13_scale_conv,
|
||||
w2_scale=w2_scale_conv,
|
||||
w1_bias=w13_bias_conv,
|
||||
w2_bias=w2_bias_conv,
|
||||
a1_scale=w13_input_scale,
|
||||
a2_scale=w2_input_scale,
|
||||
)
|
||||
|
||||
# Select activation based on backend
|
||||
activation_name = str(config["activation"])
|
||||
activation = MoEActivation[activation_name]
|
||||
|
||||
# Build kernel using oracle
|
||||
assert quant_config is not None, "Failed to create quant config"
|
||||
with set_current_vllm_config(VllmConfig()):
|
||||
kernel = make_mxfp4_moe_kernel(
|
||||
moe_quant_config=quant_config,
|
||||
moe_config=moe_config,
|
||||
mxfp4_backend=backend,
|
||||
experts_cls=experts_cls,
|
||||
routing_tables=None,
|
||||
shared_experts=None,
|
||||
)
|
||||
|
||||
# Create inputs
|
||||
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
|
||||
router_logits = torch.randn(
|
||||
num_tokens, num_experts, dtype=torch.float32, device=device
|
||||
)
|
||||
topk_weights, topk_ids = torch.topk(router_logits, k=topk, dim=-1, sorted=True)
|
||||
topk_weights = torch.nn.functional.softmax(topk_weights, dim=-1)
|
||||
|
||||
# Run kernel - use appropriate method based on impl type
|
||||
if kernel.is_monolithic:
|
||||
# Monolithic impl uses router_logits
|
||||
out = kernel.apply_monolithic(
|
||||
hidden_states=x,
|
||||
w1=w13_conv,
|
||||
w2=w2_conv,
|
||||
router_logits=router_logits,
|
||||
activation=activation,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=None,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
else:
|
||||
# Modular impl uses topk_weights and topk_ids
|
||||
out = kernel.apply(
|
||||
hidden_states=x,
|
||||
w1=w13_conv,
|
||||
w2=w2_conv,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
activation=activation,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=None,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
|
||||
# Verify output is valid (no NaN/Inf) and has expected shape
|
||||
assert out.shape == (num_tokens, hidden_size), f"Unexpected shape: {out.shape}"
|
||||
assert not torch.any(torch.isnan(out)), "Output contains NaN"
|
||||
assert not torch.any(torch.isinf(out)), "Output contains Inf"
|
||||
|
||||
# Verify output has reasonable magnitude (not all zeros)
|
||||
assert out.abs().max() > 0.01, "Output is effectively zero"
|
||||
|
||||
# Dequantize weights for reference computation
|
||||
w13_dq = upcast_from_mxfp(
|
||||
w13_quant.view(torch.uint8), w13_scale, torch.bfloat16, axis=-1
|
||||
)
|
||||
w2_dq = upcast_from_mxfp(
|
||||
w2_quant.view(torch.uint8), w2_scale, torch.bfloat16, axis=-1
|
||||
)
|
||||
|
||||
# Determine activation type and layout
|
||||
# SWIGLUOAI uses interleaved layout (gate/up alternating)
|
||||
# SILU uses chunked layout (first half gate, second half up)
|
||||
use_interleaved = activation == MoEActivation.SWIGLUOAI
|
||||
if activation in [MoEActivation.SWIGLUOAI, MoEActivation.SILU]:
|
||||
act_name = "swiglu"
|
||||
else:
|
||||
act_name = "relu2"
|
||||
|
||||
ref = reference_moe(
|
||||
router_logits,
|
||||
topk,
|
||||
num_experts,
|
||||
x.to(torch.float32),
|
||||
w13_dq.to(torch.float32),
|
||||
w13_bias.to(torch.float32),
|
||||
w2_dq.to(torch.float32),
|
||||
w2_bias.to(torch.float32),
|
||||
alpha=1.702 if activation == MoEActivation.SWIGLUOAI else 1.0,
|
||||
beta=1.0 if activation == MoEActivation.SWIGLUOAI else 0.0,
|
||||
limit=7.0 if activation == MoEActivation.SWIGLUOAI else None,
|
||||
act_type="bf16",
|
||||
activation=act_name,
|
||||
use_interleaved_layout=use_interleaved,
|
||||
)
|
||||
|
||||
# Compute and print accuracy statistics
|
||||
diff = (ref.float() - out.float()).abs()
|
||||
rel_diff = diff / (ref.float().abs() + 1e-6)
|
||||
|
||||
print(f"\n[{backend_name}] Accuracy statistics:")
|
||||
print(
|
||||
f" Reference: min={ref.min():.4f}, max={ref.max():.4f}, mean={ref.mean():.4f}"
|
||||
)
|
||||
print(
|
||||
f" Output: min={out.min():.4f}, max={out.max():.4f}, mean={out.mean():.4f}"
|
||||
)
|
||||
print(
|
||||
f" Abs diff: min={diff.min():.4f}, max={diff.max():.4f}, "
|
||||
f"mean={diff.mean():.4f}"
|
||||
)
|
||||
print(
|
||||
f" Rel diff: min={rel_diff.min():.4f}, max={rel_diff.max():.4f}, "
|
||||
f"mean={rel_diff.mean():.4f}"
|
||||
)
|
||||
|
||||
# Check what percentage of values are within various tolerances
|
||||
for rtol in [0.1, 0.5, 1.0, 2.0]:
|
||||
within_tol = (diff <= rtol * out.float().abs()).float().mean()
|
||||
print(f" Within rtol={rtol}: {within_tol * 100:.1f}%")
|
||||
|
||||
# Check accuracy using per-backend thresholds
|
||||
check_accuracy(ref, out, atol=0.1, rtol=config["rtol"], percent=config["percent"])
|
||||
|
||||
@@ -70,7 +70,8 @@ def test_sqrtsoftplus_bias_uses_deepseek_v4_routing_method():
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(), reason="This test is skipped on non-CUDA platform."
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="This test is skipped on non-CUDA platform.",
|
||||
)
|
||||
@pytest.mark.parametrize("num_tokens", [1, 33, 128])
|
||||
@pytest.mark.parametrize("hidden_size", [1024, 2048])
|
||||
@@ -125,7 +126,8 @@ def test_fused_topk_softplus_sqrt(
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(), reason="This test is skipped on non-CUDA platform."
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="This test is skipped on non-CUDA platform.",
|
||||
)
|
||||
@pytest.mark.parametrize("num_tokens", [1, 33, 128])
|
||||
@pytest.mark.parametrize("hidden_size", [1024, 2048])
|
||||
|
||||
@@ -928,6 +928,16 @@ VLM_TEST_SETTINGS = {
|
||||
),
|
||||
],
|
||||
),
|
||||
"qianfan_ocr": VLMTestInfo(
|
||||
models=["baidu/Qianfan-OCR"],
|
||||
test_type=(VLMTestType.IMAGE, VLMTestType.MULTI_IMAGE),
|
||||
prompt_formatter=lambda img_prompt: f"<|im_start|>user\n{img_prompt}<|im_end|>\n<|im_start|>assistant\n", # noqa: E501
|
||||
img_idx_to_prompt=lambda idx: "<image>",
|
||||
max_model_len=4096,
|
||||
use_tokenizer_eos=True,
|
||||
auto_cls=AutoModelForImageTextToText,
|
||||
hf_model_kwargs=model_utils.qianfan_ocr_hf_model_kwargs("baidu/Qianfan-OCR"),
|
||||
),
|
||||
"qwen_vl": VLMTestInfo(
|
||||
models=["Qwen/Qwen-VL"],
|
||||
test_type=(VLMTestType.IMAGE, VLMTestType.MULTI_IMAGE),
|
||||
|
||||
@@ -1554,3 +1554,94 @@ def moondream3_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
|
||||
|
||||
hf_model.model.generate = types.MethodType(_generate, hf_model.model)
|
||||
return hf_model
|
||||
|
||||
|
||||
def qianfan_ocr_hf_model_kwargs(model_name: str) -> dict:
|
||||
"""Return hf_model_kwargs with a patched config for QianfanOCR."""
|
||||
from vllm.transformers_utils.configs.qianfan_ocr import QianfanOCRConfig
|
||||
|
||||
config = QianfanOCRConfig.from_pretrained(model_name)
|
||||
vc = config.vision_config
|
||||
if isinstance(vc.image_size, int):
|
||||
vc.image_size = (vc.image_size, vc.image_size)
|
||||
if isinstance(vc.patch_size, int):
|
||||
vc.patch_size = (vc.patch_size, vc.patch_size)
|
||||
return {"config": config}
|
||||
|
||||
|
||||
def qianfan_ocr_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
|
||||
"""Patches an HfRunner instance to run QianfanOCR model inference.
|
||||
|
||||
QianfanOCR shares the same architecture as InternVLChatModel, so the
|
||||
patching logic mirrors ``internvl_patch_hf_runner``. The only difference
|
||||
is that we load the config via vllm's registered ``QianfanOCRConfig``
|
||||
instead of relying on ``trust_remote_code``.
|
||||
"""
|
||||
|
||||
class QianfanOCRProcessor:
|
||||
def __init__(self, hf_runner: HfRunner):
|
||||
self.tokenizer = hf_runner.tokenizer
|
||||
|
||||
from vllm.transformers_utils.configs.qianfan_ocr import QianfanOCRConfig
|
||||
|
||||
self.config = QianfanOCRConfig.from_pretrained(hf_runner.model_name)
|
||||
self.vision_config = self.config.vision_config
|
||||
self.use_thumbnail = self.config.use_thumbnail
|
||||
self.min_num = self.config.min_dynamic_patch
|
||||
self.max_num = self.config.max_dynamic_patch
|
||||
self.image_size = self.vision_config.image_size
|
||||
|
||||
# Compute num_image_token from config instead of model attribute,
|
||||
# since the transformers-native model doesn't expose it.
|
||||
image_size = self.config.force_image_size or self.vision_config.image_size
|
||||
patch_size = self.vision_config.patch_size
|
||||
downsample_ratio = self.config.downsample_ratio
|
||||
self.num_image_token = int(
|
||||
(image_size // patch_size) ** 2 * (downsample_ratio**2)
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
text: str,
|
||||
images: PIL.Image.Image | list[PIL.Image.Image] = None,
|
||||
**kwargs,
|
||||
):
|
||||
from vllm.transformers_utils.processors.internvl import (
|
||||
image_to_pixel_values_internvl,
|
||||
)
|
||||
|
||||
IMG_START = "<img>"
|
||||
IMG_END = "</img>"
|
||||
IMG_CONTEXT = "<IMG_CONTEXT>"
|
||||
|
||||
images = [images] if isinstance(images, PIL.Image.Image) else images
|
||||
pixel_values_list = [
|
||||
image_to_pixel_values_internvl(
|
||||
image,
|
||||
input_size=self.image_size,
|
||||
min_num=self.min_num,
|
||||
max_num=self.max_num,
|
||||
use_thumbnail=self.use_thumbnail,
|
||||
)
|
||||
for image in images
|
||||
]
|
||||
num_patches_list = [pv.shape[0] for pv in pixel_values_list]
|
||||
pixel_values = torch.cat(pixel_values_list, dim=0)
|
||||
|
||||
for num_patches in num_patches_list:
|
||||
context_tokens = IMG_CONTEXT * self.num_image_token * num_patches
|
||||
image_tokens = IMG_START + context_tokens + IMG_END
|
||||
text = text.replace("<image>", image_tokens, 1)
|
||||
|
||||
prompt = self.tokenizer(text, return_tensors="pt")
|
||||
prompt.update({"pixel_values": pixel_values})
|
||||
return prompt
|
||||
|
||||
img_context_token_id = hf_model.tokenizer.convert_tokens_to_ids("<IMG_CONTEXT>")
|
||||
hf_model.model.img_context_token_id = img_context_token_id
|
||||
hf_model.processor = QianfanOCRProcessor(hf_model)
|
||||
hf_model.model.get_output_embeddings = (
|
||||
lambda: hf_model.model.language_model.get_output_embeddings()
|
||||
)
|
||||
hf_model.model.generate = types.MethodType(_internvl_generate, hf_model.model)
|
||||
return hf_model
|
||||
|
||||
@@ -1264,6 +1264,10 @@ _MULTIMODAL_EXAMPLE_MODELS = {
|
||||
},
|
||||
tokenizer_mode="mistral",
|
||||
),
|
||||
"QianfanOCRForConditionalGeneration": _HfExamplesInfo(
|
||||
"baidu/Qianfan-OCR",
|
||||
min_transformers_version="5.6.0",
|
||||
),
|
||||
"QwenVLForConditionalGeneration": _HfExamplesInfo(
|
||||
"Qwen/Qwen-VL",
|
||||
extras={"chat": "Qwen/Qwen-VL-Chat"},
|
||||
|
||||
@@ -182,22 +182,100 @@ class TestTurboQuantConfig:
|
||||
|
||||
# ---- Boundary skip layers ----
|
||||
|
||||
@staticmethod
|
||||
def _dense_model_config(num_layers):
|
||||
from types import SimpleNamespace
|
||||
|
||||
return SimpleNamespace(
|
||||
is_hybrid=False,
|
||||
hf_text_config=SimpleNamespace(num_hidden_layers=num_layers),
|
||||
)
|
||||
|
||||
def test_boundary_skip_layers_basic(self):
|
||||
layers = TurboQuantConfig.get_boundary_skip_layers(32)
|
||||
mc = self._dense_model_config(32)
|
||||
layers = TurboQuantConfig.get_boundary_skip_layers(mc)
|
||||
assert layers == ["0", "1", "30", "31"]
|
||||
|
||||
def test_boundary_skip_layers_zero(self):
|
||||
assert TurboQuantConfig.get_boundary_skip_layers(32, 0) == []
|
||||
mc = self._dense_model_config(32)
|
||||
assert TurboQuantConfig.get_boundary_skip_layers(mc, 0) == []
|
||||
|
||||
def test_boundary_skip_layers_small_model(self):
|
||||
layers = TurboQuantConfig.get_boundary_skip_layers(4)
|
||||
mc = self._dense_model_config(4)
|
||||
layers = TurboQuantConfig.get_boundary_skip_layers(mc)
|
||||
assert layers == ["0", "1", "2", "3"]
|
||||
|
||||
def test_boundary_skip_layers_cap_at_half(self):
|
||||
layers = TurboQuantConfig.get_boundary_skip_layers(8, 10)
|
||||
mc = self._dense_model_config(8)
|
||||
layers = TurboQuantConfig.get_boundary_skip_layers(mc, 10)
|
||||
assert len(layers) == 8
|
||||
|
||||
|
||||
class TestHybridAttentionIndices:
|
||||
"""Regression tests for boundary protection on hybrid models.
|
||||
|
||||
Hybrid models (attention + Mamba / linear-attention) identify KV-carrying
|
||||
layers via layer_types / layers_block_type / attn_type_list. The helper
|
||||
must return the *global* layer indices of the full-attention layers so
|
||||
that kv_cache_dtype_skip_layers matches what extract_layer_index(prefix)
|
||||
reports on the Attention layers at runtime.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _fake_model_config(text_cfg=None, hf_cfg=None):
|
||||
from types import SimpleNamespace
|
||||
|
||||
return SimpleNamespace(
|
||||
hf_text_config=text_cfg if text_cfg is not None else SimpleNamespace(),
|
||||
hf_config=hf_cfg if hf_cfg is not None else SimpleNamespace(),
|
||||
)
|
||||
|
||||
def test_layer_types_full_attention(self):
|
||||
from vllm.model_executor.layers.quantization.turboquant.config import (
|
||||
_get_full_attention_layer_indices,
|
||||
)
|
||||
|
||||
cfg = type("C", (), {})()
|
||||
cfg.layer_types = [
|
||||
"linear_attention",
|
||||
"linear_attention",
|
||||
"full_attention",
|
||||
"linear_attention",
|
||||
"full_attention",
|
||||
"full_attention",
|
||||
]
|
||||
mc = self._fake_model_config(text_cfg=cfg)
|
||||
assert _get_full_attention_layer_indices(mc) == [2, 4, 5]
|
||||
|
||||
def test_layers_block_type_jamba(self):
|
||||
from vllm.model_executor.layers.quantization.turboquant.config import (
|
||||
_get_full_attention_layer_indices,
|
||||
)
|
||||
|
||||
cfg = type("C", (), {})()
|
||||
cfg.layers_block_type = ["mamba", "attention", "mamba", "attention"]
|
||||
mc = self._fake_model_config(text_cfg=cfg)
|
||||
assert _get_full_attention_layer_indices(mc) == [1, 3]
|
||||
|
||||
def test_attn_type_list_minimax(self):
|
||||
from vllm.model_executor.layers.quantization.turboquant.config import (
|
||||
_get_full_attention_layer_indices,
|
||||
)
|
||||
|
||||
hf = type("C", (), {})()
|
||||
hf.attn_type_list = [0, 1, 0, 1, 1]
|
||||
mc = self._fake_model_config(hf_cfg=hf)
|
||||
assert _get_full_attention_layer_indices(mc) == [1, 3, 4]
|
||||
|
||||
def test_no_hybrid_hints_returns_empty(self):
|
||||
from vllm.model_executor.layers.quantization.turboquant.config import (
|
||||
_get_full_attention_layer_indices,
|
||||
)
|
||||
|
||||
mc = self._fake_model_config()
|
||||
assert _get_full_attention_layer_indices(mc) == []
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Centroids tests (CPU-only)
|
||||
# ============================================================================
|
||||
|
||||
@@ -1215,8 +1215,6 @@ def test_scheduler_config_init():
|
||||
("facebook/opt-125m", 1, False, False),
|
||||
# Non-MoE model with DP>1 internal LB should need coordinator
|
||||
("facebook/opt-125m", 2, False, True),
|
||||
# Non-MoE model with DP>1 external LB should not need coordinator
|
||||
("facebook/opt-125m", 2, True, False),
|
||||
# MoE model with DP=1 should not need coordinator
|
||||
("mistralai/Mixtral-8x7B-Instruct-v0.1", 1, False, False),
|
||||
# MoE model with DP>1 internal LB should need both coordinator
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import os
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.triton_utils import jit_monitor
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_monitor():
|
||||
"""Reset global monitor state between tests."""
|
||||
jit_monitor._active = False
|
||||
yield
|
||||
jit_monitor._active = False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers — lightweight stand-ins for triton.knobs
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_fake_knobs(*, autotuning_print=False, jit_hook=None):
|
||||
"""Build a minimal fake ``triton.knobs`` namespace."""
|
||||
autotuning = SimpleNamespace(print=autotuning_print)
|
||||
runtime = SimpleNamespace(jit_post_compile_hook=jit_hook)
|
||||
return SimpleNamespace(autotuning=autotuning, runtime=runtime)
|
||||
|
||||
|
||||
def _patch_triton_knobs(fake_knobs):
|
||||
"""Context manager that makes ``from triton import knobs`` return *fake_knobs*."""
|
||||
fake_triton = SimpleNamespace(knobs=fake_knobs)
|
||||
return mock.patch.dict(sys.modules, {"triton": fake_triton})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Unit tests (no GPU required, triton is mocked)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestActivateBasic:
|
||||
def test_sets_active(self):
|
||||
assert not jit_monitor.is_active()
|
||||
with _patch_triton_knobs(_make_fake_knobs()):
|
||||
jit_monitor.activate()
|
||||
assert jit_monitor.is_active()
|
||||
|
||||
def test_idempotent(self):
|
||||
fake = _make_fake_knobs()
|
||||
with _patch_triton_knobs(fake):
|
||||
jit_monitor.activate()
|
||||
first_hook = fake.runtime.jit_post_compile_hook
|
||||
jit_monitor.activate()
|
||||
assert fake.runtime.jit_post_compile_hook is first_hook
|
||||
|
||||
def test_logs_info_on_activation(self):
|
||||
with (
|
||||
mock.patch.object(jit_monitor.logger, "info") as m,
|
||||
_patch_triton_knobs(_make_fake_knobs()),
|
||||
):
|
||||
jit_monitor.activate()
|
||||
m.assert_called_once()
|
||||
assert "Kernel JIT monitor activated" in m.call_args[0][0]
|
||||
|
||||
|
||||
class TestAutotuningPrint:
|
||||
def test_enables_autotuning_print(self):
|
||||
fake = _make_fake_knobs(autotuning_print=False)
|
||||
with _patch_triton_knobs(fake):
|
||||
jit_monitor.activate()
|
||||
assert fake.autotuning.print is True
|
||||
|
||||
def test_respects_user_opt_out(self):
|
||||
fake = _make_fake_knobs(autotuning_print=False)
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"TRITON_PRINT_AUTOTUNING": "0"}),
|
||||
_patch_triton_knobs(fake),
|
||||
):
|
||||
jit_monitor.activate()
|
||||
assert fake.autotuning.print is False
|
||||
|
||||
def test_noop_when_user_already_enabled(self):
|
||||
fake = _make_fake_knobs(autotuning_print=True)
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"TRITON_PRINT_AUTOTUNING": "1"}),
|
||||
_patch_triton_knobs(fake),
|
||||
):
|
||||
jit_monitor.activate()
|
||||
assert fake.autotuning.print is True
|
||||
|
||||
|
||||
class TestJitHook:
|
||||
def test_hook_registered(self):
|
||||
fake = _make_fake_knobs()
|
||||
assert fake.runtime.jit_post_compile_hook is None
|
||||
with _patch_triton_knobs(fake):
|
||||
jit_monitor.activate()
|
||||
assert fake.runtime.jit_post_compile_hook is not None
|
||||
|
||||
def test_hook_logs_warning(self):
|
||||
fake = _make_fake_knobs()
|
||||
with _patch_triton_knobs(fake):
|
||||
jit_monitor.activate()
|
||||
|
||||
hook = fake.runtime.jit_post_compile_hook
|
||||
mock_fn = SimpleNamespace(name="test_kernel")
|
||||
|
||||
with mock.patch.object(jit_monitor.logger, "warning") as m:
|
||||
hook(
|
||||
key="some_key",
|
||||
repr="some_repr",
|
||||
fn=mock_fn,
|
||||
compile=lambda: None,
|
||||
is_manual_warmup=False,
|
||||
already_compiled=False,
|
||||
)
|
||||
|
||||
m.assert_called_once()
|
||||
msg = m.call_args[0][0] % m.call_args[0][1:]
|
||||
assert "Triton kernel JIT compilation during inference" in msg
|
||||
assert "test_kernel" in msg
|
||||
|
||||
def test_hook_chains_existing_hook(self):
|
||||
existing = mock.MagicMock(return_value="existing_result")
|
||||
fake = _make_fake_knobs(jit_hook=existing)
|
||||
with _patch_triton_knobs(fake):
|
||||
jit_monitor.activate()
|
||||
|
||||
hook = fake.runtime.jit_post_compile_hook
|
||||
mock_fn = SimpleNamespace(name="chained_kernel")
|
||||
kwargs = dict(
|
||||
key="k",
|
||||
repr="r",
|
||||
fn=mock_fn,
|
||||
compile=lambda: None,
|
||||
is_manual_warmup=False,
|
||||
already_compiled=False,
|
||||
)
|
||||
result = hook(**kwargs)
|
||||
|
||||
existing.assert_called_once()
|
||||
assert result == "existing_result"
|
||||
|
||||
def test_hook_works_without_existing_hook(self):
|
||||
fake = _make_fake_knobs(jit_hook=None)
|
||||
with _patch_triton_knobs(fake):
|
||||
jit_monitor.activate()
|
||||
|
||||
hook = fake.runtime.jit_post_compile_hook
|
||||
mock_fn = SimpleNamespace(name="solo_kernel")
|
||||
result = hook(
|
||||
key="k",
|
||||
repr="r",
|
||||
fn=mock_fn,
|
||||
compile=lambda: None,
|
||||
is_manual_warmup=False,
|
||||
already_compiled=False,
|
||||
)
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestNoTritonFallback:
|
||||
def test_activate_without_triton(self):
|
||||
with mock.patch.object(jit_monitor, "HAS_TRITON", False):
|
||||
jit_monitor.activate()
|
||||
assert jit_monitor.is_active()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Integration tests (real Triton + GPU)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
try:
|
||||
import torch
|
||||
|
||||
_HAS_CUDA = torch.cuda.is_available()
|
||||
except ImportError:
|
||||
_HAS_CUDA = False
|
||||
|
||||
try:
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
_HAS_TRITON = True
|
||||
except ImportError:
|
||||
_HAS_TRITON = False
|
||||
|
||||
_skip_no_gpu = pytest.mark.skipif(
|
||||
not (_HAS_CUDA and _HAS_TRITON),
|
||||
reason="Requires CUDA GPU and Triton",
|
||||
)
|
||||
|
||||
|
||||
if _HAS_TRITON:
|
||||
|
||||
@triton.jit
|
||||
def _add_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr):
|
||||
pid = tl.program_id(0)
|
||||
offs = pid * BLOCK + tl.arange(0, BLOCK)
|
||||
mask = offs < n
|
||||
x = tl.load(x_ptr + offs, mask=mask)
|
||||
y = tl.load(y_ptr + offs, mask=mask)
|
||||
tl.store(out_ptr + offs, x + y, mask=mask)
|
||||
|
||||
|
||||
def _run_add_kernel(n: int, block: int = 256) -> None:
|
||||
"""Launch ``_add_kernel`` with vectors of length *n*."""
|
||||
x = torch.randn(n, device="cuda")
|
||||
y = torch.randn(n, device="cuda")
|
||||
out = torch.empty(n, device="cuda")
|
||||
grid = ((n + block - 1) // block,)
|
||||
_add_kernel[grid](x, y, out, n, BLOCK=block)
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
|
||||
@_skip_no_gpu
|
||||
class TestTritonJitHookIntegration:
|
||||
"""End-to-end: real Triton kernel, real GPU, real hook."""
|
||||
|
||||
def test_no_warning_on_cached_shape(self):
|
||||
_run_add_kernel(1024)
|
||||
|
||||
jit_monitor.activate()
|
||||
with mock.patch.object(jit_monitor.logger, "warning") as w:
|
||||
_run_add_kernel(1024)
|
||||
w.assert_not_called()
|
||||
|
||||
def test_warning_on_new_constexpr(self):
|
||||
_run_add_kernel(1024, block=256)
|
||||
|
||||
jit_monitor.activate()
|
||||
with mock.patch.object(jit_monitor.logger, "warning") as w:
|
||||
# Different BLOCK (a tl.constexpr) forces recompilation.
|
||||
_run_add_kernel(1024, block=512)
|
||||
w.assert_called()
|
||||
msg = w.call_args[0][0] % w.call_args[0][1:]
|
||||
assert "_add_kernel" in msg
|
||||
@@ -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(
|
||||
|
||||
@@ -30,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
|
||||
@@ -621,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] = (
|
||||
@@ -785,7 +799,9 @@ def test_backend_correctness(
|
||||
assert kv_lora_rank + qk_rope_head_dim == head_size, (
|
||||
f"MLA dimensions don't match: {total_head_size} != {head_size}"
|
||||
)
|
||||
scale = 1.0 / (total_head_size**0.5)
|
||||
decode_scale = 1.0 / (total_head_size**0.5)
|
||||
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 = [], [], []
|
||||
@@ -902,7 +918,7 @@ def test_backend_correctness(
|
||||
v_sdpa_in = v_mqa.unsqueeze(0).transpose(1, 2)
|
||||
|
||||
sdpa_out_i_decode = torch.nn.functional.scaled_dot_product_attention(
|
||||
q_sdpa_in, k_sdpa_in, v_sdpa_in, attn_mask=attn_mask, scale=scale
|
||||
q_sdpa_in, k_sdpa_in, v_sdpa_in, attn_mask=attn_mask, scale=decode_scale
|
||||
)
|
||||
sdpa_out_i_decode = sdpa_out_i_decode.transpose(1, 2).squeeze(
|
||||
0
|
||||
@@ -938,7 +954,7 @@ def test_backend_correctness(
|
||||
|
||||
# Single attention call with custom mask
|
||||
sdpa_out_i_prefill = torch.nn.functional.scaled_dot_product_attention(
|
||||
q_sdpa_in, k_sdpa_in, v_sdpa_in, attn_mask=attn_mask, scale=scale
|
||||
q_sdpa_in, k_sdpa_in, v_sdpa_in, attn_mask=attn_mask, scale=prefill_scale
|
||||
)
|
||||
sdpa_out_i_prefill = sdpa_out_i_prefill.transpose(1, 2).squeeze(0)
|
||||
sdpa_out_i_prefill = sdpa_out_i_prefill.flatten(start_dim=-2)
|
||||
|
||||
@@ -14,7 +14,7 @@ import requests
|
||||
from tests.utils import RemoteOpenAIServer
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
MODEL_NAME = "ibm-research/PowerMoE-3b"
|
||||
MODEL_NAME = os.getenv("MODEL_NAME", "ibm-research/PowerMoE-3b")
|
||||
|
||||
# Number of data parallel ranks for external LB testing
|
||||
DP_SIZE = int(os.getenv("DP_SIZE", "2"))
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import threading
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector import (
|
||||
MooncakeConnector,
|
||||
MooncakeConnectorWorker,
|
||||
SendBlockMeta,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.stats import (
|
||||
MooncakeKVConnectorStats,
|
||||
)
|
||||
|
||||
|
||||
def test_is_empty_on_fresh_stats():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
assert stats.is_empty()
|
||||
assert stats.num_successful_transfers == 0
|
||||
|
||||
|
||||
def test_record_transfer_and_reduce():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
# 1 MB transfer in 1 ms -> 1000 MB/s throughput
|
||||
stats.record_transfer(duration_s=0.001, total_bytes=1 * 2**20, num_descs=4)
|
||||
# 2 MB transfer in 2 ms
|
||||
stats.record_transfer(duration_s=0.002, total_bytes=2 * 2**20, num_descs=6)
|
||||
assert not stats.is_empty()
|
||||
assert stats.num_successful_transfers == 2
|
||||
|
||||
reduced = stats.reduce()
|
||||
assert reduced["Num successful transfers"] == 2
|
||||
# avg = (1 + 2) / 2 = 1.5 ms
|
||||
assert reduced["Avg xfer time (ms)"] == 1.5
|
||||
assert reduced["Avg MB per transfer"] == 1.5
|
||||
# 3 MB total / 3 ms total = 1000 MB/s
|
||||
assert reduced["Throughput (MB/s)"] == 1000.0
|
||||
assert reduced["Avg number of descriptors"] == 5.0
|
||||
assert reduced["Num failed transfers"] == 0
|
||||
assert reduced["Num failed recvs"] == 0
|
||||
assert reduced["Num KV expired reqs"] == 0
|
||||
|
||||
|
||||
def test_record_failures_keeps_stats_non_empty():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
stats.record_failed_transfer()
|
||||
stats.record_failed_recv()
|
||||
stats.record_kv_expired_req()
|
||||
assert not stats.is_empty()
|
||||
|
||||
reduced = stats.reduce()
|
||||
# No successful transfers -> latency/throughput all zero, but failure
|
||||
# counters still surface.
|
||||
assert reduced["Num successful transfers"] == 0
|
||||
assert reduced["Num failed transfers"] == 1
|
||||
assert reduced["Num failed recvs"] == 1
|
||||
assert reduced["Num KV expired reqs"] == 1
|
||||
|
||||
|
||||
def test_aggregate_sums_observations():
|
||||
a = MooncakeKVConnectorStats()
|
||||
b = MooncakeKVConnectorStats()
|
||||
a.record_transfer(duration_s=0.001, total_bytes=1 * 2**20, num_descs=1)
|
||||
b.record_transfer(duration_s=0.002, total_bytes=2 * 2**20, num_descs=2)
|
||||
b.record_failed_transfer()
|
||||
|
||||
a.aggregate(b)
|
||||
|
||||
assert a.num_successful_transfers == 2
|
||||
reduced = a.reduce()
|
||||
assert reduced["Num successful transfers"] == 2
|
||||
assert reduced["Num failed transfers"] == 1
|
||||
|
||||
|
||||
def test_aggregate_with_empty_other_is_noop():
|
||||
a = MooncakeKVConnectorStats()
|
||||
a.record_transfer(duration_s=0.001, total_bytes=1, num_descs=1)
|
||||
b = MooncakeKVConnectorStats()
|
||||
|
||||
a.aggregate(b)
|
||||
|
||||
assert a.num_successful_transfers == 1
|
||||
|
||||
|
||||
def test_getstate_drops_lock_and_setstate_recreates_it():
|
||||
# KVConnectorStats subclasses must be picklable (worker→scheduler IPC),
|
||||
# but threading.Lock isn't — so __getstate__ strips it and __setstate__
|
||||
# rebuilds a fresh per-process lock.
|
||||
original = MooncakeKVConnectorStats()
|
||||
original.record_transfer(duration_s=0.01, total_bytes=2048, num_descs=3)
|
||||
|
||||
state = original.__getstate__()
|
||||
assert "_lock" not in state
|
||||
|
||||
rebuilt = MooncakeKVConnectorStats.__new__(MooncakeKVConnectorStats)
|
||||
rebuilt.__setstate__(state)
|
||||
assert rebuilt.data == original.data
|
||||
# Lock works on the receiver side.
|
||||
rebuilt.record_transfer(duration_s=0.02, total_bytes=4096, num_descs=5)
|
||||
assert rebuilt.num_successful_transfers == 2
|
||||
|
||||
|
||||
def test_concurrent_writers_keep_row_lengths_aligned():
|
||||
# Multiple writers + a snapshot reader must never produce a snapshot
|
||||
# with mismatched column lengths — reduce()'s
|
||||
# len(descs) == num_successful_transfers assertion would fire.
|
||||
stats = MooncakeKVConnectorStats()
|
||||
stop = threading.Event()
|
||||
writer_count = 4
|
||||
snapshots: list[MooncakeKVConnectorStats] = []
|
||||
|
||||
def writer():
|
||||
i = 0
|
||||
while not stop.is_set():
|
||||
stats.record_transfer(
|
||||
duration_s=0.001 + i * 1e-9,
|
||||
total_bytes=1024 + i,
|
||||
num_descs=1 + (i % 8),
|
||||
)
|
||||
i += 1
|
||||
|
||||
def snapper():
|
||||
while not stop.is_set():
|
||||
snap = stats.clone_and_reset()
|
||||
if not snap.is_empty():
|
||||
# Force the same path the logger walks; reduce() will
|
||||
# blow up on torn rows via its internal assert.
|
||||
snap.reduce()
|
||||
snapshots.append(snap)
|
||||
|
||||
threads = [threading.Thread(target=writer) for _ in range(writer_count)]
|
||||
snapshotter = threading.Thread(target=snapper)
|
||||
for t in threads:
|
||||
t.start()
|
||||
snapshotter.start()
|
||||
# Short fixed window — long enough to interleave thousands of ops.
|
||||
threading.Event().wait(0.2)
|
||||
stop.set()
|
||||
for t in threads:
|
||||
t.join()
|
||||
snapshotter.join()
|
||||
|
||||
# Final drain so we don't lose the in-flight tail.
|
||||
final = stats.clone_and_reset()
|
||||
if not final.is_empty():
|
||||
final.reduce()
|
||||
snapshots.append(final)
|
||||
|
||||
# Every snapshot's columns must have identical lengths (the invariant
|
||||
# the lock protects), and the union must contain at least one row.
|
||||
total_rows = 0
|
||||
for snap in snapshots:
|
||||
n = len(snap.data["transfer_duration"])
|
||||
assert len(snap.data["bytes_transferred"]) == n
|
||||
assert len(snap.data["num_descriptors"]) == n
|
||||
total_rows += n
|
||||
assert total_rows > 0
|
||||
|
||||
|
||||
def test_clone_and_reset_hands_off_old_data():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
stats.record_transfer(duration_s=0.001, total_bytes=1, num_descs=1)
|
||||
stats.record_failed_recv()
|
||||
|
||||
snapshot = stats.clone_and_reset()
|
||||
|
||||
assert snapshot.num_successful_transfers == 1
|
||||
assert not snapshot.is_empty()
|
||||
# Original is now empty.
|
||||
assert stats.is_empty()
|
||||
assert stats.num_successful_transfers == 0
|
||||
# Recording on the original does not mutate the snapshot.
|
||||
stats.record_transfer(duration_s=0.005, total_bytes=2, num_descs=2)
|
||||
assert snapshot.num_successful_transfers == 1
|
||||
|
||||
|
||||
def test_build_kv_connector_stats_none_returns_empty_instance():
|
||||
out = MooncakeConnector.build_kv_connector_stats()
|
||||
assert isinstance(out, MooncakeKVConnectorStats)
|
||||
assert out.is_empty()
|
||||
|
||||
|
||||
def test_build_kv_connector_stats_with_data_round_trips():
|
||||
original = MooncakeKVConnectorStats()
|
||||
original.record_transfer(duration_s=0.01, total_bytes=1024, num_descs=3)
|
||||
original.record_failed_transfer()
|
||||
|
||||
# Serialized form is the .data dict; build should reconstruct an instance
|
||||
# that behaves the same.
|
||||
rebuilt = MooncakeConnector.build_kv_connector_stats(data=original.data)
|
||||
|
||||
assert isinstance(rebuilt, MooncakeKVConnectorStats)
|
||||
assert rebuilt.num_successful_transfers == 1
|
||||
assert rebuilt.reduce()["Num failed transfers"] == 1
|
||||
|
||||
|
||||
def _bare_worker() -> MooncakeConnectorWorker:
|
||||
"""Construct a MooncakeConnectorWorker skipping __init__ (full init requires
|
||||
a live TransferEngine). Only the attributes touched by the methods under
|
||||
test are populated; role flags and async_zmq_ctx keep __del__'s shutdown
|
||||
path a no-op."""
|
||||
worker = MooncakeConnectorWorker.__new__(MooncakeConnectorWorker)
|
||||
worker.xfer_stats = MooncakeKVConnectorStats()
|
||||
worker.engine = MagicMock()
|
||||
worker.async_zmq_ctx = MagicMock()
|
||||
worker.is_kv_consumer = True
|
||||
worker.is_kv_producer = True
|
||||
return worker
|
||||
|
||||
|
||||
def test_send_blocks_records_success():
|
||||
worker = _bare_worker()
|
||||
worker.engine.batch_transfer_sync_write.return_value = 0
|
||||
|
||||
ret = worker._send_blocks(
|
||||
"host:1234",
|
||||
src_ptrs=[0x1000, 0x2000],
|
||||
dst_ptrs=[0x3000, 0x4000],
|
||||
lengths=[1024, 2048],
|
||||
)
|
||||
|
||||
assert ret == 0
|
||||
assert worker.xfer_stats.num_successful_transfers == 1
|
||||
data = worker.xfer_stats.data
|
||||
assert data["bytes_transferred"] == [1024 + 2048]
|
||||
assert data["num_descriptors"] == [2]
|
||||
assert data["num_failed_transfers"] == []
|
||||
|
||||
|
||||
def test_send_blocks_records_failure():
|
||||
worker = _bare_worker()
|
||||
worker.engine.batch_transfer_sync_write.return_value = 1 # non-zero = fail
|
||||
|
||||
ret = worker._send_blocks("host:1234", [0x1000], [0x2000], [4096])
|
||||
|
||||
assert ret == 1
|
||||
assert worker.xfer_stats.num_successful_transfers == 0
|
||||
assert worker.xfer_stats.data["num_failed_transfers"] == [1]
|
||||
|
||||
|
||||
def test_get_kv_connector_stats_returns_none_when_empty():
|
||||
worker = _bare_worker()
|
||||
|
||||
assert worker.get_kv_connector_stats() is None
|
||||
|
||||
|
||||
def test_get_kv_connector_stats_returns_and_resets():
|
||||
worker = _bare_worker()
|
||||
worker.engine.batch_transfer_sync_write.return_value = 0
|
||||
worker._send_blocks("host:1234", [0x1000], [0x2000], [4096])
|
||||
|
||||
snapshot = worker.get_kv_connector_stats()
|
||||
assert isinstance(snapshot, MooncakeKVConnectorStats)
|
||||
assert snapshot.num_successful_transfers == 1
|
||||
|
||||
# Second call returns None because the worker's stats were reset.
|
||||
assert worker.get_kv_connector_stats() is None
|
||||
|
||||
|
||||
def test_expired_request_bumps_counter():
|
||||
import asyncio
|
||||
|
||||
worker = _bare_worker()
|
||||
worker.reqs_need_send = {
|
||||
"tid1": SendBlockMeta(
|
||||
p_req_id="req1",
|
||||
transfer_id="tid1",
|
||||
local_block_ids=[0, 1],
|
||||
ready=asyncio.Event(),
|
||||
expire_time=-1.0, # Already expired.
|
||||
sending=0,
|
||||
),
|
||||
}
|
||||
worker.finished_sending_reqs = set()
|
||||
|
||||
asyncio.run(worker.fetch_finished_sending_reqs())
|
||||
|
||||
assert worker.xfer_stats.data["num_kv_expired_reqs"] == [1]
|
||||
# Expired transfer also cleaned out of reqs_need_send.
|
||||
assert "tid1" not in worker.reqs_need_send
|
||||
@@ -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
|
||||
|
||||
@@ -185,6 +185,35 @@ def _xpu_ops_deepseek_scaling_rope_fake(
|
||||
return query, key
|
||||
|
||||
|
||||
def _topk_topp_sample_impl(
|
||||
random_sampled: torch.Tensor,
|
||||
logits_to_return: torch.Tensor | None,
|
||||
logits: torch.Tensor,
|
||||
k: torch.Tensor | None,
|
||||
p: torch.Tensor | None,
|
||||
logprobs_mode: str,
|
||||
seeds: torch.Tensor | None,
|
||||
lambda_: float = 1.0,
|
||||
) -> None:
|
||||
torch.ops._xpu_C.topk_topp_sampler(
|
||||
random_sampled, logits_to_return, logits, k, p, logprobs_mode, seeds, lambda_
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def _topk_topp_sample_fake(
|
||||
random_sampled: torch.Tensor,
|
||||
logits_to_return: torch.Tensor | None,
|
||||
logits: torch.Tensor,
|
||||
k: torch.Tensor | None,
|
||||
p: torch.Tensor | None,
|
||||
logprobs_mode: str,
|
||||
seeds: torch.Tensor | None,
|
||||
lambda_: float = 1.0,
|
||||
) -> None:
|
||||
return
|
||||
|
||||
|
||||
def _xpu_mxfp8_quantize_impl(
|
||||
x: torch.Tensor, dtype: torch.dtype | None = None
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
@@ -691,6 +720,12 @@ class xpu_ops:
|
||||
fake_impl=_gdn_attention_core_xpu_fake,
|
||||
)
|
||||
|
||||
direct_register_custom_op(
|
||||
op_name="xpu_topk_topp_sampler",
|
||||
op_func=_topk_topp_sample_impl,
|
||||
fake_impl=_topk_topp_sample_fake,
|
||||
)
|
||||
|
||||
_OPS_REGISTERED = True
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ MoEBackend = Literal[
|
||||
"flashinfer_cutedsl",
|
||||
"marlin",
|
||||
"humming",
|
||||
"triton_unfused",
|
||||
"aiter",
|
||||
"emulation",
|
||||
]
|
||||
@@ -150,6 +151,7 @@ class KernelConfig:
|
||||
- "flashinfer_cutedsl": Use FlashInfer with CuteDSL kernels (FP4 only)
|
||||
- "marlin": Use Marlin kernels (weight-only quantization)
|
||||
- "humming": Use Humming Mixed Precision kernels
|
||||
- "triton_unfused": Use Triton unfused MoE kernels
|
||||
- "aiter": Use AMD AITer kernels (ROCm only)
|
||||
- "emulation": use BF16/FP16 GEMM, dequantizing weights and
|
||||
running QDQ on activations.
|
||||
|
||||
@@ -135,8 +135,10 @@ class ParallelConfig:
|
||||
data_parallel_external_lb: bool = False
|
||||
"""Whether to use "external" DP LB mode. Applies only to online serving
|
||||
and when data_parallel_size > 0. This is useful for a "one-pod-per-rank"
|
||||
wide-EP setup in Kubernetes. Set implicitly when --data-parallel-rank
|
||||
is provided explicitly to vllm serve."""
|
||||
wide-EP setup in Kubernetes. Supported only for MoE deployments; non-MoE
|
||||
models should use independent vLLM instances without --data-parallel-*
|
||||
arguments. Set implicitly when --data-parallel-rank is provided explicitly
|
||||
to vllm serve."""
|
||||
data_parallel_hybrid_lb: bool = False
|
||||
"""Whether to use "hybrid" DP LB mode. Applies only to online serving
|
||||
and when data_parallel_size > 0. Enables running an AsyncLLM
|
||||
|
||||
@@ -626,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,
|
||||
@@ -837,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(
|
||||
|
||||
@@ -31,10 +31,14 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
|
||||
KVConnectorRole,
|
||||
SupportsHMA,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorStats
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_utils import (
|
||||
MooncakeBootstrapServer,
|
||||
RegisterWorkerPayload,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.stats import (
|
||||
MooncakeKVConnectorStats,
|
||||
)
|
||||
from vllm.distributed.parallel_state import (
|
||||
get_pp_group,
|
||||
get_tensor_model_parallel_rank,
|
||||
@@ -457,6 +461,25 @@ class MooncakeConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
def wait_for_save(self):
|
||||
pass
|
||||
|
||||
def get_kv_connector_stats(self) -> KVConnectorStats | None:
|
||||
"""Return worker-local transfer stats since the last call.
|
||||
|
||||
Note the P/D asymmetry: because Mooncake is P-push (P calls
|
||||
batch_transfer_sync_write), P records successful transfer latency,
|
||||
bytes, and descriptor counts, while D only records failures
|
||||
(recv/ZMQ errors). Aggregated NIXL-style dashboards will find
|
||||
successful-transfer metrics on the P worker, not D.
|
||||
"""
|
||||
if self.connector_worker is None:
|
||||
return None
|
||||
return self.connector_worker.get_kv_connector_stats()
|
||||
|
||||
@classmethod
|
||||
def build_kv_connector_stats(
|
||||
cls, data: dict[str, Any] | None = None
|
||||
) -> KVConnectorStats | None:
|
||||
return MooncakeKVConnectorStats(data=data or {})
|
||||
|
||||
|
||||
class MooncakeConnectorScheduler:
|
||||
"""Implementation of Scheduler side methods"""
|
||||
@@ -816,6 +839,8 @@ class MooncakeConnectorWorker:
|
||||
self.finished_sending_reqs: set[ReqId] = set()
|
||||
self.finished_recving_reqs: set[ReqId] = set()
|
||||
|
||||
self.xfer_stats = MooncakeKVConnectorStats()
|
||||
|
||||
self.block_size = vllm_config.cache_config.block_size
|
||||
self.model_config = vllm_config.model_config
|
||||
self.cache_config = vllm_config.cache_config
|
||||
@@ -1340,11 +1365,23 @@ class MooncakeConnectorWorker:
|
||||
ret_value = self.engine.batch_transfer_sync_write(
|
||||
remote_session, src_ptrs, dst_ptrs, lengths
|
||||
)
|
||||
duration = time.perf_counter() - start_time
|
||||
if ret_value == 0:
|
||||
logger.debug(
|
||||
"Sending to %s done, took %s",
|
||||
self.xfer_stats.record_transfer(
|
||||
duration_s=duration,
|
||||
total_bytes=sum(lengths),
|
||||
num_descs=len(src_ptrs),
|
||||
)
|
||||
logger.debug("Sending to %s done, took %s", remote_session, duration)
|
||||
else:
|
||||
self.xfer_stats.record_failed_transfer()
|
||||
logger.warning(
|
||||
"Sending to %s failed (ret=%s) after %s (%d descriptors, %d bytes)",
|
||||
remote_session,
|
||||
time.perf_counter() - start_time,
|
||||
ret_value,
|
||||
duration,
|
||||
len(src_ptrs),
|
||||
sum(lengths),
|
||||
)
|
||||
return ret_value
|
||||
|
||||
@@ -1445,6 +1482,7 @@ class MooncakeConnectorWorker:
|
||||
send_meta.p_req_id,
|
||||
envs.VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
self.xfer_stats.record_kv_expired_req()
|
||||
finished_sending_reqs.add(send_meta.p_req_id)
|
||||
expired_transfer_id.append(transfer_id)
|
||||
|
||||
@@ -1485,6 +1523,13 @@ class MooncakeConnectorWorker:
|
||||
|
||||
return finished_sending_reqs or None, finished_recving_reqs or None
|
||||
|
||||
def get_kv_connector_stats(self) -> KVConnectorStats | None:
|
||||
"""Return transfer stats collected since the last call, or None
|
||||
if nothing has been recorded in this interval."""
|
||||
if self.xfer_stats.is_empty():
|
||||
return None
|
||||
return self.xfer_stats.clone_and_reset()
|
||||
|
||||
async def receive_kv_from_single_worker(
|
||||
self,
|
||||
worker_addr: str,
|
||||
@@ -1531,6 +1576,7 @@ class MooncakeConnectorWorker:
|
||||
req_ids,
|
||||
response.err_msg,
|
||||
)
|
||||
self.xfer_stats.record_failed_recv()
|
||||
return
|
||||
self.process_pulling_result(response, pull_metas)
|
||||
if response.status == MooncakeXferResponseStatus.FINISH:
|
||||
@@ -1539,6 +1585,7 @@ class MooncakeConnectorWorker:
|
||||
logger.debug("ZMQ context terminated, exiting Mooncake receiver thread.")
|
||||
except Exception as e:
|
||||
logger.error("MooncakeXferMetadata transfer failed for %s: %s", req_ids, e)
|
||||
self.xfer_stats.record_failed_recv()
|
||||
return
|
||||
|
||||
def process_pulling_result(
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Stats container for the Mooncake connector."""
|
||||
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import (
|
||||
KVConnectorStats,
|
||||
)
|
||||
|
||||
# TODO(mooncake-stats): add MooncakePromMetrics (mirror NixlPromMetrics)
|
||||
# and wire it via MooncakeConnector.build_prom_metrics in a follow-up PR.
|
||||
|
||||
|
||||
@dataclass
|
||||
class MooncakeKVConnectorStats(KVConnectorStats):
|
||||
"""Container for Mooncake KV transfer performance metrics.
|
||||
|
||||
`_lock` serializes record_* against clone_and_reset so each row's
|
||||
appends are atomic and column lengths stay aligned. Writers run on
|
||||
the sender pool / receiver loop / sender loop; reader runs on the
|
||||
main worker thread.
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
self._lock = threading.Lock()
|
||||
if not self.data:
|
||||
self.reset()
|
||||
|
||||
# threading.Lock is not picklable; strip it from the wire form and
|
||||
# rebuild a fresh per-process lock on the receiver side.
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
state.pop("_lock", None)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def reset(self):
|
||||
self.data: dict[str, list[float | int]] = {
|
||||
"transfer_duration": [],
|
||||
"bytes_transferred": [],
|
||||
"num_descriptors": [],
|
||||
"num_failed_transfers": [],
|
||||
"num_failed_recvs": [],
|
||||
"num_kv_expired_reqs": [],
|
||||
}
|
||||
|
||||
def record_transfer(self, duration_s: float, total_bytes: int, num_descs: int):
|
||||
with self._lock:
|
||||
self.data["transfer_duration"].append(duration_s)
|
||||
self.data["bytes_transferred"].append(total_bytes)
|
||||
self.data["num_descriptors"].append(num_descs)
|
||||
|
||||
# Failure counters store a list of 1s so a future Prom counter can iterate
|
||||
# with .inc(list_item), mirroring NIXL's NixlPromMetrics.observe.
|
||||
def record_failed_transfer(self):
|
||||
with self._lock:
|
||||
self.data["num_failed_transfers"].append(1)
|
||||
|
||||
def record_failed_recv(self):
|
||||
with self._lock:
|
||||
self.data["num_failed_recvs"].append(1)
|
||||
|
||||
def record_kv_expired_req(self):
|
||||
with self._lock:
|
||||
self.data["num_kv_expired_reqs"].append(1)
|
||||
|
||||
def clone_and_reset(self) -> "MooncakeKVConnectorStats":
|
||||
# Copy lists under the lock for length alignment; return a fresh
|
||||
# instance so the snapshot has its own _lock.
|
||||
with self._lock:
|
||||
snapshot_data: dict[str, list[float | int]] = {
|
||||
k: list(v) for k, v in self.data.items()
|
||||
}
|
||||
self.reset()
|
||||
return MooncakeKVConnectorStats(data=snapshot_data)
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return (
|
||||
self.num_successful_transfers == 0
|
||||
and len(self.data["num_failed_transfers"]) == 0
|
||||
and len(self.data["num_failed_recvs"]) == 0
|
||||
and len(self.data["num_kv_expired_reqs"]) == 0
|
||||
)
|
||||
|
||||
def aggregate(self, other: KVConnectorStats) -> KVConnectorStats:
|
||||
if not other.is_empty():
|
||||
for k, v in other.data.items():
|
||||
accumulator = self.data[k]
|
||||
assert isinstance(accumulator, list)
|
||||
accumulator.extend(v)
|
||||
return self
|
||||
|
||||
def reduce(self) -> dict[str, int | float]:
|
||||
num_failed_transfers = len(self.data["num_failed_transfers"])
|
||||
num_failed_recvs = len(self.data["num_failed_recvs"])
|
||||
num_kv_expired_reqs = len(self.data["num_kv_expired_reqs"])
|
||||
|
||||
if self.num_successful_transfers == 0:
|
||||
return {
|
||||
"Num successful transfers": 0,
|
||||
"Avg xfer time (ms)": 0,
|
||||
"P90 xfer time (ms)": 0,
|
||||
"Avg MB per transfer": 0,
|
||||
"Throughput (MB/s)": 0,
|
||||
"Avg number of descriptors": 0,
|
||||
"Num failed transfers": num_failed_transfers,
|
||||
"Num failed recvs": num_failed_recvs,
|
||||
"Num KV expired reqs": num_kv_expired_reqs,
|
||||
}
|
||||
|
||||
xfer_time = np.asarray(self.data["transfer_duration"])
|
||||
mb = np.asarray(self.data["bytes_transferred"]) / 2**20
|
||||
descs = np.asarray(self.data["num_descriptors"], dtype=np.uint32)
|
||||
n = len(descs)
|
||||
assert n == self.num_successful_transfers
|
||||
|
||||
total_mb = mb.sum()
|
||||
avg_mb = total_mb / n
|
||||
total_time_seconds = xfer_time.sum()
|
||||
throughput_mb_s = (
|
||||
total_mb / total_time_seconds if total_time_seconds > 0 else 0.0
|
||||
)
|
||||
|
||||
return {
|
||||
"Num successful transfers": n,
|
||||
"Avg xfer time (ms)": round(xfer_time.mean() * 1e3, 3),
|
||||
"P90 xfer time (ms)": round(np.percentile(xfer_time, 90).item() * 1e3, 3),
|
||||
"Avg MB per transfer": round(avg_mb, 3),
|
||||
"Throughput (MB/s)": round(throughput_mb_s, 3),
|
||||
"Avg number of descriptors": round(descs.mean(), 1),
|
||||
"Num failed transfers": num_failed_transfers,
|
||||
"Num failed recvs": num_failed_recvs,
|
||||
"Num KV expired reqs": num_kv_expired_reqs,
|
||||
}
|
||||
|
||||
@property
|
||||
def num_successful_transfers(self) -> int:
|
||||
return len(self.data["transfer_duration"])
|
||||
+16
-18
@@ -962,7 +962,9 @@ class EngineArgs:
|
||||
"-dpn",
|
||||
type=int,
|
||||
help="Data parallel rank of this instance. "
|
||||
"When set, enables external load balancer mode.",
|
||||
"When set, enables external load balancer mode for MoE "
|
||||
"data-parallel deployments. Unsupported for non-MoE models; "
|
||||
"launch independent vLLM instances instead.",
|
||||
)
|
||||
parallel_group.add_argument(
|
||||
"--data-parallel-start-rank",
|
||||
@@ -1697,29 +1699,15 @@ class EngineArgs:
|
||||
kv_offloading_backend=self.kv_offloading_backend,
|
||||
)
|
||||
|
||||
# TurboQuant: auto-skip first/last 2 layers (boundary protection).
|
||||
# These layers are most sensitive to quantization error.
|
||||
# Users can add extra layers via --kv-cache-dtype-skip-layers.
|
||||
if resolved_cache_dtype.startswith("turboquant_"):
|
||||
if model_config.is_hybrid:
|
||||
raise NotImplementedError(
|
||||
"TurboQuant KV cache is not supported for hybrid "
|
||||
"(attention + Mamba) models. Boundary layer protection "
|
||||
"requires uniform attention layers."
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.turboquant.config import (
|
||||
TurboQuantConfig,
|
||||
)
|
||||
|
||||
num_layers = model_config.hf_text_config.num_hidden_layers
|
||||
boundary = TurboQuantConfig.get_boundary_skip_layers(num_layers)
|
||||
boundary = TurboQuantConfig.get_boundary_skip_layers(model_config)
|
||||
existing = set(cache_config.kv_cache_dtype_skip_layers)
|
||||
merged = sorted(existing | set(boundary), key=lambda x: int(x))
|
||||
cache_config.kv_cache_dtype_skip_layers = merged
|
||||
logger.info(
|
||||
"TQ: skipping layers %s for boundary protection (num_layers=%d)",
|
||||
merged,
|
||||
num_layers,
|
||||
cache_config.kv_cache_dtype_skip_layers = sorted(
|
||||
existing | set(boundary), key=int
|
||||
)
|
||||
|
||||
ray_runtime_env = None
|
||||
@@ -1793,6 +1781,16 @@ class EngineArgs:
|
||||
data_parallel_external_lb = (
|
||||
self.data_parallel_external_lb or self.data_parallel_rank is not None
|
||||
)
|
||||
if (
|
||||
self.data_parallel_size > 1
|
||||
and data_parallel_external_lb
|
||||
and not model_config.is_moe
|
||||
):
|
||||
raise ValueError(
|
||||
"Non-MoE models do not support external data parallel mode. "
|
||||
"For external load balancing, launch independent vLLM "
|
||||
"instances without --data-parallel-* arguments."
|
||||
)
|
||||
# Local DP rank = 1, use pure-external LB.
|
||||
if data_parallel_external_lb:
|
||||
assert self.data_parallel_rank is not None, (
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+6
-2
@@ -266,6 +266,7 @@ if TYPE_CHECKING:
|
||||
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS: bool = True
|
||||
VLLM_NIXL_EP_MAX_NUM_RANKS: int = 32
|
||||
VLLM_XPU_ENABLE_XPU_GRAPH: bool = False
|
||||
VLLM_XPU_USE_SAMPLER_KERNEL: bool = True
|
||||
VLLM_LORA_ENABLE_DUAL_STREAM: bool = False
|
||||
|
||||
|
||||
@@ -782,9 +783,8 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
),
|
||||
# When True and distributed_executor_backend="ray", use RayExecutorV2
|
||||
# (MQ-based) instead of RayDistributedExecutor (compiled-graph backend).
|
||||
# TODO (jeffreywang): Enabled by default in vLLM 0.20.0.
|
||||
"VLLM_USE_RAY_V2_EXECUTOR_BACKEND": lambda: bool(
|
||||
int(os.getenv("VLLM_USE_RAY_V2_EXECUTOR_BACKEND", "0"))
|
||||
int(os.getenv("VLLM_USE_RAY_V2_EXECUTOR_BACKEND", "1"))
|
||||
),
|
||||
# Use dedicated multiprocess context for workers.
|
||||
# Both spawn and fork work
|
||||
@@ -1776,6 +1776,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
"VLLM_XPU_ENABLE_XPU_GRAPH": lambda: bool(
|
||||
int(os.getenv("VLLM_XPU_ENABLE_XPU_GRAPH", "0"))
|
||||
),
|
||||
# whether use xpu specific sample kernel
|
||||
"VLLM_XPU_USE_SAMPLER_KERNEL": lambda: bool(
|
||||
int(os.getenv("VLLM_XPU_USE_SAMPLER_KERNEL", "1"))
|
||||
),
|
||||
# Enable simple KV offload.
|
||||
"VLLM_USE_SIMPLE_KV_OFFLOAD": lambda: bool(
|
||||
int(os.getenv("VLLM_USE_SIMPLE_KV_OFFLOAD", "0"))
|
||||
|
||||
@@ -312,6 +312,21 @@ class AiterFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
|
||||
As: torch.Tensor,
|
||||
Bs: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
if As.dtype != Bs.dtype:
|
||||
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
|
||||
_upcast_e8m0_to_fp32,
|
||||
)
|
||||
|
||||
if As.dtype == torch.float8_e8m0fnu:
|
||||
As = _upcast_e8m0_to_fp32(As).contiguous()
|
||||
else:
|
||||
As = As.to(torch.float32)
|
||||
|
||||
if Bs.dtype == torch.float8_e8m0fnu:
|
||||
Bs = _upcast_e8m0_to_fp32(Bs).contiguous()
|
||||
else:
|
||||
Bs = Bs.to(torch.float32)
|
||||
|
||||
out_dtype = self.config.out_dtype
|
||||
if self.use_triton:
|
||||
gemm_a8w8_blockscale_op = rocm_aiter_ops.triton_gemm_a8w8_blockscale
|
||||
|
||||
@@ -169,7 +169,9 @@ class SiluAndMulWithClamp(CustomOp):
|
||||
def __init__(self, swiglu_limit: float, *, compile_native: bool = True):
|
||||
super().__init__(compile_native=compile_native)
|
||||
self.swiglu_limit = float(swiglu_limit)
|
||||
if current_platform.is_cuda_alike() or current_platform.is_xpu():
|
||||
if current_platform.is_rocm():
|
||||
self._forward_method = self.forward_native
|
||||
elif current_platform.is_cuda_alike() or current_platform.is_xpu():
|
||||
self.op = torch.ops._C.silu_and_mul_with_clamp
|
||||
elif current_platform.is_cpu():
|
||||
self._forward_method = self.forward_native
|
||||
|
||||
@@ -259,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,
|
||||
@@ -451,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.
|
||||
@@ -1352,6 +1367,7 @@ def backend_supports_prefill_query_quantization() -> bool:
|
||||
return backend_cls.get_name() in (
|
||||
"FLASHINFER",
|
||||
"TRTLLM_RAGGED",
|
||||
"TOKENSPEED_MLA",
|
||||
)
|
||||
|
||||
|
||||
@@ -1522,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=self.model_config.get_head_size() ** -0.5,
|
||||
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(
|
||||
|
||||
@@ -300,6 +300,7 @@ class DeepseekCompressor(nn.Module):
|
||||
state_cache = self.state_cache.kv_cache
|
||||
# kv_state stored in first half, score_state stored in second half
|
||||
state_width = state_cache.shape[-1] // 2
|
||||
pdl_kwargs = {} if current_platform.is_rocm() else {"launch_pdl": False}
|
||||
|
||||
# Store the KV and score (with fused APE addition) in the state.
|
||||
# NOTE: PDL is disabled — both this kernel and _fused_kernel below
|
||||
@@ -324,7 +325,7 @@ class DeepseekCompressor(nn.Module):
|
||||
TRITON_BLOCK_SIZE=triton.next_power_of_2(kv.shape[-1]),
|
||||
STATE_WIDTH=state_width,
|
||||
COMPRESS_RATIO=self.compress_ratio,
|
||||
launch_pdl=False,
|
||||
**pdl_kwargs,
|
||||
)
|
||||
|
||||
# Fused: compress → RMSNorm → RoPE → FP8 quant → KV cache write.
|
||||
@@ -373,7 +374,7 @@ class DeepseekCompressor(nn.Module):
|
||||
SCALE_DIM=self._scale_dim,
|
||||
KV_BLOCK_STRIDE=kv_cache.stride(0),
|
||||
num_warps=self._num_warps,
|
||||
launch_pdl=False,
|
||||
**pdl_kwargs,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ from vllm.v1.attention.ops.deepseek_v4_ops import (
|
||||
fused_inv_rope_fp8_quant,
|
||||
fused_q_kv_rmsnorm,
|
||||
)
|
||||
from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
|
||||
rocm_forward_decode_fallback,
|
||||
rocm_inv_rope_einsum,
|
||||
rocm_sparse_attn_prefill,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.v1.attention.backends.mla.sparse_swa import (
|
||||
@@ -53,6 +58,7 @@ from vllm.model_executor.layers.quantization.input_quant_fp8 import (
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
GroupShape,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.multi_stream_utils import (
|
||||
execute_in_parallel,
|
||||
maybe_execute_in_parallel,
|
||||
@@ -198,8 +204,6 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
# Pick fp8_einsum recipe based on GPU arch:
|
||||
# SM90: FP32 block scales stay [g, r/128, d/128] → sfb_gran_mn=128
|
||||
# SM100: INT32 packed scales become [g, r, ...] → sfb_gran_mn=1
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
cap = current_platform.get_device_capability()
|
||||
assert cap is not None, "DeepseekV4 attention requires a CUDA device"
|
||||
self._einsum_recipe = (1, 128, 128) if cap.major <= 9 else (1, 1, 128)
|
||||
@@ -222,6 +226,7 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
+ 1 # 1B pad
|
||||
)
|
||||
|
||||
# Will be None on ROCm for now.
|
||||
self.aux_stream_list = mla_modules.aux_stream_list
|
||||
# [0]: GEMM start / post-GEMM event0. [1..3]: GEMM done events;
|
||||
# [1] doubles as post-GEMM event1. Reuse is safe: GEMM fully joins
|
||||
@@ -303,6 +308,19 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
)
|
||||
o = o_padded[:, : self.n_local_heads, :]
|
||||
|
||||
# Keep ROCm on the BF16 reference wo_a path util kernel ready.
|
||||
if current_platform.is_rocm():
|
||||
z = rocm_inv_rope_einsum(
|
||||
self.rotary_emb,
|
||||
o,
|
||||
positions,
|
||||
self.rope_head_dim,
|
||||
self.n_local_groups,
|
||||
self.o_lora_rank,
|
||||
self.wo_a,
|
||||
)
|
||||
return self.wo_b(z.flatten(1))
|
||||
|
||||
# O projection: inverse RoPE + FP8 quant + einsum + wo_b
|
||||
o_fp8, o_scale = fused_inv_rope_fp8_quant(
|
||||
o,
|
||||
@@ -336,12 +354,15 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
return self.wo_b(z.flatten(1))
|
||||
|
||||
def attn_gemm_parallel_execute(self, hidden_states) -> tuple[Any, ...]:
|
||||
assert self.aux_stream_list is not None
|
||||
assert len(self.aux_stream_list) >= 3
|
||||
aux_streams = self.aux_stream_list
|
||||
if aux_streams is not None:
|
||||
assert len(aux_streams) >= 3
|
||||
aux_streams = aux_streams[:3]
|
||||
|
||||
# fused_wqa_wkv (heaviest) on default; the three lighter input GEMMs
|
||||
# on aux streams 0..2 when their owning module exists. ln_events[0]
|
||||
# is the fan-out start event; ln_events[1..3] are per-aux done events.
|
||||
# On ROCm, aux_streams is None and execute_in_parallel runs serially.
|
||||
aux_fns: list[Callable[[], Any] | None] = [None, None, None]
|
||||
|
||||
if self.compressor is not None:
|
||||
@@ -385,7 +406,7 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
aux_fns,
|
||||
self.ln_events[0],
|
||||
self.ln_events[1:4],
|
||||
self.aux_stream_list[:3],
|
||||
aux_streams,
|
||||
enable=hidden_states.shape[0]
|
||||
<= envs.VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD,
|
||||
)
|
||||
@@ -419,8 +440,9 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
# downstream reads q on default). Indexer/compressor go on aux for
|
||||
# overlap with default's GEMM + cache write.
|
||||
if self.indexer is not None:
|
||||
assert self.aux_stream_list is not None
|
||||
aux_stream = self.aux_stream_list[0]
|
||||
aux_stream = (
|
||||
self.aux_stream_list[0] if self.aux_stream_list is not None else None
|
||||
)
|
||||
indexer = self.indexer
|
||||
# Local ref so the closure keeps a non-None type for mypy.
|
||||
assert self.compressor is not None
|
||||
@@ -448,8 +470,9 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
)
|
||||
elif self.compressor is not None:
|
||||
# wq_b + kv_insert on default, compressor on aux.
|
||||
assert self.aux_stream_list is not None
|
||||
aux_stream = self.aux_stream_list[0]
|
||||
aux_stream = (
|
||||
self.aux_stream_list[0] if self.aux_stream_list is not None else None
|
||||
)
|
||||
compressor = self.compressor
|
||||
|
||||
def wq_b_kv_insert() -> torch.Tensor:
|
||||
@@ -668,7 +691,7 @@ class DeepseekV4MLAAttention(nn.Module, AttentionLayerBase):
|
||||
vllm_config.scheduler_config.max_num_batched_tokens
|
||||
)
|
||||
self.max_model_len = vllm_config.model_config.max_model_len
|
||||
# DeepseekV4 only supports fp8 kv-cache format for now
|
||||
# DeepseekV4 only supports fp8 kv-cache format for now.
|
||||
kv_cache_dtype = cache_config.cache_dtype if cache_config is not None else "fp8"
|
||||
|
||||
assert kv_cache_dtype.startswith("fp8"), (
|
||||
@@ -816,6 +839,25 @@ class DeepseekV4MLAAttention(nn.Module, AttentionLayerBase):
|
||||
swa_indices = swa_metadata.decode_swa_indices
|
||||
swa_lens = swa_metadata.decode_swa_lens
|
||||
|
||||
if current_platform.is_rocm():
|
||||
rocm_forward_decode_fallback(
|
||||
q=q,
|
||||
kv_cache=kv_cache,
|
||||
swa_k_cache=self.swa_cache_layer.kv_cache,
|
||||
swa_only=swa_only,
|
||||
topk_indices=topk_indices,
|
||||
topk_lens=topk_lens,
|
||||
swa_indices=swa_indices,
|
||||
swa_lens=swa_lens,
|
||||
attn_sink=self.attn_sink,
|
||||
scale=self.scale,
|
||||
head_dim=self.head_dim,
|
||||
nope_head_dim=self.nope_head_dim,
|
||||
rope_head_dim=self.rope_head_dim,
|
||||
output=output,
|
||||
)
|
||||
return
|
||||
|
||||
# We treat queries in the same seq as different queries
|
||||
# and later we only attend by generated indices.
|
||||
# q arrives pre-padded to self.padded_heads by the outer wrapper.
|
||||
@@ -980,15 +1022,27 @@ class DeepseekV4MLAAttention(nn.Module, AttentionLayerBase):
|
||||
N,
|
||||
)
|
||||
|
||||
output_chunk, _, _ = flash_mla_sparse_fwd(
|
||||
q=q[query_start:query_end],
|
||||
kv=kv.view(-1, 1, q.shape[-1]),
|
||||
indices=combined_indices.unsqueeze(1),
|
||||
sm_scale=self.scale,
|
||||
attn_sink=self.attn_sink,
|
||||
topk_length=combined_lens,
|
||||
out=output[query_start:query_end],
|
||||
)
|
||||
if current_platform.is_rocm():
|
||||
rocm_sparse_attn_prefill(
|
||||
q=q[query_start:query_end],
|
||||
kv=kv.view(-1, 1, q.shape[-1]),
|
||||
indices=combined_indices.unsqueeze(1),
|
||||
topk_length=combined_lens,
|
||||
scale=self.scale,
|
||||
head_dim=self.head_dim,
|
||||
attn_sink=self.attn_sink,
|
||||
output=output[query_start:query_end],
|
||||
)
|
||||
else:
|
||||
output_chunk, _, _ = flash_mla_sparse_fwd(
|
||||
q=q[query_start:query_end],
|
||||
kv=kv.view(-1, 1, q.shape[-1]),
|
||||
indices=combined_indices.unsqueeze(1),
|
||||
sm_scale=self.scale,
|
||||
attn_sink=self.attn_sink,
|
||||
topk_length=combined_lens,
|
||||
out=output[query_start:query_end],
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV4IndexerCache(torch.nn.Module, AttentionLayerBase):
|
||||
|
||||
@@ -15,6 +15,7 @@ class MoEActivation(Enum):
|
||||
# and produce output of shape [..., d]
|
||||
SILU = "silu"
|
||||
GELU = "gelu"
|
||||
GELU_TANH = "gelu_tanh"
|
||||
RELU2 = "relu2"
|
||||
SWIGLUOAI = "swigluoai"
|
||||
SWIGLUSTEP = "swiglustep"
|
||||
@@ -24,6 +25,7 @@ class MoEActivation(Enum):
|
||||
# NOTE: Non-gated activations require the "_no_mul" suffix to be present.
|
||||
SILU_NO_MUL = "silu_no_mul"
|
||||
GELU_NO_MUL = "gelu_no_mul"
|
||||
GELU_TANH_NO_MUL = "gelu_tanh_no_mul"
|
||||
RELU2_NO_MUL = "relu2_no_mul"
|
||||
|
||||
@property
|
||||
@@ -53,6 +55,7 @@ class MoEActivation(Enum):
|
||||
@classmethod
|
||||
def from_str(cls, s: str) -> "MoEActivation":
|
||||
"""Parse from string for backward compatibility."""
|
||||
s = _STR_ALIASES.get(s, s)
|
||||
for member in cls:
|
||||
if member.value == s:
|
||||
return member
|
||||
@@ -61,20 +64,27 @@ class MoEActivation(Enum):
|
||||
|
||||
|
||||
# Module-level lookup tables used by MoEActivation functions.
|
||||
_STR_ALIASES: dict[str, str] = {
|
||||
"gelu_pytorch_tanh": "gelu_tanh",
|
||||
}
|
||||
|
||||
_CUSTOM_OP_NAMES: dict[MoEActivation, str] = {
|
||||
MoEActivation.SILU: "silu_and_mul",
|
||||
MoEActivation.GELU: "gelu_and_mul",
|
||||
MoEActivation.GELU_TANH: "gelu_tanh_and_mul",
|
||||
MoEActivation.SWIGLUOAI: "swigluoai_and_mul",
|
||||
MoEActivation.SWIGLUSTEP: "swiglustep_and_mul",
|
||||
MoEActivation.RELU2: "relu2",
|
||||
MoEActivation.SILU_NO_MUL: "silu_and_mul",
|
||||
MoEActivation.GELU_NO_MUL: "gelu_and_mul",
|
||||
MoEActivation.GELU_TANH_NO_MUL: "gelu_tanh_and_mul",
|
||||
MoEActivation.RELU2_NO_MUL: "relu2",
|
||||
}
|
||||
|
||||
_WITHOUT_MUL: dict[MoEActivation, MoEActivation] = {
|
||||
MoEActivation.SILU: MoEActivation.SILU_NO_MUL,
|
||||
MoEActivation.GELU: MoEActivation.GELU_NO_MUL,
|
||||
MoEActivation.GELU_TANH: MoEActivation.GELU_TANH_NO_MUL,
|
||||
MoEActivation.RELU2: MoEActivation.RELU2_NO_MUL,
|
||||
}
|
||||
|
||||
@@ -115,6 +125,8 @@ def apply_moe_activation(
|
||||
torch.ops._C.silu_and_mul(output, input)
|
||||
elif activation == MoEActivation.GELU:
|
||||
torch.ops._C.gelu_and_mul(output, input)
|
||||
elif activation == MoEActivation.GELU_TANH:
|
||||
torch.ops._C.gelu_tanh_and_mul(output, input)
|
||||
elif activation == MoEActivation.SWIGLUOAI:
|
||||
torch.ops._C.swigluoai_and_mul(output, input)
|
||||
elif activation == MoEActivation.SWIGLUSTEP:
|
||||
@@ -127,6 +139,8 @@ def apply_moe_activation(
|
||||
output.copy_(F.silu(input))
|
||||
elif activation == MoEActivation.GELU_NO_MUL:
|
||||
output.copy_(F.gelu(input))
|
||||
elif activation == MoEActivation.GELU_TANH_NO_MUL:
|
||||
output.copy_(F.gelu(input, approximate="tanh"))
|
||||
elif activation == MoEActivation.RELU2_NO_MUL:
|
||||
F.relu(input, inplace=True)
|
||||
torch.square(input, out=output)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import torch
|
||||
|
||||
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
FusedMoEConfig,
|
||||
FusedMoEParallelConfig,
|
||||
FusedMoEQuantConfig,
|
||||
RoutingMethodType,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
QuantKey,
|
||||
kFp8StaticTensorSym,
|
||||
kMxfp4Static,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AiterW4A8ExpertsMonolithic",
|
||||
"aiter_triton_kernel_w4a8_moe_forward",
|
||||
]
|
||||
|
||||
|
||||
def aiter_triton_kernel_w4a8_moe_forward(
|
||||
hidden_states: torch.Tensor,
|
||||
w1, # Tensor or triton_kernels.Tensor
|
||||
w2, # Tensor or triton_kernels.Tensor
|
||||
gating_output: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
activation: MoEActivation = MoEActivation.SWIGLUOAI,
|
||||
quant_config: FusedMoEQuantConfig | None = None,
|
||||
apply_router_weight_on_input: bool = False,
|
||||
global_num_experts: int = -1,
|
||||
expert_map: torch.Tensor | None = None,
|
||||
unpadded_N_w1=None,
|
||||
unpadded_K_w1=None,
|
||||
unpadded_N_w2=None,
|
||||
unpadded_K_w2=None,
|
||||
):
|
||||
assert (
|
||||
quant_config is not None
|
||||
and quant_config.use_mxfp4_w4a8
|
||||
and rocm_aiter_ops.is_enabled()
|
||||
)
|
||||
from aiter.ops.triton.moe_routing.routing import routing as aiter_routing
|
||||
|
||||
routing_data, gather_idx, scatter_idx = aiter_routing(
|
||||
gating_output, topk, sm_first=not renormalize
|
||||
)
|
||||
return triton_kernel_fused_mxfp4_w4a8_experts(
|
||||
None,
|
||||
hidden_states,
|
||||
w1,
|
||||
w2,
|
||||
routing_data,
|
||||
gather_idx,
|
||||
scatter_idx,
|
||||
activation=activation.value,
|
||||
quant_config=quant_config,
|
||||
apply_router_weight_on_input=apply_router_weight_on_input,
|
||||
global_num_experts=global_num_experts,
|
||||
expert_map=expert_map,
|
||||
unpadded_N_w1=unpadded_N_w1,
|
||||
unpadded_K_w1=unpadded_K_w1,
|
||||
unpadded_N_w2=unpadded_N_w2,
|
||||
unpadded_K_w2=unpadded_K_w2,
|
||||
)
|
||||
|
||||
|
||||
def triton_kernel_fused_mxfp4_w4a8_experts(
|
||||
output_tensor: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
w1, # Tensor or triton_kernels.Tensor
|
||||
w2, # Tensor or triton_kernels.Tensor
|
||||
routing_data, # RoutingData
|
||||
gather_indx, # GatherIndx
|
||||
scatter_indx, # ScatterIndx
|
||||
activation: str = "silu",
|
||||
quant_config: FusedMoEQuantConfig | None = None,
|
||||
swiglu_alpha: float = 1.702,
|
||||
swiglu_limit: float = 7.0,
|
||||
apply_router_weight_on_input: bool = False,
|
||||
global_num_experts: int = -1,
|
||||
expert_map: torch.Tensor | None = None,
|
||||
a1q_scale: torch.Tensor | None = None,
|
||||
unpadded_N_w1=None,
|
||||
unpadded_K_w1=None,
|
||||
unpadded_N_w2=None,
|
||||
unpadded_K_w2=None,
|
||||
) -> torch.Tensor:
|
||||
assert quant_config is not None
|
||||
# type check, uint8 means mxfp4
|
||||
assert hidden_states.dtype == torch.bfloat16
|
||||
assert quant_config.w1_bias is None or quant_config.w1_bias.dtype == torch.float32
|
||||
assert quant_config.w2_bias is None or quant_config.w2_bias.dtype == torch.float32
|
||||
|
||||
# Shape check: weights are padded (e.g. hidden_size padded for
|
||||
# GFX950 swizzle).
|
||||
assert hidden_states.shape[-1] == w1.shape[-2]
|
||||
assert w2.shape[-1] == w1.shape[1]
|
||||
|
||||
E, _, N = w1.shape
|
||||
|
||||
if global_num_experts == -1:
|
||||
global_num_experts = E
|
||||
|
||||
gammas = routing_data.gate_scal if routing_data else None
|
||||
|
||||
from aiter.ops.triton.moe_op_gemm_a8w4 import moe_gemm_a8w4
|
||||
from aiter.ops.triton.quant_moe import downcast_to_static_fp8
|
||||
|
||||
assert quant_config.w1_precision is not None, (
|
||||
"w1_precision in quant config can't be None"
|
||||
)
|
||||
assert quant_config.w2_precision is not None, (
|
||||
"w2_precision in quant config can't be None"
|
||||
)
|
||||
|
||||
hidden_states = downcast_to_static_fp8(
|
||||
hidden_states, quant_config.w1_precision.flex_ctx.lhs_data.scale
|
||||
)
|
||||
|
||||
intermediate_cache1 = moe_gemm_a8w4(
|
||||
hidden_states,
|
||||
w1.storage.data,
|
||||
None,
|
||||
quant_config.w1_precision.weight_scale.storage.data,
|
||||
quant_config.w1_precision.flex_ctx.lhs_data.scale,
|
||||
quant_config.w2_precision.flex_ctx.lhs_data.scale,
|
||||
quant_config.w1_bias,
|
||||
routing_data,
|
||||
gather_indx=gather_indx,
|
||||
gammas=gammas if apply_router_weight_on_input else None,
|
||||
swizzle_mx_scale="CDNA4_SCALE",
|
||||
out_dtype=torch.float8_e4m3fn,
|
||||
apply_swiglu=True,
|
||||
alpha=swiglu_alpha,
|
||||
limit=swiglu_limit,
|
||||
unpadded_N=unpadded_N_w1,
|
||||
unpadded_K=unpadded_K_w1,
|
||||
)
|
||||
|
||||
intermediate_cache3 = moe_gemm_a8w4(
|
||||
intermediate_cache1,
|
||||
w2.storage.data,
|
||||
None,
|
||||
quant_config.w2_precision.weight_scale.storage.data,
|
||||
quant_config.w2_precision.flex_ctx.lhs_data.scale,
|
||||
None,
|
||||
quant_config.w2_bias,
|
||||
routing_data,
|
||||
scatter_indx=scatter_indx,
|
||||
gammas=None if apply_router_weight_on_input else gammas,
|
||||
swizzle_mx_scale="CDNA4_SCALE",
|
||||
unpadded_N=unpadded_N_w2,
|
||||
unpadded_K=unpadded_K_w2,
|
||||
)
|
||||
|
||||
return intermediate_cache3
|
||||
|
||||
|
||||
class AiterW4A8ExpertsMonolithic(mk.FusedMoEExpertsMonolithic):
|
||||
"""
|
||||
Monolithic MXFP4 W4A8 expert using AITER triton kernels.
|
||||
|
||||
This backend uses:
|
||||
- aiter.ops.triton.moe_routing.routing for routing
|
||||
- aiter.ops.triton.moe_op_gemm_a8w4.moe_gemm_a8w4 for computation
|
||||
|
||||
Weight format: MXFP4 weights with GFX950 swizzle
|
||||
Activation: Static FP8 quantization
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
moe_config: FusedMoEConfig,
|
||||
quant_config: FusedMoEQuantConfig,
|
||||
):
|
||||
super().__init__(moe_config, quant_config)
|
||||
self.topk = moe_config.experts_per_token
|
||||
self.renormalize = moe_config.routing_method in (
|
||||
RoutingMethodType.Renormalize,
|
||||
RoutingMethodType.RenormalizeNaive,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def activation_format() -> mk.FusedMoEActivationFormat:
|
||||
return mk.FusedMoEActivationFormat.Standard
|
||||
|
||||
@staticmethod
|
||||
def _supports_current_device() -> bool:
|
||||
# Requires AITER and GFX950
|
||||
if not rocm_aiter_ops.is_enabled():
|
||||
return False
|
||||
from vllm.platforms.rocm import on_gfx950
|
||||
|
||||
return on_gfx950()
|
||||
|
||||
@staticmethod
|
||||
def _supports_no_act_and_mul() -> bool:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _supports_quant_scheme(
|
||||
weight_key: QuantKey | None,
|
||||
activation_key: QuantKey | None,
|
||||
) -> bool:
|
||||
# W4A8: MXFP4 weights with static FP8 activations
|
||||
SUPPORTED_W_A = [
|
||||
(kMxfp4Static, kFp8StaticTensorSym),
|
||||
]
|
||||
return (weight_key, activation_key) in SUPPORTED_W_A
|
||||
|
||||
@staticmethod
|
||||
def _supports_activation(activation: MoEActivation) -> bool:
|
||||
# Only SILU activation (swiglu) is supported
|
||||
return activation == MoEActivation.SWIGLUOAI
|
||||
|
||||
@staticmethod
|
||||
def _supports_parallel_config(
|
||||
moe_parallel_config: FusedMoEParallelConfig,
|
||||
) -> bool:
|
||||
return (
|
||||
not moe_parallel_config.use_all2all_kernels
|
||||
and not moe_parallel_config.enable_eplb
|
||||
and moe_parallel_config.dp_size <= 1
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _supports_routing_method(
|
||||
routing_method: RoutingMethodType,
|
||||
weight_key: QuantKey | None,
|
||||
activation_key: QuantKey | None,
|
||||
) -> bool:
|
||||
return routing_method in [
|
||||
RoutingMethodType.Renormalize,
|
||||
RoutingMethodType.RenormalizeNaive,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _supports_router_logits_dtype(
|
||||
router_logits_dtype: torch.dtype | None,
|
||||
routing_method: RoutingMethodType,
|
||||
) -> bool:
|
||||
return True
|
||||
|
||||
def supports_expert_map(self) -> bool:
|
||||
return False # Expert parallelism not yet supported
|
||||
|
||||
@property
|
||||
def expects_unquantized_inputs(self) -> bool:
|
||||
return True
|
||||
|
||||
def apply(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
router_logits: torch.Tensor,
|
||||
activation: MoEActivation,
|
||||
global_num_experts: int,
|
||||
expert_map: torch.Tensor | None,
|
||||
a1q_scale: torch.Tensor | None,
|
||||
apply_router_weight_on_input: bool,
|
||||
# grouped topk + fused topk bias parameters
|
||||
num_expert_group: int | None = None,
|
||||
e_score_correction_bias: torch.Tensor | None = None,
|
||||
routed_scaling_factor: float | None = None,
|
||||
topk_group: int | None = None,
|
||||
) -> torch.Tensor:
|
||||
assert self.moe_config.intermediate_size_per_partition_unpadded is not None
|
||||
assert self.moe_config.hidden_dim_unpadded is not None
|
||||
return aiter_triton_kernel_w4a8_moe_forward(
|
||||
hidden_states=hidden_states,
|
||||
w1=w1,
|
||||
w2=w2,
|
||||
gating_output=router_logits,
|
||||
topk=self.topk,
|
||||
renormalize=self.renormalize,
|
||||
global_num_experts=global_num_experts,
|
||||
expert_map=expert_map,
|
||||
quant_config=self.quant_config,
|
||||
apply_router_weight_on_input=apply_router_weight_on_input,
|
||||
unpadded_N_w1=self.moe_config.intermediate_size_per_partition_unpadded * 2,
|
||||
unpadded_K_w1=self.moe_config.hidden_dim_unpadded,
|
||||
unpadded_N_w2=self.moe_config.hidden_dim_unpadded,
|
||||
unpadded_K_w2=self.moe_config.intermediate_size_per_partition_unpadded,
|
||||
)
|
||||
@@ -5,7 +5,6 @@ import torch
|
||||
|
||||
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
@@ -286,35 +285,6 @@ def triton_kernel_moe_forward(
|
||||
unpadded_N_w2=None,
|
||||
unpadded_K_w2=None,
|
||||
) -> torch.Tensor:
|
||||
if (
|
||||
quant_config is not None
|
||||
and quant_config.use_mxfp4_w4a8
|
||||
and rocm_aiter_ops.is_enabled()
|
||||
):
|
||||
from aiter.ops.triton.moe_routing.routing import routing as aiter_routing
|
||||
|
||||
routing_data, gather_idx, scatter_idx = aiter_routing(
|
||||
gating_output, topk, sm_first=not renormalize
|
||||
)
|
||||
return triton_kernel_fused_mxfp4_w4a8_experts(
|
||||
None,
|
||||
hidden_states,
|
||||
w1,
|
||||
w2,
|
||||
routing_data,
|
||||
gather_idx,
|
||||
scatter_idx,
|
||||
activation=activation.value,
|
||||
quant_config=quant_config,
|
||||
apply_router_weight_on_input=apply_router_weight_on_input,
|
||||
global_num_experts=global_num_experts,
|
||||
expert_map=expert_map,
|
||||
unpadded_N_w1=unpadded_N_w1,
|
||||
unpadded_K_w1=unpadded_K_w1,
|
||||
unpadded_N_w2=unpadded_N_w2,
|
||||
unpadded_K_w2=unpadded_K_w2,
|
||||
)
|
||||
|
||||
from triton_kernels.topk import topk as topk_fn
|
||||
|
||||
sm_first = not renormalize
|
||||
@@ -471,99 +441,6 @@ def triton_kernel_fused_experts(
|
||||
return output_tensor
|
||||
|
||||
|
||||
# This is a triton implementation of the fused_experts function
|
||||
def triton_kernel_fused_mxfp4_w4a8_experts(
|
||||
output_tensor: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
w1, # Tensor or triton_kernels.Tensor
|
||||
w2, # Tensor or triton_kernels.Tensor
|
||||
routing_data, # RoutingData
|
||||
gather_indx, # GatherIndx
|
||||
scatter_indx, # ScatterIndx
|
||||
activation: str = "silu",
|
||||
quant_config: FusedMoEQuantConfig | None = None,
|
||||
swiglu_alpha: float = 1.702,
|
||||
swiglu_limit: float = 7.0,
|
||||
apply_router_weight_on_input: bool = False,
|
||||
global_num_experts: int = -1,
|
||||
expert_map: torch.Tensor | None = None,
|
||||
a1q_scale: torch.Tensor | None = None,
|
||||
unpadded_N_w1=None,
|
||||
unpadded_K_w1=None,
|
||||
unpadded_N_w2=None,
|
||||
unpadded_K_w2=None,
|
||||
) -> torch.Tensor:
|
||||
assert quant_config is not None
|
||||
# type check, uint8 means mxfp4
|
||||
assert hidden_states.dtype == torch.bfloat16
|
||||
assert quant_config.w1_bias is None or quant_config.w1_bias.dtype == torch.float32
|
||||
assert quant_config.w2_bias is None or quant_config.w2_bias.dtype == torch.float32
|
||||
|
||||
# Shape check: weights are padded (e.g. hidden_size padded for
|
||||
# GFX950 swizzle).
|
||||
assert hidden_states.shape[-1] == w1.shape[-2]
|
||||
assert w2.shape[-1] == w1.shape[1]
|
||||
|
||||
E, _, N = w1.shape
|
||||
|
||||
if global_num_experts == -1:
|
||||
global_num_experts = E
|
||||
|
||||
gammas = routing_data.gate_scal if routing_data else None
|
||||
|
||||
from aiter.ops.triton.moe_op_gemm_a8w4 import moe_gemm_a8w4
|
||||
from aiter.ops.triton.quant_moe import downcast_to_static_fp8
|
||||
|
||||
assert quant_config.w1_precision is not None, (
|
||||
"w1_precision in quant config can't be None"
|
||||
)
|
||||
assert quant_config.w2_precision is not None, (
|
||||
"w2_precision in quant config can't be None"
|
||||
)
|
||||
|
||||
hidden_states = downcast_to_static_fp8(
|
||||
hidden_states, quant_config.w1_precision.flex_ctx.lhs_data.scale
|
||||
)
|
||||
|
||||
intermediate_cache1 = moe_gemm_a8w4(
|
||||
hidden_states,
|
||||
w1.storage.data,
|
||||
None,
|
||||
quant_config.w1_precision.weight_scale.storage.data,
|
||||
quant_config.w1_precision.flex_ctx.lhs_data.scale,
|
||||
quant_config.w2_precision.flex_ctx.lhs_data.scale,
|
||||
quant_config.w1_bias,
|
||||
routing_data,
|
||||
gather_indx=gather_indx,
|
||||
gammas=gammas if apply_router_weight_on_input else None,
|
||||
swizzle_mx_scale="CDNA4_SCALE",
|
||||
out_dtype=torch.float8_e4m3fn,
|
||||
apply_swiglu=True,
|
||||
alpha=swiglu_alpha,
|
||||
limit=swiglu_limit,
|
||||
unpadded_N=unpadded_N_w1,
|
||||
unpadded_K=unpadded_K_w1,
|
||||
)
|
||||
|
||||
intermediate_cache3 = moe_gemm_a8w4(
|
||||
intermediate_cache1,
|
||||
w2.storage.data,
|
||||
None,
|
||||
quant_config.w2_precision.weight_scale.storage.data,
|
||||
quant_config.w2_precision.flex_ctx.lhs_data.scale,
|
||||
None,
|
||||
quant_config.w2_bias,
|
||||
routing_data,
|
||||
scatter_indx=scatter_indx,
|
||||
gammas=None if apply_router_weight_on_input else gammas,
|
||||
swizzle_mx_scale="CDNA4_SCALE",
|
||||
unpadded_N=unpadded_N_w2,
|
||||
unpadded_K=unpadded_K_w2,
|
||||
)
|
||||
|
||||
return intermediate_cache3
|
||||
|
||||
|
||||
def make_routing_data(
|
||||
topk_ids: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
|
||||
@@ -62,7 +62,7 @@ class XPUExperts(mk.FusedMoEExpertsModular):
|
||||
|
||||
@staticmethod
|
||||
def _supports_no_act_and_mul() -> bool:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _supports_activation(activation: MoEActivation) -> bool:
|
||||
@@ -70,6 +70,7 @@ class XPUExperts(mk.FusedMoEExpertsModular):
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.GELU,
|
||||
MoEActivation.SWIGLUOAI,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -786,9 +786,11 @@ class BatchedTritonExperts(mk.FusedMoEExpertsModular):
|
||||
return activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.GELU,
|
||||
MoEActivation.GELU_TANH,
|
||||
MoEActivation.SWIGLUOAI,
|
||||
MoEActivation.SILU_NO_MUL,
|
||||
MoEActivation.GELU_NO_MUL,
|
||||
MoEActivation.GELU_TANH_NO_MUL,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
|
||||
|
||||
@@ -152,10 +152,12 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
|
||||
return activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.GELU,
|
||||
MoEActivation.GELU_TANH,
|
||||
MoEActivation.SWIGLUOAI,
|
||||
MoEActivation.SWIGLUSTEP,
|
||||
MoEActivation.SILU_NO_MUL,
|
||||
MoEActivation.GELU_NO_MUL,
|
||||
MoEActivation.GELU_TANH_NO_MUL,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
|
||||
|
||||
@@ -613,10 +613,12 @@ class MarlinExpertsBase(mk.FusedMoEExpertsModular):
|
||||
return activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.GELU,
|
||||
MoEActivation.GELU_TANH,
|
||||
MoEActivation.SWIGLUOAI,
|
||||
MoEActivation.SWIGLUSTEP,
|
||||
MoEActivation.SILU_NO_MUL,
|
||||
MoEActivation.GELU_NO_MUL,
|
||||
MoEActivation.GELU_TANH_NO_MUL,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
|
||||
|
||||
@@ -1941,10 +1941,12 @@ class TritonExperts(LoRAExpertsMixin, mk.FusedMoEExpertsModular):
|
||||
return activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.GELU,
|
||||
MoEActivation.GELU_TANH,
|
||||
MoEActivation.SWIGLUOAI,
|
||||
MoEActivation.SWIGLUSTEP,
|
||||
MoEActivation.SILU_NO_MUL,
|
||||
MoEActivation.GELU_NO_MUL,
|
||||
MoEActivation.GELU_TANH_NO_MUL,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
|
||||
|
||||
@@ -538,9 +538,11 @@ class FusedMoE(PluggableLayer):
|
||||
# for heuristic purposes, so it must be initialized first.
|
||||
self.quant_method: FusedMoEMethodBase = _get_quant_method()
|
||||
|
||||
if not self.moe_config.is_act_and_mul and not current_platform.is_cuda_alike():
|
||||
if not self.moe_config.is_act_and_mul and not (
|
||||
current_platform.is_cuda_alike() or current_platform.is_xpu()
|
||||
):
|
||||
raise NotImplementedError(
|
||||
"is_act_and_mul=False is supported only for CUDA and ROCm for now"
|
||||
"is_act_and_mul=False is supported only for CUDA and XPU for now"
|
||||
)
|
||||
|
||||
if self.enable_eplb and not self.quant_method.supports_eplb:
|
||||
|
||||
@@ -18,7 +18,9 @@ from vllm.model_executor.layers.fused_moe.all2all_utils import (
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
FusedMoEQuantConfig,
|
||||
FusedMoEQuantDesc,
|
||||
RoutingMethodType,
|
||||
mxfp4_mxfp8_moe_quant_config,
|
||||
mxfp4_w4a8_moe_quant_config,
|
||||
mxfp4_w4a16_moe_quant_config,
|
||||
ocp_mx_moe_quant_config,
|
||||
)
|
||||
@@ -26,9 +28,11 @@ from vllm.model_executor.layers.quantization.utils.mxfp4_utils import _swizzle_m
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
QuantKey,
|
||||
kFp8Dynamic128Sym,
|
||||
kFp8StaticTensorSym,
|
||||
kMxfp4Static,
|
||||
kMxfp8Dynamic,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.w8a8_utils import all_close_1d
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.import_utils import has_triton_kernels
|
||||
from vllm.utils.math_utils import round_up
|
||||
@@ -59,8 +63,11 @@ class Mxfp4MoeBackend(Enum):
|
||||
# Marlin
|
||||
BATCHED_MARLIN = "BATCHED_MARLIN"
|
||||
MARLIN = "MARLIN"
|
||||
# ROCm AITER
|
||||
AITER = "AITER"
|
||||
# ROCm AITER backends
|
||||
AITER_MXFP4_BF16 = "AITER_MXFP4_BF16" # W4A16: CK kernel
|
||||
# Keep the legacy name as an alias while the ROCm split backend rename settles.
|
||||
AITER = "AITER_MXFP4_BF16"
|
||||
AITER_MXFP4_FP8 = "AITER_MXFP4_FP8" # W4A8: triton kernel
|
||||
# Triton
|
||||
TRITON = "TRITON"
|
||||
TRITON_UNFUSED = "TRITON_UNFUSED"
|
||||
@@ -72,6 +79,13 @@ class Mxfp4MoeBackend(Enum):
|
||||
HUMMING = "HUMMING"
|
||||
|
||||
|
||||
# AITER backends group
|
||||
AITER_BACKENDS = (
|
||||
Mxfp4MoeBackend.AITER_MXFP4_BF16,
|
||||
Mxfp4MoeBackend.AITER_MXFP4_FP8,
|
||||
)
|
||||
|
||||
|
||||
# Backends that share the same TRTLLM weight format
|
||||
TRTLLM_BACKENDS = (
|
||||
Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_BF16,
|
||||
@@ -159,13 +173,20 @@ def backend_to_kernel_cls(
|
||||
|
||||
return [BatchedMarlinExperts]
|
||||
|
||||
elif backend == Mxfp4MoeBackend.AITER:
|
||||
elif backend == Mxfp4MoeBackend.AITER_MXFP4_BF16:
|
||||
from vllm.model_executor.layers.fused_moe.rocm_aiter_fused_moe import (
|
||||
AiterExperts,
|
||||
)
|
||||
|
||||
return [AiterExperts]
|
||||
|
||||
elif backend == Mxfp4MoeBackend.AITER_MXFP4_FP8:
|
||||
from vllm.model_executor.layers.fused_moe.experts.aiter_mxfp4_w4a8_moe import (
|
||||
AiterW4A8ExpertsMonolithic,
|
||||
)
|
||||
|
||||
return [AiterW4A8ExpertsMonolithic]
|
||||
|
||||
elif backend == Mxfp4MoeBackend.XPU:
|
||||
from vllm.model_executor.layers.fused_moe.experts.xpu_moe import XPUExpertsMXFp4
|
||||
|
||||
@@ -194,7 +215,8 @@ def map_mxfp4_backend(runner_backend: MoEBackend) -> Mxfp4MoeBackend:
|
||||
"triton_unfused": Mxfp4MoeBackend.TRITON_UNFUSED,
|
||||
"humming": Mxfp4MoeBackend.HUMMING,
|
||||
"marlin": Mxfp4MoeBackend.MARLIN,
|
||||
"aiter": Mxfp4MoeBackend.AITER,
|
||||
"aiter": Mxfp4MoeBackend.AITER_MXFP4_BF16,
|
||||
"aiter_mxfp4_fp8": Mxfp4MoeBackend.AITER_MXFP4_FP8,
|
||||
"xpu": Mxfp4MoeBackend.XPU,
|
||||
"emulation": Mxfp4MoeBackend.EMULATION,
|
||||
}
|
||||
@@ -213,7 +235,8 @@ def _get_priority_backends_for_gpt_oss() -> list[Mxfp4MoeBackend]:
|
||||
"""
|
||||
_AVAILABLE_BACKENDS = [
|
||||
Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_BF16,
|
||||
Mxfp4MoeBackend.AITER,
|
||||
Mxfp4MoeBackend.AITER_MXFP4_BF16,
|
||||
Mxfp4MoeBackend.AITER_MXFP4_FP8,
|
||||
Mxfp4MoeBackend.TRITON,
|
||||
Mxfp4MoeBackend.FLASHINFER_CUTLASS_MXFP4_BF16,
|
||||
# TRITON_UNFUSED has bug with MTP support
|
||||
@@ -233,6 +256,8 @@ def _get_priority_backends() -> list[Mxfp4MoeBackend]:
|
||||
TRTLLM MXFP8; SM90 falls through to Triton_unfused or Marlin (the
|
||||
backend-level ``is_supported_config`` check filters by device capability).
|
||||
"""
|
||||
if current_platform.is_rocm():
|
||||
return [Mxfp4MoeBackend.AITER_MXFP4_BF16]
|
||||
_AVAILABLE_BACKENDS = [
|
||||
Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_MXFP8,
|
||||
Mxfp4MoeBackend.DEEPGEMM_MXFP4,
|
||||
@@ -254,16 +279,28 @@ def _backend_activation_key(backend: Mxfp4MoeBackend) -> QuantKey | None:
|
||||
Mxfp4MoeBackend.FLASHINFER_CUTLASS_MXFP4_MXFP8,
|
||||
):
|
||||
return kMxfp8Dynamic
|
||||
return None
|
||||
if backend == Mxfp4MoeBackend.AITER_MXFP4_FP8:
|
||||
return kFp8StaticTensorSym
|
||||
return None # BF16 activation
|
||||
|
||||
|
||||
def select_gpt_oss_mxfp4_moe_backend(
|
||||
def select_mxfp4_moe_backend(
|
||||
config: FusedMoEConfig,
|
||||
activation_key: QuantKey | None = None,
|
||||
) -> tuple[Mxfp4MoeBackend, type[mk.FusedMoEExperts] | None]:
|
||||
"""
|
||||
Select the primary MXFP4 MoE backend.
|
||||
|
||||
Args:
|
||||
config: MoE configuration
|
||||
activation_key: Optional activation quantization key. If provided,
|
||||
overrides the default activation key for backend selection.
|
||||
Use kFp8StaticTensorSym for W4A8 scheme.
|
||||
|
||||
Note: Shape-specific fallbacks may still occur at runtime.
|
||||
"""
|
||||
# If activation_key is explicitly provided (e.g., W4A8), use it
|
||||
requested_activation_key = activation_key
|
||||
device_capability = current_platform.get_device_capability()
|
||||
triton_kernels_supported = (
|
||||
has_triton_kernels()
|
||||
@@ -332,11 +369,17 @@ def select_gpt_oss_mxfp4_moe_backend(
|
||||
and requested_backend == Mxfp4MoeBackend.MARLIN
|
||||
):
|
||||
requested_backend = Mxfp4MoeBackend.BATCHED_MARLIN
|
||||
# Use requested_activation_key if provided, otherwise use backend default
|
||||
act_key = (
|
||||
requested_activation_key
|
||||
if requested_activation_key is not None
|
||||
else _backend_activation_key(requested_backend)
|
||||
)
|
||||
return _return_or_raise(
|
||||
requested_backend,
|
||||
config,
|
||||
kMxfp4Static,
|
||||
_backend_activation_key(requested_backend),
|
||||
act_key,
|
||||
activation_format,
|
||||
)
|
||||
|
||||
@@ -408,10 +451,15 @@ def select_gpt_oss_mxfp4_moe_backend(
|
||||
)
|
||||
|
||||
for backend in AVAILABLE_BACKENDS:
|
||||
activation_key = _backend_activation_key(backend)
|
||||
# Use requested_activation_key if provided, otherwise use backend default
|
||||
act_key = (
|
||||
requested_activation_key
|
||||
if requested_activation_key is not None
|
||||
else _backend_activation_key(backend)
|
||||
)
|
||||
for k_cls in backend_to_kernel_cls(backend):
|
||||
supported, reason = k_cls.is_supported_config(
|
||||
k_cls, config, kMxfp4Static, activation_key, activation_format
|
||||
k_cls, config, kMxfp4Static, act_key, activation_format
|
||||
)
|
||||
if supported:
|
||||
logger.info_once(_make_log_backend(backend))
|
||||
@@ -438,7 +486,7 @@ def select_gpt_oss_mxfp4_moe_backend(
|
||||
return Mxfp4MoeBackend.NONE, None
|
||||
|
||||
|
||||
def select_mxfp4_moe_backend(
|
||||
def select_deepseek_v4_mxfp4_moe_backend(
|
||||
config: FusedMoEConfig,
|
||||
) -> tuple[Mxfp4MoeBackend, type[mk.FusedMoEExperts] | None]:
|
||||
"""
|
||||
@@ -500,8 +548,22 @@ def select_mxfp4_moe_backend(
|
||||
activation_format,
|
||||
)
|
||||
|
||||
# DeepSeek-V4 on ROCm is more accurate with the unfused Triton MXFP4 path
|
||||
# than the default AITER path. Prefer Triton-unfused for this routing mode,
|
||||
# while keeping AITER as a fallback if Triton-unfused rejects the config.
|
||||
if (
|
||||
current_platform.is_rocm()
|
||||
and config.routing_method == RoutingMethodType.DeepseekV4
|
||||
):
|
||||
priority_backends = [
|
||||
Mxfp4MoeBackend.TRITON_UNFUSED,
|
||||
Mxfp4MoeBackend.AITER_MXFP4_BF16,
|
||||
]
|
||||
else:
|
||||
priority_backends = _get_priority_backends()
|
||||
|
||||
# Iterate priority backends: TRTLLM MXFP8, then Triton.
|
||||
for backend in _get_priority_backends():
|
||||
for backend in priority_backends:
|
||||
activation_key = _backend_activation_key(backend)
|
||||
for k_cls in backend_to_kernel_cls(backend):
|
||||
supported, reason = k_cls.is_supported_config(
|
||||
@@ -836,7 +898,7 @@ def convert_gpt_oss_weight_to_mxfp4_moe_kernel_format(
|
||||
w2_bias,
|
||||
)
|
||||
|
||||
elif mxfp4_backend == Mxfp4MoeBackend.AITER:
|
||||
elif mxfp4_backend == Mxfp4MoeBackend.AITER_MXFP4_BF16:
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
|
||||
if w13_bias is not None:
|
||||
@@ -898,6 +960,63 @@ def convert_gpt_oss_weight_to_mxfp4_moe_kernel_format(
|
||||
w2_bias,
|
||||
)
|
||||
|
||||
elif mxfp4_backend == Mxfp4MoeBackend.AITER_MXFP4_FP8:
|
||||
# W4A8: MXFP4 weights + static FP8 activations (triton kernel)
|
||||
from triton_kernels.matmul_ogs import FlexCtx, PrecisionConfig
|
||||
from triton_kernels.numerics import InFlexData
|
||||
|
||||
if w13_bias is not None:
|
||||
w13_bias = w13_bias.to(torch.float32)
|
||||
if w2_bias is not None:
|
||||
w2_bias = w2_bias.to(torch.float32)
|
||||
|
||||
# Process static FP8 input scales (reduce to scalar, warn if not uniform)
|
||||
w13_input_scale = layer.w13_input_scale
|
||||
w2_input_scale = layer.w2_input_scale
|
||||
if w13_input_scale is None or w2_input_scale is None:
|
||||
raise ValueError(
|
||||
"W4A8 (AITER_MXFP4_FP8) requires static input scales, but found "
|
||||
"w13_input_scale or w2_input_scale is None."
|
||||
)
|
||||
if not all_close_1d(w13_input_scale) or not all_close_1d(w2_input_scale):
|
||||
logger.warning_once(
|
||||
"Found input_scales that are not equal for "
|
||||
"fp8 MoE layer. Using the maximum across experts "
|
||||
"for each layer."
|
||||
)
|
||||
w13_input_scale = w13_input_scale.max().to(torch.float32)
|
||||
w2_input_scale = w2_input_scale.max().to(torch.float32)
|
||||
|
||||
# Swizzle weights for GFX950
|
||||
w13_weight, w13_flex, w13_scale = _swizzle_mxfp4(w13_weight, w13_weight_scale)
|
||||
w2_weight, w2_flex, w2_scale = _swizzle_mxfp4(w2_weight, w2_weight_scale)
|
||||
|
||||
# Create InFlexData for activation scales
|
||||
lhs_data13 = InFlexData(scale=w13_input_scale)
|
||||
lhs_data2 = InFlexData(scale=w2_input_scale)
|
||||
|
||||
# Create PrecisionConfig with both weight and activation info
|
||||
w13_precision_config = PrecisionConfig(
|
||||
weight_scale=w13_scale,
|
||||
flex_ctx=FlexCtx(rhs_data=w13_flex, lhs_data=lhs_data13),
|
||||
)
|
||||
w2_precision_config = PrecisionConfig(
|
||||
weight_scale=w2_scale,
|
||||
flex_ctx=FlexCtx(rhs_data=w2_flex, lhs_data=lhs_data2),
|
||||
)
|
||||
|
||||
del layer.w13_weight
|
||||
del layer.w2_weight
|
||||
|
||||
return (
|
||||
w13_weight,
|
||||
w2_weight,
|
||||
w13_precision_config,
|
||||
w2_precision_config,
|
||||
w13_bias,
|
||||
w2_bias,
|
||||
)
|
||||
|
||||
elif mxfp4_backend in TRITON_BACKENDS:
|
||||
from triton_kernels.matmul_ogs import FlexCtx, PrecisionConfig
|
||||
|
||||
@@ -1152,6 +1271,64 @@ def convert_weight_to_mxfp4_moe_kernel_format(
|
||||
w2_bias,
|
||||
)
|
||||
|
||||
elif mxfp4_backend == Mxfp4MoeBackend.AITER_MXFP4_BF16:
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
|
||||
if w13_bias is not None:
|
||||
w13_bias = w13_bias.data.to(torch.float32)
|
||||
if w2_bias is not None:
|
||||
w2_bias = w2_bias.data.to(torch.float32)
|
||||
|
||||
e, n, k = w13_weight.shape
|
||||
|
||||
w13_weight.view(torch.uint8).copy_(
|
||||
w13_weight.data.view(torch.uint8)
|
||||
.view(e, n // 2, 2, k)
|
||||
.permute(0, 2, 1, 3)
|
||||
.contiguous()
|
||||
.view(e, n, k)
|
||||
)
|
||||
w13_weight_scale.data = (
|
||||
w13_weight_scale.data.view(e, n // 2, 2, -1)
|
||||
.permute(0, 2, 1, 3)
|
||||
.contiguous()
|
||||
.view(e, n, -1)
|
||||
)
|
||||
|
||||
w13_weight.data = w13_weight.data.view(torch.float4_e2m1fn_x2)
|
||||
w2_weight.data = w2_weight.data.view(torch.float4_e2m1fn_x2)
|
||||
|
||||
w13_weight.data = rocm_aiter_ops.shuffle_weight_a16w4(w13_weight, 16, True)
|
||||
shuffled_w13_scale = rocm_aiter_ops.shuffle_scale_a16w4(
|
||||
w13_weight_scale.view(-1, w13_weight_scale.shape[-1]),
|
||||
num_experts,
|
||||
True,
|
||||
)
|
||||
|
||||
w2_weight.data = rocm_aiter_ops.shuffle_weight_a16w4(w2_weight, 16, False)
|
||||
shuffled_w2_scale = rocm_aiter_ops.shuffle_scale_a16w4(
|
||||
w2_weight_scale.view(-1, w2_weight_scale.shape[-1]),
|
||||
num_experts,
|
||||
False,
|
||||
)
|
||||
|
||||
if w13_bias is not None:
|
||||
w13_bias = (
|
||||
w13_bias.data.view(-1, n // 2, 2)
|
||||
.permute(0, 2, 1)
|
||||
.contiguous()
|
||||
.view(-1, n)
|
||||
)
|
||||
|
||||
return (
|
||||
w13_weight,
|
||||
w2_weight,
|
||||
shuffled_w13_scale,
|
||||
shuffled_w2_scale,
|
||||
w13_bias,
|
||||
w2_bias,
|
||||
)
|
||||
|
||||
elif mxfp4_backend in TRITON_BACKENDS:
|
||||
from triton_kernels.matmul_ogs import FlexCtx, PrecisionConfig
|
||||
|
||||
@@ -1207,7 +1384,7 @@ def convert_weight_to_mxfp4_moe_kernel_format(
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported mxfp4_backend for Mxfp4MoEMethod: {mxfp4_backend}. "
|
||||
f"Expected TRTLLM or Triton backend."
|
||||
f"Expected TRTLLM, Triton, or AITER backend."
|
||||
)
|
||||
|
||||
|
||||
@@ -1220,6 +1397,8 @@ def make_mxfp4_moe_quant_config(
|
||||
swiglu_limit: float | None = None,
|
||||
w1_bias: torch.Tensor | None = None,
|
||||
w2_bias: torch.Tensor | None = None,
|
||||
a1_scale: torch.Tensor | None = None,
|
||||
a2_scale: torch.Tensor | None = None,
|
||||
layer: torch.nn.Module | None = None,
|
||||
) -> FusedMoEQuantConfig | None:
|
||||
"""Create a FusedMoEQuantConfig for the given MXFP4 backend."""
|
||||
@@ -1262,6 +1441,17 @@ def make_mxfp4_moe_quant_config(
|
||||
gemm1_beta=gemm1_beta,
|
||||
gemm1_clamp_limit=swiglu_limit,
|
||||
)
|
||||
elif mxfp4_backend == Mxfp4MoeBackend.AITER_MXFP4_FP8:
|
||||
# W4A8: MXFP4 weights + static FP8 activations
|
||||
return mxfp4_w4a8_moe_quant_config(
|
||||
w1_scale=w1_scale,
|
||||
w2_scale=w2_scale,
|
||||
a1_scale=a1_scale,
|
||||
a2_scale=a2_scale,
|
||||
w1_bias=w1_bias,
|
||||
w2_bias=w2_bias,
|
||||
block_shape=None,
|
||||
)
|
||||
elif mxfp4_backend in (
|
||||
Mxfp4MoeBackend.MARLIN,
|
||||
Mxfp4MoeBackend.BATCHED_MARLIN,
|
||||
@@ -1269,7 +1459,7 @@ def make_mxfp4_moe_quant_config(
|
||||
Mxfp4MoeBackend.TRITON_UNFUSED,
|
||||
Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_BF16,
|
||||
Mxfp4MoeBackend.FLASHINFER_CUTLASS_MXFP4_BF16,
|
||||
Mxfp4MoeBackend.AITER,
|
||||
Mxfp4MoeBackend.AITER_MXFP4_BF16,
|
||||
):
|
||||
return mxfp4_w4a16_moe_quant_config(
|
||||
w1_bias=w1_bias,
|
||||
|
||||
@@ -268,10 +268,13 @@ class LinearBase(PluggableLayer):
|
||||
self.quant_config = quant_config
|
||||
self.prefix = prefix
|
||||
self.allow_fp8_block_shape_mismatch = False
|
||||
self.quant_method: QuantizeMethodBase
|
||||
if quant_config is None:
|
||||
self.quant_method: QuantizeMethodBase | None = UnquantizedLinearMethod()
|
||||
self.quant_method = UnquantizedLinearMethod()
|
||||
elif quant_method := quant_config.get_quant_method(self, prefix=prefix):
|
||||
self.quant_method = quant_method
|
||||
else:
|
||||
self.quant_method = quant_config.get_quant_method(self, prefix=prefix)
|
||||
raise ValueError("All linear layers should support quant method.")
|
||||
self.return_bias = return_bias
|
||||
self.disable_tp = disable_tp
|
||||
self.tp_rank = get_tensor_model_parallel_rank() if not disable_tp else 0
|
||||
@@ -335,8 +338,6 @@ class ReplicatedLinear(LinearBase):
|
||||
disable_tp=disable_tp,
|
||||
)
|
||||
|
||||
# All the linear layer supports quant method.
|
||||
assert self.quant_method is not None
|
||||
self.quant_method.create_weights(
|
||||
self,
|
||||
self.input_size,
|
||||
@@ -389,7 +390,6 @@ class ReplicatedLinear(LinearBase):
|
||||
x: torch.Tensor,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, Parameter | None]:
|
||||
bias = self.bias if not self.skip_bias_add else None
|
||||
assert self.quant_method is not None
|
||||
|
||||
output = self.quant_method.apply(self, x, bias)
|
||||
|
||||
@@ -474,7 +474,6 @@ class ColumnParallelLinear(LinearBase):
|
||||
self._maybe_allow_fp8_block_shape_mismatch()
|
||||
self.gather_output = gather_output
|
||||
|
||||
assert self.quant_method is not None
|
||||
self.quant_method.create_weights(
|
||||
layer=self,
|
||||
input_size_per_partition=self.input_size_per_partition,
|
||||
@@ -583,7 +582,6 @@ class ColumnParallelLinear(LinearBase):
|
||||
bias = self.bias if not self.skip_bias_add else None
|
||||
|
||||
# Matrix multiply.
|
||||
assert self.quant_method is not None
|
||||
output_parallel = self.quant_method.apply(self, input_, bias)
|
||||
|
||||
if self.gather_output and self.tp_size > 1:
|
||||
@@ -1463,7 +1461,6 @@ class RowParallelLinear(LinearBase):
|
||||
self.input_is_parallel = input_is_parallel
|
||||
self.reduce_results = reduce_results
|
||||
|
||||
assert self.quant_method is not None
|
||||
self.quant_method.create_weights(
|
||||
layer=self,
|
||||
input_size_per_partition=self.input_size_per_partition,
|
||||
@@ -1553,7 +1550,6 @@ class RowParallelLinear(LinearBase):
|
||||
input_parallel = split_input[self.tp_rank].contiguous()
|
||||
|
||||
# Matrix multiply.
|
||||
assert self.quant_method is not None
|
||||
# Only fuse bias add into GEMM for rank 0 (this ensures that
|
||||
# bias will not get added more than once in TP>1 case)
|
||||
bias_ = None if (self.tp_rank > 0 or self.skip_bias_add) else self.bias
|
||||
|
||||
@@ -234,6 +234,39 @@ def mhc_pre(
|
||||
num_tokens = residual_flat.shape[0]
|
||||
fn_flat = fn
|
||||
|
||||
if current_platform.is_rocm():
|
||||
x = residual_flat.view(num_tokens, hc_mult * hidden_size).to(torch.float32)
|
||||
mixes = torch.matmul(x, fn_flat.t())
|
||||
sqrsum = x.square().sum(dim=-1, keepdim=True)
|
||||
mixes = mixes * torch.rsqrt(sqrsum / (hc_mult * hidden_size) + rms_eps)
|
||||
|
||||
pre_logits = mixes[:, :hc_mult] * hc_scale[0] + hc_base[:hc_mult]
|
||||
pre_mix = torch.sigmoid(pre_logits) + hc_pre_eps
|
||||
|
||||
post_logits = (
|
||||
mixes[:, hc_mult : 2 * hc_mult] * hc_scale[1]
|
||||
+ hc_base[hc_mult : 2 * hc_mult]
|
||||
)
|
||||
post_mix = torch.sigmoid(post_logits) * hc_post_mult_value
|
||||
|
||||
comb_logits = mixes[:, 2 * hc_mult :].view(
|
||||
num_tokens, hc_mult, hc_mult
|
||||
) * hc_scale[2] + hc_base[2 * hc_mult :].view(1, hc_mult, hc_mult)
|
||||
comb_mix = torch.softmax(comb_logits, dim=-1) + hc_sinkhorn_eps
|
||||
comb_mix = comb_mix / (comb_mix.sum(dim=-2, keepdim=True) + hc_sinkhorn_eps)
|
||||
for _ in range(sinkhorn_repeat - 1):
|
||||
comb_mix = comb_mix / (comb_mix.sum(dim=-1, keepdim=True) + hc_sinkhorn_eps)
|
||||
comb_mix = comb_mix / (comb_mix.sum(dim=-2, keepdim=True) + hc_sinkhorn_eps)
|
||||
|
||||
layer_input = torch.sum(
|
||||
pre_mix.unsqueeze(-1) * residual_flat.to(torch.float32), dim=1
|
||||
).to(torch.bfloat16)
|
||||
return (
|
||||
post_mix.view(*outer_shape, hc_mult, 1),
|
||||
comb_mix.view(*outer_shape, hc_mult, hc_mult),
|
||||
layer_input.view(*outer_shape, hidden_size),
|
||||
)
|
||||
|
||||
# these number are from deepgemm kernel impl
|
||||
block_k = 64
|
||||
block_m = 64
|
||||
@@ -414,6 +447,14 @@ def mhc_post(
|
||||
post_layer_mix: torch.Tensor,
|
||||
comb_res_mix: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
if current_platform.is_rocm():
|
||||
mixed_residual = torch.einsum(
|
||||
"...ij,...ih->...jh",
|
||||
comb_res_mix.to(torch.float32),
|
||||
residual.to(torch.float32),
|
||||
)
|
||||
post_term = post_layer_mix.to(torch.float32) * x.unsqueeze(-2).to(torch.float32)
|
||||
return (mixed_residual + post_term).to(residual.dtype)
|
||||
out = torch.empty_like(residual)
|
||||
mhc_post_tilelang(
|
||||
comb_res_mix,
|
||||
@@ -551,6 +592,49 @@ def hc_head_fuse_tilelang(
|
||||
T.pdl_trigger()
|
||||
|
||||
|
||||
def _hc_head_fused_reference(
|
||||
hs_flat: torch.Tensor,
|
||||
fn: torch.Tensor,
|
||||
hc_scale: torch.Tensor,
|
||||
hc_base: torch.Tensor,
|
||||
out: torch.Tensor,
|
||||
hidden_size: int,
|
||||
rms_eps: float,
|
||||
hc_eps: float,
|
||||
hc_mult: int,
|
||||
) -> None:
|
||||
"""Pure-PyTorch reference for `hc_head_fuse_tilelang`.
|
||||
|
||||
Used on platforms where the tilelang HIP/CUDA backend is not available
|
||||
(e.g. ROCm builds shipping a tilelang wheel without `target.build.tilelang_hip`).
|
||||
Mirrors the math of the tilelang kernel exactly:
|
||||
|
||||
x = hs_flat.flatten(-2, -1) # (T, hc_mult * H), fp32
|
||||
mixes = x @ fn.T # (T, hc_mult)
|
||||
rsqrt = 1 / sqrt(||x||^2 / (hc_mult * H) + rms_eps)
|
||||
pre[m] = sigmoid(mixes[m] * rsqrt * hc_scale[0] + hc_base[m]) + hc_eps
|
||||
out = sum_m pre[m] * hs_flat[:, m, :] # cast back to bf16
|
||||
|
||||
`out` is mutated in place to keep the same op contract
|
||||
(`mutates_args=["out"]`).
|
||||
"""
|
||||
num_tokens = hs_flat.shape[0]
|
||||
if num_tokens == 0:
|
||||
return
|
||||
x = hs_flat.reshape(num_tokens, hc_mult * hidden_size).to(torch.float32)
|
||||
# fn: (hc_mult, hc_mult * hidden_size) → mixes: (T, hc_mult)
|
||||
mixes = torch.matmul(x, fn.t())
|
||||
sqrsum = x.square().sum(dim=-1, keepdim=True)
|
||||
rsqrt = torch.rsqrt(sqrsum / (hc_mult * hidden_size) + rms_eps)
|
||||
# hc_scale has shape (1,); hc_base has shape (hc_mult,)
|
||||
pre_mix = torch.sigmoid(mixes * rsqrt * hc_scale[0] + hc_base) + hc_eps
|
||||
# weighted sum over the hc_mult channel dim
|
||||
result = torch.sum(pre_mix.unsqueeze(-1) * hs_flat.to(torch.float32), dim=1).to(
|
||||
out.dtype
|
||||
)
|
||||
out.copy_(result)
|
||||
|
||||
|
||||
def _hc_head_fused_kernel(
|
||||
hs_flat: torch.Tensor,
|
||||
fn: torch.Tensor,
|
||||
@@ -563,8 +647,15 @@ def _hc_head_fused_kernel(
|
||||
hc_mult: int,
|
||||
) -> None:
|
||||
"""Fill pre-allocated `out` (T, H) in-place with the hc_head result."""
|
||||
if hs_flat.shape[0] > 0:
|
||||
hc_head_fuse_tilelang(
|
||||
if hs_flat.shape[0] == 0:
|
||||
return
|
||||
if current_platform.is_rocm():
|
||||
# tilelang ships only the CUDA codegen in upstream wheels, so the HIP
|
||||
# FFI target (`target.build.tilelang_hip`) is missing and the JIT call
|
||||
# would raise `ValueError: Cannot find global function ...`. Use a
|
||||
# numerically equivalent torch fallback instead. `mhc_pre` and
|
||||
# `mhc_post` already follow this same pattern above.
|
||||
_hc_head_fused_reference(
|
||||
hs_flat,
|
||||
fn,
|
||||
hc_scale,
|
||||
@@ -575,6 +666,18 @@ def _hc_head_fused_kernel(
|
||||
hc_eps,
|
||||
hc_mult,
|
||||
)
|
||||
return
|
||||
hc_head_fuse_tilelang(
|
||||
hs_flat,
|
||||
fn,
|
||||
hc_scale,
|
||||
hc_base,
|
||||
out,
|
||||
hidden_size,
|
||||
rms_eps,
|
||||
hc_eps,
|
||||
hc_mult,
|
||||
)
|
||||
|
||||
|
||||
direct_register_custom_op(
|
||||
|
||||
@@ -68,21 +68,23 @@ class MeanPool(SequencePoolingMethod):
|
||||
"partial prefill not supported with MEAN pooling"
|
||||
)
|
||||
|
||||
prompt_lens = pooling_cursor.prompt_lens_cpu.to(
|
||||
hidden_states.device, dtype=torch.int64, non_blocking=True
|
||||
)
|
||||
|
||||
num_seqs = prompt_lens.numel()
|
||||
prompt_lens_cpu = pooling_cursor.prompt_lens_cpu
|
||||
num_seqs = prompt_lens_cpu.numel()
|
||||
hidden_size = hidden_states.shape[-1]
|
||||
|
||||
if num_seqs == 0:
|
||||
# early return for empty batch
|
||||
return hidden_states.new_empty((0, hidden_size), dtype=torch.float32)
|
||||
|
||||
# eg. [2, 1, 3] -> [0, 0, 1, 2, 2, 2]
|
||||
# Build segment_ids on CPU so repeat_interleave doesn't need to sync
|
||||
# GPU->CPU to learn its data-dependent output length, then upload
|
||||
# non-blocking. eg. [2, 1, 3] -> [0, 0, 1, 2, 2, 2]
|
||||
segment_ids = torch.repeat_interleave(
|
||||
torch.arange(num_seqs, device=hidden_states.device, dtype=torch.long),
|
||||
prompt_lens,
|
||||
torch.arange(num_seqs, dtype=torch.long),
|
||||
prompt_lens_cpu,
|
||||
).to(hidden_states.device, non_blocking=True)
|
||||
prompt_lens = prompt_lens_cpu.to(
|
||||
hidden_states.device, dtype=torch.int64, non_blocking=True
|
||||
)
|
||||
segment_sums = torch.zeros(
|
||||
(num_seqs, hidden_size),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import dataclasses
|
||||
from collections.abc import Mapping, Set
|
||||
from itertools import groupby
|
||||
|
||||
@@ -80,9 +81,11 @@ class DispatchPooler(Pooler):
|
||||
pooling_metadata: PoolingMetadata,
|
||||
) -> PoolerOutput:
|
||||
poolers_by_task = self.poolers_by_task
|
||||
cursor = pooling_metadata.pooling_cursor
|
||||
|
||||
outputs = list[torch.Tensor | None]()
|
||||
offset = 0
|
||||
token_offset = 0
|
||||
for task, group in groupby(pooling_metadata.tasks):
|
||||
if not (pooler := poolers_by_task.get(task)):
|
||||
raise ValueError(
|
||||
@@ -91,10 +94,37 @@ class DispatchPooler(Pooler):
|
||||
)
|
||||
|
||||
num_items = len(list(group))
|
||||
group_output: PoolerOutput = pooler(
|
||||
hidden_states,
|
||||
pooling_metadata[offset : offset + num_items],
|
||||
)
|
||||
group_metadata = pooling_metadata[offset : offset + num_items]
|
||||
if cursor is None:
|
||||
group_hidden_states = hidden_states
|
||||
else:
|
||||
# Slice out this group's tokens so sub-poolers see only their
|
||||
# portion of the batch. Token offset is computed from the CPU
|
||||
# `num_scheduled_tokens_cpu` to avoid a GPU->CPU sync.
|
||||
group_cursor = group_metadata.pooling_cursor
|
||||
num_group_tokens = int(group_cursor.num_scheduled_tokens_cpu.sum())
|
||||
group_hidden_states = hidden_states[
|
||||
token_offset : token_offset + num_group_tokens
|
||||
]
|
||||
if token_offset:
|
||||
# Shift first/last indices to be relative to the slice
|
||||
# so seqwise poolers (which index `hidden_states` directly)
|
||||
# remain correct.
|
||||
pooling_cursor = dataclasses.replace(
|
||||
group_cursor,
|
||||
first_token_indices_gpu=(
|
||||
group_cursor.first_token_indices_gpu - token_offset
|
||||
),
|
||||
last_token_indices_gpu=(
|
||||
group_cursor.last_token_indices_gpu - token_offset
|
||||
),
|
||||
)
|
||||
group_metadata = dataclasses.replace(
|
||||
group_metadata, pooling_cursor=pooling_cursor
|
||||
)
|
||||
token_offset += num_group_tokens
|
||||
|
||||
group_output: PoolerOutput = pooler(group_hidden_states, group_metadata)
|
||||
|
||||
outputs.extend(group_output)
|
||||
offset += num_items
|
||||
|
||||
@@ -47,17 +47,12 @@ class AllPool(TokenPoolingMethod):
|
||||
pooling_metadata: PoolingMetadata,
|
||||
) -> list[TokenPoolingMethodOutputItem]:
|
||||
pooling_cursor = pooling_metadata.get_pooling_cursor()
|
||||
split_sizes = pooling_cursor.num_scheduled_tokens_cpu.tolist()
|
||||
if split_sizes:
|
||||
# DispatchPooler passes the full hidden_states tensor.
|
||||
# slice out the subgroup once, then split it by
|
||||
# per-request token counts
|
||||
group_start = int(pooling_cursor.first_token_indices_gpu[0].item())
|
||||
group_end = int(pooling_cursor.last_token_indices_gpu[-1].item()) + 1
|
||||
hidden_states_group = hidden_states[group_start:group_end]
|
||||
hidden_states_lst = list(hidden_states_group.split(split_sizes))
|
||||
else:
|
||||
hidden_states_lst = []
|
||||
# Use the already-CPU num_scheduled_tokens tensor so `.tolist()`
|
||||
# doesn't trigger a GPU->CPU sync. torch.split produces the same
|
||||
# consecutive slices as indexing with first/last per-sequence indices.
|
||||
hidden_states_lst = list(
|
||||
torch.split(hidden_states, pooling_cursor.num_scheduled_tokens_cpu.tolist())
|
||||
)
|
||||
|
||||
if not self.enable_chunked_prefill:
|
||||
return hidden_states_lst
|
||||
@@ -95,12 +90,14 @@ class StepPool(AllPool):
|
||||
pooling_metadata: PoolingMetadata,
|
||||
) -> list[TokenPoolingMethodOutputItem]:
|
||||
pooled_data_lst = super().forward(hidden_states, pooling_metadata)
|
||||
prompt_token_ids = pooling_metadata.get_prompt_token_ids()
|
||||
# Use the CPU copy of prompt_token_ids so the step_tag_id mask can be
|
||||
# resolved to indices without a d2h sync from boolean indexing.
|
||||
prompt_token_ids_cpu = pooling_metadata.get_prompt_token_ids_cpu()
|
||||
pooling_params = pooling_metadata.pooling_params
|
||||
|
||||
pooled_data = list[torch.Tensor | None]()
|
||||
for data, token_id, pooling_param in zip(
|
||||
pooled_data_lst, prompt_token_ids, pooling_params
|
||||
for data, token_id_cpu, pooling_param in zip(
|
||||
pooled_data_lst, prompt_token_ids_cpu, pooling_params
|
||||
):
|
||||
# for unfinished chunked prefill
|
||||
if data is None:
|
||||
@@ -113,7 +110,9 @@ class StepPool(AllPool):
|
||||
data = data[:, returned_token_ids]
|
||||
|
||||
if step_tag_id is not None:
|
||||
data = data[token_id == step_tag_id]
|
||||
idx_cpu = (token_id_cpu == step_tag_id).nonzero(as_tuple=True)[0]
|
||||
idx = idx_cpu.to(data.device, non_blocking=True)
|
||||
data = data[idx]
|
||||
|
||||
pooled_data.append(data)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
|
||||
make_mxfp4_moe_kernel,
|
||||
make_mxfp4_moe_quant_config,
|
||||
mxfp4_round_up_hidden_size_and_intermediate_size,
|
||||
select_gpt_oss_mxfp4_moe_backend,
|
||||
select_deepseek_v4_mxfp4_moe_backend,
|
||||
select_mxfp4_moe_backend,
|
||||
)
|
||||
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
|
||||
@@ -140,7 +140,7 @@ class GptOssMxfp4MoEMethod(FusedMoEMethodBase):
|
||||
def __init__(self, moe: FusedMoEConfig):
|
||||
super().__init__(moe)
|
||||
self.weight_dtype = "gpt_oss_mxfp4"
|
||||
self.mxfp4_backend, self.experts_cls = select_gpt_oss_mxfp4_moe_backend(moe)
|
||||
self.mxfp4_backend, self.experts_cls = select_mxfp4_moe_backend(moe)
|
||||
|
||||
self.max_capture_size = (
|
||||
get_current_vllm_config().compilation_config.max_cudagraph_capture_size
|
||||
@@ -468,7 +468,7 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
|
||||
def __init__(self, moe: FusedMoEConfig):
|
||||
super().__init__(moe)
|
||||
self.weight_dtype = "mxfp4"
|
||||
self.mxfp4_backend, self.experts_cls = select_mxfp4_moe_backend(moe)
|
||||
self.mxfp4_backend, self.experts_cls = select_deepseek_v4_mxfp4_moe_backend(moe)
|
||||
|
||||
self.max_capture_size = (
|
||||
get_current_vllm_config().compilation_config.max_cudagraph_capture_size
|
||||
|
||||
@@ -35,19 +35,19 @@ from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
|
||||
make_mxfp4_moe_kernel,
|
||||
make_mxfp4_moe_quant_config,
|
||||
mxfp4_round_up_hidden_size_and_intermediate_size,
|
||||
select_gpt_oss_mxfp4_moe_backend,
|
||||
select_mxfp4_moe_backend,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils_fp8 import (
|
||||
prepare_fp8_moe_layer_for_marlin,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
|
||||
_swizzle_mxfp4,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
|
||||
OCP_MX_BLOCK_SIZE,
|
||||
OCP_MX_Scheme,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
GroupShape,
|
||||
kFp8StaticTensorSym,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
|
||||
all_close_1d,
|
||||
normalize_e4m3fn_to_e4m3fnuz,
|
||||
@@ -62,7 +62,6 @@ logger = init_logger(__name__)
|
||||
__all__ = [
|
||||
"QuarkMoEMethod",
|
||||
"QuarkOCP_MX_MoEMethod",
|
||||
"QuarkOCP_MX_MoEMethod_OSS",
|
||||
]
|
||||
|
||||
|
||||
@@ -94,22 +93,9 @@ class QuarkMoEMethod(FusedMoEMethodBase):
|
||||
elif quant_config._is_fp8_w8a8(weight_config, input_config):
|
||||
return QuarkW8A8Fp8MoEMethod(weight_config, input_config, module.moe_config)
|
||||
elif quant_config._is_w_ocp_mx_a_x(weight_config, input_config):
|
||||
emulate = not current_platform.supports_mx() or not (
|
||||
rocm_aiter_ops.is_fused_moe_enabled()
|
||||
)
|
||||
if (
|
||||
input_config is not None
|
||||
and input_config.get("dtype") == "fp8_e4m3"
|
||||
and not input_config.get("is_dynamic")
|
||||
and not emulate
|
||||
):
|
||||
return QuarkOCP_MX_MoEMethod_OSS(
|
||||
weight_config, input_config, module.moe_config
|
||||
)
|
||||
else:
|
||||
return QuarkOCP_MX_MoEMethod(
|
||||
weight_config, input_config, module.moe_config
|
||||
)
|
||||
# All OCP MX schemes (W4A16, W4A8, etc.) handled by QuarkOCP_MX_MoEMethod
|
||||
# Backend selection happens inside via oracle
|
||||
return QuarkOCP_MX_MoEMethod(weight_config, input_config, module.moe_config)
|
||||
elif quant_config._is_static_tensor_w8a8(
|
||||
weight_config, input_config
|
||||
) or quant_config._is_dynamic_per_token_w8a8(weight_config, input_config):
|
||||
@@ -993,7 +979,7 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
self.experts_cls: type[mk.FusedMoEExperts] | None = None
|
||||
self.moe_kernel: mk.FusedMoEKernel | None = None
|
||||
|
||||
# Used for triton kernel precision configs
|
||||
# Used for triton kernel precision configs (W4A8, TRITON backends)
|
||||
self.w13_precision_config = None
|
||||
self.w2_precision_config = None
|
||||
|
||||
@@ -1002,6 +988,17 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
else:
|
||||
self.static_input_scales = False
|
||||
|
||||
# Select backend based on OCP MX scheme
|
||||
if self.ocp_mx_scheme == "w_mxfp4":
|
||||
# W4A16: weight-only MXFP4
|
||||
self.mxfp4_backend, self.experts_cls = select_mxfp4_moe_backend(moe)
|
||||
elif self.ocp_mx_scheme == "w_mxfp4_a_fp8" and self.static_input_scales:
|
||||
# W4A8: MXFP4 weights + static FP8 activations
|
||||
self.mxfp4_backend, self.experts_cls = select_mxfp4_moe_backend(
|
||||
moe, activation_key=kFp8StaticTensorSym
|
||||
)
|
||||
|
||||
# Validation for unsupported schemes
|
||||
if any(
|
||||
self.ocp_mx_scheme.endswith(a_scheme)
|
||||
for a_scheme in ["a_mxfp4", "a_mxfp6_e3m2", "a_mxfp6_e2m3"]
|
||||
@@ -1026,7 +1023,7 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
)
|
||||
|
||||
# TODO: Remove once all OCP MX schemes use the kernel abstraction
|
||||
_AITER_NATIVE_OCP_MX_SCHEMES = ("w_mxfp4", "w_mxfp4_a_mxfp4")
|
||||
_AITER_NATIVE_OCP_MX_SCHEMES = ("w_mxfp4", "w_mxfp4_a_mxfp4", "w_mxfp4_a_fp8")
|
||||
self.emulate = (
|
||||
not current_platform.supports_mx()
|
||||
or self.ocp_mx_scheme not in _AITER_NATIVE_OCP_MX_SCHEMES
|
||||
@@ -1034,9 +1031,6 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
self.mxfp4_backend is Mxfp4MoeBackend.NONE or not self.use_rocm_aiter_moe
|
||||
)
|
||||
|
||||
if self.ocp_mx_scheme == "w_mxfp4":
|
||||
self.mxfp4_backend, self.experts_cls = select_gpt_oss_mxfp4_moe_backend(moe)
|
||||
|
||||
if self.emulate:
|
||||
# We use the same code path between MXFP4/MXFP6 emulation.
|
||||
self.mxfp4_backend = Mxfp4MoeBackend.EMULATION
|
||||
@@ -1046,7 +1040,12 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
if self.mxfp4_backend != Mxfp4MoeBackend.NONE:
|
||||
self.experts_cls = backend_to_kernel_cls(self.mxfp4_backend)[0]
|
||||
|
||||
if self.emulate:
|
||||
# Log backend selection
|
||||
if self.mxfp4_backend != Mxfp4MoeBackend.NONE:
|
||||
logger.info_once(
|
||||
f"Using {self.mxfp4_backend.value} backend for {self.ocp_mx_scheme}"
|
||||
)
|
||||
elif self.emulate:
|
||||
logger.warning_once(
|
||||
f"The current mode (supports_mx={current_platform.supports_mx()}, "
|
||||
f"use_rocm_aiter_moe={self.use_rocm_aiter_moe}, "
|
||||
@@ -1056,10 +1055,6 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
"QDQ (quantize and dequantize) will be used, with the linear "
|
||||
"layers computed in high precision."
|
||||
)
|
||||
else:
|
||||
logger.warning_once(
|
||||
"The current mode supports native MoE MXFP4 computation"
|
||||
)
|
||||
|
||||
def maybe_roundup_sizes(
|
||||
self,
|
||||
@@ -1204,6 +1199,11 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
layer.w2_input_scale = None
|
||||
|
||||
def process_weights_after_loading(self, layer):
|
||||
# For MXFP4 schemes with native backend, use oracle
|
||||
if self.mxfp4_backend != Mxfp4MoeBackend.NONE:
|
||||
self._setup_kernel(layer)
|
||||
return
|
||||
|
||||
if self.static_input_scales and self.input_dtype == "fp8":
|
||||
# firstly, process activations if fp8 static input
|
||||
if layer.w13_input_scale is None or layer.w2_input_scale is None:
|
||||
@@ -1252,14 +1252,6 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
w2_input_scale, requires_grad=False
|
||||
)
|
||||
|
||||
# For w_mxfp4, use oracle functions
|
||||
if self.emulate or (
|
||||
self.ocp_mx_scheme == "w_mxfp4"
|
||||
and self.mxfp4_backend != Mxfp4MoeBackend.NONE
|
||||
):
|
||||
self._setup_kernel_via_oracle(layer)
|
||||
return
|
||||
|
||||
# TODO(bowenbao): gradually migrate to oracles.
|
||||
# Existing AITER path for w_mxfp4_a_mxfp4 and other schemes
|
||||
from aiter.utility.fp4_utils import e8m0_shuffle
|
||||
@@ -1298,46 +1290,48 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
self.moe_quant_config = self.get_fused_moe_quant_config(layer)
|
||||
torch.accelerator.empty_cache()
|
||||
|
||||
def _setup_kernel_via_oracle(self, layer: FusedMoE):
|
||||
"""Setup kernel using oracle functions for w_mxfp4 scheme."""
|
||||
w13 = layer.w13_weight
|
||||
w2 = layer.w2_weight
|
||||
w13_scale = layer.w13_weight_scale
|
||||
w2_scale = layer.w2_weight_scale
|
||||
def _setup_kernel(self, layer: FusedMoE):
|
||||
"""Setup kernel using oracle functions for MXFP4 schemes (W4A16, W4A8)."""
|
||||
w13_bias = getattr(layer, "w13_bias", None)
|
||||
w2_bias = getattr(layer, "w2_bias", None)
|
||||
|
||||
# Convert weights to kernel format
|
||||
# Convert weights to kernel format (handles all backend-specific logic)
|
||||
w13, w2, w13_scale, w2_scale, w13_bias, w2_bias = (
|
||||
convert_gpt_oss_weight_to_mxfp4_moe_kernel_format(
|
||||
mxfp4_backend=self.mxfp4_backend,
|
||||
layer=layer,
|
||||
w13_weight=w13,
|
||||
w2_weight=w2,
|
||||
w13_weight_scale=w13_scale,
|
||||
w2_weight_scale=w2_scale,
|
||||
w13_weight=layer.w13_weight,
|
||||
w2_weight=layer.w2_weight,
|
||||
w13_weight_scale=layer.w13_weight_scale,
|
||||
w2_weight_scale=layer.w2_weight_scale,
|
||||
w13_bias=w13_bias,
|
||||
w2_bias=w2_bias,
|
||||
)
|
||||
)
|
||||
|
||||
# For TRITON backends, weights are wrapped tensors from triton_kernels
|
||||
# that don't support .detach(). Manually assign parameters.
|
||||
if self.mxfp4_backend not in TRITON_BACKENDS:
|
||||
replace_parameter(layer, "w13_weight", w13)
|
||||
replace_parameter(layer, "w2_weight", w2)
|
||||
replace_parameter(layer, "w13_weight_scale", w13_scale)
|
||||
replace_parameter(layer, "w2_weight_scale", w2_scale)
|
||||
else:
|
||||
# Handle weight/scale assignment based on backend type
|
||||
if self.mxfp4_backend in TRITON_BACKENDS or self.mxfp4_backend in (
|
||||
Mxfp4MoeBackend.AITER_MXFP4_FP8,
|
||||
):
|
||||
# Triton-based backends: w13/w2 are triton_kernels.tensor.Tensor
|
||||
# Store on layer for apply(), scales are PrecisionConfig
|
||||
layer.w13_weight = w13
|
||||
layer.w2_weight = w2
|
||||
self.w13_precision_config = w13_scale
|
||||
self.w2_precision_config = w2_scale
|
||||
else:
|
||||
# Standard backends: replace parameters
|
||||
replace_parameter(layer, "w13_weight", w13)
|
||||
replace_parameter(layer, "w2_weight", w2)
|
||||
replace_parameter(layer, "w13_weight_scale", w13_scale)
|
||||
replace_parameter(layer, "w2_weight_scale", w2_scale)
|
||||
|
||||
if w13_bias is not None and w2_bias is not None:
|
||||
replace_parameter(layer, "w13_bias", w13_bias)
|
||||
replace_parameter(layer, "w2_bias", w2_bias)
|
||||
|
||||
torch.accelerator.empty_cache()
|
||||
|
||||
# Build quant config and kernel
|
||||
self.moe_quant_config = self.get_fused_moe_quant_config(layer)
|
||||
if self.moe_quant_config is not None and self.experts_cls is not None:
|
||||
@@ -1353,22 +1347,26 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
def get_fused_moe_quant_config(
|
||||
self, layer: torch.nn.Module
|
||||
) -> FusedMoEQuantConfig | None:
|
||||
# For w_mxfp4 with oracle backend, use oracle function
|
||||
if self.ocp_mx_scheme == "w_mxfp4" and self.mxfp4_backend not in (
|
||||
Mxfp4MoeBackend.NONE,
|
||||
Mxfp4MoeBackend.EMULATION,
|
||||
):
|
||||
w1_scale = layer.w13_weight_scale
|
||||
w2_scale = layer.w2_weight_scale
|
||||
if self.mxfp4_backend in TRITON_BACKENDS:
|
||||
# For oracle-based backends (W4A16, W4A8), use make_mxfp4_moe_quant_config
|
||||
if self.mxfp4_backend not in (Mxfp4MoeBackend.NONE, Mxfp4MoeBackend.EMULATION):
|
||||
# Determine scale source based on backend type
|
||||
if self.mxfp4_backend in TRITON_BACKENDS or self.mxfp4_backend in (
|
||||
Mxfp4MoeBackend.AITER_MXFP4_FP8,
|
||||
):
|
||||
w1_scale = self.w13_precision_config
|
||||
w2_scale = self.w2_precision_config
|
||||
else:
|
||||
w1_scale = layer.w13_weight_scale
|
||||
w2_scale = layer.w2_weight_scale
|
||||
|
||||
return make_mxfp4_moe_quant_config(
|
||||
mxfp4_backend=self.mxfp4_backend,
|
||||
w1_scale=w1_scale,
|
||||
w2_scale=w2_scale,
|
||||
w1_bias=getattr(layer, "w13_bias", None),
|
||||
w2_bias=getattr(layer, "w2_bias", None),
|
||||
a1_scale=getattr(layer, "w13_input_scale", None),
|
||||
a2_scale=getattr(layer, "w2_input_scale", None),
|
||||
)
|
||||
|
||||
# Emulation and other schemes
|
||||
@@ -1421,7 +1419,7 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
topk_ids: torch.Tensor,
|
||||
shared_experts_input: torch.Tensor | None,
|
||||
) -> torch.Tensor:
|
||||
# For oracle kernel or emulation kernel
|
||||
# For oracle-based kernels (W4A16, W4A8) or emulation kernel
|
||||
if self.moe_kernel is not None:
|
||||
return self.moe_kernel.apply(
|
||||
hidden_states=x,
|
||||
@@ -1473,135 +1471,3 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
|
||||
expert_map=layer.expert_map,
|
||||
apply_router_weight_on_input=layer.apply_router_weight_on_input,
|
||||
)
|
||||
|
||||
|
||||
class QuarkOCP_MX_MoEMethod_OSS(QuarkOCP_MX_MoEMethod):
|
||||
def __init__(
|
||||
self,
|
||||
weight_config: dict[str, Any],
|
||||
input_config: dict[str, Any],
|
||||
moe: FusedMoEConfig,
|
||||
):
|
||||
super().__init__(weight_config, input_config, moe)
|
||||
|
||||
def process_weights_after_loading(self, layer):
|
||||
from triton_kernels.matmul_ogs import FlexCtx, PrecisionConfig
|
||||
|
||||
w13_bias = layer.w13_bias.to(torch.float32)
|
||||
w2_bias = layer.w2_bias.to(torch.float32)
|
||||
|
||||
layer.w13_bias = torch.nn.Parameter(w13_bias, requires_grad=False)
|
||||
layer.w2_bias = torch.nn.Parameter(w2_bias, requires_grad=False)
|
||||
|
||||
# FIXME warp need to be adjusted based on batch size
|
||||
# only apply to batched mode
|
||||
if self.moe.use_ep:
|
||||
num_warps = 4 if self.moe.max_num_tokens <= 512 else 8
|
||||
else:
|
||||
num_warps = 8
|
||||
|
||||
w13_weight, w13_flex, w13_scale = _swizzle_mxfp4(
|
||||
layer.w13_weight, layer.w13_weight_scale, num_warps
|
||||
)
|
||||
w2_weight, w2_flex, w2_scale = _swizzle_mxfp4(
|
||||
layer.w2_weight, layer.w2_weight_scale, num_warps
|
||||
)
|
||||
|
||||
self.w13_weight_triton_tensor = w13_weight
|
||||
self.w2_weight_triton_tensor = w2_weight
|
||||
|
||||
# need to delete the original weights to save memory on single GPU
|
||||
del layer.w13_weight
|
||||
del layer.w2_weight
|
||||
layer.w13_weight = None
|
||||
layer.w2_weight = None
|
||||
torch.accelerator.empty_cache()
|
||||
|
||||
if self.static_input_scales:
|
||||
if layer.w13_input_scale is None or layer.w2_input_scale is None:
|
||||
raise ValueError(
|
||||
"QuantConfig has static quantization, but found "
|
||||
"activation scales are None."
|
||||
)
|
||||
if not all_close_1d(layer.w13_input_scale) or not all_close_1d(
|
||||
layer.w2_input_scale
|
||||
):
|
||||
logger.warning_once(
|
||||
"Found input_scales that are not equal for "
|
||||
"fp8 MoE layer. Using the maximum across experts "
|
||||
"for each layer."
|
||||
)
|
||||
|
||||
layer.w13_input_scale = torch.nn.Parameter(
|
||||
layer.w13_input_scale.max().to(torch.float32), requires_grad=False
|
||||
)
|
||||
layer.w2_input_scale = torch.nn.Parameter(
|
||||
layer.w2_input_scale.max().to(torch.float32), requires_grad=False
|
||||
)
|
||||
|
||||
from triton_kernels.numerics import InFlexData
|
||||
|
||||
lhs_data13 = InFlexData(scale=layer.w13_input_scale)
|
||||
lhs_data2 = InFlexData(scale=layer.w2_input_scale)
|
||||
|
||||
self.w13_precision_config = PrecisionConfig(
|
||||
weight_scale=w13_scale,
|
||||
flex_ctx=FlexCtx(rhs_data=w13_flex, lhs_data=lhs_data13),
|
||||
)
|
||||
|
||||
self.w2_precision_config = PrecisionConfig(
|
||||
weight_scale=w2_scale,
|
||||
flex_ctx=FlexCtx(rhs_data=w2_flex, lhs_data=lhs_data2),
|
||||
)
|
||||
|
||||
def get_fused_moe_quant_config(
|
||||
self, layer: torch.nn.Module
|
||||
) -> FusedMoEQuantConfig | None:
|
||||
return mxfp4_w4a8_moe_quant_config(
|
||||
w1_scale=self.w13_precision_config,
|
||||
w2_scale=self.w2_precision_config,
|
||||
a1_scale=layer.w13_input_scale,
|
||||
a2_scale=layer.w2_input_scale,
|
||||
w1_bias=layer.w13_bias,
|
||||
w2_bias=layer.w2_bias,
|
||||
block_shape=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_monolithic(self) -> bool:
|
||||
return True
|
||||
|
||||
def apply_monolithic(
|
||||
self,
|
||||
layer: FusedMoE,
|
||||
x: torch.Tensor,
|
||||
router_logits: torch.Tensor,
|
||||
input_ids: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
if layer.enable_eplb:
|
||||
raise NotImplementedError(
|
||||
f"EPLB not supported for {self.__class__.__name__} yet."
|
||||
)
|
||||
|
||||
from vllm.model_executor.layers.fused_moe.experts.gpt_oss_triton_kernels_moe import ( # noqa: E501
|
||||
triton_kernel_moe_forward,
|
||||
)
|
||||
|
||||
assert self.moe.hidden_dim_unpadded is not None
|
||||
assert self.moe.intermediate_size_per_partition_unpadded is not None
|
||||
return triton_kernel_moe_forward(
|
||||
hidden_states=x,
|
||||
w1=self.w13_weight_triton_tensor,
|
||||
w2=self.w2_weight_triton_tensor,
|
||||
gating_output=router_logits,
|
||||
topk=layer.top_k,
|
||||
renormalize=layer.renormalize,
|
||||
global_num_experts=layer.global_num_experts,
|
||||
expert_map=layer.expert_map,
|
||||
quant_config=self.moe_quant_config,
|
||||
apply_router_weight_on_input=layer.apply_router_weight_on_input,
|
||||
unpadded_N_w1=self.moe.intermediate_size_per_partition_unpadded * 2,
|
||||
unpadded_K_w1=self.moe.hidden_dim_unpadded,
|
||||
unpadded_N_w2=self.moe.hidden_dim_unpadded,
|
||||
unpadded_K_w2=self.moe.intermediate_size_per_partition_unpadded,
|
||||
)
|
||||
|
||||
@@ -2,8 +2,17 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""TurboQuant configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import ModelConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Named TQ presets: each maps to frozen config parameters.
|
||||
# key_quant_bits: 8 = FP8 keys, 3-4 = MSE (Lloyd-Max) quantized keys.
|
||||
@@ -159,12 +168,34 @@ class TurboQuantConfig:
|
||||
return s + (s % 2) # round up to even
|
||||
|
||||
@staticmethod
|
||||
def get_boundary_skip_layers(num_layers: int, n: int = 2) -> list[str]:
|
||||
"""Get layer indices to skip TQ compression (boundary protection).
|
||||
def get_boundary_skip_layers(
|
||||
model_config: ModelConfig,
|
||||
n: int = 2,
|
||||
) -> list[str]:
|
||||
"""Layer indices to skip TQ compression (boundary protection).
|
||||
|
||||
Returns first N and last N layer indices as strings, suitable for
|
||||
kv_cache_dtype_skip_layers.
|
||||
For hybrid models (attention + Mamba/linear-attention), boundary
|
||||
protection is disabled — hybrids typically have only 8-12
|
||||
full-attention layers and a hard n=2 on each side would cover
|
||||
~40 % of them. The dense GSM8K baselines that motivate n=2
|
||||
don't apply to hybrids.
|
||||
|
||||
For dense models, skips first N and last N attention layers.
|
||||
Empirically required for aggressive presets (k3v4_nc, 3bit_nc)
|
||||
— without it GSM8K drops ~30 points on Qwen3-4B.
|
||||
"""
|
||||
if model_config.is_hybrid:
|
||||
attn_indices = _get_full_attention_layer_indices(model_config)
|
||||
if not attn_indices:
|
||||
raise NotImplementedError(
|
||||
"TurboQuant KV cache requires identifiable "
|
||||
"full-attention layers, but none were found in "
|
||||
"the hybrid model config."
|
||||
)
|
||||
logger.info("TQ hybrid: full-attention layers %s", attn_indices)
|
||||
return []
|
||||
|
||||
num_layers = model_config.hf_text_config.num_hidden_layers
|
||||
if n <= 0 or num_layers <= 0:
|
||||
return []
|
||||
n = min(n, num_layers // 2) # don't skip more than half
|
||||
@@ -175,7 +206,7 @@ class TurboQuantConfig:
|
||||
return [str(i) for i in indices]
|
||||
|
||||
@staticmethod
|
||||
def from_cache_dtype(cache_dtype: str, head_dim: int) -> "TurboQuantConfig":
|
||||
def from_cache_dtype(cache_dtype: str, head_dim: int) -> TurboQuantConfig:
|
||||
"""Create config from a named preset.
|
||||
|
||||
Valid presets: turboquant_k8v4, turboquant_4bit_nc, etc.
|
||||
@@ -193,3 +224,31 @@ class TurboQuantConfig:
|
||||
value_quant_bits=preset["value_quant_bits"],
|
||||
norm_correction=preset["norm_correction"],
|
||||
)
|
||||
|
||||
|
||||
def _get_full_attention_layer_indices(model_config: ModelConfig) -> list[int]:
|
||||
"""Global indices of full-attention layers in a hybrid model.
|
||||
|
||||
Covers the conventions used across vLLM: ``layer_types`` (Qwen3.5/Next),
|
||||
``layers_block_type`` (Jamba/Zamba2), ``attn_type_list`` (Minimax).
|
||||
"""
|
||||
text_cfg = model_config.hf_text_config
|
||||
hf_cfg = model_config.hf_config
|
||||
|
||||
layer_types = getattr(text_cfg, "layer_types", None)
|
||||
if layer_types is not None:
|
||||
return [
|
||||
i for i, t in enumerate(layer_types) if t in ("full_attention", "attention")
|
||||
]
|
||||
|
||||
layers_block_type = getattr(text_cfg, "layers_block_type", None)
|
||||
if layers_block_type is not None:
|
||||
return [
|
||||
i for i, t in enumerate(layers_block_type) if t in ("attention", "hybrid")
|
||||
]
|
||||
|
||||
attn_type_list = getattr(hf_cfg, "attn_type_list", None)
|
||||
if attn_type_list is not None:
|
||||
return [i for i, t in enumerate(attn_type_list) if t == 1]
|
||||
|
||||
return []
|
||||
|
||||
@@ -843,6 +843,15 @@ def w8a8_triton_block_scaled_mm(
|
||||
assert len(block_size) == 2
|
||||
block_n, block_k = block_size[0], block_size[1]
|
||||
|
||||
# Triton cannot currently bind E8M0 scale tensors directly. On ROCm,
|
||||
# DeepSeek-V4 checkpoints store block scales in exponent-only E8M0 format,
|
||||
# so decode them to fp32 before launching the kernel.
|
||||
if current_platform.is_rocm():
|
||||
if As.dtype == torch.float8_e8m0fnu:
|
||||
As = _upcast_e8m0_to_fp32(As).contiguous()
|
||||
if Bs.dtype == torch.float8_e8m0fnu:
|
||||
Bs = _upcast_e8m0_to_fp32(Bs).contiguous()
|
||||
|
||||
assert A.shape[-1] == B.shape[-1]
|
||||
assert A.shape[:-1] == As.shape[:-1] and A.is_contiguous()
|
||||
assert triton.cdiv(A.shape[-1], block_k) == As.shape[-1]
|
||||
|
||||
@@ -499,13 +499,31 @@ class SparseAttnIndexer(CustomOp):
|
||||
k: torch.Tensor,
|
||||
weights: torch.Tensor,
|
||||
):
|
||||
assert not self.skip_k_cache_insert, (
|
||||
"AMD platform doesn't support skip cache insert yet"
|
||||
)
|
||||
assert not self.use_fp4_cache, "AMD platform doesn't support fp4 cache yet"
|
||||
assert isinstance(q_quant, torch.Tensor), (
|
||||
"AMD sparse_attn_indexer expects a single FP8 q_quant tensor"
|
||||
)
|
||||
if self.skip_k_cache_insert or not rocm_aiter_ops.is_enabled():
|
||||
from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
|
||||
rocm_aiter_sparse_attn_indexer_native,
|
||||
)
|
||||
|
||||
return rocm_aiter_sparse_attn_indexer_native(
|
||||
hidden_states,
|
||||
_encode_layer_name(self.k_cache.prefix),
|
||||
self.k_cache.kv_cache,
|
||||
q_quant,
|
||||
k,
|
||||
weights,
|
||||
self.quant_block_size,
|
||||
self.scale_fmt,
|
||||
self.topk_tokens,
|
||||
self.head_dim,
|
||||
self.max_model_len,
|
||||
self.max_total_seq_len,
|
||||
self.topk_indices_buffer,
|
||||
skip_k_cache_insert=self.skip_k_cache_insert,
|
||||
)
|
||||
if rocm_aiter_ops.is_enabled():
|
||||
return torch.ops.vllm.rocm_aiter_sparse_attn_indexer(
|
||||
hidden_states,
|
||||
@@ -522,8 +540,4 @@ class SparseAttnIndexer(CustomOp):
|
||||
self.max_total_seq_len,
|
||||
self.topk_indices_buffer,
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Sparse attention indexer ROCm custom op requires ROCm "
|
||||
"Aiter ops to be enabled."
|
||||
)
|
||||
raise RuntimeError("Sparse attention indexer ROCm path could not be selected.")
|
||||
|
||||
@@ -37,6 +37,7 @@ from vllm.sequence import IntermediateTensors
|
||||
from .commandr import LayerNorm
|
||||
from .interfaces import SupportsPP, SupportsQuant
|
||||
from .utils import (
|
||||
AutoWeightsLoader,
|
||||
extract_layer_index,
|
||||
is_pp_missing_parameter,
|
||||
make_empty_intermediate_tensors_factory,
|
||||
@@ -330,6 +331,7 @@ class CohereMoeModel(nn.Module):
|
||||
quant_config = vllm_config.quant_config
|
||||
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
self.vocab_size = config.vocab_size
|
||||
self.org_vocab_size = config.vocab_size
|
||||
self.embed_tokens = VocabParallelEmbedding(
|
||||
@@ -378,63 +380,6 @@ class CohereMoeModel(nn.Module):
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
return hidden_states
|
||||
|
||||
|
||||
class CohereMoeForCausalLM(nn.Module, SupportsPP, SupportsQuant):
|
||||
is_text_generation_model = True
|
||||
|
||||
packed_modules_mapping = {
|
||||
"qkv_proj": [
|
||||
"q_proj",
|
||||
"k_proj",
|
||||
"v_proj",
|
||||
],
|
||||
"gate_up_proj": [
|
||||
"gate_proj",
|
||||
"up_proj",
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
config = vllm_config.model_config.hf_config
|
||||
quant_config = vllm_config.quant_config
|
||||
self.config = config
|
||||
assert getattr(config, "tie_word_embeddings", True)
|
||||
self.unpadded_vocab_size = config.vocab_size
|
||||
self.quant_config = quant_config
|
||||
self.logits_scale = config.logit_scale
|
||||
self.logits_processor = LogitsProcessor(
|
||||
self.unpadded_vocab_size, config.vocab_size, scale=self.logits_scale
|
||||
)
|
||||
self.model = CohereMoeModel(
|
||||
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
|
||||
)
|
||||
self.make_empty_intermediate_tensors = (
|
||||
self.model.make_empty_intermediate_tensors
|
||||
)
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.get_input_embeddings(input_ids)
|
||||
|
||||
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.get_input_embeddings(input_ids)
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
return self.model(input_ids, positions, intermediate_tensors, inputs_embeds)
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
) -> torch.Tensor | None:
|
||||
return self.logits_processor(self.model.embed_tokens, hidden_states)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
stacked_params_mapping = [
|
||||
("qkv_proj", "q_proj", "q"),
|
||||
@@ -507,8 +452,6 @@ class CohereMoeForCausalLM(nn.Module, SupportsPP, SupportsQuant):
|
||||
)
|
||||
break
|
||||
else:
|
||||
if "lm_head.weight" in name:
|
||||
continue
|
||||
if (
|
||||
name.endswith(".bias") or name.endswith("_bias")
|
||||
) and name not in params_dict:
|
||||
@@ -526,3 +469,64 @@ class CohereMoeForCausalLM(nn.Module, SupportsPP, SupportsQuant):
|
||||
loaded_params.add(name)
|
||||
|
||||
return loaded_params
|
||||
|
||||
|
||||
class CohereMoeForCausalLM(nn.Module, SupportsPP, SupportsQuant):
|
||||
is_text_generation_model = True
|
||||
|
||||
packed_modules_mapping = {
|
||||
"qkv_proj": [
|
||||
"q_proj",
|
||||
"k_proj",
|
||||
"v_proj",
|
||||
],
|
||||
"gate_up_proj": [
|
||||
"gate_proj",
|
||||
"up_proj",
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
config = vllm_config.model_config.hf_config
|
||||
quant_config = vllm_config.quant_config
|
||||
self.config = config
|
||||
assert getattr(config, "tie_word_embeddings", True)
|
||||
self.unpadded_vocab_size = config.vocab_size
|
||||
self.quant_config = quant_config
|
||||
self.logits_scale = config.logit_scale
|
||||
self.logits_processor = LogitsProcessor(
|
||||
self.unpadded_vocab_size, config.vocab_size, scale=self.logits_scale
|
||||
)
|
||||
self.model = CohereMoeModel(
|
||||
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
|
||||
)
|
||||
self.make_empty_intermediate_tensors = (
|
||||
self.model.make_empty_intermediate_tensors
|
||||
)
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.get_input_embeddings(input_ids)
|
||||
|
||||
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.get_input_embeddings(input_ids)
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
return self.model(input_ids, positions, intermediate_tensors, inputs_embeds)
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
) -> torch.Tensor | None:
|
||||
return self.logits_processor(self.model.embed_tokens, hidden_states)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
loader = AutoWeightsLoader(self, skip_prefixes=["lm_head."])
|
||||
return loader.load_weights(weights)
|
||||
|
||||
@@ -1245,7 +1245,12 @@ class DeepseekV4Model(nn.Module):
|
||||
# DeepseekV4MultiHeadLatentAttentionWrapper.attn_gemm_parallel_execute
|
||||
# (compressor kv_score, indexer.weights_proj, indexer.compressor
|
||||
# kv_score). fused_wqa_wkv stays on the default stream.
|
||||
aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
|
||||
# Disable them on ROCm because of hang issues.
|
||||
aux_stream_list = (
|
||||
None
|
||||
if current_platform.is_rocm()
|
||||
else [torch.cuda.Stream() for _ in range(3)]
|
||||
)
|
||||
|
||||
self.device = current_platform.device_type
|
||||
# Reserved topk indices buffer for all Indexer layers to reuse.
|
||||
|
||||
@@ -167,8 +167,12 @@ class DeepSeekV4MultiTokenPredictor(nn.Module):
|
||||
)
|
||||
|
||||
# Three aux streams shared across all MTP layers, mirroring
|
||||
# DeepseekV4Model.
|
||||
aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
|
||||
# DeepseekV4Model. ROCm runs the same work serially for now.
|
||||
aux_stream_list = (
|
||||
None
|
||||
if current_platform.is_rocm()
|
||||
else [torch.cuda.Stream() for _ in range(3)]
|
||||
)
|
||||
|
||||
# to map the exact layer index from weights
|
||||
self.layers = torch.nn.ModuleDict(
|
||||
|
||||
@@ -360,7 +360,7 @@ class Gemma4MoE(nn.Module):
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.experts",
|
||||
custom_routing_function=routing_function,
|
||||
activation="gelu",
|
||||
activation="gelu_tanh",
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor, router_logits: torch.Tensor) -> torch.Tensor:
|
||||
|
||||
@@ -797,6 +797,83 @@ class Plamo2Model(torch.nn.Module):
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
return hidden_states
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
params_dict = dict(self.named_parameters())
|
||||
loaded_params: set[str] = set()
|
||||
for name, loaded_weight in weights:
|
||||
# Update the weight names to be compatible with the vllm version
|
||||
# of the model.
|
||||
# Do not change the order of the replacements.
|
||||
replacements = {
|
||||
# Rename incompatible weight names.
|
||||
".A_log": ".A",
|
||||
".B_norm_weight": ".B_norm.weight",
|
||||
".C_norm_weight": ".C_norm.weight",
|
||||
".dt_norm_weight": ".dt_norm.weight",
|
||||
".q_weight": ".q_norm.weight",
|
||||
".k_weight": ".k_norm.weight",
|
||||
}
|
||||
# Apply replacements based on the defined mappings
|
||||
for old, new in replacements.items():
|
||||
if old in name:
|
||||
name = name.replace(old, new)
|
||||
|
||||
# Reshape the in_proj weights to match the shape expected
|
||||
# by MergedColumnParallelLinear.
|
||||
# This works both for unquantized weights and
|
||||
# for quantized weights.
|
||||
# In the quantized case, the weights are already transposed.
|
||||
# Also, in addition to the quantized weights,
|
||||
# the zero points and scales have to be reshaped as well.
|
||||
# Packing should not be affected by this.
|
||||
if (
|
||||
".mixer.in_proj.weight" in name
|
||||
or "mixer.in_proj.qweight" in name
|
||||
or "mixer.in_proj.scales" in name
|
||||
or "mixer.in_proj.qzeros" in name
|
||||
):
|
||||
if "mixer.in_proj.weight" in name:
|
||||
loaded_weight = loaded_weight.transpose(0, 1)
|
||||
# for weight:
|
||||
# loaded_weight.shape[0] == self.config.hidden_size
|
||||
# for qweight:
|
||||
# loaded_weight.shape[0] == self.config.hidden_size // param.pack_factor # noqa
|
||||
# for scales and qzeros:
|
||||
# loaded_weight.shape[0] == self.config.hidden_size // self.vllm_config.quant_config.group_size # noqa
|
||||
loaded_weight = loaded_weight.reshape(
|
||||
loaded_weight.shape[0], self.config.mamba_num_heads, -1
|
||||
)
|
||||
gate_weight, hidden_states_weight = loaded_weight.chunk(2, dim=-1)
|
||||
gate_weight = gate_weight.reshape(loaded_weight.shape[0], -1)
|
||||
hidden_states_weight = hidden_states_weight.reshape(
|
||||
loaded_weight.shape[0], -1
|
||||
)
|
||||
loaded_weight = torch.cat([gate_weight, hidden_states_weight], dim=-1)
|
||||
if "mixer.in_proj.weight" in name:
|
||||
loaded_weight = loaded_weight.transpose(0, 1)
|
||||
|
||||
# Offset parameter with vllm's RMSNorm haven't been supported yet.
|
||||
if ".pre_mixer_norm" in name:
|
||||
loaded_weight += 1.0
|
||||
elif ".post_mixer_norm" in name:
|
||||
loaded_weight += 1.0 / 5
|
||||
elif ".pre_mlp_norm" in name:
|
||||
loaded_weight += 1.0
|
||||
elif ".post_mlp_norm" in name:
|
||||
loaded_weight += 1.0 / (5**1.5)
|
||||
elif name == "norm.weight":
|
||||
loaded_weight += 1.0
|
||||
|
||||
# Skip layers on other devices.
|
||||
if is_pp_missing_parameter(name, self):
|
||||
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
|
||||
|
||||
|
||||
class Plamo2ForCausalLM(
|
||||
torch.nn.Module, HasInnerState, SupportsLoRA, SupportsPP, IsHybrid
|
||||
@@ -906,88 +983,9 @@ class Plamo2ForCausalLM(
|
||||
logits = self.logits_processor(self.lm_head, hidden_states)
|
||||
return logits
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
|
||||
params_dict = dict(self.named_parameters())
|
||||
for name, loaded_weight in weights:
|
||||
# Both tie_word_embeddings=True and lm_head.weight in the safetensor
|
||||
# at the same time causes dict key access error.
|
||||
if name == "lm_head.weight" and self.config.tie_word_embeddings:
|
||||
assert "lm_head.weight" not in params_dict
|
||||
continue
|
||||
# Same workaround as AutoWeightsLoader for GPTQModel
|
||||
if any(
|
||||
substr in name
|
||||
for substr in AutoWeightsLoader.ROTARY_EMBEDS_UNUSED_WEIGHTS
|
||||
):
|
||||
continue
|
||||
|
||||
# Update the weight names to be compatible with the vllm version
|
||||
# of the model.
|
||||
# Do not change the order of the replacements.
|
||||
replacements = {
|
||||
# Rename incompatible weight names.
|
||||
".A_log": ".A",
|
||||
".B_norm_weight": ".B_norm.weight",
|
||||
".C_norm_weight": ".C_norm.weight",
|
||||
".dt_norm_weight": ".dt_norm.weight",
|
||||
".q_weight": ".q_norm.weight",
|
||||
".k_weight": ".k_norm.weight",
|
||||
}
|
||||
# Apply replacements based on the defined mappings
|
||||
for old, new in replacements.items():
|
||||
if old in name:
|
||||
name = name.replace(old, new)
|
||||
|
||||
# Reshape the in_proj weights to match the shape expected
|
||||
# by MergedColumnParallelLinear.
|
||||
# This works both for unquantized weights and
|
||||
# for quantized weights.
|
||||
# In the quantized case, the weights are already transposed.
|
||||
# Also, in addition to the quantized weights,
|
||||
# the zero points and scales have to be reshaped as well.
|
||||
# Packing should not be affected by this.
|
||||
if (
|
||||
".mixer.in_proj.weight" in name
|
||||
or "mixer.in_proj.qweight" in name
|
||||
or "mixer.in_proj.scales" in name
|
||||
or "mixer.in_proj.qzeros" in name
|
||||
):
|
||||
if "mixer.in_proj.weight" in name:
|
||||
loaded_weight = loaded_weight.transpose(0, 1)
|
||||
# for weight:
|
||||
# loaded_weight.shape[0] == self.config.hidden_size
|
||||
# for qweight:
|
||||
# loaded_weight.shape[0] == self.config.hidden_size // param.pack_factor # noqa
|
||||
# for scales and qzeros:
|
||||
# loaded_weight.shape[0] == self.config.hidden_size // self.vllm_config.quant_config.group_size # noqa
|
||||
loaded_weight = loaded_weight.reshape(
|
||||
loaded_weight.shape[0], self.config.mamba_num_heads, -1
|
||||
)
|
||||
gate_weight, hidden_states_weight = loaded_weight.chunk(2, dim=-1)
|
||||
gate_weight = gate_weight.reshape(loaded_weight.shape[0], -1)
|
||||
hidden_states_weight = hidden_states_weight.reshape(
|
||||
loaded_weight.shape[0], -1
|
||||
)
|
||||
loaded_weight = torch.cat([gate_weight, hidden_states_weight], dim=-1)
|
||||
if "mixer.in_proj.weight" in name:
|
||||
loaded_weight = loaded_weight.transpose(0, 1)
|
||||
|
||||
# Offset parameter with vllm's RMSNorm haven't been supported yet.
|
||||
if ".pre_mixer_norm" in name:
|
||||
loaded_weight += 1.0
|
||||
elif ".post_mixer_norm" in name:
|
||||
loaded_weight += 1.0 / 5
|
||||
elif ".pre_mlp_norm" in name:
|
||||
loaded_weight += 1.0
|
||||
elif ".post_mlp_norm" in name:
|
||||
loaded_weight += 1.0 / (5**1.5)
|
||||
elif "model.norm.weight" in name:
|
||||
loaded_weight += 1.0
|
||||
|
||||
# Skip layers on other devices.
|
||||
if is_pp_missing_parameter(name, self):
|
||||
continue
|
||||
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
loader = AutoWeightsLoader(
|
||||
self,
|
||||
skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
|
||||
)
|
||||
return loader.load_weights(weights)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# QianfanOCR is built on InternVL with a Qwen3 language backbone.
|
||||
# The model architecture and weights are fully compatible with InternVLChatModel,
|
||||
# only the config model_type / architectures strings differ.
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
from vllm.model_executor.layers.quantization import QuantizationConfig
|
||||
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
|
||||
from vllm.multimodal import MULTIMODAL_REGISTRY
|
||||
from vllm.transformers_utils.processors.internvl import (
|
||||
InternVLImageProcessor,
|
||||
InternVLProcessor,
|
||||
)
|
||||
|
||||
from .internvl import (
|
||||
BaseInternVLDummyInputsBuilder,
|
||||
BaseInternVLMultiModalProcessor,
|
||||
BaseInternVLProcessingInfo,
|
||||
InternVLChatModel,
|
||||
)
|
||||
|
||||
|
||||
class QianfanOCRProcessingInfo(BaseInternVLProcessingInfo):
|
||||
"""Image-only ProcessingInfo for QianfanOCR (no video support)."""
|
||||
|
||||
def get_hf_processor(self, **kwargs: object) -> InternVLProcessor:
|
||||
config = self.get_hf_config()
|
||||
vision_config = config.vision_config
|
||||
|
||||
kwargs = self.ctx.get_merged_mm_kwargs(kwargs)
|
||||
kwargs.setdefault("image_size", vision_config.image_size)
|
||||
kwargs.setdefault("min_dynamic_patch", config.min_dynamic_patch)
|
||||
kwargs.setdefault("max_dynamic_patch", config.max_dynamic_patch)
|
||||
kwargs.setdefault("dynamic_image_size", config.dynamic_image_size)
|
||||
kwargs.setdefault("use_thumbnail", config.use_thumbnail)
|
||||
|
||||
image_processor = InternVLImageProcessor(**kwargs)
|
||||
image_size = image_processor.image_size
|
||||
patch_size = vision_config.patch_size
|
||||
downsample_ratio = config.downsample_ratio
|
||||
image_seq_length = int((image_size // patch_size) ** 2 * (downsample_ratio**2))
|
||||
|
||||
return InternVLProcessor(
|
||||
tokenizer=self.get_tokenizer(),
|
||||
image_processor=image_processor,
|
||||
video_processor=None,
|
||||
image_seq_length=image_seq_length,
|
||||
ctx_video_token=None,
|
||||
)
|
||||
|
||||
|
||||
@MULTIMODAL_REGISTRY.register_processor(
|
||||
BaseInternVLMultiModalProcessor,
|
||||
info=QianfanOCRProcessingInfo,
|
||||
dummy_inputs=BaseInternVLDummyInputsBuilder,
|
||||
)
|
||||
class QianfanOCRForConditionalGeneration(InternVLChatModel):
|
||||
"""QianfanOCR multimodal model.
|
||||
|
||||
Identical in structure to InternVLChatModel (InternViT vision encoder +
|
||||
pixel-shuffle MLP connector + Qwen3 language model). This class exists
|
||||
solely to register the ``QianfanOCRForConditionalGeneration`` architecture
|
||||
name that appears in the model's config.json.
|
||||
"""
|
||||
|
||||
def _patch_quant_config(
|
||||
self, config: PretrainedConfig, quant_config: QuantizationConfig
|
||||
) -> None:
|
||||
super()._patch_quant_config(config, quant_config)
|
||||
# ignore vit layers to preserve model performance
|
||||
if isinstance(quant_config, Fp8Config):
|
||||
_FP8_IGNORED_LAYERS = [
|
||||
*(
|
||||
layer
|
||||
for i in range(config.vision_config.num_hidden_layers)
|
||||
for layer in [
|
||||
f"vision_model.encoder.layers.{i}.attn.qkv",
|
||||
f"vision_model.encoder.layers.{i}.attn.proj",
|
||||
f"vision_model.encoder.layers.{i}.mlp.fc1",
|
||||
f"vision_model.encoder.layers.{i}.mlp.fc2",
|
||||
]
|
||||
),
|
||||
"language_model.lm_head",
|
||||
"mlp1.1",
|
||||
"mlp1.3",
|
||||
]
|
||||
for layer in _FP8_IGNORED_LAYERS:
|
||||
if layer not in quant_config.ignored_layers:
|
||||
quant_config.ignored_layers.append(layer)
|
||||
@@ -511,6 +511,10 @@ _MULTIMODAL_MODELS = {
|
||||
"Phi4ForCausalLMV": ("phi4siglip", "Phi4ForCausalLMV"),
|
||||
"Phi4MMForCausalLM": ("phi4mm", "Phi4MMForCausalLM"),
|
||||
"PixtralForConditionalGeneration": ("pixtral", "PixtralForConditionalGeneration"),
|
||||
"QianfanOCRForConditionalGeneration": (
|
||||
"qianfan_ocr",
|
||||
"QianfanOCRForConditionalGeneration",
|
||||
),
|
||||
"QwenVLForConditionalGeneration": ("qwen_vl", "QwenVLForConditionalGeneration"),
|
||||
"Qwen2VLForConditionalGeneration": ("qwen2_vl", "Qwen2VLForConditionalGeneration"),
|
||||
"Qwen2_5_VLForConditionalGeneration": (
|
||||
|
||||
@@ -268,28 +268,6 @@ class InputProcessingContext:
|
||||
try:
|
||||
output = hf_processor(**data, **allowed_kwargs)
|
||||
except Exception as exc:
|
||||
# See https://github.com/huggingface/tokenizers/issues/537
|
||||
if (
|
||||
isinstance(exc, RuntimeError)
|
||||
and exc
|
||||
and exc.args[0] == "Already borrowed"
|
||||
and num_tries < max_tries
|
||||
):
|
||||
logger.warning(
|
||||
"Failed to acquire tokenizer in current thread. "
|
||||
"Retrying (%d/%d)...",
|
||||
num_tries,
|
||||
max_tries,
|
||||
)
|
||||
time.sleep(0.5)
|
||||
return self.call_hf_processor(
|
||||
hf_processor,
|
||||
data,
|
||||
kwargs,
|
||||
num_tries=num_tries + 1,
|
||||
max_tries=max_tries,
|
||||
)
|
||||
|
||||
msg = (
|
||||
f"Failed to apply {type(hf_processor).__name__} "
|
||||
f"on data={data} with kwargs={allowed_kwargs}"
|
||||
|
||||
@@ -545,6 +545,42 @@ class Platform:
|
||||
dtype=kv_cache_dtype,
|
||||
kv_quant_mode=kv_quant_mode,
|
||||
).page_size_bytes
|
||||
elif cache_config.cache_dtype.startswith("turboquant_"):
|
||||
# TQ has a packed K|V layout; the standard FullAttentionSpec
|
||||
# formula over-sizes it and trips unify_kv_cache_spec_page_size
|
||||
# when all attention layers are TQ. With mixed skip+TQ the skip
|
||||
# layers still use the standard layout — take max so mamba
|
||||
# padding covers the largest actual page.
|
||||
from vllm.model_executor.layers.quantization.turboquant.config import (
|
||||
TurboQuantConfig,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import TQFullAttentionSpec
|
||||
|
||||
tq_cfg = TurboQuantConfig.from_cache_dtype(
|
||||
cache_config.cache_dtype, model_config.get_head_size()
|
||||
)
|
||||
tq_page = TQFullAttentionSpec(
|
||||
block_size=1,
|
||||
num_kv_heads=model_config.get_num_kv_heads(parallel_config),
|
||||
head_size=model_config.get_head_size(),
|
||||
head_size_v=model_config.get_head_size(),
|
||||
dtype=kv_cache_dtype,
|
||||
kv_quant_mode=kv_quant_mode,
|
||||
tq_slot_size=tq_cfg.slot_size_aligned,
|
||||
).page_size_bytes
|
||||
if cache_config.kv_cache_dtype_skip_layers:
|
||||
skip_page = FullAttentionSpec(
|
||||
block_size=1,
|
||||
num_kv_heads=model_config.get_num_kv_heads(parallel_config),
|
||||
head_size=model_config.get_head_size(),
|
||||
dtype=model_config.dtype,
|
||||
).page_size_bytes
|
||||
# lcm, not max: skip_page is often not a multiple of
|
||||
# tq_page, so max would leave per-layer page sizes
|
||||
# un-unifiable downstream.
|
||||
attn_page_size_1_token = lcm(tq_page, skip_page)
|
||||
else:
|
||||
attn_page_size_1_token = tq_page
|
||||
else:
|
||||
attn_page_size_1_token = FullAttentionSpec(
|
||||
block_size=1,
|
||||
|
||||
@@ -409,6 +409,7 @@ class RocmPlatform(Platform):
|
||||
"gptq",
|
||||
"gptq_marlin", # will be overwritten with gptq
|
||||
"fp8",
|
||||
"deepseek_v4_fp8",
|
||||
"compressed-tensors",
|
||||
"fbgemm_fp8",
|
||||
"gguf",
|
||||
|
||||
+2
-11
@@ -1,7 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import asyncio
|
||||
import copy
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping, Sequence
|
||||
@@ -108,17 +107,10 @@ class BaseRenderer(ABC, Generic[_T]):
|
||||
if mm_registry.supports_multimodal_inputs(config.model_config):
|
||||
mm_processor_cache = mm_registry.processor_cache_from_config(config)
|
||||
|
||||
# Deep-copy the tokenizer so the multimodal processor gets its
|
||||
# own Rust tokenizer backend. Without this, concurrent access
|
||||
# from AsyncMicrobatchTokenizer and call_hf_processor causes
|
||||
# "RuntimeError: Already borrowed" from the Rust RefCell.
|
||||
# See: https://github.com/huggingface/tokenizers/issues/537
|
||||
mm_tokenizer = copy.deepcopy(tokenizer)
|
||||
|
||||
with set_default_torch_num_threads():
|
||||
self.mm_processor = mm_registry.create_processor(
|
||||
config.model_config,
|
||||
tokenizer=mm_tokenizer,
|
||||
tokenizer=self.tokenizer,
|
||||
cache=mm_processor_cache,
|
||||
)
|
||||
|
||||
@@ -130,11 +122,10 @@ class BaseRenderer(ABC, Generic[_T]):
|
||||
# requests don't pollute the sender cache.
|
||||
ro_cache = mm_registry.processor_only_cache_from_config(config)
|
||||
if ro_cache is not None:
|
||||
ro_tokenizer = copy.deepcopy(tokenizer)
|
||||
with set_default_torch_num_threads():
|
||||
self._readonly_mm_processor = mm_registry.create_processor(
|
||||
config.model_config,
|
||||
tokenizer=ro_tokenizer,
|
||||
tokenizer=self.tokenizer,
|
||||
cache=ro_cache,
|
||||
)
|
||||
|
||||
|
||||
+15
-1
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import itertools
|
||||
import weakref
|
||||
@@ -42,7 +43,7 @@ from vllm.multimodal.processing.processor import (
|
||||
apply_token_matches,
|
||||
find_mm_placeholders,
|
||||
)
|
||||
from vllm.tokenizers.hf import HfTokenizer
|
||||
from vllm.tokenizers.hf import HfTokenizer, maybe_make_thread_pool
|
||||
from vllm.transformers_utils.chat_templates import get_chat_template_fallback_path
|
||||
from vllm.transformers_utils.processor import cached_get_processor
|
||||
from vllm.utils.async_utils import make_async
|
||||
@@ -785,6 +786,14 @@ class HfRenderer(BaseRenderer[HfTokenizer]):
|
||||
config: VllmConfig,
|
||||
tokenizer: HfTokenizer | None,
|
||||
) -> None:
|
||||
# Ensure the og tokenizer is never modified by maybe_make_thread_pool
|
||||
tokenizer = copy.copy(tokenizer)
|
||||
if (
|
||||
# Skip for mock configs and tokenizers
|
||||
getattr(config.model_config, "enable_prompt_embeds", False)
|
||||
and isinstance(tokenizer, HfTokenizer)
|
||||
):
|
||||
_ensure_prompt_embeds_placeholder_token(tokenizer)
|
||||
super().__init__(config, tokenizer)
|
||||
|
||||
self.use_unified_vision_chunk = getattr(
|
||||
@@ -795,6 +804,11 @@ class HfRenderer(BaseRenderer[HfTokenizer]):
|
||||
safe_apply_chat_template, executor=self._executor
|
||||
)
|
||||
|
||||
if self.tokenizer is not None:
|
||||
maybe_make_thread_pool(
|
||||
self.tokenizer, config.model_config.renderer_num_workers + 1
|
||||
)
|
||||
|
||||
def render_messages(
|
||||
self,
|
||||
messages: list[ChatCompletionMessageParam],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from .hf import maybe_make_thread_pool
|
||||
from .protocol import TokenizerLike
|
||||
from .registry import (
|
||||
TokenizerRegistry,
|
||||
@@ -15,4 +16,5 @@ __all__ = [
|
||||
"cached_get_tokenizer",
|
||||
"get_tokenizer",
|
||||
"cached_tokenizer_from_config",
|
||||
"maybe_make_thread_pool",
|
||||
]
|
||||
|
||||
+92
-2
@@ -2,8 +2,9 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import contextlib
|
||||
import copy
|
||||
import queue
|
||||
from pathlib import Path
|
||||
from typing import TypeAlias
|
||||
from typing import TypeAlias, TypeVar
|
||||
|
||||
from transformers import AutoTokenizer, PreTrainedTokenizer, PreTrainedTokenizerFast
|
||||
|
||||
@@ -12,6 +13,92 @@ from vllm.transformers_utils.config import get_sentence_transformer_tokenizer_co
|
||||
from .protocol import TokenizerLike
|
||||
|
||||
HfTokenizer: TypeAlias = PreTrainedTokenizer | PreTrainedTokenizerFast
|
||||
_T = TypeVar("_T", bound=TokenizerLike)
|
||||
|
||||
|
||||
class ThreadSafeHFTokenizerMixin:
|
||||
"""Mixin class for thread-safe HF fast tokenizers."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def maybe_make_thread_pool(tokenizer: _T, copies: int = 1):
|
||||
"""
|
||||
If `tokenizer` is a `PreTrainedTokenizerFast`, modify the tokenizer
|
||||
in-place to make the public interface thread-safe by routing calls
|
||||
through a deep-copied tokenizer pool.
|
||||
|
||||
Note that:
|
||||
- Only ``TokenizerLike``'s public interface is thread-safe.
|
||||
This doesn't include ``_tokenizer`` property nor any mutation
|
||||
methods like ``add_special_tokens`` or ``add_tokens``.
|
||||
- Adjacent method calls could happen on different deep copies.
|
||||
"""
|
||||
if not isinstance(tokenizer, PreTrainedTokenizerFast) or isinstance(
|
||||
tokenizer, ThreadSafeHFTokenizerMixin
|
||||
):
|
||||
return tokenizer
|
||||
|
||||
og_tokenizer = copy.copy(tokenizer)
|
||||
|
||||
tokenizer_pool: queue.Queue[PreTrainedTokenizerFast] = queue.Queue()
|
||||
for _ in range(copies):
|
||||
tokenizer_pool.put(copy.deepcopy(og_tokenizer))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _borrow_from_pool():
|
||||
try:
|
||||
tok = tokenizer_pool.get_nowait()
|
||||
yield tok
|
||||
except queue.Empty:
|
||||
tok = copy.deepcopy(og_tokenizer)
|
||||
yield tok
|
||||
finally:
|
||||
tokenizer_pool.put(tok)
|
||||
|
||||
class TokenizerPool(tokenizer.__class__, ThreadSafeHFTokenizerMixin): # type: ignore
|
||||
def apply_chat_template(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.apply_chat_template(*args, **kwargs)
|
||||
|
||||
def batch_decode(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.batch_decode(*args, **kwargs)
|
||||
|
||||
def batch_encode(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.batch_encode(*args, **kwargs)
|
||||
|
||||
def convert_tokens_to_ids(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.convert_tokens_to_ids(*args, **kwargs)
|
||||
|
||||
def convert_ids_to_tokens(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.convert_ids_to_tokens(*args, **kwargs)
|
||||
|
||||
def convert_tokens_to_string(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.convert_tokens_to_string(*args, **kwargs)
|
||||
|
||||
def decode(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.decode(*args, **kwargs)
|
||||
|
||||
def encode(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok.encode(*args, **kwargs)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
with _borrow_from_pool() as tok:
|
||||
return tok(*args, **kwargs)
|
||||
|
||||
def __reduce__(self):
|
||||
return maybe_make_thread_pool, (og_tokenizer, copies)
|
||||
|
||||
TokenizerPool.__name__ = f"TokenizerPool{og_tokenizer.__class__.__name__}"
|
||||
|
||||
tokenizer.__class__ = TokenizerPool
|
||||
|
||||
|
||||
def get_cached_tokenizer(tokenizer: HfTokenizer) -> HfTokenizer:
|
||||
@@ -103,7 +190,10 @@ class CachedHfTokenizer(TokenizerLike):
|
||||
"is a custom tokenizer not yet available in the "
|
||||
"HuggingFace transformers library, consider "
|
||||
"setting `trust_remote_code=True` in LLM or using "
|
||||
"the `--trust-remote-code` flag in the CLI."
|
||||
"the `--trust-remote-code` flag in the CLI. If the "
|
||||
"model was created with a newer version of "
|
||||
"transformers, consider upgrading: "
|
||||
"`uv pip install --upgrade transformers`"
|
||||
)
|
||||
raise RuntimeError(err_msg) from e
|
||||
else:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user