Compare commits

...
Author SHA1 Message Date
Yongye ZhuandClaude Opus 4.7 58c8a5eaa5 [Attention][TokenSpeed MLA] Also warm up prefill kernel from decode impl
The prefill backend may be paired with flash_attn / trtllm in production —
in that case the prefill backend's __init__ never runs and the prefill
kernel's first call pays a 1.5–2 minute JIT cost. Add the same idempotent
`warmup_compile_prefill` invocation to TokenspeedMLAImpl.__init__ (the
decode-side backend, always present when tokenspeed is selected).

The function dedupes by config key, so the double call is a no-op when both
backends are tokenspeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 07:45:16 +00:00
Yongye Zhu c4547482ca update version
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 07:28:20 +00:00
Yongye Zhu 91ef0afcb2 adding to cuda.txt dependency
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 07:20:48 +00:00
Yongye Zhu 97cd2c41ad fix precommit
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 03:14:49 +00:00
Yongye ZhuandClaude Opus 4.7 c001535038 [Attention][TokenSpeed MLA] Force prefill V tensor contiguous before kernel call
`v` arrives at both `run_prefill_new_tokens` and `run_prefill_context_chunk`
as the second half of `kv_nope.split([qk_nope_head_dim, v_head_dim], dim=-1)`
in mla_attention.py — a non-contiguous view along the last dim. The kernel
internally does `v.reshape(1, total_kv, h_k, 1, d_v)` which silently copies
when the input is non-contiguous; pull that copy out so the layout is
predictable at the kernel boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 02:53:46 +00:00
Yongye ZhuandClaude Opus 4.7 de6bc297df [Attention][TokenSpeed MLA] Surface install hint when package missing
Previously a user explicitly selecting TOKENSPEED_MLA without `tokenspeed_mla`
installed got either a generic "required dependencies not available" message
(prefill backend) or a raw ModuleNotFoundError deep inside forward_mqa at the
first request (decode backend). Now both backends fail at startup with the
exact install command: `uv pip install tokenspeed-mla`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 02:49:09 +00:00
Yongye ZhuandClaude Opus 4.7 0012818287 [Attention][TokenSpeed MLA] Fix trtllm LSE parity test: log2 → natural log
trtllm_ragged_attention_deepseek returns LSE in log2; tokenspeed and
merge_attn_states use natural log. Multiply the trtllm reference by ln 2
before comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 02:49:09 +00:00
Yongye ZhuandClaude Opus 4.7 73cd7e25ae [Attention][TokenSpeed MLA] Warm up BF16 prefill compile, drop seq_lens computation
Pre-JIT both BF16 and FP8 prefill kernels at backend init since the dtype
isn't visible from `__init__` — depends on `use_prefill_query_quantization`.
Move the per-forward `seq_lens` computation into `prepare_metadata` and
document the cuda-graph padding interaction with `query_start_loc`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 02:49:09 +00:00
Yongye ZhuandClaude Opus 4.7 964c6eb485 [Attention][TokenSpeed MLA] Fix decode FP8 numerics: pass output_scale and assert FP8 Q
The decode kernel needs both bmm scales to recover correct outputs from an
FP8 KV cache: bmm1 (softmax_scale = scale * q_scale * k_scale) and bmm2
(output_scale = k_scale, since V is stored as V_real / k_scale). We were
only passing bmm1, which left bmm2 = 1.0 and produced silently wrong output.

Also assert query dtype is float8_e4m3fn on entry to forward_mqa.
supports_quant_query_input=True (inherited from MLACommonImpl) tells the
upstream pipeline to FP8-quantize Q via _decode_concat_quant_fp8_op; the
kernel is shape-specialized for FP8 Q + FP8 KV, so any other dtype here
means the upstream quant path didn't run and the kernel will produce
garbage. Failing loud beats failing silent.

Verified: gsm8k matches reference with TOKENSPEED_MLA decode +
FLASH_ATTN prefill on Kimi-K2.5-NVFP4 / TP=4 / B200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 02:49:09 +00:00
Yongye ZhuandClaude Opus 4.7 d0e6514bf8 [Attention] Add TOKENSPEED_MLA backend for DeepSeek R1 prefill + decode on Blackwell
Wires the tokenspeed_mla CuTe DSL kernels into vLLM as a new MLA backend,
covering both prefill (tokenspeed_mla_prefill) and decode
(tokenspeed_mla_decode). Targets Blackwell (SM100) with FP8 KV cache and
DeepSeek R1 MLA dimensions; users opt in via -ac
'{"backend":"TOKENSPEED_MLA","mla_prefill_backend":"TOKENSPEED_MLA"}'.
Includes numeric parity tests against the trtllm reference kernels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-05-06 02:49:09 +00:00
ChaunceyandGitHub c7aa186d67 [Frontend] Supports resubmitting output items with missing fields in Responses API (#41355)
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
2026-05-05 22:21:33 -04:00
f653761252 [CI] Route part of B200 jobs to b200-k8s (#41453)
Signed-off-by: khluu <khluu000@gmail.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-05-05 19:00:30 -07:00
Andreas KaratzasandGitHub 4a8ae26e53 [ROCm][CI] Use vLLM generation defaults for DeepSeek prefetch-offload eval (#41575)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
2026-05-06 01:08:12 +00:00
Kevin H. LuuandGitHub 1333864408 [CI] Automate Docker Hub release image publishing (#40415)
Signed-off-by: khluu <khluu000@gmail.com>
2026-05-06 00:15:23 +00:00
Matthew BonanniandGitHub 01b9b5af67 [Attention] Minor refactor: layer takes ownership of the MLA prefill backend (#41744)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>
2026-05-05 23:22:41 +00:00
8c57b6e7bc Bump model-hosting-container-standards to >= 0.1.14 (#39755)
Signed-off-by: EC2 Default User <ec2-user@ip-172-31-20-13.us-west-2.compute.internal>
Co-authored-by: EC2 Default User <ec2-user@ip-172-31-20-13.us-west-2.compute.internal>
Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
2026-05-05 19:09:57 -04:00
Lanze LiuandGitHub 79246b5ea6 [Spec Decode] Fix max_model_len logging in speculative config for draft model (#41571)
Signed-off-by: Lanze Liu <lanzetech@gmail.com>
2026-05-05 21:56:06 +00:00
48954de237 Fix DeepGEMM ep_scatter output address overflow (#39213)
Signed-off-by: S1ro1 <matej.sirovatka@gmail.com>
Co-authored-by: Tyler Michael Smith <tyler@neuralmagic.com>
2026-05-05 18:56:56 +00:00
Julien DenizeandGitHub c6235ed180 [BUGFIX] Support streamed_args_for_tool in MistralToolParser (#41730)
Signed-off-by: juliendenize <julien.denize@mistral.ai>
2026-05-05 17:48:53 +00:00
628c436301 [New Model][ROCm] Add AMD support for DeepSeek V4 (#40871)
Signed-off-by: ganyi <ygan@amd.com>
Signed-off-by: whx-sjtu <xiaowang990929@gmail.com>
Signed-off-by: tjtanaa <tunjian.tan@embeddedllm.com>
Signed-off-by: tjtanaavllm <tunjian.tan@amd.com>
Co-authored-by: ganyi <ygan@amd.com>
Co-authored-by: tjtanaa <tunjian.tan@embeddedllm.com>
Co-authored-by: tjtanaavllm <tunjian.tan@amd.com>
2026-05-05 08:55:37 -07:00
Canlin GuoandGitHub 2228fe6868 [Attention] Move FA3→FA4 upgrade into get_flash_attn_version() (#40815)
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
2026-05-05 15:43:03 +00:00
Harry MellorandGitHub 84bd8a3c1e Remove unnecessary runtime asserts from linear layers (#41729)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
2026-05-05 14:42:56 +00:00
Lidang JiangandGitHub b786ec8e74 [Bugfix] Suggest upgrading Transformers for tokenizer class errors (#38099)
Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
2026-05-05 14:10:45 +00:00
20dcd984f9 [Bugfix] Fix RuntimeError: Already borrowed by adding thread-safe Hugging Face fast-tokenizer wrappers (#41181)
Signed-off-by: Yifan Zong <yzong@redhat.com>
Co-authored-by: wang.yuqi <yuqi.wang@daocloud.io>
2026-05-05 14:04:01 +00:00
Martin HickeyandGitHub 6fca518157 [BugFix][MyPy]: Module has no attribute "sched_getaffinity" [attr-defined] (#41465)
Signed-off-by: Martin Hickey <martin.hickey@ie.ibm.com>
2026-05-05 13:20:37 +00:00
98661fe012 [Bugfix][KVConnector] Support DCP/PCP in OffloadingConnector (#41549)
Signed-off-by: Itay Etelis <itay.etelis@ibm.com>
Co-authored-by: Itay Etelis <itay.etelis@ibm.com>
2026-05-05 14:54:29 +03:00
Harry MellorandGitHub b0765bee17 Fix DeepSeek-OCR for Transformers v4 (#41460)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
2026-05-05 11:11:21 +00:00
bairongzGitHubzhuangbaironggemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
0a201b60cf [Model] support Qianfan-OCR model (#40136)
Signed-off-by: bairongz <baiyuu.cs@gmail.com>
Signed-off-by: zhuangbairong <zhuangbairong@baidu.com>
Co-authored-by: zhuangbairong <zhuangbairong@baidu.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-05-05 10:51:25 +00:00
8b9ea2f881 [Feature] Add Triton kernel JIT compilation monitor for inference (#40137)
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com>
Co-authored-by: Jiangyun Zhu <riverclouds.zhu@qq.com>
2026-05-05 14:08:57 +04:00
Kunshang JiandGitHub 2ceea42958 [XPU] use xpu topk topp sample kernel (#39285)
Signed-off-by: Kunshang Ji <kunshang.ji@intel.com>
2026-05-05 18:05:17 +08:00
bee126165f [P/D][Mooncake] Add KVConnectorStats for transfer observability (#40414)
Signed-off-by: Zhewen Li <zhewenli@inferact.ai>
Co-authored-by: Zhewen Li <zhewenli@inferact.ai>
2026-05-05 02:17:38 -07:00
BitTobyandGitHub 27cc676be3 [Model] Use AutoWeightsLoader for Plamo2 (#41699)
Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>
2026-05-05 08:56:24 +00:00
4845aee6b7 [Benchmark] Add --trust-remote-code flag to multi-turn benchmark (#41661)
Signed-off-by: Dao Le <daole@inferact.ai>
Signed-off-by: Dao Le <Dao007forever@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-05 01:00:37 -07:00
BitTobyandGitHub 0c620d2e08 [Model] Use AutoWeightsLoader for CohereMoe (#41690)
Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>
2026-05-05 04:44:15 +00:00
6bb924bbf3 [Model] Fix Gemma4 MoE activation mismatch (#41574)
Signed-off-by: Luciano Martins <lucianommartins@users.noreply.github.com>
Co-authored-by: Luciano Martins <lucianommartins@users.noreply.github.com>
2026-05-05 04:34:11 +00:00
czhu-cohereandGitHub eaec7be446 [BugFix] Preserve max_seq_len in ubatch metadata during CUDA graph capture (#40961)
Signed-off-by: root <conway.zhu@cohere.com>
Signed-off-by: <conway.zhu@cohere.com>
2026-05-05 04:29:34 +00:00
Jeffrey WangandGitHub f04fd1677b [Ray] Enable RayExecutorV2 by default (#41421)
Signed-off-by: Jeffrey Wang <jeffreywang@anyscale.com>
2026-05-05 04:27:34 +00:00
420b0a5c95 [Hardware][Power]Add Power VSX Attention Backend and fix l2 Cache Crash (#40451)
Signed-off-by: Akash Kaothalkar <akashkaothalkar@akashs-mbp.bl1-in.ibm.com>
Signed-off-by: Akash Kaothalkar <akash.kaothalkar@ibm.com>
Signed-off-by: Akash kaothalkar <akash.kaothalkar@ibm.com>
Co-authored-by: Akash Kaothalkar <akashkaothalkar@akashs-mbp.bl1-in.ibm.com>
Co-authored-by: Akash Kaothalkar <akash.kaothalkar@ibm.com>
Co-authored-by: Li, Jiang <jiang1.li@intel.com>
2026-05-04 20:51:09 -07:00
Bowen BaoandGitHub 1e9500410a [ROCm][Quantization][2/N] Refactor quark_moe w4a8 w/ oracle (#39136)
Signed-off-by: Bowen Bao <bowenbao@amd.com>
2026-05-04 19:50:38 -07:00
Nick HillandGitHub 416f9cdede [Perf][2/n] Eliminate GPU<->CPU syncs in pooling code (#41433)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
2026-05-05 02:43:25 +00:00
685bf811d6 [XPU] enable is_act_and_mul for xpu (#37481)
Signed-off-by: Chendi Xue <chendi.xue@intel.com>
Co-authored-by: Kunshang Ji <kunshang.ji@intel.com>
2026-05-05 01:07:39 +00:00
Giancarlo DelfinandGitHub e1e4646b06 [Model Runner V2] Rebuild attn metadata between draft decode steps (#41162)
Signed-off-by: Giancarlo Delfin <gdelfin@inferact.ai>
2026-05-05 00:44:55 +00:00
4f2af1a7c0 [Feature] TurboQuant: support hybrid models and uniform quantization (#39931)
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: Jim Smith <jhsmith0@me.com>
Co-authored-by: Jim Smith <jhsmith0@me.com>
Co-authored-by: Sandermage <sandermage@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-04 20:14:01 -04:00
Wentao YeandGitHub 577b9623e6 [Bug] Fix status update address for non-MOE model within external dp mode (#40839)
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-05-04 16:37:16 -07:00
Andreas KaratzasandGitHub 1cb0838721 [ROCm][CI] Fix MLA prefill scale for DeepSeek GSM8K (#41569)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
2026-05-04 16:32:55 -07:00
128 changed files with 5572 additions and 1114 deletions
+38 -1
View File
@@ -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:
+1 -93
View File
@@ -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
+180
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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/"
+4 -4
View File
@@ -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/
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+2 -1
View File
@@ -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
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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)
+4
View File
@@ -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);
}
+4 -1
View File
@@ -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
+2 -2
View File
@@ -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)));
}
}
+359
View File
@@ -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
+4
View File
@@ -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__) \
+13 -2
View File
@@ -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
View File
@@ -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
+32 -21
View File
@@ -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);
}
+1 -2
View File
@@ -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) -> ()");
-2
View File
@@ -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(
+2
View File
@@ -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 |
+1
View File
@@ -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. | ✅︎ | ✅︎ |
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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,
)
+388 -8
View File
@@ -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"])
+4 -2
View File
@@ -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
+4
View File
@@ -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"},
+82 -4
View File
@@ -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)
# ============================================================================
-2
View File
@@ -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
+240
View File
@@ -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(
+19 -3
View File
@@ -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)
+1 -1
View File
@@ -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
+21
View File
@@ -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
+35
View File
@@ -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
+2
View File
@@ -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.
+4 -2
View File
@@ -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
+9 -1
View File
@@ -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
View File
@@ -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, (
+60 -10
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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,
+5 -9
View File
@@ -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
+105 -2
View File
@@ -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),
+34 -4
View File
@@ -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.")
+63 -59
View File
@@ -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)
+6 -1
View File
@@ -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(
+1 -1
View File
@@ -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:
+83 -85
View File
@@ -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)
+92
View File
@@ -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)
+4
View File
@@ -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": (
-22
View File
@@ -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}"
+36
View File
@@ -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,
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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],
+2
View File
@@ -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
View File
@@ -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:
+26 -15
View File
@@ -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