forked from Karylab-cklius/vllm
Compare commits
120
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4e83934d | ||
|
|
628c436301 | ||
|
|
2228fe6868 | ||
|
|
84bd8a3c1e | ||
|
|
b786ec8e74 | ||
|
|
20dcd984f9 | ||
|
|
6fca518157 | ||
|
|
98661fe012 | ||
|
|
b0765bee17 | ||
|
|
0a201b60cf | ||
|
|
8b9ea2f881 | ||
|
|
2ceea42958 | ||
|
|
bee126165f | ||
|
|
27cc676be3 | ||
|
|
4845aee6b7 | ||
|
|
0c620d2e08 | ||
|
|
6bb924bbf3 | ||
|
|
eaec7be446 | ||
|
|
f04fd1677b | ||
|
|
420b0a5c95 | ||
|
|
1e9500410a | ||
|
|
416f9cdede | ||
|
|
685bf811d6 | ||
|
|
e1e4646b06 | ||
|
|
4f2af1a7c0 | ||
|
|
577b9623e6 | ||
|
|
1cb0838721 | ||
|
|
be5983b874 | ||
|
|
9c07342fdc | ||
|
|
844df54269 | ||
|
|
422dd02598 | ||
|
|
8c780943b4 | ||
|
|
e724b0ea8d | ||
|
|
712ad0286c | ||
|
|
321fa2d6d1 | ||
|
|
3e1ad4435f | ||
|
|
8decbfa02c | ||
|
|
62ba7516e8 | ||
|
|
6f53753fc9 | ||
|
|
6ec9bbec38 | ||
|
|
01d4d1ad37 | ||
|
|
c103c02a1a | ||
|
|
67058ca326 | ||
|
|
894a02500b | ||
|
|
66dfee7121 | ||
|
|
db9a84e0cd | ||
|
|
cb03fee32b | ||
|
|
c51df43005 | ||
|
|
54dc64d5d3 | ||
|
|
e6ff3e9c83 | ||
|
|
08834cc3ce | ||
|
|
856ec4804a | ||
|
|
1c607d7b2c | ||
|
|
4f7309fcc0 | ||
|
|
0a9362d6ab | ||
|
|
cfd2573f23 | ||
|
|
c3ad791e1a | ||
|
|
8586369f61 | ||
|
|
ae3b4deb8a | ||
|
|
c293ccc58e | ||
|
|
d58c42e19c | ||
|
|
3e49479c4b | ||
|
|
964a4bc2a5 | ||
|
|
c408fdd663 | ||
|
|
5737770c6c | ||
|
|
0c99629ede | ||
|
|
edd60ac93a | ||
|
|
bcf5cac9fb | ||
|
|
a9484dac7b | ||
|
|
f3fef12350 | ||
|
|
51295793a2 | ||
|
|
3ccc1ff495 | ||
|
|
529c671e80 | ||
|
|
bc635fad23 | ||
|
|
c3e64696cd | ||
|
|
4f7bde572a | ||
|
|
2fa1f8ec00 | ||
|
|
7075df79b3 | ||
|
|
0dbaf9daad | ||
|
|
a3ec4a35f5 | ||
|
|
32964e7700 | ||
|
|
a07642667d | ||
|
|
c3868bbbe4 | ||
|
|
947138b6c2 | ||
|
|
941fb50835 | ||
|
|
6b6ac6c3c7 | ||
|
|
b542bdf7fb | ||
|
|
415a879899 | ||
|
|
7198940b39 | ||
|
|
14043dfecd | ||
|
|
1adaa5056b | ||
|
|
4d5c89295b | ||
|
|
dd5506a157 | ||
|
|
a3c83ff2fd | ||
|
|
9c61864bf8 | ||
|
|
71725f6730 | ||
|
|
b4806c8ee1 | ||
|
|
526927be94 | ||
|
|
75a4c166f2 | ||
|
|
2917d6363a | ||
|
|
efb4cdf2b8 | ||
|
|
92a7c121b6 | ||
|
|
307b17ce33 | ||
|
|
3ca6ca210f | ||
|
|
10558f5f46 | ||
|
|
121dbe7a22 | ||
|
|
f03d82efdd | ||
|
|
a7fb008510 | ||
|
|
ff449b6426 | ||
|
|
3527229517 | ||
|
|
b55b26520c | ||
|
|
3179e53135 | ||
|
|
efdc95674d | ||
|
|
54146a9bf9 | ||
|
|
ca97f7b9bb | ||
|
|
a04e0cf3b8 | ||
|
|
cb1b02d0e8 | ||
|
|
a749a33d8d | ||
|
|
c42981d034 | ||
|
|
0ff1bf9bb1 |
@@ -37,7 +37,7 @@ steps:
|
||||
agents:
|
||||
queue: arm64_cpu_queue_release
|
||||
commands:
|
||||
- "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64}\" --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ."
|
||||
- "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64}\" --build-arg BUILD_OS=manylinux --build-arg BUILD_BASE_IMAGE=pytorch/manylinuxaarch64-builder:cuda13.0 --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ."
|
||||
- "mkdir artifacts"
|
||||
- "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'"
|
||||
- "bash .buildkite/scripts/upload-nightly-wheels.sh"
|
||||
@@ -76,7 +76,7 @@ steps:
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86}\" --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ."
|
||||
- "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86}\" --build-arg BUILD_OS=manylinux --build-arg BUILD_BASE_IMAGE=pytorch/manylinux2_28-builder:cuda13.0 --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ."
|
||||
- "mkdir artifacts"
|
||||
- "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'"
|
||||
- "bash .buildkite/scripts/upload-nightly-wheels.sh"
|
||||
@@ -723,7 +723,7 @@ steps:
|
||||
- "bash tools/vllm-rocm/generate-rocm-wheels-root-index.sh"
|
||||
env:
|
||||
S3_BUCKET: "vllm-wheels"
|
||||
VARIANT: "rocm721"
|
||||
VARIANT: "rocm722"
|
||||
|
||||
# ROCm Job 6: Build ROCm Release Docker Image
|
||||
- label: ":docker: Build release image - x86_64 - ROCm"
|
||||
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# Usage: ./ci-fetch-log.sh <buildkite_job_url> [output_file]
|
||||
# ./ci-fetch-log.sh <build_number> <job_uuid> [output_file]
|
||||
#
|
||||
# Downloads the raw log for a Buildkite job from the public, unauthenticated
|
||||
# /organizations/<org>/pipelines/<pipeline>/builds/<n>/jobs/<uuid>/download
|
||||
# endpoint, then strips ANSI/timestamps via ci-clean-log.sh.
|
||||
#
|
||||
# Find <build_number> and <job_uuid> via:
|
||||
# gh pr checks <PR> --repo vllm-project/vllm
|
||||
# Each failing row's URL is .../builds/<build_number>#<job_uuid>.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ORG="vllm"
|
||||
PIPELINE="ci"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <buildkite_job_url> [output_file]"
|
||||
echo " $0 <build_number> <job_uuid> [output_file]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# -lt 1 ]; then usage; fi
|
||||
|
||||
if [[ "$1" == https://* ]]; then
|
||||
BUILD=$(echo "$1" | sed -nE 's#.*/builds/([0-9]+).*#\1#p')
|
||||
JOB=$(echo "$1" | grep -oE '[0-9a-f]{8}-[0-9a-f-]+' | head -n 1)
|
||||
OUT="${2:-ci-${BUILD}-${JOB:0:8}.log}"
|
||||
else
|
||||
if [ $# -lt 2 ]; then usage; fi
|
||||
BUILD="$1"
|
||||
JOB="$2"
|
||||
OUT="${3:-ci-${BUILD}-${JOB:0:8}.log}"
|
||||
fi
|
||||
|
||||
if [ -z "$BUILD" ] || [ -z "$JOB" ]; then
|
||||
echo "Could not parse build number or job UUID from: $1" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
COOKIES=$(mktemp)
|
||||
trap 'rm -f "$COOKIES"' EXIT
|
||||
|
||||
# Buildkite issues a session cookie on first hit; subsequent /download needs it.
|
||||
curl -fsSL -c "$COOKIES" -A "vllm-ci-fetch-log" \
|
||||
"https://buildkite.com/${ORG}/${PIPELINE}/builds/${BUILD}" -o /dev/null
|
||||
|
||||
curl -fsSL -b "$COOKIES" -A "vllm-ci-fetch-log" \
|
||||
"https://buildkite.com/organizations/${ORG}/pipelines/${PIPELINE}/builds/${BUILD}/jobs/${JOB}/download" \
|
||||
-o "$OUT"
|
||||
|
||||
bash "$(dirname "$0")/ci-clean-log.sh" "$OUT"
|
||||
|
||||
echo "$OUT"
|
||||
@@ -1108,6 +1108,7 @@ steps:
|
||||
- export VLLM_TEST_CLEAN_GPU_MEMORY=1
|
||||
- VLLM_TEST_CLEAN_GPU_MEMORY=1 pytest -v -s tests/compile/passes/distributed/test_async_tp.py
|
||||
- pytest -v -s tests/compile/passes/distributed/test_sequence_parallelism.py
|
||||
- pytest -v -s tests/compile/passes/distributed/test_tp2_ar_rms.py::test_tp2_ar_rms_fusions
|
||||
|
||||
#----------------------------------------------------------- mi300 · cuda ------------------------------------------------------------#
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -89,6 +89,16 @@ steps:
|
||||
commands:
|
||||
- pytest -v -s kernels/mamba
|
||||
|
||||
- label: Kernels KDA Test
|
||||
timeout_in_minutes: 20
|
||||
source_file_dependencies:
|
||||
- vllm/model_executor/layers/fla/ops/kda.py
|
||||
- vllm/model_executor/layers/fla/ops/chunk_delta_h.py
|
||||
- vllm/model_executor/layers/fla/ops/l2norm.py
|
||||
- tests/kernels/test_kda.py
|
||||
commands:
|
||||
- pytest -v -s kernels/test_kda.py
|
||||
|
||||
- label: Kernels DeepGEMM Test (H100)
|
||||
key: kernels-deepgemm-test-h100
|
||||
timeout_in_minutes: 45
|
||||
|
||||
@@ -137,3 +137,10 @@ steps:
|
||||
commands:
|
||||
- uv pip install --system 'gpt-oss[eval]==0.0.5'
|
||||
- pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-b200.txt
|
||||
|
||||
- label: MRCR Eval Small Models
|
||||
timeout_in_minutes: 30
|
||||
source_file_dependencies:
|
||||
- tests/evals/mrcr/
|
||||
commands:
|
||||
- pytest -s -v evals/mrcr/test_mrcr_correctness.py --config-list-file=evals/mrcr/configs/models-small.txt
|
||||
|
||||
@@ -97,3 +97,16 @@ steps:
|
||||
commands:
|
||||
- export VLLM_ALLOW_INSECURE_SERIALIZATION=1
|
||||
- pytest -v -s v1/spec_decode/test_speculators_dflash.py -m slow_test
|
||||
|
||||
- label: Spec Decode MTP hybrid (B200)
|
||||
timeout_in_minutes: 30
|
||||
device: b200
|
||||
optional: true
|
||||
source_file_dependencies:
|
||||
- vllm/v1/spec_decode/
|
||||
- vllm/v1/worker/gpu/spec_decode/
|
||||
- vllm/model_executor/models/qwen3_5.py
|
||||
- vllm/model_executor/models/qwen3_5_mtp.py
|
||||
- tests/v1/e2e/spec_decode/
|
||||
commands:
|
||||
- pytest -v -s v1/e2e/spec_decode -k "qwen3_5-hybrid"
|
||||
|
||||
@@ -16,11 +16,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
pre-run-check:
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
(github.event.action != 'labeled' ||
|
||||
github.event.label.name == 'ready' ||
|
||||
github.event.label.name == 'verified')
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR label and author merge count
|
||||
@@ -49,12 +45,7 @@ jobs:
|
||||
|
||||
pre-commit:
|
||||
needs: pre-run-check
|
||||
if: >-
|
||||
always() &&
|
||||
(github.event.action != 'labeled' ||
|
||||
github.event.label.name == 'ready' ||
|
||||
github.event.label.name == 'verified') &&
|
||||
(needs.pre-run-check.result == 'success' || needs.pre-run-check.result == 'skipped')
|
||||
if: always() && (needs.pre-run-check.result == 'success' || needs.pre-run-check.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
+6
-7
@@ -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,14 +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/router_gemm.cu"
|
||||
"csrc/moe/topk_softplus_sqrt_kernels.cu")
|
||||
"csrc/moe/grouped_topk_kernels.cu")
|
||||
endif()
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
|
||||
@@ -217,6 +217,7 @@ async def send_request(
|
||||
min_tokens: int | None = None,
|
||||
max_tokens: int | None = None,
|
||||
timeout_sec: int = 120,
|
||||
conversation_id: str | None = None,
|
||||
) -> ServerResponse:
|
||||
payload = {
|
||||
"model": model,
|
||||
@@ -225,6 +226,9 @@ async def send_request(
|
||||
"temperature": 0.0,
|
||||
}
|
||||
|
||||
if conversation_id is not None:
|
||||
payload["conversation_id"] = conversation_id
|
||||
|
||||
if stream:
|
||||
payload["stream"] = True
|
||||
payload["stream_options"] = {"include_usage": False}
|
||||
@@ -419,6 +423,7 @@ async def send_turn(
|
||||
min_tokens,
|
||||
max_tokens,
|
||||
req_args.timeout_sec,
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
|
||||
if response.valid is False:
|
||||
@@ -1468,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)
|
||||
@@ -1510,7 +1521,9 @@ async def main() -> None:
|
||||
np.random.seed(args.seed)
|
||||
|
||||
logger.info("Loading tokenizer")
|
||||
tokenizer = AutoTokenizer.from_pretrained(args.model)
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
args.model, trust_remote_code=args.trust_remote_code
|
||||
)
|
||||
|
||||
await get_server_info(args.url)
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ torch::Tensor get_scheduler_metadata(
|
||||
isa = cpu_attention::ISA::NEON;
|
||||
} else if (isa_hint == "vxe") {
|
||||
isa = cpu_attention::ISA::VXE;
|
||||
} else if (isa_hint == "vsx") {
|
||||
isa = cpu_attention::ISA::VSX;
|
||||
} else {
|
||||
TORCH_CHECK(false, "Unsupported CPU attention ISA hint: " + isa_hint);
|
||||
}
|
||||
@@ -129,6 +131,8 @@ void cpu_attn_reshape_and_cache(
|
||||
return cpu_attention::ISA::NEON;
|
||||
} else if (isa == "vxe") {
|
||||
return cpu_attention::ISA::VXE;
|
||||
} else if (isa == "vsx") {
|
||||
return cpu_attention::ISA::VSX;
|
||||
} else {
|
||||
TORCH_CHECK(false, "Invalid ISA type: " + isa);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "cpu/utils.hpp"
|
||||
|
||||
namespace cpu_attention {
|
||||
enum class ISA { AMX, VEC, VEC16, NEON, VXE };
|
||||
enum class ISA { AMX, VEC, VEC16, NEON, VXE, VSX };
|
||||
|
||||
// Mirrors csrc/attention/dtype_fp8.cuh Fp8KVCacheDataType exactly.
|
||||
enum class Fp8KVCacheDataType {
|
||||
@@ -164,6 +164,9 @@ struct AttentionMetadata {
|
||||
case ISA::VXE:
|
||||
ss << "VXE, ";
|
||||
break;
|
||||
case ISA::VSX:
|
||||
ss << "VSX, ";
|
||||
break;
|
||||
}
|
||||
ss << "workitem_group_num: " << workitem_group_num
|
||||
<< ", reduction_item_num: " << reduction_item_num
|
||||
|
||||
@@ -27,8 +27,8 @@ FORCE_INLINE std::pair<vec_op::FP32Vec16, vec_op::FP32Vec16> load_b_pair_vec(
|
||||
return {vec_op::FP32Vec16(bf16_b_reg, 0), vec_op::FP32Vec16(bf16_b_reg, 1)};
|
||||
} else {
|
||||
using load_vec_t = typename VecTypeTrait<kv_cache_t>::vec_t;
|
||||
return {vec_op::FP32Vec16(load_vec_t(ptr)),
|
||||
vec_op::FP32Vec16(load_vec_t(ptr + 16))};
|
||||
return std::make_pair(vec_op::FP32Vec16(load_vec_t(ptr)),
|
||||
vec_op::FP32Vec16(load_vec_t(ptr + 16)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
#ifndef CPU_ATTN_VSX_HPP
|
||||
#define CPU_ATTN_VSX_HPP
|
||||
|
||||
#include "cpu_attn_impl.hpp"
|
||||
#include <altivec.h>
|
||||
#include <type_traits>
|
||||
|
||||
namespace cpu_attention {
|
||||
|
||||
namespace {
|
||||
|
||||
// ppc64le Vector = 16 bytes (128 bits)
|
||||
#define BLOCK_SIZE_ALIGNMENT 32
|
||||
#define HEAD_SIZE_ALIGNMENT 32
|
||||
#define MAX_Q_HEAD_NUM_PER_ITER 16
|
||||
|
||||
template <typename kv_cache_t>
|
||||
FORCE_INLINE void load_row8_B_as_f32(const kv_cache_t* p, __vector float& b0,
|
||||
__vector float& b1);
|
||||
|
||||
// [1] Float Specialization
|
||||
template <>
|
||||
FORCE_INLINE void load_row8_B_as_f32<float>(const float* p, __vector float& b0,
|
||||
__vector float& b1) {
|
||||
b0 = vec_xl(0, const_cast<float*>(p));
|
||||
b1 = vec_xl(0, const_cast<float*>(p + 4));
|
||||
}
|
||||
|
||||
// [2] BFloat16 Specialization (Little Endian ppc64le)
|
||||
// On ppc64le (LE): BF16 bits should land in the HIGH 16 bits of each float32.
|
||||
// Byte layout of float32 on LE: [byte0(LSB), byte1, byte2, byte3(MSB)]
|
||||
// We need BF16 in bytes2-3 (high half) with bytes0-1 zeroed.
|
||||
// vec_mergeh on LE interleaves elements 0..3: result_i = {a[i], b[i]}
|
||||
// So vec_mergeh(zeros_u16, raw_u16) gives for each uint16 pair:
|
||||
// uint16[2i] = zeros[i] -> low 16 bits of uint32 -> zeroed mantissa LSBs
|
||||
// uint16[2i+1] = raw[i] -> high 16 bits of uint32 -> BF16 bits
|
||||
// Cast to float32 gives exactly (bf16_bits << 16) per element.
|
||||
template <>
|
||||
FORCE_INLINE void load_row8_B_as_f32<c10::BFloat16>(const c10::BFloat16* p,
|
||||
__vector float& b0,
|
||||
__vector float& b1) {
|
||||
__vector unsigned short raw = vec_xl(
|
||||
0, reinterpret_cast<unsigned short*>(const_cast<c10::BFloat16*>(p)));
|
||||
__vector unsigned short zeros = vec_splat_u16(0);
|
||||
|
||||
// LE: zeros in low 16 bits, raw in high 16 bits → bf16 << 16 == float32
|
||||
b0 = (__vector float)vec_mergeh(zeros, raw);
|
||||
b1 = (__vector float)vec_mergel(zeros, raw);
|
||||
}
|
||||
|
||||
// Note: c10::Half (FP16) is not supported on PowerPC architecture
|
||||
|
||||
template <int32_t M, typename kv_cache_t>
|
||||
FORCE_INLINE void gemm_micro_ppc64le_Mx8_Ku4(
|
||||
const float* __restrict A, // [M x K]
|
||||
const kv_cache_t* __restrict B, // [K x 8]
|
||||
float* __restrict C, // [M x 8]
|
||||
int64_t lda, int64_t ldb, int64_t ldc, int32_t K, bool accumulate) {
|
||||
static_assert(1 <= M && M <= 8, "M must be in [1,8]");
|
||||
|
||||
#define ROWS_APPLY(OP) OP(0) OP(1) OP(2) OP(3) OP(4) OP(5) OP(6) OP(7)
|
||||
#define IF_M(i) if constexpr (M > (i))
|
||||
|
||||
// 1. Define A pointers
|
||||
#define DECL_A(i) const float* a##i = A + (i) * lda;
|
||||
ROWS_APPLY(DECL_A)
|
||||
#undef DECL_A
|
||||
|
||||
// 2. Define Accumulators (2 vectors covers 8 columns)
|
||||
#define DECL_ACC(i) __vector float acc##i##_0, acc##i##_1;
|
||||
ROWS_APPLY(DECL_ACC)
|
||||
#undef DECL_ACC
|
||||
|
||||
// 3. Initialize Accumulators (Load C or Zero)
|
||||
#define INIT_ACC(i) \
|
||||
IF_M(i) { \
|
||||
if (accumulate) { \
|
||||
acc##i##_0 = vec_xl(0, const_cast<float*>(C + (i) * ldc + 0)); \
|
||||
acc##i##_1 = vec_xl(0, const_cast<float*>(C + (i) * ldc + 4)); \
|
||||
} else { \
|
||||
acc##i##_0 = vec_splats(0.0f); \
|
||||
acc##i##_1 = vec_splats(0.0f); \
|
||||
} \
|
||||
}
|
||||
ROWS_APPLY(INIT_ACC)
|
||||
#undef INIT_ACC
|
||||
|
||||
int32_t k = 0;
|
||||
|
||||
for (; k + 3 < K; k += 4) {
|
||||
// Load 4 values of A for each Row M: A[k...k+3]
|
||||
#define LOAD_A4(i) \
|
||||
__vector float a##i##v; \
|
||||
IF_M(i) a##i##v = vec_xl(0, const_cast<float*>(a##i + k));
|
||||
ROWS_APPLY(LOAD_A4)
|
||||
#undef LOAD_A4
|
||||
|
||||
// FMA for specific lane L of A
|
||||
// ppc64le: vec_madd(b, vec_splat(a, lane), acc)
|
||||
#define FMAS_LANE(i, aiv, L) \
|
||||
IF_M(i) { \
|
||||
__vector float a_broad = vec_splat(aiv, L); \
|
||||
acc##i##_0 = vec_madd(b0, a_broad, acc##i##_0); \
|
||||
acc##i##_1 = vec_madd(b1, a_broad, acc##i##_1); \
|
||||
}
|
||||
|
||||
// Unroll K=0..3
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 0) * ldb, b0, b1);
|
||||
#define STEP_K0(i) FMAS_LANE(i, a##i##v, 0)
|
||||
ROWS_APPLY(STEP_K0)
|
||||
#undef STEP_K0
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 1) * ldb, b0, b1);
|
||||
#define STEP_K1(i) FMAS_LANE(i, a##i##v, 1)
|
||||
ROWS_APPLY(STEP_K1)
|
||||
#undef STEP_K1
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 2) * ldb, b0, b1);
|
||||
#define STEP_K2(i) FMAS_LANE(i, a##i##v, 2)
|
||||
ROWS_APPLY(STEP_K2)
|
||||
#undef STEP_K2
|
||||
}
|
||||
{
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)(k + 3) * ldb, b0, b1);
|
||||
#define STEP_K3(i) FMAS_LANE(i, a##i##v, 3)
|
||||
ROWS_APPLY(STEP_K3)
|
||||
#undef STEP_K3
|
||||
}
|
||||
#undef FMAS_LANE
|
||||
}
|
||||
|
||||
for (; k < K; ++k) {
|
||||
__vector float b0, b1;
|
||||
load_row8_B_as_f32<kv_cache_t>(B + (int64_t)k * ldb, b0, b1);
|
||||
#define TAIL_ROW(i) \
|
||||
IF_M(i) { \
|
||||
__vector float ai = vec_splats(*(a##i + k)); \
|
||||
acc##i##_0 = vec_madd(b0, ai, acc##i##_0); \
|
||||
acc##i##_1 = vec_madd(b1, ai, acc##i##_1); \
|
||||
}
|
||||
ROWS_APPLY(TAIL_ROW)
|
||||
#undef TAIL_ROW
|
||||
}
|
||||
|
||||
#define STORE_ROW(i) \
|
||||
IF_M(i) { \
|
||||
vec_xst(acc##i##_0, 0, C + (i) * ldc + 0); \
|
||||
vec_xst(acc##i##_1, 0, C + (i) * ldc + 4); \
|
||||
}
|
||||
ROWS_APPLY(STORE_ROW)
|
||||
#undef STORE_ROW
|
||||
|
||||
#undef ROWS_APPLY
|
||||
#undef IF_M
|
||||
}
|
||||
|
||||
template <int32_t N, typename kv_cache_t>
|
||||
FORCE_INLINE void gemm_macro_ppc64le_Mx8_Ku4(const float* __restrict A,
|
||||
const kv_cache_t* __restrict B,
|
||||
float* __restrict C, int32_t M,
|
||||
int32_t K, int64_t lda,
|
||||
int64_t ldb, int64_t ldc,
|
||||
bool accumulate) {
|
||||
static_assert(N % 8 == 0, "N must be a multiple of 8");
|
||||
for (int32_t m = 0; m < M;) {
|
||||
int32_t mb = (M - m >= 8) ? 8 : (M - m >= 4) ? 4 : (M - m >= 2) ? 2 : 1;
|
||||
const float* Ab = A + m * lda;
|
||||
float* Cb = C + m * ldc;
|
||||
|
||||
for (int32_t n = 0; n < N; n += 8) {
|
||||
const kv_cache_t* Bn = B + n;
|
||||
float* Cn = Cb + n;
|
||||
switch (mb) {
|
||||
case 8:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<8, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
case 4:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<4, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
case 2:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<2, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
default:
|
||||
gemm_micro_ppc64le_Mx8_Ku4<1, kv_cache_t>(Ab, Bn, Cn, lda, ldb, ldc,
|
||||
K, accumulate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m += mb;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename kv_cache_t>
|
||||
class TileGemmPPC64 {
|
||||
public:
|
||||
template <AttentionGemmPhase phase, int32_t k_size>
|
||||
FORCE_INLINE static void gemm(const int32_t m_size,
|
||||
float* __restrict__ a_tile,
|
||||
kv_cache_t* __restrict__ b_tile,
|
||||
float* __restrict__ c_tile, const int64_t lda,
|
||||
const int64_t ldb, const int64_t ldc,
|
||||
const int32_t block_size,
|
||||
const int32_t dynamic_k_size,
|
||||
const bool accum_c) {
|
||||
if constexpr (phase == AttentionGemmPhase::QK) {
|
||||
gemm_macro_ppc64le_Mx8_Ku4<BLOCK_SIZE_ALIGNMENT, kv_cache_t>(
|
||||
a_tile, b_tile, c_tile, m_size, k_size, lda, ldb, ldc, accum_c);
|
||||
} else {
|
||||
gemm_macro_ppc64le_Mx8_Ku4<HEAD_SIZE_ALIGNMENT, kv_cache_t>(
|
||||
a_tile, b_tile, c_tile, m_size, dynamic_k_size, lda, ldb, ldc,
|
||||
accum_c);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
template <typename scalar_t, int64_t head_dim>
|
||||
class AttentionImpl<ISA::VSX, scalar_t, head_dim> {
|
||||
public:
|
||||
using query_t = scalar_t;
|
||||
using q_buffer_t = float;
|
||||
using kv_cache_t = scalar_t;
|
||||
using logits_buffer_t = float;
|
||||
using partial_output_buffer_t = float;
|
||||
using prob_buffer_t = float;
|
||||
|
||||
constexpr static int64_t BlockSizeAlignment = BLOCK_SIZE_ALIGNMENT;
|
||||
constexpr static int64_t HeadDimAlignment = HEAD_SIZE_ALIGNMENT;
|
||||
constexpr static int64_t MaxQHeadNumPerIteration = MAX_Q_HEAD_NUM_PER_ITER;
|
||||
constexpr static int64_t HeadDim = head_dim;
|
||||
constexpr static ISA ISAType = ISA::VSX;
|
||||
constexpr static bool scale_on_logits =
|
||||
false; // Scale is applied to Q during copy
|
||||
|
||||
public:
|
||||
AttentionImpl() {}
|
||||
|
||||
template <template <typename tile_gemm_t> typename attention>
|
||||
FORCE_INLINE void execute_attention(DEFINE_CPU_ATTENTION_PARAMS) {
|
||||
attention<TileGemmPPC64<kv_cache_t>> attention_iteration;
|
||||
attention_iteration(CPU_ATTENTION_PARAMS);
|
||||
}
|
||||
|
||||
// Strides for Memory Layout
|
||||
constexpr static int64_t k_cache_token_group_stride(
|
||||
const int32_t block_size) {
|
||||
return BlockSizeAlignment; // [head_dim, block_size] layout
|
||||
}
|
||||
|
||||
constexpr static int64_t v_cache_token_group_stride(
|
||||
const int32_t block_size) {
|
||||
return head_dim * BlockSizeAlignment;
|
||||
}
|
||||
|
||||
constexpr static int64_t v_cache_head_group_stride(const int32_t block_size) {
|
||||
return HeadDimAlignment;
|
||||
}
|
||||
|
||||
static void copy_q_heads_tile(scalar_t* __restrict__ src,
|
||||
float* __restrict__ q_buffer,
|
||||
const int32_t q_num,
|
||||
const int32_t q_heads_per_kv,
|
||||
const int64_t q_num_stride,
|
||||
const int64_t q_head_stride, float scale) {
|
||||
__vector float scale_vec = vec_splats(scale);
|
||||
constexpr bool is_bf16 = std::is_same<scalar_t, c10::BFloat16>::value;
|
||||
|
||||
for (int32_t i = 0; i < q_num; ++i) {
|
||||
for (int32_t h = 0; h < q_heads_per_kv; ++h) {
|
||||
scalar_t* curr_src = src + i * q_num_stride + h * q_head_stride;
|
||||
float* curr_dst =
|
||||
q_buffer + i * q_heads_per_kv * head_dim + h * head_dim;
|
||||
|
||||
int32_t d = 0;
|
||||
for (; d <= head_dim - 8; d += 8) {
|
||||
__vector float v0, v1;
|
||||
load_row8_B_as_f32<scalar_t>(curr_src + d, v0, v1);
|
||||
|
||||
v0 = vec_mul(v0, scale_vec);
|
||||
v1 = vec_mul(v1, scale_vec);
|
||||
|
||||
vec_xst(v0, 0, curr_dst + d);
|
||||
vec_xst(v1, 0, curr_dst + d + 4);
|
||||
}
|
||||
|
||||
for (; d < head_dim; ++d) {
|
||||
float val = static_cast<float>(curr_src[d]);
|
||||
curr_dst[d] = val * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void reshape_and_cache(
|
||||
const scalar_t* __restrict__ key, const scalar_t* __restrict__ value,
|
||||
scalar_t* __restrict__ key_cache, scalar_t* __restrict__ value_cache,
|
||||
const int64_t* __restrict__ slot_mapping, const int64_t token_num,
|
||||
const int64_t key_token_num_stride, const int64_t value_token_num_stride,
|
||||
const int64_t head_num, const int64_t key_head_num_stride,
|
||||
const int64_t value_head_num_stride, const int64_t num_blocks,
|
||||
const int64_t num_blocks_stride, const int64_t cache_head_num_stride,
|
||||
const int64_t block_size, const int64_t block_size_stride,
|
||||
const float k_inv = 0.0f, const float v_inv = 0.0f) {
|
||||
// k_inv and v_inv are unused on VSX: FP8 KV cache is not supported on
|
||||
// PowerPC. The parameters are present to match the common interface.
|
||||
#pragma omp parallel for collapse(2)
|
||||
for (int64_t token_idx = 0; token_idx < token_num; ++token_idx) {
|
||||
for (int64_t head_idx = 0; head_idx < head_num; ++head_idx) {
|
||||
const int64_t pos = slot_mapping[token_idx];
|
||||
if (pos < 0) continue;
|
||||
|
||||
const int64_t block_idx = pos / block_size;
|
||||
const int64_t block_offset = pos % block_size;
|
||||
|
||||
{
|
||||
const scalar_t* key_src = key + token_idx * key_token_num_stride +
|
||||
head_idx * key_head_num_stride;
|
||||
scalar_t* key_dst = key_cache + block_idx * num_blocks_stride +
|
||||
head_idx * cache_head_num_stride + block_offset;
|
||||
|
||||
for (int64_t i = 0, j = 0; i < head_dim; ++i, j += block_size) {
|
||||
key_dst[j] = key_src[i];
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const scalar_t* val_src = value + token_idx * value_token_num_stride +
|
||||
head_idx * value_head_num_stride;
|
||||
scalar_t* val_dst = value_cache + block_idx * num_blocks_stride +
|
||||
head_idx * cache_head_num_stride +
|
||||
block_offset * head_dim;
|
||||
|
||||
std::memcpy(val_dst, val_src, sizeof(scalar_t) * head_dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace cpu_attention
|
||||
|
||||
#undef BLOCK_SIZE_ALIGNMENT
|
||||
#undef HEAD_SIZE_ALIGNMENT
|
||||
#undef MAX_Q_HEAD_NUM_PER_ITER
|
||||
|
||||
#endif // CPU_ATTN_VSX_HPP
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
namespace vec_op {
|
||||
|
||||
// FP8 tag types for tag dispatch (see cpu_attn_vec.hpp)
|
||||
struct fp8_e4m3_tag {};
|
||||
struct fp8_e5m2_tag {};
|
||||
|
||||
// FIXME: FP16 is not fully supported in Torch-CPU
|
||||
#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \
|
||||
AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \
|
||||
|
||||
@@ -20,6 +20,7 @@ ISA_TYPES = {
|
||||
"VEC16": 2,
|
||||
"NEON": 3,
|
||||
"VXE": 4,
|
||||
"VSX": 5,
|
||||
}
|
||||
|
||||
# KV cache index: 0 = auto (same as scalar_t), 1 = fp8_e4m3, 2 = fp8_e5m2
|
||||
@@ -37,7 +38,7 @@ KV_CACHE_CPP_TYPES = {
|
||||
}
|
||||
|
||||
# ISAs supported for head_dims divisible by 32
|
||||
ISA_FOR_32 = ["AMX", "NEON", "VEC", "VEC16", "VXE"]
|
||||
ISA_FOR_32 = ["AMX", "NEON", "VEC", "VEC16", "VXE", "VSX"]
|
||||
|
||||
# ISAs supported for head_dims divisible by 16 only
|
||||
ISA_FOR_16 = ["VEC16"]
|
||||
@@ -148,6 +149,10 @@ def generate_header_file() -> str:
|
||||
#include "cpu_attn_vxe.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef __powerpc__
|
||||
#include "cpu_attn_vsx.hpp"
|
||||
#endif
|
||||
|
||||
"""
|
||||
|
||||
header += generate_helper_function()
|
||||
@@ -207,6 +212,11 @@ def generate_header_file() -> str:
|
||||
["VXE", "VEC", "VEC16"],
|
||||
fp8=False,
|
||||
)
|
||||
header += _macro_block(
|
||||
"#elif defined(__powerpc__)",
|
||||
["VSX", "VEC", "VEC16"],
|
||||
fp8=False,
|
||||
)
|
||||
header += _macro_block(
|
||||
"#elif defined(__AVX512F__)",
|
||||
["VEC", "VEC16"],
|
||||
@@ -223,7 +233,8 @@ def generate_header_file() -> str:
|
||||
fp8=False,
|
||||
)
|
||||
header += (
|
||||
"#endif /* CPU_CAPABILITY_AMXBF16 / __aarch64__ / __s390x__ */\n\n"
|
||||
"#endif /* CPU_CAPABILITY_AMXBF16 / __aarch64__ / "
|
||||
"__s390x__ / __powerpc__ */\n\n"
|
||||
"#endif // CPU_ATTN_DISPATCH_GENERATED_H\n"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ struct Counter {
|
||||
};
|
||||
|
||||
inline int64_t get_available_l2_size() {
|
||||
#if defined(__s390x__)
|
||||
#if defined(__s390x__) || defined(__powerpc__)
|
||||
static int64_t size = []() {
|
||||
uint32_t l2_cache_size = 0;
|
||||
auto caps = at::cpu::get_cpu_capabilities();
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include <cuda_fp8.h>
|
||||
#ifndef USE_ROCM
|
||||
#include <cuda_fp8.h>
|
||||
#else
|
||||
#include <hip/hip_fp8.h>
|
||||
#endif
|
||||
#include <cuda_runtime.h>
|
||||
#include <type_traits>
|
||||
|
||||
@@ -42,7 +46,23 @@
|
||||
#include "type_convert.cuh"
|
||||
|
||||
#ifndef FINAL_MASK
|
||||
#define FINAL_MASK 0xffffffffu
|
||||
#ifdef USE_ROCM
|
||||
#define FINAL_MASK 0xffffffffffffffffULL
|
||||
#else
|
||||
#define FINAL_MASK 0xffffffffu
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ROCM
|
||||
// ROCm-compatible FP8 conversion helpers
|
||||
__device__ __forceinline__ uint8_t rocm_cvt_float_to_fp8_e4m3(float val) {
|
||||
#if defined(HIP_FP8_TYPE_OCP)
|
||||
__hip_fp8_e4m3 fp8_val(val);
|
||||
#else
|
||||
__hip_fp8_e4m3_fnuz fp8_val(val);
|
||||
#endif
|
||||
return reinterpret_cast<uint8_t&>(fp8_val);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace vllm {
|
||||
@@ -314,9 +334,13 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel(
|
||||
for (int i = 0; i < kElemsPerLane; i++) {
|
||||
float scaled = elements[i] * inv_scale;
|
||||
scaled = fminf(fmaxf(scaled, -kFp8Max), kFp8Max);
|
||||
#ifndef USE_ROCM
|
||||
__nv_fp8_storage_t s =
|
||||
__nv_cvt_float_to_fp8(scaled, __NV_SATFINITE, __NV_E4M3);
|
||||
out_bytes[i] = static_cast<uint8_t>(s);
|
||||
#else
|
||||
out_bytes[i] = rocm_cvt_float_to_fp8_e4m3(scaled);
|
||||
#endif
|
||||
}
|
||||
// One 16-byte STG per lane.
|
||||
*reinterpret_cast<uint4*>(token_fp8_ptr + dim_base) =
|
||||
@@ -384,6 +408,7 @@ void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert(
|
||||
// PDL: enable programmatic stream serialization whenever the hardware
|
||||
// supports it (SM90+). On pre-Hopper GPUs the attribute is unavailable,
|
||||
// so leave numAttrs = 0 and launch as a regular kernel.
|
||||
#ifndef USE_ROCM
|
||||
static int const sm_version = getSMVersion();
|
||||
// Host-side guard: the device kernel body is compiled as a no-op for
|
||||
// bf16 on pre-Ampere (sm_70/sm_75) because _typeConvert<BFloat16> is
|
||||
@@ -410,6 +435,15 @@ void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert(
|
||||
q_inout, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache, eps,
|
||||
num_tokens_full, num_tokens_insert, num_heads_q, cache_block_size,
|
||||
kv_block_stride);
|
||||
#else
|
||||
// ROCm: use standard kernel launch syntax (no PDL/stream serialization)
|
||||
// clang-format off
|
||||
fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel<scalar_t_in>
|
||||
<<<grid, kBlockSize, 0, stream>>>(
|
||||
q_inout, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache,
|
||||
eps, num_tokens_full, num_tokens_insert, num_heads_q,
|
||||
cache_block_size, kv_block_stride);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace deepseek_v4_fused_ops
|
||||
|
||||
@@ -236,17 +236,41 @@ void per_token_group_quant_8bit(const torch::stable::Tensor& input,
|
||||
#undef LAUNCH_KERNEL
|
||||
}
|
||||
|
||||
template <typename T, typename DST_DTYPE>
|
||||
__global__ void per_token_group_quant_8bit_packed_kernel(
|
||||
// Register-resident fast path for group_size==128.
|
||||
//
|
||||
// Each thread holds 16 source elements (32 B = uint4 x 2) in registers across
|
||||
// the absmax reduce -> scale compute -> quantize pipeline. No shared memory.
|
||||
// UE8M0 scale extracted via bit math (bit-exact with exp2f(ceilf(log2f))).
|
||||
//
|
||||
// Loads two contiguous uint4s (16 B + 16 B = 32 B) per thread; on Blackwell
|
||||
// nvcc fuses these into a single 256-bit LDG.E.256.
|
||||
//
|
||||
// Constraints: GROUP_SIZE % (THREADS_PER_GROUP * VEC_SIZE) == 0; for
|
||||
// THREADS_PER_GROUP=8 and bf16/fp16 (VEC_SIZE=16), this means GROUP_SIZE=128.
|
||||
template <typename T, typename DST_DTYPE, int GROUP_SIZE>
|
||||
__global__ void per_token_group_quant_8bit_packed_register_kernel(
|
||||
const T* __restrict__ input, void* __restrict__ output_q,
|
||||
unsigned int* __restrict__ output_s_packed, const int group_size,
|
||||
const int num_groups_padded, const int groups_per_block,
|
||||
const int padded_groups_per_row, const int groups_per_row, const int mn,
|
||||
const int tma_aligned_mn, const int num_scale_elems, const float eps,
|
||||
unsigned int* __restrict__ output_s_packed, const int64_t num_groups_padded,
|
||||
const int groups_per_block, const int padded_groups_per_row,
|
||||
const int groups_per_row, const int mn, const int output_q_mn_extent,
|
||||
const int tma_aligned_mn, const int64_t num_scale_elems, const float eps,
|
||||
const float min_8bit, const float max_8bit) {
|
||||
const int threads_per_group = 16;
|
||||
const int64_t local_group_id = threadIdx.x / threads_per_group;
|
||||
const int lane_id = threadIdx.x % threads_per_group;
|
||||
static_assert(GROUP_SIZE == 128, "fast path supports GROUP_SIZE==128");
|
||||
constexpr int THREADS_PER_GROUP = 8;
|
||||
constexpr int VEC_SIZE = 32 / sizeof(T); // 16 for bf16/fp16
|
||||
static_assert(GROUP_SIZE == THREADS_PER_GROUP * VEC_SIZE,
|
||||
"GROUP_SIZE must equal THREADS_PER_GROUP * VEC_SIZE");
|
||||
// Each group's 8 threads must live in a single warp octet so the
|
||||
// 0xffu << (threadIdx.x & 24u) shuffle mask selects exactly the lanes
|
||||
// that share a group. Requires 32 % THREADS_PER_GROUP == 0 and the host
|
||||
// to launch num_threads as a multiple of THREADS_PER_GROUP (which it does
|
||||
// via num_threads = groups_per_block * THREADS_PER_GROUP).
|
||||
static_assert(32 % THREADS_PER_GROUP == 0,
|
||||
"THREADS_PER_GROUP must divide warp size for the shuffle "
|
||||
"mask to be valid");
|
||||
|
||||
const int local_group_id = threadIdx.x / THREADS_PER_GROUP;
|
||||
const int lane_id = threadIdx.x % THREADS_PER_GROUP;
|
||||
|
||||
const int64_t block_group_id = blockIdx.x * groups_per_block;
|
||||
const int64_t global_group_id = block_group_id + local_group_id;
|
||||
@@ -254,141 +278,207 @@ __global__ void per_token_group_quant_8bit_packed_kernel(
|
||||
return;
|
||||
}
|
||||
|
||||
// map flat group id to 2D indices (mn_idx, sf_k_idx)
|
||||
const int sf_k_idx =
|
||||
static_cast<int>(global_group_id % padded_groups_per_row);
|
||||
const int mn_idx = static_cast<int>(global_group_id / padded_groups_per_row);
|
||||
|
||||
// whether it is a valid group (not padding)
|
||||
const bool is_valid_group = (mn_idx < mn) && (sf_k_idx < groups_per_row);
|
||||
|
||||
// shared memory to cache each group's data to avoid double DRAM reads.
|
||||
extern __shared__ __align__(16) char smem_raw[];
|
||||
T* smem = reinterpret_cast<T*>(smem_raw);
|
||||
T* smem_group = smem + local_group_id * group_size;
|
||||
|
||||
// compute scale for valid groups
|
||||
float y_s = 0.f;
|
||||
// Load 16 input elements (32 B) into registers as two adjacent uint4
|
||||
// loads. nvcc keeps these as 2x LDG.E.128 on sm_100; the per-thread cost
|
||||
// is dominated by HBM bandwidth at large MN, so a fused 256-bit load via
|
||||
// inline PTX gave no measurable speedup.
|
||||
// alignas(16) is required so the uint4* reinterpret_cast below is
|
||||
// well-defined for T == bf16/fp16 (default alignof is 2).
|
||||
alignas(16) T regs[VEC_SIZE];
|
||||
float local_absmax = eps;
|
||||
if (is_valid_group) {
|
||||
const T* group_input =
|
||||
input + static_cast<int64_t>(mn_idx) * groups_per_row * group_size +
|
||||
sf_k_idx * group_size;
|
||||
y_s = ComputeGroupScale<T, true>(group_input, smem_group, group_size,
|
||||
lane_id, threads_per_group, eps, max_8bit);
|
||||
input + static_cast<int64_t>(mn_idx) * groups_per_row * GROUP_SIZE +
|
||||
sf_k_idx * GROUP_SIZE + lane_id * VEC_SIZE;
|
||||
uint4* dst = reinterpret_cast<uint4*>(®s[0]);
|
||||
const uint4* src = reinterpret_cast<const uint4*>(group_input);
|
||||
dst[0] = src[0];
|
||||
dst[1] = src[1];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < VEC_SIZE; ++i) {
|
||||
float v = fabsf(static_cast<float>(regs[i]));
|
||||
local_absmax = fmaxf(local_absmax, v);
|
||||
}
|
||||
}
|
||||
|
||||
// pack 4 scales into a uint32 exponent
|
||||
// 8-lane subgroup shuffle reduce (octet of the warp). The mask selects the
|
||||
// 8 lanes within the warp that share a group.
|
||||
unsigned mask = 0xffu << (threadIdx.x & 24u);
|
||||
local_absmax = fmaxf(local_absmax, __shfl_xor_sync(mask, local_absmax, 4));
|
||||
local_absmax = fmaxf(local_absmax, __shfl_xor_sync(mask, local_absmax, 2));
|
||||
local_absmax = fmaxf(local_absmax, __shfl_xor_sync(mask, local_absmax, 1));
|
||||
|
||||
float y_s = local_absmax / max_8bit;
|
||||
y_s = fmaxf(y_s, 1e-10f);
|
||||
uint32_t bits = __float_as_uint(y_s);
|
||||
uint32_t exp_bits = (bits >> 23) & 0xffu;
|
||||
uint32_t mant_bits = bits & 0x7fffffu;
|
||||
uint8_t exp_byte =
|
||||
static_cast<uint8_t>(exp_bits + (mant_bits != 0u ? 1u : 0u));
|
||||
|
||||
// Lane 0 writes the packed scale byte.
|
||||
if (lane_id == 0) {
|
||||
// each uint32 in output_s_packed stores 4 packed scales
|
||||
const int sf_k_pack_idx = sf_k_idx / 4;
|
||||
const int pos = sf_k_idx % 4;
|
||||
const int out_idx = sf_k_pack_idx * tma_aligned_mn + mn_idx;
|
||||
|
||||
if (is_valid_group) {
|
||||
// reinterpret the UE8M0 scale y_s as IEEE bits, extract the 8-bit
|
||||
// exponent, and place it into the correct byte of the 32-bit word.
|
||||
const unsigned int bits = __float_as_uint(y_s);
|
||||
const uint8_t exponent = static_cast<uint8_t>((bits >> 23u) & 0xffu);
|
||||
reinterpret_cast<uint8_t*>(output_s_packed)[out_idx * 4 + pos] = exponent;
|
||||
reinterpret_cast<uint8_t*>(output_s_packed)[out_idx * 4 + pos] = exp_byte;
|
||||
} else if (out_idx < num_scale_elems) {
|
||||
// write zero for padding groups if within bounds of output_s_packed
|
||||
reinterpret_cast<uint8_t*>(output_s_packed)[out_idx * 4 + pos] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
|
||||
if (is_valid_group) {
|
||||
DST_DTYPE* group_output =
|
||||
static_cast<DST_DTYPE*>(output_q) +
|
||||
static_cast<int64_t>(mn_idx) * groups_per_row * group_size +
|
||||
sf_k_idx * group_size;
|
||||
QuantizeGroup<T, DST_DTYPE>(smem_group, group_output, group_size, lane_id,
|
||||
threads_per_group, y_s, min_8bit, max_8bit);
|
||||
// For padded mn rows that fall within output_q's allocated extent, write
|
||||
// a uint4 of zeros to keep the buffer clean for downstream TMA loads.
|
||||
// Skip writes for sf_k padding (those positions don't exist in output_q).
|
||||
if (!is_valid_group) {
|
||||
if (sf_k_idx < groups_per_row && mn_idx >= mn &&
|
||||
mn_idx < output_q_mn_extent) {
|
||||
DST_DTYPE* group_output =
|
||||
static_cast<DST_DTYPE*>(output_q) +
|
||||
static_cast<int64_t>(mn_idx) * groups_per_row * GROUP_SIZE +
|
||||
sf_k_idx * GROUP_SIZE + lane_id * VEC_SIZE;
|
||||
*reinterpret_cast<uint4*>(group_output) = make_uint4(0, 0, 0, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reconstruct y_s as a power-of-2 float and use its reciprocal.
|
||||
float y_s_q = __uint_as_float(static_cast<uint32_t>(exp_byte) << 23);
|
||||
float inv_y = 1.0f / y_s_q;
|
||||
|
||||
// Quantize and pack into 16 fp8/int8 bytes (= uint4). VEC_SIZE==16 so we
|
||||
// fill four 32-bit words, four bytes each.
|
||||
uint32_t packed_lo = 0;
|
||||
uint32_t packed_lo_hi = 0;
|
||||
uint32_t packed_hi_lo = 0;
|
||||
uint32_t packed_hi = 0;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < VEC_SIZE; ++i) {
|
||||
float q =
|
||||
fminf(fmaxf(static_cast<float>(regs[i]) * inv_y, min_8bit), max_8bit);
|
||||
DST_DTYPE qb = DST_DTYPE(q);
|
||||
uint8_t byte = *reinterpret_cast<uint8_t*>(&qb);
|
||||
const int shift = (i & 3) * 8;
|
||||
if (i < 4) {
|
||||
packed_lo |= static_cast<uint32_t>(byte) << shift;
|
||||
} else if (i < 8) {
|
||||
packed_lo_hi |= static_cast<uint32_t>(byte) << shift;
|
||||
} else if (i < 12) {
|
||||
packed_hi_lo |= static_cast<uint32_t>(byte) << shift;
|
||||
} else {
|
||||
packed_hi |= static_cast<uint32_t>(byte) << shift;
|
||||
}
|
||||
}
|
||||
|
||||
uint4 packed_out =
|
||||
make_uint4(packed_lo, packed_lo_hi, packed_hi_lo, packed_hi);
|
||||
DST_DTYPE* group_output =
|
||||
static_cast<DST_DTYPE*>(output_q) +
|
||||
static_cast<int64_t>(mn_idx) * groups_per_row * GROUP_SIZE +
|
||||
sf_k_idx * GROUP_SIZE + lane_id * VEC_SIZE;
|
||||
*reinterpret_cast<uint4*>(group_output) = packed_out;
|
||||
}
|
||||
|
||||
// Public entry point: register-resident packed quant kernel.
|
||||
// Constraints: group_size == 128 and bf16/fp16 input.
|
||||
void per_token_group_quant_8bit_packed(const torch::stable::Tensor& input,
|
||||
torch::stable::Tensor& output_q,
|
||||
torch::stable::Tensor& output_s_packed,
|
||||
int64_t group_size, double eps,
|
||||
double min_8bit, double max_8bit) {
|
||||
STD_TORCH_CHECK(group_size == 128,
|
||||
"per_token_group_quant_8bit_packed only supports "
|
||||
"group_size==128, got ",
|
||||
group_size, ".");
|
||||
const auto in_dtype = input.scalar_type();
|
||||
STD_TORCH_CHECK(
|
||||
in_dtype == torch::headeronly::ScalarType::Half ||
|
||||
in_dtype == torch::headeronly::ScalarType::BFloat16,
|
||||
"per_token_group_quant_8bit_packed only supports bf16/fp16 input.");
|
||||
|
||||
STD_TORCH_CHECK(input.is_contiguous());
|
||||
STD_TORCH_CHECK(output_q.is_contiguous());
|
||||
|
||||
const int64_t k = input.size(-1);
|
||||
STD_TORCH_CHECK(k % group_size == 0, "Last dimension (", k,
|
||||
") must be divisible by group_size (", group_size, ").");
|
||||
STD_TORCH_CHECK(k % group_size == 0, "input last dim k=", k,
|
||||
" is not divisible by group_size=", group_size, ".");
|
||||
|
||||
const int64_t mn = input.numel() / k;
|
||||
const int64_t groups_per_row = k / group_size;
|
||||
|
||||
STD_TORCH_CHECK(output_s_packed.dim() == 2,
|
||||
"output_s_packed must be 2D, got dim=", output_s_packed.dim(),
|
||||
".");
|
||||
|
||||
const int64_t k_num_packed_sfk = (groups_per_row + 3) / 4;
|
||||
const int64_t tma_aligned_mn = ((mn + 3) / 4) * 4;
|
||||
|
||||
// output_q may be allocated with extra padded mn rows (e.g.,
|
||||
// (tma_aligned_mn, k)) so the kernel can zero-fill them in-line and the
|
||||
// caller can use torch.empty instead of torch.zeros. The grid only covers
|
||||
// up to tma_aligned_mn, so we cap the extent there.
|
||||
const int64_t output_q_mn_actual = output_q.numel() / k;
|
||||
STD_TORCH_CHECK(output_q_mn_actual >= mn,
|
||||
"output_q must have at least mn rows; got ",
|
||||
output_q_mn_actual, " rows for mn=", mn, ".");
|
||||
const int64_t output_q_mn_extent =
|
||||
output_q_mn_actual < tma_aligned_mn ? output_q_mn_actual : tma_aligned_mn;
|
||||
|
||||
STD_TORCH_CHECK(
|
||||
output_s_packed.scalar_type() == torch::headeronly::ScalarType::Int,
|
||||
"output_s_packed must have dtype int32 for UE8M0-packed scales.");
|
||||
// DeepGEMM expects SFA scales in MN-major form with shape
|
||||
// [mn, ceil_div(K, 128 * 4)] and TMA-aligned stride on the last
|
||||
// dimension.
|
||||
"output_s_packed must be int32 for UE8M0-packed scales.");
|
||||
STD_TORCH_CHECK(output_s_packed.size(0) == mn &&
|
||||
output_s_packed.size(1) == k_num_packed_sfk,
|
||||
"output_s_packed shape must be [", mn, ", ", k_num_packed_sfk,
|
||||
"], but got [", output_s_packed.size(0), ", ",
|
||||
"]; got [", output_s_packed.size(0), ", ",
|
||||
output_s_packed.size(1), "].");
|
||||
// Verify column-major TMA-aligned layout
|
||||
STD_TORCH_CHECK(output_s_packed.stride(0) == 1 &&
|
||||
output_s_packed.stride(1) == tma_aligned_mn,
|
||||
"output_s_packed must have strides [1, ", tma_aligned_mn,
|
||||
"], but got [", output_s_packed.stride(0), ", ",
|
||||
"output_s_packed strides must be [1, ", tma_aligned_mn,
|
||||
"]; got [", output_s_packed.stride(0), ", ",
|
||||
output_s_packed.stride(1), "].");
|
||||
|
||||
cudaStream_t stream = get_current_cuda_stream();
|
||||
|
||||
constexpr int THREADS_PER_GROUP = 16;
|
||||
|
||||
// Expand the grid to cover MN and K padding so every byte in
|
||||
// output_s_packed is written (padding bytes get zeroed by the kernel).
|
||||
constexpr int THREADS_PER_GROUP = 8;
|
||||
const int64_t padded_groups_per_row = k_num_packed_sfk * 4;
|
||||
const int64_t num_groups_padded = tma_aligned_mn * padded_groups_per_row;
|
||||
// Number of elements in output_s_packed.
|
||||
const int64_t num_scale_elems = mn + (k_num_packed_sfk - 1) * tma_aligned_mn;
|
||||
|
||||
const int groups_per_block = GetGroupsPerBlock(num_groups_padded);
|
||||
|
||||
auto dst_type = output_q.scalar_type();
|
||||
const int num_blocks = num_groups_padded / groups_per_block;
|
||||
const int64_t num_blocks = num_groups_padded / groups_per_block;
|
||||
const int num_threads = groups_per_block * THREADS_PER_GROUP;
|
||||
// CUDA caps grid.x at 2^31 - 1; this fits any realistic shape but guard
|
||||
// against pathological inputs.
|
||||
STD_TORCH_CHECK(num_blocks <= static_cast<int64_t>(INT32_MAX),
|
||||
"per_token_group_quant_8bit_packed grid too large: ",
|
||||
num_blocks, " blocks (max ", INT32_MAX, ").");
|
||||
|
||||
#define LAUNCH_PACKED_KERNEL(T, DST_DTYPE) \
|
||||
do { \
|
||||
dim3 grid(num_blocks); \
|
||||
dim3 block(num_threads); \
|
||||
size_t smem_bytes = \
|
||||
static_cast<size_t>(groups_per_block) * group_size * sizeof(T); \
|
||||
per_token_group_quant_8bit_packed_kernel<T, DST_DTYPE> \
|
||||
<<<grid, block, smem_bytes, stream>>>( \
|
||||
static_cast<const T*>(input.data_ptr()), output_q.data_ptr(), \
|
||||
reinterpret_cast<unsigned int*>(output_s_packed.data_ptr()), \
|
||||
static_cast<int>(group_size), static_cast<int>(num_groups_padded), \
|
||||
groups_per_block, static_cast<int>(padded_groups_per_row), \
|
||||
static_cast<int>(groups_per_row), static_cast<int>(mn), \
|
||||
static_cast<int>(tma_aligned_mn), \
|
||||
static_cast<int>(num_scale_elems), static_cast<float>(eps), \
|
||||
static_cast<float>(min_8bit), static_cast<float>(max_8bit)); \
|
||||
#define LAUNCH_REG_KERNEL(T, DST_DTYPE) \
|
||||
do { \
|
||||
dim3 grid(static_cast<unsigned int>(num_blocks)); \
|
||||
dim3 block(num_threads); \
|
||||
per_token_group_quant_8bit_packed_register_kernel<T, DST_DTYPE, 128> \
|
||||
<<<grid, block, 0, stream>>>( \
|
||||
static_cast<const T*>(input.data_ptr()), output_q.data_ptr(), \
|
||||
reinterpret_cast<unsigned int*>(output_s_packed.data_ptr()), \
|
||||
num_groups_padded, groups_per_block, \
|
||||
static_cast<int>(padded_groups_per_row), \
|
||||
static_cast<int>(groups_per_row), static_cast<int>(mn), \
|
||||
static_cast<int>(output_q_mn_extent), \
|
||||
static_cast<int>(tma_aligned_mn), num_scale_elems, \
|
||||
static_cast<float>(eps), static_cast<float>(min_8bit), \
|
||||
static_cast<float>(max_8bit)); \
|
||||
} while (0)
|
||||
|
||||
VLLM_STABLE_DISPATCH_FLOATING_TYPES(
|
||||
input.scalar_type(), "per_token_group_quant_8bit_packed", ([&] {
|
||||
VLLM_STABLE_DISPATCH_HALF_TYPES(
|
||||
input.scalar_type(), "per_token_group_quant_8bit_packed_register", ([&] {
|
||||
if (dst_type == torch::headeronly::ScalarType::Float8_e4m3fn) {
|
||||
LAUNCH_PACKED_KERNEL(scalar_t, __nv_fp8_e4m3);
|
||||
LAUNCH_REG_KERNEL(scalar_t, __nv_fp8_e4m3);
|
||||
} else if (dst_type == torch::headeronly::ScalarType::Char) {
|
||||
LAUNCH_PACKED_KERNEL(scalar_t, int8_t);
|
||||
LAUNCH_REG_KERNEL(scalar_t, int8_t);
|
||||
} else {
|
||||
STD_TORCH_CHECK(
|
||||
false,
|
||||
@@ -397,7 +487,7 @@ void per_token_group_quant_8bit_packed(const torch::stable::Tensor& input,
|
||||
}
|
||||
}));
|
||||
|
||||
#undef LAUNCH_PACKED_KERNEL
|
||||
#undef LAUNCH_REG_KERNEL
|
||||
}
|
||||
|
||||
void per_token_group_quant_fp8(const torch::stable::Tensor& input,
|
||||
|
||||
@@ -8,3 +8,13 @@ void per_token_group_quant_8bit(const torch::stable::Tensor& input,
|
||||
torch::stable::Tensor& output_s,
|
||||
int64_t group_size, double eps, double min_8bit,
|
||||
double max_8bit, bool scale_ue8m0 = false);
|
||||
|
||||
// Public op: register-resident packed quant for the DeepGEMM Blackwell path.
|
||||
// Restricted to group_size == 128 and bf16/fp16 input; other configurations
|
||||
// raise STD_TORCH_CHECK. The legacy shared-memory fallback was removed because
|
||||
// no production caller (deep_gemm_moe / input_quant_fp8) uses other shapes.
|
||||
void per_token_group_quant_8bit_packed(const torch::stable::Tensor& input,
|
||||
torch::stable::Tensor& output_q,
|
||||
torch::stable::Tensor& output_s_packed,
|
||||
int64_t group_size, double eps,
|
||||
double min_8bit, double max_8bit);
|
||||
|
||||
@@ -67,10 +67,6 @@ void shuffle_rows(const torch::Tensor& input_tensor,
|
||||
torch::Tensor& output_tensor);
|
||||
|
||||
#ifndef USE_ROCM
|
||||
// cuBLAS bf16 x bf16 -> fp32 router GEMM (fallback for non-SM90 / batch > 16)
|
||||
torch::Tensor router_gemm_bf16_fp32(torch::Tensor const& input,
|
||||
torch::Tensor const& weight);
|
||||
|
||||
// DeepSeek V3 optimized router GEMM kernel for SM90+
|
||||
// Computes output = mat_a @ mat_b.T where:
|
||||
// mat_a: [num_tokens, hidden_dim] in bf16
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
// bf16 x bf16 -> fp32 router GEMM via cuBLAS.
|
||||
// Uses CUBLAS_COMPUTE_32F so bf16 operands accumulate into fp32,
|
||||
// matching TRT-LLM's cuBLAS fallback behaviour in dsv3RouterGemmOp.
|
||||
|
||||
#include <torch/all.h>
|
||||
#include <ATen/cuda/CUDAContext.h>
|
||||
#include <cublas_v2.h>
|
||||
|
||||
// cuBLAS column-major math for row-major PyTorch tensors:
|
||||
// weight[N,K]_row lda=K -> cuBLAS sees (K,N) col-major; CUBLAS_OP_T ->
|
||||
// (N,K) input[M,K]_row ldb=K -> cuBLAS sees (K,M) col-major; CUBLAS_OP_N
|
||||
// -> (K,M) out[M,N]_row ldc=N -> cuBLAS sees (N,M) col-major (written as
|
||||
// output^T)
|
||||
// cuBLAS: C(N,M) = weight(N,K) @ input(K,M) => C^T = output[M,N]
|
||||
// params: m=N, n=M, k=K, lda=K (weight), ldb=K (input), ldc=N (output)
|
||||
|
||||
torch::Tensor router_gemm_bf16_fp32(torch::Tensor const& input,
|
||||
torch::Tensor const& weight) {
|
||||
TORCH_CHECK(input.dtype() == torch::kBFloat16,
|
||||
"router_gemm_bf16_fp32: input must be bfloat16");
|
||||
TORCH_CHECK(weight.dtype() == torch::kBFloat16,
|
||||
"router_gemm_bf16_fp32: weight must be bfloat16");
|
||||
TORCH_CHECK(input.dim() == 2 && weight.dim() == 2,
|
||||
"router_gemm_bf16_fp32: input and weight must be 2-D");
|
||||
TORCH_CHECK(input.size(1) == weight.size(1),
|
||||
"router_gemm_bf16_fp32: inner dimensions must match");
|
||||
|
||||
int64_t const M = input.size(0);
|
||||
int64_t const N = weight.size(0);
|
||||
int64_t const K = input.size(1);
|
||||
|
||||
auto out = torch::empty({M, N}, input.options().dtype(torch::kFloat32));
|
||||
|
||||
cublasHandle_t handle = at::cuda::getCurrentCUDABlasHandle();
|
||||
TORCH_CUDABLAS_CHECK(
|
||||
cublasSetStream(handle, at::cuda::getCurrentCUDAStream()));
|
||||
|
||||
float const alpha = 1.0f;
|
||||
float const beta = 0.0f;
|
||||
|
||||
TORCH_CUDABLAS_CHECK(cublasGemmEx(
|
||||
handle, CUBLAS_OP_T, CUBLAS_OP_N, static_cast<int>(N),
|
||||
static_cast<int>(M), static_cast<int>(K), &alpha, weight.data_ptr(),
|
||||
CUDA_R_16BF, static_cast<int>(K), input.data_ptr(), CUDA_R_16BF,
|
||||
static_cast<int>(K), &beta, out.data_ptr(), CUDA_R_32F,
|
||||
static_cast<int>(N), CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT));
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -60,15 +60,6 @@ __device__ __forceinline__ float toFloat(T value) {
|
||||
}
|
||||
}
|
||||
|
||||
#define FINAL_MASK 0xffffffff
|
||||
template <typename T>
|
||||
__inline__ __device__ T warpReduceSum(T val) {
|
||||
#pragma unroll
|
||||
for (int mask = 16; mask > 0; mask >>= 1)
|
||||
val += __shfl_xor_sync(FINAL_MASK, val, mask, 32);
|
||||
return val;
|
||||
}
|
||||
|
||||
// ====================== TopK softplus_sqrt things
|
||||
// ===============================
|
||||
|
||||
@@ -272,8 +263,14 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
|
||||
}
|
||||
}
|
||||
// Compute per-thread scale (using warp reduction when renormalizing).
|
||||
// THREADS_PER_ROW-parameterized butterfly works for both warp sizes (32
|
||||
// on CUDA, 64 on ROCm CDNA) and any THREADS_PER_ROW the dispatch picks.
|
||||
if (renormalize) {
|
||||
selected_sum = warpReduceSum(selected_sum);
|
||||
#pragma unroll
|
||||
for (int mask = THREADS_PER_ROW / 2; mask > 0; mask /= 2) {
|
||||
selected_sum +=
|
||||
VLLM_SHFL_XOR_SYNC_WIDTH(selected_sum, mask, THREADS_PER_ROW);
|
||||
}
|
||||
}
|
||||
float scale = static_cast<float>(routed_scaling_factor);
|
||||
if (renormalize) {
|
||||
@@ -544,7 +541,6 @@ void topkGatingSoftplusSqrtKernelLauncher(
|
||||
const IndType* tid2eid, cudaStream_t stream) {
|
||||
static constexpr int WARPS_PER_TB = 4;
|
||||
static constexpr int BYTES_PER_LDG_POWER_OF_2 = 16;
|
||||
#ifndef USE_ROCM
|
||||
// for bfloat16 dtype, we need 4 bytes loading to make sure num_experts
|
||||
// elements can be loaded by a warp
|
||||
static constexpr int BYTES_PER_LDG_MULTIPLE_64 =
|
||||
@@ -552,6 +548,19 @@ void topkGatingSoftplusSqrtKernelLauncher(
|
||||
std::is_same_v<InputType, __half>)
|
||||
? 4
|
||||
: 8;
|
||||
// Narrower LDG (ELTS_PER_LDG=1) used by 192/320/448/576 on ROCm WARP_SIZE=64
|
||||
// where ELTS_PER_LDG=2 fails the EXPERTS%(ELTS_PER_LDG*WARP_SIZE)==0 check.
|
||||
// On CUDA WARP_SIZE=32 the wider LDG already aligns, so the alias collapses
|
||||
// back to BYTES_PER_LDG_MULTIPLE_64 — no behavioral change for CUDA.
|
||||
#ifdef USE_ROCM
|
||||
static constexpr int BYTES_PER_LDG_MULTIPLE_64_NARROW =
|
||||
(std::is_same_v<InputType, __nv_bfloat16> ||
|
||||
std::is_same_v<InputType, __half>)
|
||||
? 2
|
||||
: 4;
|
||||
#else
|
||||
static constexpr int BYTES_PER_LDG_MULTIPLE_64_NARROW =
|
||||
BYTES_PER_LDG_MULTIPLE_64;
|
||||
#endif
|
||||
switch (num_experts) {
|
||||
case 1:
|
||||
@@ -584,27 +593,29 @@ void topkGatingSoftplusSqrtKernelLauncher(
|
||||
case 512:
|
||||
LAUNCH_SOFTPLUS_SQRT(512, WARPS_PER_TB, BYTES_PER_LDG_POWER_OF_2);
|
||||
break;
|
||||
// (CUDA only) support multiples of 64 when num_experts is not power of 2.
|
||||
// ROCm uses WARP_SIZE 64 so 8 bytes loading won't fit for some of
|
||||
// num_experts, alternatively we can test 4 bytes loading and enable it in
|
||||
// future.
|
||||
#ifndef USE_ROCM
|
||||
// Multiples of 64 that are not powers of 2. The kernel requires
|
||||
// EXPERTS % (ELTS_PER_LDG * WARP_SIZE) == 0. With ELTS_PER_LDG=2
|
||||
// (BYTES_PER_LDG_MULTIPLE_64), this holds for all five values on CUDA
|
||||
// WARP_SIZE=32 but only for 384 on ROCm WARP_SIZE=64. The other four
|
||||
// use BYTES_PER_LDG_MULTIPLE_64_NARROW (ELTS_PER_LDG=1), which
|
||||
// satisfies the assertion for any multiple of 64 on either backend;
|
||||
// on CUDA the narrow alias collapses back to the wider load, so CUDA
|
||||
// behavior is unchanged.
|
||||
case 192:
|
||||
LAUNCH_SOFTPLUS_SQRT(192, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(192, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
case 320:
|
||||
LAUNCH_SOFTPLUS_SQRT(320, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(320, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
case 384:
|
||||
LAUNCH_SOFTPLUS_SQRT(384, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
break;
|
||||
case 448:
|
||||
LAUNCH_SOFTPLUS_SQRT(448, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(448, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
case 576:
|
||||
LAUNCH_SOFTPLUS_SQRT(576, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64);
|
||||
LAUNCH_SOFTPLUS_SQRT(576, WARPS_PER_TB, BYTES_PER_LDG_MULTIPLE_64_NARROW);
|
||||
break;
|
||||
#endif
|
||||
default: {
|
||||
TORCH_CHECK(false, "Unsupported expert number: ", num_experts);
|
||||
}
|
||||
|
||||
@@ -16,14 +16,13 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, m) {
|
||||
"bias) -> ()");
|
||||
m.impl("topk_sigmoid", torch::kCUDA, &topk_sigmoid);
|
||||
|
||||
#ifndef USE_ROCM
|
||||
m.def(
|
||||
"topk_softplus_sqrt(Tensor! topk_weights, Tensor! topk_indices, Tensor! "
|
||||
"token_expert_indices, Tensor gating_output, bool renormalize, float "
|
||||
"routed_scaling_factor, Tensor? "
|
||||
"bias, Tensor? input_ids, Tensor? tid2eid) -> ()");
|
||||
m.impl("topk_softplus_sqrt", torch::kCUDA, &topk_softplus_sqrt);
|
||||
#endif
|
||||
|
||||
// Calculate the result of moe by summing up the partial results
|
||||
// from all selected experts.
|
||||
m.def("moe_sum(Tensor input, Tensor! output) -> ()");
|
||||
@@ -133,10 +132,6 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, m) {
|
||||
"Tensor)");
|
||||
m.impl("grouped_topk", torch::kCUDA, &grouped_topk);
|
||||
|
||||
// cuBLAS bf16 x bf16 -> fp32 router GEMM (fallback for non-SM90 / batch > 16)
|
||||
m.def("router_gemm_bf16_fp32(Tensor input, Tensor weight) -> Tensor");
|
||||
m.impl("router_gemm_bf16_fp32", torch::kCUDA, &router_gemm_bf16_fp32);
|
||||
|
||||
// DeepSeek V3 optimized router GEMM for SM90+
|
||||
m.def("dsv3_router_gemm(Tensor! output, Tensor mat_a, Tensor mat_b) -> ()");
|
||||
// conditionally compiled so impl registration is in source file
|
||||
|
||||
@@ -887,27 +887,14 @@ __global__ void __launch_bounds__(kThreadsPerBlock, 2)
|
||||
uint32_t* shared_ordered =
|
||||
reinterpret_cast<uint32_t*>(smem_raw + kFixedSmemLarge);
|
||||
|
||||
// RadixRowState for multi-CTA cooperative radix
|
||||
// RadixRowState for multi-CTA cooperative radix.
|
||||
// Zero-initialization is done host-side via cudaMemsetAsync in topk.cu
|
||||
// before launch — that gives a stream-ordered happens-before edge for all
|
||||
// CTAs, which the previous in-kernel init (CTA-0 only + intra-CTA
|
||||
// __syncthreads) did not provide and which manifested as a race against
|
||||
// CTA-1+'s first red_release on arrival_counter.
|
||||
RadixRowState* state = ¶ms.row_states[group_id];
|
||||
|
||||
// -- Initialize RadixRowState (only needed if large rows exist) --
|
||||
if (params.max_seq_len > RADIX_THRESHOLD) {
|
||||
if (cta_in_group == 0) {
|
||||
for (uint32_t buf = 0; buf < 3; buf++) {
|
||||
for (uint32_t i = tx; i < RADIX; i += kThreadsPerBlock) {
|
||||
state->histogram[buf][i] = 0;
|
||||
}
|
||||
}
|
||||
if (tx == 0) {
|
||||
state->remaining_k = 0;
|
||||
state->prefix = 0;
|
||||
state->arrival_counter = 0;
|
||||
state->output_counter = 0;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
int barrier_phase = 0;
|
||||
const uint32_t total_iters = (params.num_rows + num_groups - 1) / num_groups;
|
||||
|
||||
|
||||
+82
-4
@@ -82,22 +82,100 @@ void launch_persistent_topk(const torch::Tensor& logits,
|
||||
size_t smem_size = P::kFixedSmemLarge + chunk_size * sizeof(uint32_t);
|
||||
if (smem_size < P::kSmemMedium) smem_size = P::kSmemMedium;
|
||||
|
||||
// Query occupancy for the instantiation that will actually launch;
|
||||
// overestimating it deadlocks the cooperative barrier.
|
||||
int occupancy = 1;
|
||||
cudaOccupancyMaxActiveBlocksPerMultiprocessor(
|
||||
&occupancy, P::persistent_topk_kernel<TopK, 4>, P::kThreadsPerBlock,
|
||||
smem_size);
|
||||
cudaError_t occ_err = cudaSuccess;
|
||||
if (vec_size == 4) {
|
||||
occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
|
||||
&occupancy, P::persistent_topk_kernel<TopK, 4>, P::kThreadsPerBlock,
|
||||
smem_size);
|
||||
} else if (vec_size == 2) {
|
||||
occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
|
||||
&occupancy, P::persistent_topk_kernel<TopK, 2>, P::kThreadsPerBlock,
|
||||
smem_size);
|
||||
} else {
|
||||
occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
|
||||
&occupancy, P::persistent_topk_kernel<TopK, 1>, P::kThreadsPerBlock,
|
||||
smem_size);
|
||||
}
|
||||
TORCH_CHECK(occ_err == cudaSuccess,
|
||||
"persistent_topk occupancy query failed: ",
|
||||
cudaGetErrorString(occ_err));
|
||||
if (occupancy < 1) occupancy = 1;
|
||||
|
||||
uint32_t max_resident_ctas = static_cast<uint32_t>(num_sms) * occupancy;
|
||||
// The cooperative spin-wait barrier only runs when at least one row hits
|
||||
// the radix path (seq_len > RADIX_THRESHOLD). Below that, non-CTA-0 CTAs
|
||||
// early-exit, so oversubscription can't deadlock and headroom is wasted.
|
||||
const bool needs_cooperative =
|
||||
static_cast<uint32_t>(max_seq_len) > P::RADIX_THRESHOLD;
|
||||
|
||||
const uint32_t hw_resident_cap =
|
||||
static_cast<uint32_t>(num_sms) * static_cast<uint32_t>(occupancy);
|
||||
uint32_t max_resident_ctas = hw_resident_cap;
|
||||
if (needs_cooperative) {
|
||||
// Reserve one CTA per SM when occupancy allows; fall back to a single
|
||||
// CTA when occupancy == 1 (the most deadlock-prone case — any straggler
|
||||
// kernel that takes the only slot on one SM hangs the barrier). Never
|
||||
// drop below one full group's worth.
|
||||
uint32_t headroom = (occupancy > 1) ? static_cast<uint32_t>(num_sms) : 1u;
|
||||
if (max_resident_ctas >= headroom + ctas_per_group) {
|
||||
max_resident_ctas -= headroom;
|
||||
}
|
||||
}
|
||||
uint32_t num_groups = std::min(max_resident_ctas / ctas_per_group,
|
||||
static_cast<uint32_t>(num_rows));
|
||||
if (num_groups == 0) num_groups = 1;
|
||||
uint32_t total_ctas = num_groups * ctas_per_group;
|
||||
|
||||
// If the cooperative launch wouldn't fit, fall back to FilteredTopK
|
||||
// instead of deadlocking. Only relevant when needs_cooperative.
|
||||
if (needs_cooperative && total_ctas > hw_resident_cap) {
|
||||
TORCH_CHECK(max_smem_per_block >= 128 * 1024,
|
||||
"persistent_topk would oversubscribe and the FilteredTopK "
|
||||
"fallback requires >=128KB smem per block (have ",
|
||||
max_smem_per_block, "). total_ctas=", total_ctas,
|
||||
" > num_sms*occupancy=", hw_resident_cap, " (TopK=", TopK,
|
||||
", vec_size=", vec_size, ", ctas_per_group=", ctas_per_group,
|
||||
", smem=", smem_size, ").");
|
||||
cudaError_t status =
|
||||
vllm::FilteredTopKRaggedTransform<float, int32_t, TopK>(
|
||||
logits.data_ptr<float>(), output.data_ptr<int32_t>(),
|
||||
lengths.data_ptr<int32_t>(), static_cast<uint32_t>(num_rows),
|
||||
static_cast<uint32_t>(TopK), static_cast<uint32_t>(stride),
|
||||
stream);
|
||||
TORCH_CHECK(status == cudaSuccess,
|
||||
"FilteredTopK fallback failed: ", cudaGetErrorString(status));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t state_bytes = num_groups * sizeof(P::RadixRowState);
|
||||
TORCH_CHECK(workspace.size(0) >= static_cast<int64_t>(state_bytes),
|
||||
"workspace too small, need ", state_bytes, " bytes");
|
||||
|
||||
// Zero the per-group RadixRowState region before launch — only when the
|
||||
// radix path will actually run (max_seq_len > RADIX_THRESHOLD). The
|
||||
// RadixRowState fields (arrival_counter, histograms) are only touched by
|
||||
// radix_topk; the decode/medium paths inside the persistent kernel
|
||||
// operate purely in shared memory and never read these globals, so a
|
||||
// stale workspace is harmless for them.
|
||||
//
|
||||
// Why we need the memset (when needs_cooperative is true):
|
||||
// 1. arrival_counter accumulates within a launch and is never reset,
|
||||
// so a prior call leaves it at a large positive value. Without this
|
||||
// reset, the very first wait_ge in the next call sees counter >>
|
||||
// target and returns instantly, breaking the barrier.
|
||||
// 2. The previous in-kernel init only ran in CTA-0 with intra-CTA
|
||||
// __syncthreads(), so it had no happens-before edge to CTA-1+'s
|
||||
// first red_release. cudaMemsetAsync is stream-ordered: the zero
|
||||
// is globally visible before any CTA runs.
|
||||
if (needs_cooperative) {
|
||||
cudaError_t mz_err = cudaMemsetAsync(workspace.data_ptr<uint8_t>(), 0,
|
||||
state_bytes, stream);
|
||||
TORCH_CHECK(mz_err == cudaSuccess,
|
||||
"row_states memset failed: ", cudaGetErrorString(mz_err));
|
||||
}
|
||||
|
||||
P::PersistentTopKParams params;
|
||||
params.input = logits.data_ptr<float>();
|
||||
params.output = output.data_ptr<int32_t>();
|
||||
|
||||
@@ -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(
|
||||
|
||||
+67
-26
@@ -41,6 +41,13 @@ ARG BUILD_BASE_IMAGE=nvidia/cuda:${CUDA_VERSION}-devel-ubuntu22.04
|
||||
# Using cuda base image with minimal dependencies necessary for JIT compilation (FlashInfer, DeepGEMM, EP kernels)
|
||||
ARG FINAL_BASE_IMAGE=nvidia/cuda:${CUDA_VERSION}-base-ubuntu${UBUNTU_VERSION}
|
||||
|
||||
# OS family of BUILD_BASE_IMAGE. Controls package manager (apt vs dnf) and
|
||||
# Python bootstrap. Set to "manylinux" alongside a manylinux build base such
|
||||
# as pytorch/manylinux2_28-builder:cuda13.0 to produce wheels with a glibc
|
||||
# 2.28 floor (matches PyTorch's own published wheels). Default stays on
|
||||
# Ubuntu for backwards compatibility.
|
||||
ARG BUILD_OS=ubuntu
|
||||
|
||||
# By parameterizing the Deadsnakes repository URL, we allow third-party to use
|
||||
# their own mirror. When doing so, we don't benefit from the transparent
|
||||
# installation of the GPG key of the PPA, as done by add-apt-repository, so we
|
||||
@@ -94,35 +101,64 @@ FROM ${BUILD_BASE_IMAGE} AS base
|
||||
|
||||
ARG CUDA_VERSION
|
||||
ARG PYTHON_VERSION
|
||||
ARG BUILD_OS
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install system dependencies including build tools
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ccache \
|
||||
software-properties-common \
|
||||
git \
|
||||
curl \
|
||||
sudo \
|
||||
python3-pip \
|
||||
libibverbs-dev \
|
||||
# Upgrade to GCC 10 to avoid https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92519
|
||||
# as it was causing spam when compiling the CUTLASS kernels
|
||||
gcc-10 \
|
||||
g++-10 \
|
||||
&& update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 110 --slave /usr/bin/g++ g++ /usr/bin/g++-10 \
|
||||
# Install python dev headers if available (needed for cmake FindPython on Ubuntu 24.04
|
||||
# which ships cmake 3.28 and requires Development.SABIModule; silently skipped on
|
||||
# Ubuntu 20.04/22.04 where python3.x-dev is not available without a PPA)
|
||||
&& (apt-get install -y --no-install-recommends python${PYTHON_VERSION}-dev 2>/dev/null || true) \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& $HOME/.local/bin/uv venv /opt/venv --python ${PYTHON_VERSION} \
|
||||
# Install system dependencies including build tools.
|
||||
# The Ubuntu path uses apt + deadsnakes-via-uv for Python; the manylinux path
|
||||
# (AlmaLinux 8, e.g. pytorch/manylinux2_28-builder) uses dnf and the Python
|
||||
# interpreters pre-installed at /opt/python/cpXY-cpXY/.
|
||||
RUN if [ "${BUILD_OS}" = "manylinux" ]; then \
|
||||
# rdma-core-devel provides libibverbs headers; ccache lives in EPEL,
|
||||
# which the pytorch manylinux image already enables. git/curl/sudo
|
||||
# are typically pre-installed but listed defensively.
|
||||
dnf install -y --setopt=install_weak_deps=False \
|
||||
ccache \
|
||||
git \
|
||||
curl \
|
||||
sudo \
|
||||
rdma-core-devel \
|
||||
&& dnf clean all \
|
||||
&& rm -rf /var/cache/dnf; \
|
||||
else \
|
||||
apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ccache \
|
||||
software-properties-common \
|
||||
git \
|
||||
curl \
|
||||
sudo \
|
||||
python3-pip \
|
||||
libibverbs-dev \
|
||||
# Upgrade to GCC 10 to avoid https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92519
|
||||
# as it was causing spam when compiling the CUTLASS kernels
|
||||
gcc-10 \
|
||||
g++-10 \
|
||||
&& update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 110 --slave /usr/bin/g++ g++ /usr/bin/g++-10 \
|
||||
# Install python dev headers if available (needed for cmake FindPython on Ubuntu 24.04
|
||||
# which ships cmake 3.28 and requires Development.SABIModule; silently skipped on
|
||||
# Ubuntu 20.04/22.04 where python3.x-dev is not available without a PPA)
|
||||
&& (apt-get install -y --no-install-recommends python${PYTHON_VERSION}-dev 2>/dev/null || true) \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install uv and bootstrap /opt/venv. Both paths converge on /opt/venv so all
|
||||
# downstream stages stay distro-agnostic.
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& if [ "${BUILD_OS}" = "manylinux" ]; then \
|
||||
# manylinux images ship Python at /opt/python/cpXY-cpXY/; point uv
|
||||
# at the matching interpreter rather than letting it fetch one.
|
||||
PYV_NODOT=$(echo ${PYTHON_VERSION} | tr -d '.') \
|
||||
&& MANYLINUX_PY=/opt/python/cp${PYV_NODOT}-cp${PYV_NODOT}/bin/python${PYTHON_VERSION} \
|
||||
&& $HOME/.local/bin/uv venv /opt/venv --python "$MANYLINUX_PY"; \
|
||||
else \
|
||||
$HOME/.local/bin/uv venv /opt/venv --python ${PYTHON_VERSION}; \
|
||||
fi \
|
||||
&& rm -f /usr/bin/python3 /usr/bin/python3-config /usr/bin/pip \
|
||||
&& ln -s /opt/venv/bin/python3 /usr/bin/python3 \
|
||||
&& ln -s /opt/venv/bin/python3-config /usr/bin/python3-config \
|
||||
&& ln -s /opt/venv/bin/pip /usr/bin/pip \
|
||||
&& ln -sf /opt/venv/bin/python3 /usr/bin/python3 \
|
||||
&& ln -sf /opt/venv/bin/python3-config /usr/bin/python3-config \
|
||||
&& ln -sf /opt/venv/bin/pip /usr/bin/pip \
|
||||
&& python3 --version && python3 -m pip --version
|
||||
|
||||
# Activate virtual environment and add uv to PATH
|
||||
@@ -433,6 +469,7 @@ FROM base AS dev
|
||||
ARG PIP_INDEX_URL UV_INDEX_URL
|
||||
ARG PIP_EXTRA_INDEX_URL UV_EXTRA_INDEX_URL
|
||||
ARG PYTORCH_CUDA_INDEX_BASE_URL
|
||||
ARG BUILD_OS
|
||||
|
||||
# This timeout (in seconds) is necessary when installing some dependencies via uv since it's likely to time out
|
||||
# Reference: https://github.com/astral-sh/uv/pull/1694
|
||||
@@ -442,7 +479,11 @@ ENV UV_INDEX_STRATEGY="unsafe-best-match"
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install libnuma-dev, required by fastsafetensors (fixes #20384)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libnuma-dev && rm -rf /var/lib/apt/lists/*
|
||||
RUN if [ "${BUILD_OS}" = "manylinux" ]; then \
|
||||
dnf install -y numactl-devel && dnf clean all && rm -rf /var/cache/dnf; \
|
||||
else \
|
||||
apt-get update && apt-get install -y --no-install-recommends libnuma-dev && rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
|
||||
# We can specify the standard or nightly build of PyTorch
|
||||
|
||||
+18
-10
@@ -124,9 +124,9 @@ COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/vllm/v1 /vllm_v1
|
||||
|
||||
# RIXL/UCX build stages
|
||||
FROM base AS build_rixl
|
||||
ARG RIXL_BRANCH="bf4a7214"
|
||||
ARG RIXL_BRANCH="39be1de8"
|
||||
ARG RIXL_REPO="https://github.com/ROCm/RIXL.git"
|
||||
ARG UCX_BRANCH="7009d7a1"
|
||||
ARG UCX_BRANCH="bfb51733"
|
||||
ARG UCX_REPO="https://github.com/openucx/ucx.git"
|
||||
ENV ROCM_PATH=/opt/rocm
|
||||
ENV UCX_HOME=/usr/local/ucx
|
||||
@@ -192,6 +192,7 @@ RUN cd /opt/rixl && \
|
||||
sed -i "s/--exclude 'libamdhip64\*'/--exclude 'libamdhip64*' --exclude 'libcore*' --exclude 'libpull*'/" \
|
||||
contrib/build-wheel.sh && \
|
||||
mkdir -p /app/install && \
|
||||
_ucx_install_dir=${UCX_HOME} \
|
||||
./contrib/build-wheel.sh \
|
||||
--output-dir /app/install \
|
||||
--rocm-dir ${ROCM_PATH} \
|
||||
@@ -200,9 +201,9 @@ RUN cd /opt/rixl && \
|
||||
|
||||
# DeepEP build stage
|
||||
FROM base AS build_deep
|
||||
ARG ROCSHMEM_BRANCH="ba0bf0f3"
|
||||
ARG ROCSHMEM_BRANCH="f0acb0c6"
|
||||
ARG ROCSHMEM_REPO="https://github.com/ROCm/rocm-systems.git"
|
||||
ARG DEEPEP_BRANCH="5d90af8b"
|
||||
ARG DEEPEP_BRANCH="a9ea9774"
|
||||
ARG DEEPEP_REPO="https://github.com/ROCm/DeepEP.git"
|
||||
ARG DEEPEP_NIC="cx7"
|
||||
ARG DEEPEP_ROCM_ARCH="gfx942;gfx950"
|
||||
@@ -213,18 +214,15 @@ RUN git clone ${ROCSHMEM_REPO} \
|
||||
&& git checkout ${ROCSHMEM_BRANCH} \
|
||||
&& mkdir -p projects/rocshmem/build \
|
||||
&& cd projects/rocshmem/build \
|
||||
&& bash ../scripts/build_configs/all_backends \
|
||||
-DCMAKE_INSTALL_PREFIX="${ROCSHMEM_DIR}" \
|
||||
-DROCM_PATH=/opt/rocm \
|
||||
-DGPU_TARGETS="${DEEPEP_ROCM_ARCH}" \
|
||||
-DUSE_EXTERNAL_MPI=OFF
|
||||
&& INSTALL_PREFIX=${ROCSHMEM_DIR} \
|
||||
../scripts/build_configs/all_backends -DUSE_EXTERNAL_MPI=OFF
|
||||
|
||||
# Build DeepEP wheel.
|
||||
# DeepEP looks for rocshmem at ROCSHMEM_DIR.
|
||||
RUN git clone ${DEEPEP_REPO} \
|
||||
&& cd DeepEP \
|
||||
&& git checkout ${DEEPEP_BRANCH} \
|
||||
&& python3 setup.py --variant rocm --nic ${DEEPEP_NIC} bdist_wheel --dist-dir=/app/deep_install
|
||||
&& python3 setup.py --variant rocm --rocm-explicit-ctx --nic ${DEEPEP_NIC} bdist_wheel --dist-dir=/app/deep_install
|
||||
|
||||
# MoRI runtime dependencies live in Dockerfile.rocm so NIC backend changes do
|
||||
# not force users to rebuild the long-lived Dockerfile.rocm_base image.
|
||||
@@ -388,6 +386,16 @@ RUN --mount=type=bind,from=export_vllm,src=/,target=/install \
|
||||
# above are not available once that RUN step completes.
|
||||
COPY --from=export_vllm /*.whl /opt/vllm-wheels/
|
||||
|
||||
# Update rdma-core to support latest rocshmem
|
||||
ARG DEEPEP_NIC
|
||||
RUN if [ "${DEEPEP_NIC}" = "cx7" ] || [ "${DEEPEP_NIC}" = "io" ]; then \
|
||||
git clone --branch v62.0 --depth 1 https://github.com/linux-rdma/rdma-core.git /tmp/rdma-core && \
|
||||
cd /tmp/rdma-core && \
|
||||
mkdir -p build && cd build && \
|
||||
cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr -DNO_MAN_PAGES=1 .. && \
|
||||
ninja && ninja install && ldconfig && rm -rf /tmp/rdma-core; \
|
||||
fi
|
||||
|
||||
# Install RIXL wheel
|
||||
RUN --mount=type=bind,from=build_rixl,src=/app/install,target=/rixl_install \
|
||||
uv pip install --system /rixl_install/*.whl
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG BASE_IMAGE=rocm/dev-ubuntu-22.04:7.2.1-complete
|
||||
ARG BASE_IMAGE=rocm/dev-ubuntu-22.04:7.2.2-complete
|
||||
ARG TRITON_BRANCH="ba5c1517"
|
||||
ARG TRITON_REPO="https://github.com/ROCm/triton.git"
|
||||
ARG PYTORCH_BRANCH="8514f051" # release/2.10 as of 3/17
|
||||
@@ -9,7 +9,7 @@ ARG PYTORCH_AUDIO_BRANCH="v2.9.0"
|
||||
ARG PYTORCH_AUDIO_REPO="https://github.com/pytorch/audio.git"
|
||||
ARG FA_BRANCH="0e60e394"
|
||||
ARG FA_REPO="https://github.com/Dao-AILab/flash-attention.git"
|
||||
ARG AITER_BRANCH="v0.1.10.post3"
|
||||
ARG AITER_BRANCH="v0.1.12.post2"
|
||||
ARG AITER_REPO="https://github.com/ROCm/aiter.git"
|
||||
ARG MORI_BRANCH="v1.1.0"
|
||||
ARG MORI_REPO="https://github.com/ROCm/mori.git"
|
||||
@@ -104,6 +104,28 @@ ENV SCCACHE_REGION=${USE_SCCACHE:+${SCCACHE_REGION_NAME}}
|
||||
ENV SCCACHE_S3_NO_CREDENTIALS=${USE_SCCACHE:+${SCCACHE_S3_NO_CREDENTIALS}}
|
||||
ENV SCCACHE_IDLE_TIMEOUT=${USE_SCCACHE:+0}
|
||||
|
||||
# torch profiler hotfix for 7.2.2: rebuild CLR with https://github.com/ROCm/rocm-systems/pull/5062
|
||||
# will be removed once we move to ROCm 7.2.3
|
||||
RUN apt-get update && apt-get install -y rocm-llvm-dev
|
||||
RUN pip install CppHeaderParser
|
||||
RUN git clone --no-checkout --filter=blob:none https://github.com/ROCm/rocm-systems /tmp/rocm-systems \
|
||||
&& cd /tmp/rocm-systems \
|
||||
&& git sparse-checkout init --cone \
|
||||
&& git sparse-checkout set projects/hip projects/clr \
|
||||
&& git checkout 35e8c7bf8911862e5389509800e65fdf125412b3 \
|
||||
&& export CLR_DIR=/tmp/rocm-systems/projects/clr \
|
||||
&& export HIP_DIR=/tmp/rocm-systems/projects/hip \
|
||||
&& mkdir -p $CLR_DIR/build && cd $CLR_DIR/build \
|
||||
&& cmake \
|
||||
-DHIP_COMMON_DIR=$HIP_DIR \
|
||||
-DCMAKE_PREFIX_PATH="/opt/rocm/" \
|
||||
-DCLR_BUILD_HIP=ON \
|
||||
-DCLR_BUILD_OCL=OFF \
|
||||
-DHIP_PLATFORM=amd \
|
||||
.. \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/rocm-systems
|
||||
|
||||
###
|
||||
### Triton Build
|
||||
@@ -153,8 +175,6 @@ RUN git clone ${PYTORCH_REPO} pytorch
|
||||
RUN cd pytorch && git checkout ${PYTORCH_BRANCH}
|
||||
RUN cd pytorch \
|
||||
&& pip install -r requirements.txt && git submodule update --init --recursive
|
||||
RUN cd pytorch/third_party/kineto \
|
||||
&& git remote add rocm https://github.com/ROCm/kineto && git fetch rocm && git checkout 2d73be3
|
||||
RUN cd pytorch && python3 tools/amd_build/build_amd.py \
|
||||
&& if [ "$USE_SCCACHE" = "1" ]; then \
|
||||
export HIP_CLANG_PATH=/opt/sccache-wrappers \
|
||||
|
||||
+14
-5
@@ -5,9 +5,6 @@ WORKDIR /workspace/
|
||||
ARG PYTHON_VERSION=3.12
|
||||
ARG PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/xpu"
|
||||
|
||||
RUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | tee /etc/apt/sources.list.d/oneAPI.list
|
||||
|
||||
RUN apt clean && apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends --fix-missing \
|
||||
curl \
|
||||
@@ -26,8 +23,20 @@ RUN apt clean && apt-get update -y && \
|
||||
python3.12-dev \
|
||||
python3-pip
|
||||
|
||||
RUN apt update && apt upgrade -y && \
|
||||
apt install -y intel-oneapi-compiler-dpcpp-cpp-2025.3
|
||||
# Add oneAPI repo, pin oneAPI to 2025.3, then install pinned packages in one layer.
|
||||
RUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | tee /etc/apt/sources.list.d/oneAPI.list && \
|
||||
printf '%s\n' \
|
||||
'Package: intel-oneapi-* intel-deep-learning-essentials* intel-pti*' \
|
||||
'Pin: version 2025.3*' \
|
||||
'Pin-Priority: 1001' \
|
||||
> /etc/apt/preferences.d/oneapi-2025.3.pref && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
intel-oneapi-compiler-dpcpp-cpp-2025.3 \
|
||||
intel-oneapi-mkl-devel-2025.3 \
|
||||
intel-oneapi-dnnl-devel-2025.3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install UMD
|
||||
RUN mkdir neo && \
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"FINAL_BASE_IMAGE": {
|
||||
"default": "nvidia/cuda:13.0.2-base-ubuntu22.04"
|
||||
},
|
||||
"BUILD_OS": {
|
||||
"default": "ubuntu"
|
||||
},
|
||||
"GET_PIP_URL": {
|
||||
"default": "https://bootstrap.pypa.io/get-pip.py"
|
||||
},
|
||||
|
||||
@@ -60,9 +60,19 @@ the failure?
|
||||
|
||||
## Logs Wrangling
|
||||
|
||||
Download the full log file from Buildkite locally.
|
||||
Download a job's log (no Buildkite login required):
|
||||
|
||||
Strip timestamps and colorization:
|
||||
[.buildkite/scripts/ci-fetch-log.sh](../../../.buildkite/scripts/ci-fetch-log.sh)
|
||||
|
||||
```bash
|
||||
# Find the failing job. Each row's URL is .../builds/<N>#<job_uuid>:
|
||||
gh pr checks <PR> --repo vllm-project/vllm
|
||||
|
||||
# Download + strip timestamps/ANSI in one step:
|
||||
.buildkite/scripts/ci-fetch-log.sh "https://buildkite.com/vllm/ci/builds/<N>#<job_uuid>"
|
||||
```
|
||||
|
||||
To clean an already-downloaded log:
|
||||
|
||||
[.buildkite/scripts/ci-clean-log.sh](../../../.buildkite/scripts/ci-clean-log.sh)
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ Priority is **1 = highest** (tried first).
|
||||
| **Block Sizes** | Supported KV cache block sizes (%N means multiples of N) |
|
||||
| **Head Sizes** | Supported attention head sizes |
|
||||
| **Sink** | Attention sink support (for StreamingLLM) |
|
||||
| **Non-Causal** | Non-causal (bidirectional) attention support for decoder models |
|
||||
| **Sparse** | Sparse attention support (MLA only) |
|
||||
| **MM Prefix** | Multimodal prefix full attention support |
|
||||
| **DCP** | Decode Context Parallelism support (`--decode-context-parallel-size`) |
|
||||
@@ -165,22 +166,22 @@ Priority is **1 = highest** (tried first).
|
||||
|
||||
## Standard Attention (MHA, MQA, GQA) Backends
|
||||
|
||||
| Backend | Version | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | MM Prefix | DCP | Attention Types | Compute Cap. |
|
||||
| ------- | ------- | ------ | --------- | ----------- | ---------- | ---- | --------- | --- | --------------- | ------------ |
|
||||
| `CPU_ATTN` | | fp16, bf16, fp32 | `auto`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | Any | 32, 64, 80, 96, 112, 128, 160, 192, 224, 256, 512 | ❌ | ❌ | ❌ | All | N/A |
|
||||
| `FLASHINFER` | Native† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64 | 64, 128, 256 | ❌ | ❌ | ✅ | Decoder | 7.x-9.x |
|
||||
| `FLASHINFER` | TRTLLM† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64 | 64, 128, 256 | ✅ | ❌ | ✅ | Decoder | 10.x |
|
||||
| `FLASH_ATTN` | FA2* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ✅ | All | ≥8.0 |
|
||||
| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | Any | ✅ | ❌ | ✅ | All | 9.x |
|
||||
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ❌ | ✅ | All | ≥10.0 |
|
||||
| `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ✅ | Decoder | Any |
|
||||
| `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | Any | Any | ❌ | ✅ | ❌ | Decoder, Encoder Only | Any |
|
||||
| `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ❌ | ❌ | ❌ | Decoder | N/A |
|
||||
| `ROCM_AITER_UNIFIED_ATTN` | | fp16, bf16 | `auto` | %16 | Any | ✅ | ✅ | ❌ | All | N/A |
|
||||
| `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
|
||||
| `TREE_ATTN` | | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | 32, 64, 96, 128, 160, 192, 224, 256 | ❌ | ❌ | ❌ | Decoder | Any |
|
||||
| `TRITON_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `int8_per_token_head`, `fp8_per_token_head` | %16 | Any | ✅ | ✅ | ❌ | All | Any |
|
||||
| `TURBOQUANT` | | fp16, bf16 | `turboquant_k8v4`, `turboquant_4bit_nc`, `turboquant_k3v4_nc`, `turboquant_3bit_nc` | 16, 32, 64, 128 | Any | ❌ | ❌ | ❌ | Decoder | Any |
|
||||
| Backend | Version | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | MM Prefix | DCP | Attention Types | Compute Cap. |
|
||||
| ------- | ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | --------- | --- | --------------- | ------------ |
|
||||
| `CPU_ATTN` | | fp16, bf16, fp32 | `auto`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | Any | 32, 64, 80, 96, 112, 128, 160, 192, 224, 256, 512 | ❌ | ❌ | ❌ | ❌ | All | N/A |
|
||||
| `FLASHINFER` | Native† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64 | 64, 128, 256 | ❌ | ❌ | ❌ | ✅ | Decoder | 7.x-9.x |
|
||||
| `FLASHINFER` | TRTLLM† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `nvfp4` | 16, 32, 64 | 64, 128, 256 | ✅ | ❌ | ❌ | ✅ | Decoder | 10.x |
|
||||
| `FLASH_ATTN` | FA2* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ❌ | ✅ | All | ≥8.0 |
|
||||
| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | 9.x |
|
||||
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
|
||||
| `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
|
||||
| `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder Only | Any |
|
||||
| `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
|
||||
| `ROCM_AITER_UNIFIED_ATTN` | | fp16, bf16 | `auto` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | N/A |
|
||||
| `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
|
||||
| `TREE_ATTN` | | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | 32, 64, 96, 128, 160, 192, 224, 256 | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
|
||||
| `TRITON_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `int8_per_token_head`, `fp8_per_token_head` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | Any |
|
||||
| `TURBOQUANT` | | fp16, bf16 | `turboquant_k8v4`, `turboquant_4bit_nc`, `turboquant_k3v4_nc`, `turboquant_3bit_nc` | 16, 32, 64, 128 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
|
||||
|
||||
> **†** FlashInfer uses TRTLLM attention on Blackwell (SM100), which supports sinks. Disable via `--attention-config.use_trtllm_attention=0`.
|
||||
>
|
||||
@@ -192,31 +193,35 @@ MLA uses separate backends for prefill and decode phases.
|
||||
|
||||
### Prefill Backends
|
||||
|
||||
The prefill backend is selected at runtime based on hardware and
|
||||
configuration.
|
||||
To explicitly select a prefill backend, use
|
||||
`-ac.mla_prefill_backend=<BACKEND>` (e.g., `FLASH_ATTN`, `FLASHINFER`).
|
||||
Otherwise, the prefill backend is selected automatically at runtime based on
|
||||
hardware and configuration.
|
||||
|
||||
| Backend | Description | Compute Cap. | Enable | Disable | Notes |
|
||||
| ------- | ----------- | ------------ | ------ | ------- | ----- |
|
||||
| TRT-LLM Ragged‡ | TensorRT-LLM ragged attention | 10.x | Default on SM100 | `-ac.use_trtllm_ragged_deepseek_prefill=0` | DeepSeek R1 dims only |
|
||||
| FlashInfer | FlashInfer CUTLASS backend | 10.x | `-ac.disable_flashinfer_prefill=0` | `-ac.disable_flashinfer_prefill=1` | DeepSeek R1 dims only |
|
||||
| cuDNN | cuDNN-based attention | 10.x | `-ac.use_cudnn_prefill=1` | `-ac.use_cudnn_prefill=0` | |
|
||||
| FlashAttention | FlashAttention varlen (FA2/FA3) | Any | Default fallback | Use other backends | FA3 on SM90, FA2 otherwise |
|
||||
| Backend | Description | Dtypes | Compute Cap. | Notes |
|
||||
| ------- | ----------- | ------ | ------------ | ----- |
|
||||
| `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 |
|
||||
|
||||
> **‡** TRT-LLM Ragged is the default on Blackwell (SM100).
|
||||
> On other GPUs, FlashAttention is used as the default.
|
||||
|
||||
### Decode Backends
|
||||
|
||||
| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
|
||||
| ------- | ------ | --------- | ----------- | ---------- | ---- | ------ | --------- | --- | --------------- | ------------ |
|
||||
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
|
||||
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
|
||||
| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
|
||||
| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
|
||||
| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 512, 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
|
||||
| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
|
||||
| `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` | 1 | Any | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
|
||||
| `ROCM_AITER_TRITON_MLA` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
|
||||
| `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 |
|
||||
MLA decode backends are selected using the standard
|
||||
`-ac.backend=<BACKEND>` argument (e.g., `FLASHMLA`, `TRITON_MLA`).
|
||||
|
||||
| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
|
||||
| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
|
||||
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
|
||||
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
|
||||
| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
|
||||
| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
|
||||
| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 512, 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
|
||||
| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
@@ -86,9 +86,11 @@ Models opt-in to encoder CUDA Graphs by implementing the [SupportsEncoderCudaGra
|
||||
| Architecture | Models | CG for Image | CG for Video |
|
||||
| ------------ | ------ | ------------ | ------------ |
|
||||
| `Qwen3VLForConditionalGeneration` | `Qwen3-VL` | ✅︎ | ✅︎ |
|
||||
| `Qwen2_5_VLForConditionalGeneration` | `Qwen2.5-VL` | ✅︎ | ✅︎ |
|
||||
|
||||
!!! note
|
||||
Encoder CUDA Graphs have currently been tested with `--mm-encoder-attn-backend=FLASH_ATTN` and `--mm-encoder-attn-backend=FLASHINFER` on Blackwell GPUs.
|
||||
For Qwen2.5-VL only FA2 and FA3 has been tested.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ TL;DR:
|
||||
- use tlparse to acquire torch.compile logs. Include these logs in bug reports and/or support asks.
|
||||
- The vLLM-torch.compile integration is multiple pieces. vLLM exposes flags to turn off each piece:
|
||||
|
||||
| Online Flag | Offline Flag | Result |
|
||||
| ----------- | ------------ | ------ |
|
||||
| --enforce-eager | enforce_eager=True | Turn off torch.compile and CUDAGraphs |
|
||||
| -cc.mode=0 | mode=CompilationMode.NONE | Turn off torch.compile only |
|
||||
| -cc.cudagraph_mode=NONE | compilation_config=CompilationConfig(cudagraph_mode=CUDAGraphMode.NONE) | Turn off CUDAGraphs only |
|
||||
| -cc.backend=eager | compilation_config=CompilationConfig(backend='eager') | Turn off TorchInductor |
|
||||
| Online Flag | Offline Flag | Result |
|
||||
|--------------------------------|--------------------------------------------------------------------------------|------------------------------------------------------|
|
||||
| --enforce-eager | enforce_eager=True | Turn off torch.compile and CUDAGraphs |
|
||||
| -cc.mode=0 | compilation_config=CompilationConfig(mode=CompilationMode.NONE) | Turn off torch.compile only |
|
||||
| -cc.mode=1 | compilation_config=CompilationConfig(mode=CompilationMode.STOCK_TORCH_COMPILE) | Turn off vLLM-compile modifications to torch.compile |
|
||||
| -cc.cudagraph_mode=NONE | compilation_config=CompilationConfig(cudagraph_mode=CUDAGraphMode.NONE) | Turn off CUDAGraphs only |
|
||||
| -cc.backend=eager | compilation_config=CompilationConfig(backend='eager') | Turn off TorchInductor |
|
||||
| -cc.ir_enable_torch_wrap=False | compilation_config=CompilationConfig(ir_enable_torch_wrap=False) | Turn off vLLM IR wrapping |
|
||||
|
||||
## vLLM-torch.compile overview
|
||||
|
||||
@@ -22,7 +24,7 @@ Most notably, vLLM-compile is NOT torch.compile, it is a custom compiler built u
|
||||
|
||||
- Given a model, we do a full graph capture via TorchDynamo that is dynamic on the batch size (number of tokens)
|
||||
- vLLM then optionally splits and/or specializes this graph and then uses TorchInductor to compile each graph into a compiled artifact.
|
||||
This step may use vLLM custom Inductor passes to further optimize the graph.
|
||||
This step may use vLLM custom Inductor passes to further optimize the graph. This includes vLLM IR lowering to remove dispatch overhead.
|
||||
- The compiled artifact is saved to vLLM's compile cache so that it can be loaded in the future.
|
||||
- vLLM applies CUDAGraphs to reduce CPU overheads.
|
||||
|
||||
@@ -34,6 +36,7 @@ For more details on the design, please see the following resources:
|
||||
|
||||
- [Introduction to vLLM-torch.compile blogpost](https://blog.vllm.ai/2025/08/20/torch-compile.html)
|
||||
- [vLLM-torch.compile integration design](./torch_compile.md)
|
||||
- [vLLM IR design](./vllm_ir.md)
|
||||
- [vLLM Office Hours #26](https://www.youtube.com/live/xLyxc7hxCJc?si=Xulo9pe53C6ywf0V&t=561)
|
||||
- [Talk at PyTorch Conference 2025](https://youtu.be/1wV1ESbGrVQ?si=s1GqymUfwiwOrDTg&t=725)
|
||||
|
||||
@@ -117,6 +120,21 @@ from vllm.config.compilation import CompilationConfig, CUDAGraphMode
|
||||
LLM(model, compilation_config=CompilationConfig(cudagraph_mode=CUDAGraphMode.NONE))
|
||||
```
|
||||
|
||||
vLLM IR makes heavy use of the compilation pipeline, from functionalization, custom fusions, and lowering.
|
||||
To turn that off and capture eager-mode dispatching behavior of vLLM IR, run with `ir_enable_torch_wrap=False`.
|
||||
IR torch wrap is only enabled by default when using `mode=VLLM_COMPILE` and `backend="inductor"` (default).
|
||||
|
||||
```sh
|
||||
# Online
|
||||
vllm serve -cc.ir_enable_torch_wrap=False
|
||||
```
|
||||
|
||||
```py
|
||||
# Offline
|
||||
from vllm.config.compilation import CompilationConfig
|
||||
LLM(model, compilation_config=CompilationConfig(ir_enable_torch_wrap=False))
|
||||
```
|
||||
|
||||
## Debugging TorchDynamo
|
||||
|
||||
vLLM requires model code be capturable into a full graph via TorchDynamo (torch.compile's frontend).
|
||||
|
||||
@@ -36,7 +36,7 @@ th {
|
||||
| deepep_high_throughput | standard | fp8 | G(128),A,T<sup>2</sup> | Y | Y | [`DeepEPHTPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_ht.DeepEPHTPrepareAndFinalize] |
|
||||
| deepep_low_latency | batched | fp8 | G(128),A,T<sup>3</sup> | Y | Y | [`DeepEPLLPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_ll.DeepEPLLPrepareAndFinalize] |
|
||||
| flashinfer_nvlink_two_sided | standard | nvfp4,fp8 | G,A,T | N | N | [`FlashInferNVLinkTwoSidedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_two_sided.FlashInferNVLinkTwoSidedPrepareAndFinalize] |
|
||||
| flashinfer_nvlink_one_sided | standard | nvfp4 | G,A,T | N | N | [`FlashInferNVLinkOneSidedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_one_sided.FlashInferNVLinkOneSidedPrepareAndFinalize] |
|
||||
| flashinfer_nvlink_one_sided | standard | nvfp4,bf16,mxfp8 | G,A,T | N | N | [`FlashInferNVLinkOneSidedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_one_sided.FlashInferNVLinkOneSidedPrepareAndFinalize] |
|
||||
|
||||
!!! info "Table key"
|
||||
1. All types: mxfp4, nvfp4, int4, int8, fp8
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
# vLLM IR: Functional Intermediate Representation
|
||||
|
||||
## Motivation
|
||||
|
||||
vLLM IR is a **functional intermediate representation (IR)** that fills the gap between
|
||||
low-level `torch` ops and vLLM layers like `RMSNorm` and quantization operators,
|
||||
By separating operator **semantics** from the **implementation** and **dispatching**,
|
||||
vLLM IR simplifies both compilation and kernel registration & dispatching simultaneously.
|
||||
It operates as a **dialect** in the torch FX representation, allowing full interoperability
|
||||
with “regular” torch ops & custom torch ops/kernels, as well as a piecewise migration from
|
||||
the previous `CustomOp` approach.
|
||||
|
||||
Key design principles:
|
||||
|
||||
- **Eager-compile consistency**: identical behavior (barring minor numerics) in eager and compiled modes
|
||||
- **Simple, transparent, yet powerful kernel selection**: good visibility and control allowing easy debugging
|
||||
- **Convention over configuration**: near-zero boilerplate required to register ops and implementations
|
||||
- **Extensibility**: ops and implementations can be registered anywhere, in-tree or out-of-tree
|
||||
- **Interoperability**: fully compatible with “regular” torch ops & custom torch ops/kernels,
|
||||
reducing developer friction and allowing piecewise migration
|
||||
|
||||
The clean semantics/implementation separation enables a unified and extensible dispatching mechanism,
|
||||
allowing multiple kernels per-platform and powerful kernel selection. The separation also facilitates
|
||||
cleaner testing and benchmarking, removing much of the boilerplate standard for legacy approaches.
|
||||
|
||||
By delaying kernel selection until late in the compilation process, the compiler can operate on
|
||||
a higher-level representation, which has the following main benefits:
|
||||
|
||||
- Pattern matching in fusion/transformation passes only requires a single, simple pattern per op
|
||||
- OOT compiler backends can lower from the higher-level representation (in-progress)
|
||||
- The compiler can autotune over available implementations (future feature)
|
||||
|
||||
## Quick Overview
|
||||
|
||||
### Declaring an IR Operation
|
||||
|
||||
IR operations are declared using the `@register_op` decorator with a native PyTorch implementation that defines the op's semantics:
|
||||
|
||||
```python
|
||||
# vllm/ir/ops/layernorm.py
|
||||
from torch import Tensor
|
||||
from vllm.ir import register_op
|
||||
|
||||
@register_op
|
||||
def rms_norm(x: Tensor, weight: Tensor | None, epsilon: float, variance_size: int | None = None) -> Tensor:
|
||||
"""Weighted root-mean-square layer normalization"""
|
||||
orig_dtype = x.dtype
|
||||
x = x.to(torch.float32)
|
||||
x_var = x if variance_size is None else x[..., :variance_size]
|
||||
variance = x_var.pow(2).mean(dim=-1, keepdim=True)
|
||||
x = x * torch.rsqrt(variance + epsilon)
|
||||
x = x.to(orig_dtype)
|
||||
if weight is not None:
|
||||
x = x * weight
|
||||
return x
|
||||
```
|
||||
|
||||
The native implementation serves three purposes:
|
||||
|
||||
1. **Semantic definition**: Specifies the exact semantics of the operation, including shapes and strides
|
||||
2. **Default implementation**: Used when no other (better) implementation is available
|
||||
3. **Reference for testing**: Other implementations must match these semantics
|
||||
|
||||
### Registering Implementations
|
||||
|
||||
Kernel implementations are registered using the `register_impl` decorator on the IR op object:
|
||||
|
||||
```python
|
||||
# vllm/kernels/vllm_c.py
|
||||
from vllm import ir
|
||||
|
||||
rms_norm_no_var = lambda x, weight, epsilon, variance_size=None: variance_size is None
|
||||
|
||||
@ir.ops.rms_norm.register_impl("vllm_c", supports_args=rms_norm_no_var, supported=current_platform.is_cuda_alike())
|
||||
def rms_norm(x: Tensor, weight: Tensor | None, epsilon: float, variance_size: int | None = None) -> Tensor:
|
||||
output = torch.empty_like(x)
|
||||
torch.ops._C.rms_norm(output, x, weight, epsilon)
|
||||
return output
|
||||
```
|
||||
|
||||
Implementations can specify:
|
||||
|
||||
- `supported`: Static boolean indicating if this implementation is available
|
||||
- `supports_args`: Function checking if the implementation supports specific arguments
|
||||
- `inplace`: Whether this implementation reuses input memory for outputs
|
||||
|
||||
### Using IR Operations in Models
|
||||
|
||||
IR operations are imported and called directly in model code:
|
||||
|
||||
```python
|
||||
# vllm/model_executor/layers/layernorm.py
|
||||
from vllm import ir
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(hidden_size))
|
||||
self.variance_epsilon = eps
|
||||
|
||||
def forward(self, x: Tensor, residual: Tensor | None = None):
|
||||
if residual is None:
|
||||
return ir.ops.rms_norm(x, self.weight, self.variance_epsilon)
|
||||
|
||||
# Use maybe_inplace overload to allow implementation to reuse input memory for outputs
|
||||
# (using x or residual after this call is undefined behavior)
|
||||
return ir.ops.fused_add_rms_norm.maybe_inplace(
|
||||
x, residual, self.weight, self.variance_epsilon
|
||||
)
|
||||
```
|
||||
|
||||
### Configuring Kernel Selection
|
||||
|
||||
Kernel selection is controlled via priority lists in the configuration.
|
||||
Priority lists specify the order in which implementations are considered,
|
||||
with the first supported implementation being selected.
|
||||
This includes the static support check (`supported=...`) and
|
||||
the dynamic arg support check (`supports_args=...`).
|
||||
|
||||
#### Command Line Configuration
|
||||
|
||||
Use `--ir-op-priority.<op_name>=<provider1>,<provider2>,...`:
|
||||
|
||||
```bash
|
||||
# CUDA: Use vllm_c implementation for rms_norm
|
||||
vllm serve meta-llama/Llama-3.2-1B \
|
||||
--ir-op-priority.rms_norm=vllm_c
|
||||
|
||||
# ROCm: Try aiter first, fall back to vllm_c, then native
|
||||
vllm serve meta-llama/Llama-3.2-1B \
|
||||
--ir-op-priority.rms_norm=aiter,vllm_c,native
|
||||
|
||||
# Configure multiple operations
|
||||
vllm serve meta-llama/Llama-3.2-1B \
|
||||
--ir-op-priority.rms_norm=vllm_c \
|
||||
--ir-op-priority.fused_add_rms_norm=vllm_c
|
||||
```
|
||||
|
||||
#### Python Configuration
|
||||
|
||||
```python
|
||||
from vllm import LLM
|
||||
from vllm.config import VllmConfig, KernelConfig
|
||||
|
||||
llm = LLM(
|
||||
model="meta-llama/Llama-3.2-1B",
|
||||
vllm_config=VllmConfig(
|
||||
kernel_config=KernelConfig(
|
||||
ir_op_priority={
|
||||
"rms_norm": ["vllm_c", "native"],
|
||||
"fused_add_rms_norm": ["vllm_c", "native"],
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
#### Platform Defaults
|
||||
|
||||
Each platform provides default priority lists that are automatically applied:
|
||||
|
||||
```python
|
||||
# CUDA/XPU/ROCm platform defaults (when compiling with Inductor)
|
||||
{
|
||||
"rms_norm": ["native"], # Native torch is default
|
||||
"fused_add_rms_norm": ["native"],
|
||||
}
|
||||
|
||||
# CUDA platform defaults (eager or Dynamo-only)
|
||||
{
|
||||
"rms_norm": ["vllm_c", "native"],
|
||||
"fused_add_rms_norm": ["vllm_c", "native"],
|
||||
}
|
||||
|
||||
# ROCm platform defaults (future - currently same as CUDA)
|
||||
{
|
||||
"rms_norm": ["aiter", "vllm_c", "native"],
|
||||
"fused_add_rms_norm": ["aiter", "vllm_c", "native"],
|
||||
}
|
||||
|
||||
# XPU platform defaults (eager or Dynamo-only)
|
||||
{
|
||||
"rms_norm": ["xpu_kernels", "native"],
|
||||
"fused_add_rms_norm": ["xpu_kernels", "native"],
|
||||
}
|
||||
```
|
||||
|
||||
User-specified priorities are prepended to platform defaults,
|
||||
so you only need to specify the out-of-order implementations,
|
||||
other implementations are appended automatically.
|
||||
|
||||
## Compilation Pipeline
|
||||
|
||||
vLLM IR heavily customizes the `torch.compile`-based compilation process to allow custom compile
|
||||
passes to operate on high-level IR while still producing efficient low-level code at the end.
|
||||
The compilation pipeline consists of several stages:
|
||||
|
||||
### 1. Dynamo Tracing
|
||||
|
||||
When `torch.compile` traces the model's forward pass, vLLM IR operations appear as custom operations
|
||||
in the `vllm_ir` torch library. These operations are opaque to Dynamo, meaning they appear directly
|
||||
in the FX graph without decomposition:
|
||||
|
||||
```python
|
||||
# Python code (epsilon=1e-5)
|
||||
x1 = ir.ops.rms_norm(x, weight, epsilon)
|
||||
x2, residual_out = ir.ops.fused_add_rms_norm.maybe_inplace(x1, residual, weight, epsilon)
|
||||
|
||||
# FX graph after Dynamo tracing
|
||||
x1 = torch.ops.vllm_ir.rms_norm.default(x, weight, 1e-5); x = None
|
||||
out = torch.ops.vllm_ir.fused_add_rms_norm.maybe_inplace(x1, residual, weight, 1e-5); x1 = residual = None
|
||||
x2 = out[0]
|
||||
residual_out = out[1]
|
||||
```
|
||||
|
||||
### 2. AOTAutograd and Functionalization
|
||||
|
||||
AOTAutograd functionalizes the graph, converting any mutating operations to functional equivalents.
|
||||
For vLLM IR operations with `maybe_inplace` overloads, we perform this manually before AOTAutograd,
|
||||
converting them to the functional `default` overload using the pre-grad custom pass hook.
|
||||
|
||||
```python
|
||||
# After functionalization
|
||||
x1 = torch.ops.vllm_ir.rms_norm.default(x, weight, 1e-5); x = None
|
||||
out = torch.ops.vllm_ir.fused_add_rms_norm.default(x1, residual, weight, 1e-5); x1 = residual = None
|
||||
x2 = out[0]
|
||||
residual_out = out[1]
|
||||
```
|
||||
|
||||
The pass also tracks which inputs were "donated" (passed to `maybe_inplace`),
|
||||
storing this information in vLLM's `PassContext` for later use in clone elimination.
|
||||
|
||||
### 3. IR Fusion and Transformation Passes
|
||||
|
||||
After functionalization, custom vLLM passes operate on the functional FX graph containing high-level IR operations.
|
||||
These passes can perform fusion, distribute operations for sequence parallelism, and other transformations:
|
||||
|
||||
```python
|
||||
# Example: Sequence Parallelism (see SequenceParallelismPass)
|
||||
# Before SP pass
|
||||
|
||||
all_reduce = torch.ops.vllm.all_reduce(x, "tp:0")
|
||||
rms_norm = torch.ops.vllm_ir.rms_norm(all_reduce, weight, 1e-5)
|
||||
|
||||
# after SP pass
|
||||
reduce_scatter = torch.ops.vllm.reduce_scatter(x, "tp:0")
|
||||
rms_norm = torch.ops.vllm_ir.rms_norm(all_reduce, weight, 1e-5)
|
||||
all_gather = torch.ops.vllm.all_gather(x, "tp:0")
|
||||
```
|
||||
|
||||
Fusion passes benefit from the high-level representation: they don't need to match against low-level PyTorch operations,
|
||||
handle different kernel implementations separately, or deal with functionalization of custom kernels.
|
||||
|
||||
### 4. IR Lowering
|
||||
|
||||
The lowering pass (`VllmIRLoweringPass`) replaces each vLLM IR operation with its selected implementation.
|
||||
The implementation is chosen based on the priority list and support predicates,
|
||||
using the **fake tensors** in the graph's metadata in place of op arguments:
|
||||
|
||||
```python
|
||||
# Implementation selection, same in eager dispatch and compile lowering
|
||||
def dispatch(*args) -> IrOpImpl:
|
||||
for provider in priority_list: # e.g., ["vllm_c", "native"]
|
||||
impl = ir_op.impls[provider]
|
||||
if not impl.supported:
|
||||
continue
|
||||
if impl.supports_args and not impl.supports_args(*args):
|
||||
continue
|
||||
return impl
|
||||
|
||||
# make_fx uses torch.fx.symbolic_trace
|
||||
impl_graph = make_fx(selected_impl.impl_fn)
|
||||
# Replace IR op node with impl_graph's nodes
|
||||
match.replace_by_example(selected_impl.impl_fn, node.args)
|
||||
```
|
||||
|
||||
For example, lowering `rms_norm` with the `vllm_c` implementation:
|
||||
|
||||
```python
|
||||
# Before lowering (IR op)
|
||||
rms_norm = torch.ops.vllm_ir.rms_norm.default(x, weight, 1e-5)
|
||||
|
||||
# After lowering (vllm_c implementation traced)
|
||||
# Note: Lowering does not currently functionalize, this will likely change in the future.
|
||||
empty = torch.ops.aten.empty.memory_format(x.shape, ...)
|
||||
rms_norm = torch.ops._C.rms_norm(empty, x, weight, 1e-5)
|
||||
```
|
||||
|
||||
When lowering an implementation that mutates inputs (`inplace=True`),
|
||||
the lowering pass inserts clones to preserve functional semantics:
|
||||
|
||||
```python
|
||||
# vllm_c implementation for fused_add_rms_norm mutates its first two arguments
|
||||
# Lowered with clones for safety
|
||||
clone_default = torch.ops.aten.clone.default(x)
|
||||
clone_default_1 = torch.ops.aten.clone.default(residual)
|
||||
fused_add_rms_norm = torch.ops._C.fused_add_rms_norm.default(clone_default, clone_default_1, weight, 1e-5)
|
||||
```
|
||||
|
||||
### 5. Clone Cleanup
|
||||
|
||||
After lowering, the clone elimination pass (`UnsafeCloneEliminationPass`) removes unnecessary clones introduced during lowering.
|
||||
This pass is essential for achieving zero-copy behavior when using in-place kernels with `maybe_inplace`.
|
||||
The pass removes a clone if:
|
||||
|
||||
- the cloned input is created in the graph and not used again in the graph
|
||||
- the cloned input is a graph parameter, marked as donated
|
||||
|
||||
```python
|
||||
# After cleanup (donated inputs, no subsequent uses)
|
||||
fused_add_rms_norm = torch.ops._C.fused_add_rms_norm.default(x, residual, weight, 1e-5)
|
||||
```
|
||||
|
||||
The combination of inplace functionalization (tracking donated inputs) and clone cleanup enables the compiler to safely
|
||||
use in-place kernels without adding redundant copies or increasing the memory usage.
|
||||
|
||||
### 6. Inductor Optimization and Codegen
|
||||
|
||||
After IR lowering and cleanup, the graph contains only standard PyTorch operations and platform-specific custom ops.
|
||||
Inductor then performs its standard codegen:
|
||||
|
||||
- **Inductor lowering and pointwise fusion**: Fusing element-wise operations, reductions, etc.
|
||||
- **Memory planning**: Determining buffer allocation and reuse
|
||||
- **Kernel generation**: Generating Triton or C++ code for fused operations
|
||||
- **Autotuning**: Selecting the best kernel configurations
|
||||
|
||||
### Pipeline Summary
|
||||
|
||||
```text
|
||||
Model Forward Pass
|
||||
↓
|
||||
[Dynamo Tracing] → FX Graph with vllm_ir.* ops
|
||||
↓
|
||||
[Pre-grad: Inplace Functionalization] → maybe_inplace → default, track donated inputs
|
||||
↓
|
||||
[AOTAutograd] → Functionalization
|
||||
↓
|
||||
[Post-grad: IR Fusion Passes] → Fuse high-level IR ops (e.g., rms_norm + quant)
|
||||
↓
|
||||
[Post-grad: IR Lowering] → vllm_ir.* ops → impl ops (with clones if needed)
|
||||
↓
|
||||
[Post-grad: Clone Cleanup] → Remove unnecessary clones using donated input info
|
||||
↓
|
||||
[Inductor] → Pattern matching, fusion, memory planning, codegen
|
||||
↓
|
||||
Compiled Code
|
||||
```
|
||||
|
||||
## Core vLLM IR Concepts
|
||||
|
||||
### Operation Declaration
|
||||
|
||||
Operations are declared with the `@register_op` decorator, which creates an `IrOp` object:
|
||||
|
||||
```python
|
||||
@register_op(
|
||||
name=None, # Operation name (defaults to function name)
|
||||
activations=None, # List of activation parameters (defaults to params starting with 'x')
|
||||
allow_inplace=False, # Whether to create a maybe_inplace overload
|
||||
)
|
||||
def op_name(...):
|
||||
...
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `activations`: List of parameter names considered "activations" (typically consumed by `maybe_inplace`). Defaults to parameters starting with `x`.
|
||||
- `allow_inplace`: Creates a `maybe_inplace` overload for memory-efficient execution (see below).
|
||||
|
||||
### The `maybe_inplace` Overload
|
||||
|
||||
The `maybe_inplace` overload is a critical feature for memory efficiency in LLM inference.
|
||||
It signals that the caller doesn't need to preserve the activation inputs after the operation,
|
||||
allowing in-place implementations to reuse input memory for outputs.
|
||||
|
||||
#### Semantics and Usage
|
||||
|
||||
```python
|
||||
# Standard usage: inputs are preserved
|
||||
out, res_out = ir.ops.fused_add_rms_norm(x, residual, weight, epsilon)
|
||||
# x and residual are unchanged, out and res_out are new tensors
|
||||
|
||||
# maybe_inplace: inputs may be modified
|
||||
out, res_out = ir.ops.fused_add_rms_norm.maybe_inplace(x, residual, weight, epsilon)
|
||||
# x and residual may be modified (undefined behavior to use them after this)
|
||||
# out and res_out may alias x and residual
|
||||
```
|
||||
|
||||
Using an activation input after passing it to `maybe_inplace` is **undefined behavior**:
|
||||
|
||||
```python
|
||||
# WRONG: Using x after donating it
|
||||
out, res_out = ir.ops.fused_add_rms_norm.maybe_inplace(x, residual, weight, epsilon)
|
||||
result = out + x # ERROR: x was donated!
|
||||
```
|
||||
|
||||
If you need to preserve an input, either use the default overload or clone manually:
|
||||
|
||||
```python
|
||||
# Option 1: Use default overload
|
||||
out, res_out = ir.ops.fused_add_rms_norm(x, residual, weight, epsilon)
|
||||
result = out + x # OK: x is preserved
|
||||
|
||||
# Option 2: Clone before maybe_inplace
|
||||
out, res_out = ir.ops.fused_add_rms_norm.maybe_inplace(x.clone(), residual, weight, epsilon)
|
||||
result = out + x # OK: x is preserved, clone was donated
|
||||
```
|
||||
|
||||
#### Compilation Behavior
|
||||
|
||||
During compilation, the inplace functionalization pass validates that donated inputs are
|
||||
not used again and converts `maybe_inplace` to the functional `default` overload:
|
||||
|
||||
```python
|
||||
# Inplace functionalization pass (pre-grad)
|
||||
for node in graph.nodes:
|
||||
if node.target == torch.ops.vllm_ir.fused_add_rms_norm.maybe_inplace:
|
||||
# Check that activation inputs aren't used after this node
|
||||
for activation_arg in activation_inputs:
|
||||
for user in activation_arg.users:
|
||||
if user appears after node:
|
||||
raise ValueError(f"Input {activation_arg} donated but used again")
|
||||
|
||||
# Convert to default overload
|
||||
node.target = torch.ops.vllm_ir.fused_add_rms_norm.default
|
||||
|
||||
# Track donated graph inputs for later clone elimination
|
||||
for i, arg in enumerate(node.args):
|
||||
if arg.op == "placeholder" and i in activation_indices:
|
||||
pass_context.donated_input_ids.add(node_to_idx[arg])
|
||||
```
|
||||
|
||||
The donated input information is then used by the clone cleanup pass to eliminate
|
||||
unnecessary copies when in-place kernels are lowered.
|
||||
|
||||
#### Eager Mode Behavior
|
||||
|
||||
In eager mode (without `torch.compile`), `maybe_inplace` enables **maximally memory-efficient**
|
||||
execution by allowing the IR operation to dispatch directly to in-place implementations:
|
||||
|
||||
```python
|
||||
# Eager dispatch logic for maybe_inplace
|
||||
impl: IrOpImpl = ir_op.dispatch(*args)
|
||||
return impl.impl_fn(*args)
|
||||
|
||||
# Eager dispatch logic for default:
|
||||
impl: IrOpImpl = ir_op.dispatch(*args)
|
||||
if impl.inplace:
|
||||
args = [
|
||||
arg.clone() if i in ir_op.activations else arg
|
||||
for i, arg in enumerate(args)
|
||||
]
|
||||
return impl.impl_fn(*args)
|
||||
```
|
||||
|
||||
The combination of `maybe_inplace` in model code and in-place kernel implementations provides optimal memory efficiency
|
||||
in both eager and compiled modes, with identical semantics in both cases.
|
||||
|
||||
#### Memory Savings Example
|
||||
|
||||
Consider a transformer layer with residual connections:
|
||||
|
||||
```python
|
||||
# Without maybe_inplace (2 allocations per layer)
|
||||
hidden_states = self.attention(input)
|
||||
normed, residual = ir.ops.fused_add_rms_norm(hidden_states, input, weight, eps)
|
||||
# Memory: input (preserved), hidden_states (preserved), normed (new), residual (new)
|
||||
|
||||
# With maybe_inplace (0 allocations per layer when using in-place kernel)
|
||||
hidden_states = self.attention(input)
|
||||
normed, residual = ir.ops.fused_add_rms_norm.maybe_inplace(hidden_states, input, weight, eps)
|
||||
# Memory: normed (reuses hidden_states), residual (reuses input)
|
||||
```
|
||||
|
||||
### Implementation Registration
|
||||
|
||||
Implementations are registered using the `register_impl` method:
|
||||
|
||||
```python
|
||||
@ir.ops.op_name.register_impl(
|
||||
provider="provider_name", # Unique identifier (e.g., "vllm_c", "aiter", "triton")
|
||||
supported=True, # Static availability check
|
||||
supports_args=None, # Dynamic argument support check
|
||||
)
|
||||
def impl_fn(...):
|
||||
...
|
||||
```
|
||||
|
||||
**Provider naming conventions:**
|
||||
|
||||
- `native`: Reserved for the native torch implementation (declared with `@register_op`)
|
||||
- `vllm_c`: C++/CUDA kernels via `torch.ops._C`
|
||||
- `aiter`: AMD AITER library
|
||||
- `xpu_kernels`: SYCL/SYCLTLA kernels implemented in `vllm-xpu-kernels`
|
||||
- `triton_*`: Triton kernels
|
||||
- Platform/library names for other implementations
|
||||
|
||||
**Support checking:**
|
||||
|
||||
- `supported`: Static boolean, checked once at import time (e.g., `HAS_TRITON`, `is_cuda_alike()`)
|
||||
- `supports_args`: Function `(*args, **kwargs) -> bool` checking argument compatibility
|
||||
- Called with **fake tensors** during compilation for zero-cost checking
|
||||
- Called with **real tensors** during eager mode dispatch
|
||||
- Should NOT check batch sizes or add guards based on values
|
||||
|
||||
Example support predicate:
|
||||
|
||||
```python
|
||||
def aiter_rms_norm_supports(x, weight, epsilon, variance_size=None):
|
||||
# Check dtype (OK: doesn't depend on batch size)
|
||||
if x.dtype not in [torch.float16, torch.bfloat16]:
|
||||
return False
|
||||
# Check optional parameter (OK: static check)
|
||||
if variance_size is not None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@ir.ops.rms_norm.register_impl("aiter", supports_args=aiter_rms_norm_supports)
|
||||
def rms_norm(...):
|
||||
...
|
||||
```
|
||||
|
||||
Batch-invariant kernels are automatically selected when `VLLM_BATCH_INVARIANT=1` is set.
|
||||
|
||||
### Eager Mode vs Compile Mode
|
||||
|
||||
vLLM IR operations behave identically in eager and compile modes:
|
||||
|
||||
**Eager mode:**
|
||||
|
||||
- Direct dispatch to implementation based on priority list
|
||||
- Support checked with real tensor arguments
|
||||
- Minimal overhead (can be optimized further if needed)
|
||||
|
||||
**Compile mode:**
|
||||
|
||||
- IR ops appear in FX graph as `torch.ops.vllm_ir.*` custom ops
|
||||
- Lowering selects implementation using fake tensors
|
||||
- Full integration with Inductor optimizations
|
||||
|
||||
This consistency enables:
|
||||
|
||||
- Prototyping in eager mode with confidence
|
||||
- Debugging by disabling compilation
|
||||
- Gradual migration from eager to compiled execution
|
||||
|
||||
## Other Topics
|
||||
|
||||
### Out-of-Tree Implementations
|
||||
|
||||
External platforms can register implementations without modifying vLLM:
|
||||
|
||||
```python
|
||||
# In external package
|
||||
from vllm import ir
|
||||
|
||||
@ir.ops.rms_norm.register_impl("my_platform", supported=is_my_platform())
|
||||
def rms_norm(x, weight, epsilon, variance_size=None):
|
||||
return my_platform.rms_norm(x, weight, epsilon)
|
||||
```
|
||||
|
||||
Then configure priority to use your implementation:
|
||||
|
||||
```python
|
||||
class MyPlatform(Platform):
|
||||
def get_default_ir_op_priority(self):
|
||||
return IrOpPriorityConfig(rms_norm=['my_platform', 'native'])
|
||||
|
||||
# Users can still override priority in the same way
|
||||
llm = LLM(ir_op_priority=IrOpPriorityConfig(rms_norm=['custom_oot_kernel']))
|
||||
```
|
||||
|
||||
### Debugging and Observability
|
||||
|
||||
!!! note
|
||||
Please let us know how observability can be improved for your use-case!
|
||||
|
||||
Enable debug logging to see kernel selection:
|
||||
|
||||
```bash
|
||||
VLLM_LOGGING_LEVEL=DEBUG vllm serve ...
|
||||
```
|
||||
|
||||
This logs:
|
||||
|
||||
- Which implementations are selected for each operation
|
||||
- Why implementations were rejected (unsupported, args not supported)
|
||||
- Compilation cache hits/misses
|
||||
- IR lowering statistics
|
||||
|
||||
Check selected implementations in compiled graphs:
|
||||
|
||||
```python
|
||||
# After compilation, inspect the lowering pass
|
||||
lowering_pass = backend.lowering_pass
|
||||
print(lowering_pass.selected_impls)
|
||||
# Output: {'rms_norm': {'node_123': 'vllm_c', 'node_456': 'vllm_c'}}
|
||||
```
|
||||
|
||||
## Migration from CustomOp
|
||||
|
||||
vLLM IR is designed to coexist with and gradually replace `CustomOp`:
|
||||
|
||||
1. **Op declaration**: Convert `CustomOp` class `PluggableLayer` and move `forward_native` to `@register_op` function
|
||||
2. **Implementation registration**: Use `@ir.ops.op_name.register_impl` instead of overriding methods
|
||||
3. **Layer usage**: Replace `self.op(...)` with `ir.ops.op_name(...)`
|
||||
4. **Configuration**: Migrate `--compilation-config.custom-ops` to `--ir-op-priority`
|
||||
|
||||
The migration can be done incrementally, one operation at a time.
|
||||
|
||||
## See Also
|
||||
|
||||
- [torch.compile Integration](torch_compile.md) - General compilation infrastructure
|
||||
- [Fusions](fusions.md) - Custom fusion and transformation passes in vLLM
|
||||
- [Custom Operations](custom_op.md) - Legacy custom op system
|
||||
@@ -52,10 +52,10 @@ th:not(:first-child) {
|
||||
| [mm](multimodal_inputs.md) | ✅ | ✅ | [🟠](https://github.com/vllm-project/vllm/pull/4194)<sup>^</sup> | ❔ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | | | |
|
||||
| best-of | ✅ | ✅ | ✅ | [❌](https://github.com/vllm-project/vllm/issues/6137) | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | [❌](https://github.com/vllm-project/vllm/issues/7968) | ✅ | ✅ | | |
|
||||
| beam-search | ✅ | ✅ | ✅ | [❌](https://github.com/vllm-project/vllm/issues/6137) | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | [❌](https://github.com/vllm-project/vllm/issues/7968) | ❔ | ✅ | ✅ | |
|
||||
| [prompt-embeds](prompt_embeds.md) | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❔ | ❔ | ❌ | ❔ | ❔ | ✅ |
|
||||
| [prompt-embeds](prompt_embeds.md) | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❔ | ❔ | ✅ | ❔ | ❔ | ✅ |
|
||||
|
||||
\* Chunked prefill and prefix caching are only applicable to last-token or all pooling with causal attention.
|
||||
<sup>^</sup> LoRA is only applicable to the language backbone of multimodal models.
|
||||
<sup>^</sup> LoRA is only applicable to the language backbone of multimodal models.
|
||||
|
||||
### Feature x Hardware
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ Batch invariance has been tested and verified on the following models:
|
||||
|
||||
- **DeepSeek series**: `deepseek-ai/DeepSeek-V3`, `deepseek-ai/DeepSeek-V3-0324`, `deepseek-ai/DeepSeek-R1`, `deepseek-ai/DeepSeek-V3.1`
|
||||
- **Qwen3 (Dense)**: `Qwen/Qwen3-1.7B`, `Qwen/Qwen3-8B`, `Qwen/Qwen3-4B-AWQ`, `Qwen/Qwen3-8B-AWQ`
|
||||
- **Qwen3 (MoE)**: `Qwen/Qwen3-30B-A3B`, `Qwen/Qwen3-Next-80B-A3B-Instruct`
|
||||
- **Qwen3 (MoE)**: `Qwen/Qwen3-30B-A3B`, `Qwen/Qwen3-Next-80B-A3B-Instruct`, `Qwen/Qwen3-30B-A3B-Thinking-2507-FP8`
|
||||
- **Qwen2.5**: `Qwen/Qwen2.5-0.5B-Instruct`, `Qwen/Qwen2.5-1.5B-Instruct`, `Qwen/Qwen2.5-3B-Instruct`, `Qwen/Qwen2.5-7B-Instruct`, `Qwen/Qwen2.5-14B-Instruct`, `Qwen/Qwen2.5-32B-Instruct`
|
||||
- **Llama 3**: `meta-llama/Llama-3.1-8B-Instruct`, `meta-llama/Llama-3.2-1B-Instruct`
|
||||
- **GPT-OSS**: `openai/gpt-oss-20b`, `openai/gpt-oss-120b`
|
||||
|
||||
@@ -215,6 +215,67 @@ When loading RGBA images (images with transparency), vLLM converts them to RGB f
|
||||
- This setting only affects RGBA images with transparency; RGB images are unchanged
|
||||
- If not specified, the default white background `(255, 255, 255)` is used for backward compatibility
|
||||
|
||||
#### Moondream3 Prompt Recipes { #moondream3-prompt-recipes }
|
||||
|
||||
`Moondream3ForCausalLM` supports two task-specific prompt formats:
|
||||
|
||||
- `query`: ask a question about the image.
|
||||
- `caption`: generate a caption for the image.
|
||||
|
||||
```python
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
llm = LLM(
|
||||
model="moondream/moondream3-preview",
|
||||
tokenizer="moondream/starmie-v1",
|
||||
trust_remote_code=True,
|
||||
max_model_len=2048,
|
||||
limit_mm_per_prompt={"image": 1},
|
||||
)
|
||||
|
||||
image = ImageAsset("stop_sign").pil_image
|
||||
|
||||
|
||||
def make_query_prompt(question: str) -> str:
|
||||
return (
|
||||
"<|endoftext|><image><|md_reserved_0|>query<|md_reserved_1|>"
|
||||
f"{question}<|md_reserved_2|>"
|
||||
)
|
||||
|
||||
|
||||
def make_caption_prompt(length: str = "normal") -> str:
|
||||
return (
|
||||
"<|endoftext|><image><|md_reserved_0|>"
|
||||
f"describe<|md_reserved_1|>{length}<|md_reserved_2|>"
|
||||
)
|
||||
|
||||
|
||||
query_out = llm.generate(
|
||||
{
|
||||
"prompt": make_query_prompt("What is shown in this image?"),
|
||||
"multi_modal_data": {"image": image},
|
||||
},
|
||||
SamplingParams(max_tokens=64, temperature=0),
|
||||
)[0].outputs[0].text
|
||||
|
||||
caption_out = llm.generate(
|
||||
{
|
||||
"prompt": make_caption_prompt(),
|
||||
"multi_modal_data": {"image": image},
|
||||
},
|
||||
SamplingParams(max_tokens=100, temperature=0),
|
||||
)[0].outputs[0].text
|
||||
|
||||
print("query:", query_out)
|
||||
print("caption:", caption_out)
|
||||
```
|
||||
|
||||
!!! note
|
||||
The native Moondream3 model also has `detect` and `point` skills. Those
|
||||
require custom coordinate decoding and are not exposed by this vLLM
|
||||
implementation.
|
||||
|
||||
### Video Inputs
|
||||
|
||||
You can pass a list of NumPy arrays directly to the `'video'` field of the multi-modal dictionary
|
||||
|
||||
@@ -20,12 +20,47 @@ You can pass prompt embeddings from Hugging Face Transformers models to the `'p
|
||||
|
||||
## Online Serving
|
||||
|
||||
Our OpenAI-compatible server accepts prompt embeddings inputs via the [Completions API](https://platform.openai.com/docs/api-reference/completions). Prompt embeddings inputs are added via a new `'prompt_embeds'` key in the JSON package and are enabled by the `--enable-prompt-embeds` flag in `vllm serve`.
|
||||
Our OpenAI-compatible server accepts prompt embeddings inputs via both the [Completions API](https://platform.openai.com/docs/api-reference/completions) and the [Chat Completions API](https://platform.openai.com/docs/api-reference/chat). Both are enabled by the `--enable-prompt-embeds` flag in `vllm serve`.
|
||||
|
||||
### Completions API
|
||||
|
||||
Prompt embeddings inputs are added via a `'prompt_embeds'` key in the JSON request body.
|
||||
|
||||
When a mixture of `'prompt_embeds'` and `'prompt'` inputs are provided in a single request, the prompt embeds are always returned first.
|
||||
|
||||
Prompt embeddings are passed in as base64 encoded torch tensors.
|
||||
|
||||
The Completions endpoint does **not** apply a chat template to `prompt_embeds`. If the model assumes some chat template, the caller is responsible for producing embeddings for the full, already-templated prompt: apply the chat template, then embed the resulting token IDs. Anything the model would normally need (system prompt, role markers, generation prompt, etc.) must already be baked into the embedded tokens.
|
||||
|
||||
### Chat Completions API
|
||||
|
||||
Prompt embeddings can be included as content parts in chat messages, interleaved with text:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"type": "text", "text": "You are a helpful assistant."},
|
||||
{"type": "prompt_embeds", "data": "<base64_encoded_tensor>"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "prompt_embeds", "data": "<base64_encoded_tensor>"},
|
||||
{"type": "text", "text": "Summarize the above."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each `prompt_embeds` content part contains a `data` field with a base64-encoded `torch.Tensor` of shape `(num_tokens, hidden_size)`. Multiple `prompt_embeds` parts can appear in any message, in any position relative to text parts. The server expands each part into the correct number of placeholder tokens during chat template rendering, then splices the pre-computed embeddings into the model's input at the corresponding positions.
|
||||
|
||||
Unlike the Completions API, a `prompt_embeds` content part should encode **only** the content, not a templated conversation. The server wraps the chat template around the embedded content at request time, the same way it would for a plain text `content` string. Embedding a full templated conversation here would double-apply the template and produce incorrect inputs to the model.
|
||||
|
||||
!!! warning
|
||||
The vLLM engine may crash if incorrect shape of embeddings is passed.
|
||||
Only enable this flag for trusted users!
|
||||
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
import textwrap
|
||||
import traceback
|
||||
from argparse import SUPPRESS, Action, HelpFormatter
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Callable, Iterable
|
||||
from importlib.machinery import ModuleSpec
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
@@ -38,8 +38,21 @@ class MockCustomOp:
|
||||
return decorator
|
||||
|
||||
|
||||
class MockPluggableLayer:
|
||||
@staticmethod
|
||||
def register(name):
|
||||
def decorator(cls):
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
mock_if_no_torch("vllm._C", MagicMock())
|
||||
mock_if_no_torch("vllm.model_executor.custom_op", MagicMock(CustomOp=MockCustomOp))
|
||||
mock_if_no_torch("vllm._C_stable_libtorch", MagicMock())
|
||||
mock_if_no_torch(
|
||||
"vllm.model_executor.custom_op",
|
||||
MagicMock(CustomOp=MockCustomOp, PluggableLayer=MockPluggableLayer),
|
||||
)
|
||||
mock_if_no_torch(
|
||||
"vllm.utils.torch_utils", MagicMock(direct_register_custom_op=lambda *a, **k: None)
|
||||
)
|
||||
@@ -55,6 +68,31 @@ importlib.metadata.version = lambda name: VERSIONS.get(name) or "0.0.0"
|
||||
mock_if_no_torch("torch.nn", MagicMock(Parameter=object))
|
||||
|
||||
|
||||
# Mock torch.library.infer_schema for vllm.ir.ops.IrOpInplaceOverload.__init__
|
||||
# We need to return the corresponding number of inputs, as IR infra will assert it
|
||||
def get_outputs(native_fn: Callable) -> str:
|
||||
"""
|
||||
Extract output schema from function's return type annotation,
|
||||
e.g. 'Tensor' or 'Tensor, Tensor'.
|
||||
"""
|
||||
import typing
|
||||
|
||||
return_type = typing.get_type_hints(native_fn)["return"]
|
||||
origin = typing.get_origin(return_type)
|
||||
arg_name = lambda a: a.__name__ if hasattr(a, "__name__") else str(a)
|
||||
if origin is tuple:
|
||||
args = typing.get_args(return_type)
|
||||
return ", ".join(arg_name(arg) for arg in args)
|
||||
else:
|
||||
return f"{arg_name(return_type)}"
|
||||
|
||||
|
||||
mock_if_no_torch(
|
||||
"torch.library",
|
||||
MagicMock(infer_schema=lambda fn, **k: f"(Tensor x) -> {get_outputs(fn)}"),
|
||||
)
|
||||
|
||||
|
||||
class PydanticMagicMock(MagicMock):
|
||||
"""`MagicMock` that's able to generate pydantic-core schemas."""
|
||||
|
||||
|
||||
@@ -599,6 +599,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
|
||||
| `Mistral3ForConditionalGeneration` | Mistral3 (HF Transformers) | T + I<sup>+</sup> | `mistralai/Mistral-Small-3.1-24B-Instruct-2503`, etc. | ✅︎ | ✅︎ |
|
||||
| `MolmoForCausalLM` | Molmo | T + I<sup>+</sup> | `allenai/Molmo-7B-D-0924`, `allenai/Molmo-7B-O-0924`, etc. | ✅︎ | ✅︎ |
|
||||
| `Molmo2ForConditionalGeneration` | Molmo2 | T + I<sup>+</sup> / V | `allenai/Molmo2-4B`, `allenai/Molmo2-8B`, `allenai/Molmo2-O-7B` | ✅︎ | ✅︎ |
|
||||
| `Moondream3ForCausalLM` | Moondream3 | T + I | `moondream/moondream3-preview` | | ✅︎ |
|
||||
| `MusicFlamingoForConditionalGeneration` | MusicFlamingo | T + A | `nvidia/music-flamingo-2601-hf`, `nvidia/music-flamingo-think-2601-hf` | ✅︎ | ✅︎ |
|
||||
| `NVLM_D_Model` | NVLM-D 1.0 | T + I<sup>+</sup> | `nvidia/NVLM-D-72B`, etc. | | ✅︎ |
|
||||
| `OpenCUAForConditionalGeneration` | OpenCUA-7B | T + I<sup>E+</sup> | `xlangai/OpenCUA-7B` | ✅︎ | ✅︎ |
|
||||
@@ -613,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. | ✅︎ | ✅︎ |
|
||||
@@ -661,6 +663,12 @@ Some models are supported only via the [Transformers modeling backend](#transfor
|
||||
!!! note
|
||||
For `InternVLChatModel`, only InternVL2.5 with Qwen2.5 text backbone (`OpenGVLab/InternVL2.5-1B` etc.), InternVL3 and InternVL3.5 have video inputs support currently.
|
||||
|
||||
!!! note
|
||||
`Moondream3ForCausalLM` uses task-specific prompt templates for `query`
|
||||
and `caption`. The native `detect` and `point` skills require custom
|
||||
coordinate decoding and are not exposed by this vLLM implementation.
|
||||
See [Moondream3 prompt recipes](../features/multimodal_inputs.md#moondream3-prompt-recipes).
|
||||
|
||||
!!! note
|
||||
To use `TIGER-Lab/Mantis-8B-siglip-llama3`, you have to pass `--hf_overrides '{"architectures": ["MantisForConditionalGeneration"]}'` when running vLLM.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Codex
|
||||
|
||||
[Codex](https://github.com/openai/codex) is OpenAI's official agentic coding tool that lives in your terminal. It can understand your codebase, edit files, run commands, and help you write code more efficiently.
|
||||
|
||||
By pointing Codex at a vLLM server, you can use your own models as the backend instead of the OpenAI API. This is useful for:
|
||||
|
||||
- Running fully local/private coding assistance
|
||||
- Using open-weight models with tool calling capabilities
|
||||
- Testing and developing with custom models
|
||||
|
||||
## How It Works
|
||||
|
||||
vLLM implements the OpenAI-Responses API, which is the same API that Codex uses to communicate with OpenAI's servers. By configuring Codex to point at your vLLM server, Codex sends its requests to vLLM instead of OpenAI. vLLM then translates these requests to work with your local model and returns responses in the format Codex expects.
|
||||
|
||||
This means any model served by vLLM with proper tool calling support can act as a drop-in replacement for OpenAI models in Codex.
|
||||
|
||||
## Requirements
|
||||
|
||||
Codex requires a model with strong tool calling capabilities. The model must support the OpenAI-Responses tool calling API. See [Tool Calling](../../features/tool_calling.md) for details on enabling tool calling for your model.
|
||||
|
||||
## Installation
|
||||
|
||||
First, install Codex by following the [official installation guide](https://github.com/openai/codex).
|
||||
|
||||
## Starting the vLLM Server
|
||||
|
||||
Start vLLM with a tool-calling capable model - here's an example using `Qwen/Qwen3-27B`:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3.6-27B --port 8000 --tensor-parallel-size 8 --max-model-len 262144 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
|
||||
|
||||
```
|
||||
|
||||
For other models, you'll need to enable tool calling explicitly with `--enable-auto-tool-choice` and the right `--tool-call-parser`. Refer to the [Tool Calling documentation](../../features/tool_calling.md) for the correct flags for your model.
|
||||
|
||||
## Configuring Codex
|
||||
|
||||
Codex is configured via a TOML file located at `~/.codex/config.toml`. Create or edit this file to point Codex at your vLLM server:
|
||||
|
||||
```toml
|
||||
model = "my-model"
|
||||
model_provider = "vllm"
|
||||
|
||||
[model_providers.vllm]
|
||||
name = "vLLM"
|
||||
env_key = "VLLM_API_KEY"
|
||||
base_url = "http://localhost:8000/v1"
|
||||
wire_api = "responses"
|
||||
```
|
||||
|
||||
The configuration fields:
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| `model` | The model name to use. Must match the `--served-model-name` you passed to vLLM. |
|
||||
| `model_provider` | Set to `"vllm"` to use your local vLLM server. |
|
||||
| `[model_providers.vllm]` | Configuration section for the vLLM provider. |
|
||||
| `name` | A display name for your vLLM provider. |
|
||||
| `env_key` | The name of an environment variable that Codex will read for the API key. vLLM does not require authentication by default, so this can be any value. |
|
||||
| `base_url` | The URL of your vLLM server's OpenAI-compatible API endpoint (default is `http://localhost:8000/v1`). |
|
||||
| `wire_api` | The API style to use. Set to `"responses"` for the OpenAI Responses API |
|
||||
|
||||
!!! tip
|
||||
You can set the `env_key` to any dummy environment variable since vLLM doesn't require authentication by default:
|
||||
```bash
|
||||
export VLLM_API_KEY=dummy
|
||||
```
|
||||
|
||||
!!! warning
|
||||
When using the `responses` API, ensure your vLLM version supports the OpenAI Responses API.
|
||||
|
||||
## Testing the Setup
|
||||
|
||||
Once Codex is configured, launch it in your project directory:
|
||||
|
||||
```bash
|
||||
codex
|
||||
```
|
||||
|
||||
Try a simple prompt to verify the connection, such as asking it to explain a file in your project. If the model responds correctly, your setup is working. You can now use Codex with your vLLM-served model for coding tasks.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection refused**: Ensure vLLM is running and accessible at the specified URL. Check that the port matches and that `base_url` includes the `/v1` path suffix.
|
||||
|
||||
**Tool calls not working**: Verify that your model supports tool calling and that you've enabled it with the correct `--tool-call-parser` flag. See [Tool Calling](../../features/tool_calling.md).
|
||||
|
||||
**Model not found**: Ensure the `model` field in `~/.codex/config.toml` matches the `--served-model-name` you passed to vLLM.
|
||||
@@ -1,12 +1,29 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
vLLM OpenAI-Compatible Client with Prompt Embeddings
|
||||
"""vLLM OpenAI-Compatible Client with Prompt Embeddings.
|
||||
|
||||
This script demonstrates how to:
|
||||
1. Generate prompt embeddings using Hugging Face Transformers
|
||||
2. Encode them in base64 format
|
||||
3. Send them to a vLLM server via the OpenAI-compatible Completions API
|
||||
1. Generate prompt embeddings using Hugging Face Transformers.
|
||||
2. Encode them in base64 format.
|
||||
3. Send them to a vLLM server for inference via both:
|
||||
- OpenAI-compatible Chat Completions API
|
||||
- OpenAI-compatible Completions API
|
||||
|
||||
Important distinction between the two APIs:
|
||||
|
||||
- Chat Completions API: `prompt_embeds` content parts should encode ONLY
|
||||
the user-provided content, not a templated conversation. The server
|
||||
renders the surrounding chat template around the embedded content at
|
||||
request time, the same way it would for a plain text `content` string.
|
||||
Embedding a full templated conversation here would double-apply the
|
||||
template and likely produce undesirable results.
|
||||
|
||||
- Completions API: the server does NOT apply a chat template to
|
||||
`prompt_embeds`. The caller is responsible for producing embeddings for
|
||||
the full, already-templated prompt (i.e. apply the chat template first,
|
||||
then embed the resulting token IDs). Anything the model would normally
|
||||
need (system prompt, role markers, generation prompt, etc.) must already
|
||||
be baked into the embedded tokens.
|
||||
|
||||
Run the vLLM server first:
|
||||
vllm serve meta-llama/Llama-3.2-1B-Instruct \
|
||||
@@ -34,34 +51,68 @@ from openai import OpenAI
|
||||
from vllm.utils.serial_utils import tensor2base64
|
||||
|
||||
|
||||
def main():
|
||||
client = OpenAI(
|
||||
api_key="EMPTY",
|
||||
base_url="http://localhost:8000/v1",
|
||||
def run_chat_completion_prompt_embeds(
|
||||
client: OpenAI,
|
||||
model_name: str,
|
||||
tokenizer: transformers.PreTrainedTokenizerBase,
|
||||
embedding_layer,
|
||||
messages: list[dict],
|
||||
) -> None:
|
||||
"""Run a Chat Completions API request using prompt_embeds content parts.
|
||||
|
||||
This example embeds ONLY the user-provided content of the final user turn, the
|
||||
vLLM server applies the chat template around it at request time.
|
||||
"""
|
||||
user_content = messages[-1]["content"]
|
||||
content_token_ids = tokenizer(
|
||||
user_content, return_tensors="pt", add_special_tokens=False
|
||||
).input_ids
|
||||
content_prompt_embeds = embedding_layer(content_token_ids).squeeze(0)
|
||||
encoded_embeds = tensor2base64(content_prompt_embeds)
|
||||
|
||||
api_messages = [
|
||||
*messages[:-1],
|
||||
{
|
||||
"role": messages[-1]["role"],
|
||||
"content": [{"type": "prompt_embeds", "data": encoded_embeds}],
|
||||
},
|
||||
]
|
||||
|
||||
chat_completion = client.chat.completions.create(
|
||||
model=model_name,
|
||||
max_tokens=6,
|
||||
temperature=0.0,
|
||||
messages=api_messages,
|
||||
)
|
||||
|
||||
model_name = "meta-llama/Llama-3.2-1B-Instruct"
|
||||
print("-" * 30)
|
||||
print("Chat Completions API")
|
||||
print(chat_completion.choices[0].message.content)
|
||||
print("-" * 30)
|
||||
|
||||
# Transformers
|
||||
tokenizer = transformers.AutoTokenizer.from_pretrained(model_name)
|
||||
transformers_model = transformers.AutoModelForCausalLM.from_pretrained(model_name)
|
||||
|
||||
# Refer to the HuggingFace repo for the correct format to use
|
||||
chat = [{"role": "user", "content": "Please tell me about the capital of France."}]
|
||||
token_ids = tokenizer.apply_chat_template(
|
||||
chat, add_generation_prompt=True, return_tensors="pt", return_dict=True
|
||||
def run_completion_prompt_embeds(
|
||||
client: OpenAI,
|
||||
model_name: str,
|
||||
tokenizer: transformers.PreTrainedTokenizerBase,
|
||||
embedding_layer,
|
||||
messages: list[dict],
|
||||
) -> None:
|
||||
"""Run a Completions API request using prompt embeddings.
|
||||
|
||||
The Completions endpoint does not apply a chat template,
|
||||
so the caller must apply it and embed the full templated prompt.
|
||||
"""
|
||||
templated_token_ids = tokenizer.apply_chat_template(
|
||||
messages, add_generation_prompt=True, return_tensors="pt", return_dict=True
|
||||
).input_ids
|
||||
|
||||
embedding_layer = transformers_model.get_input_embeddings()
|
||||
prompt_embeds = embedding_layer(token_ids).squeeze(0)
|
||||
|
||||
# Prompt embeddings
|
||||
encoded_embeds = tensor2base64(prompt_embeds)
|
||||
templated_prompt_embeds = embedding_layer(templated_token_ids).squeeze(0)
|
||||
encoded_embeds = tensor2base64(templated_prompt_embeds)
|
||||
|
||||
completion = client.completions.create(
|
||||
model=model_name,
|
||||
prompt=None,
|
||||
max_tokens=5,
|
||||
max_tokens=6,
|
||||
temperature=0.0,
|
||||
# NOTE: The OpenAI client allows passing in extra JSON body via the
|
||||
# `extra_body` argument.
|
||||
@@ -69,9 +120,39 @@ def main():
|
||||
)
|
||||
|
||||
print("-" * 30)
|
||||
print("Completions API")
|
||||
print(completion.choices[0].text)
|
||||
print("-" * 30)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = OpenAI(
|
||||
api_key="EMPTY",
|
||||
base_url="http://localhost:8000/v1",
|
||||
)
|
||||
|
||||
model_name = "meta-llama/Llama-3.2-1B-Instruct"
|
||||
|
||||
tokenizer = transformers.AutoTokenizer.from_pretrained(model_name)
|
||||
transformers_model = transformers.AutoModelForCausalLM.from_pretrained(model_name)
|
||||
embedding_layer = transformers_model.get_input_embeddings()
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Please tell me about the capital of France."}
|
||||
]
|
||||
|
||||
# Chat Completions API: embed ONLY the user content. The server wraps
|
||||
# the embedding in the chat template when it renders the messages.
|
||||
run_chat_completion_prompt_embeds(
|
||||
client, model_name, tokenizer, embedding_layer, messages
|
||||
)
|
||||
|
||||
# Completions API: embed the FULL templated prompt. The caller must
|
||||
# apply the chat template up-front.
|
||||
run_completion_prompt_embeds(
|
||||
client, model_name, tokenizer, embedding_layer, messages
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2466,6 +2466,7 @@ MODELS_NEED_VIDEO_METADATA = [
|
||||
MODELS_SUPPORT_VIT_CUDA_GRAPH = [
|
||||
"qwen3_vl",
|
||||
"qwen3_vl_moe",
|
||||
"qwen2_5_vl",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Disaggregated Prefill/Decode Proxy with Bidirectional KV Transfer
|
||||
|
||||
This proxy sits between clients and a vLLM Prefill/Decode (P/D) deployment,
|
||||
routing multi-turn chat requests so that each turn reuses KV cache blocks
|
||||
from the previous turn's Decode node via bidirectional KV transfer.
|
||||
|
||||
Architecture:
|
||||
Client ──► Proxy ──► Prefill (P) ──► Decode (D)
|
||||
│ │ │
|
||||
│ kv_transfer_params flow: │
|
||||
│ D finish ──► proxy caches │
|
||||
│ next turn ──► proxy sends │
|
||||
│ cached D blocks to P ──► │
|
||||
│ P reads D blocks (bidir) │
|
||||
│ P sends its blocks to D │
|
||||
|
||||
Per-request flow:
|
||||
1. Client sends chat/completions request to proxy.
|
||||
2. Proxy looks up cached D block info from the previous turn
|
||||
(keyed by conversation_id).
|
||||
3. If cache hit, proxy attaches D's block info to the request
|
||||
so P can read D's KV blocks instead of recomputing.
|
||||
4. Proxy sends request to P (max_tokens=1, non-streaming).
|
||||
5. P returns kv_transfer_params with its own block info.
|
||||
6. Proxy forwards request + P's block info to D (streaming).
|
||||
7. D streams the response. The final chunk includes D's
|
||||
kv_transfer_params, which the proxy caches for the next turn.
|
||||
8. Proxy returns D's response to the client.
|
||||
|
||||
Conversation isolation:
|
||||
Each request must include a ``conversation_id`` field (top-level in
|
||||
the JSON body) to scope the KV cache across turns. Without it, the
|
||||
proxy cannot link turns and falls back to no-cache behavior.
|
||||
|
||||
Usage:
|
||||
python disagg_proxy_multiturn.py \\
|
||||
--host 0.0.0.0 --port 8000 \\
|
||||
--prefiller-host 10.0.0.1 --prefiller-port 8100 \\
|
||||
--decoder-host 10.0.0.2 --decoder-port 8200
|
||||
|
||||
Dependencies:
|
||||
pip install fastapi uvicorn httpx
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
level=logging.INFO,
|
||||
)
|
||||
logger = logging.getLogger("disagg_proxy")
|
||||
|
||||
|
||||
# Data structures
|
||||
@dataclass
|
||||
class CachedKVEntry:
|
||||
"""KV transfer parameters cached from D's response for one turn."""
|
||||
|
||||
kv_transfer_params: dict[str, Any]
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
class ConversationKVCache:
|
||||
"""Per-conversation KV block cache.
|
||||
|
||||
Each conversation is identified by a ``conversation_id`` supplied by
|
||||
the client. After D finishes a turn, its ``kv_transfer_params`` are
|
||||
stored here. On the next turn, the proxy retrieves them so P can
|
||||
read D's blocks via bidirectional KV transfer.
|
||||
"""
|
||||
|
||||
def __init__(self, ttl_seconds: float = 600.0) -> None:
|
||||
self._store: dict[str, CachedKVEntry] = {}
|
||||
self._ttl = ttl_seconds
|
||||
|
||||
def get(self, conversation_id: str) -> dict[str, Any] | None:
|
||||
"""Retrieve and consume cached KV params for a conversation.
|
||||
|
||||
Returns a *copy* of the kv_transfer_params dict, or None.
|
||||
The entry is removed after retrieval (single-use).
|
||||
"""
|
||||
entry = self._store.pop(conversation_id, None)
|
||||
if entry is None:
|
||||
return None
|
||||
age = time.time() - entry.timestamp
|
||||
if age > self._ttl:
|
||||
logger.info(
|
||||
"conv=%s: stale cache entry (age=%.1fs > ttl=%.1fs), discarding",
|
||||
conversation_id,
|
||||
age,
|
||||
self._ttl,
|
||||
)
|
||||
return None
|
||||
logger.info(
|
||||
"conv=%s: cache HIT (age=%.1fs)",
|
||||
conversation_id,
|
||||
age,
|
||||
)
|
||||
return dict(entry.kv_transfer_params)
|
||||
|
||||
def put(self, conversation_id: str, kv_params: dict[str, Any]) -> None:
|
||||
"""Store D's kv_transfer_params for a conversation."""
|
||||
self._store[conversation_id] = CachedKVEntry(
|
||||
kv_transfer_params=dict(kv_params), # defensive copy
|
||||
)
|
||||
logger.info(
|
||||
"conv=%s: cached D blocks (remote_request_id=%s, blocks=%d)",
|
||||
conversation_id,
|
||||
kv_params.get("remote_request_id", "?"),
|
||||
len(kv_params.get("remote_block_ids", [[]])[0])
|
||||
if kv_params.get("remote_block_ids")
|
||||
else 0,
|
||||
)
|
||||
|
||||
def evict_stale(self) -> int:
|
||||
"""Remove entries older than TTL. Returns count of evicted entries."""
|
||||
now = time.time()
|
||||
stale = [
|
||||
cid
|
||||
for cid, entry in self._store.items()
|
||||
if now - entry.timestamp > self._ttl
|
||||
]
|
||||
for cid in stale:
|
||||
del self._store[cid]
|
||||
return len(stale)
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return len(self._store)
|
||||
|
||||
|
||||
# Global state
|
||||
kv_cache = ConversationKVCache(
|
||||
ttl_seconds=450.0
|
||||
) # Must be < VLLM_NIXL_ABORT_REQUEST_TIMEOUT (480s)
|
||||
|
||||
|
||||
# Service client helpers
|
||||
@dataclass
|
||||
class ServiceClient:
|
||||
"""Wrapper around an httpx.AsyncClient for a P or D instance."""
|
||||
|
||||
client: httpx.AsyncClient
|
||||
host: str
|
||||
port: int
|
||||
id: int
|
||||
|
||||
|
||||
def _make_headers(request_id: str) -> dict[str, str]:
|
||||
"""Build HTTP headers for upstream requests."""
|
||||
headers = {"X-Request-Id": request_id}
|
||||
api_key = os.environ.get("OPENAI_API_KEY")
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
return headers
|
||||
|
||||
|
||||
async def _send_to_prefill(
|
||||
client: ServiceClient,
|
||||
endpoint: str,
|
||||
req_data: dict[str, Any],
|
||||
request_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Send a non-streaming prefill request (max_tokens=1).
|
||||
|
||||
Returns the JSON response from P, which includes kv_transfer_params.
|
||||
"""
|
||||
payload = req_data.copy()
|
||||
payload["stream"] = False
|
||||
payload["max_tokens"] = 1
|
||||
payload.pop("max_completion_tokens", None)
|
||||
payload.pop("min_tokens", None)
|
||||
payload.pop("stream_options", None)
|
||||
|
||||
resp = await client.client.post(
|
||||
endpoint,
|
||||
json=payload,
|
||||
headers=_make_headers(request_id),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def _stream_from_decode(
|
||||
client: ServiceClient,
|
||||
endpoint: str,
|
||||
req_data: dict[str, Any],
|
||||
request_id: str,
|
||||
conversation_id: str,
|
||||
) -> tuple[str, str | None, dict[str, Any] | None, str, str | None, int | None]:
|
||||
"""Stream response from D, capturing text and kv_transfer_params.
|
||||
|
||||
Returns (collected_text, finish_reason, kv_params, response_id, created).
|
||||
Also stores kv_params in the conversation cache.
|
||||
"""
|
||||
payload = req_data.copy()
|
||||
payload["stream"] = True
|
||||
|
||||
collected_text = ""
|
||||
finish_reason: str | None = None
|
||||
response_id: str | None = None
|
||||
model_name: str | None = None
|
||||
created: int | None = None
|
||||
captured_kv: dict[str, Any] | None = None
|
||||
|
||||
async with client.client.stream(
|
||||
"POST",
|
||||
endpoint,
|
||||
json=payload,
|
||||
headers=_make_headers(request_id),
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
async for line in resp.aiter_lines():
|
||||
if not line or not line.startswith("data: "):
|
||||
continue
|
||||
if line == "data: [DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(line[6:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if response_id is None:
|
||||
response_id = chunk.get("id")
|
||||
model_name = chunk.get("model")
|
||||
created = chunk.get("created")
|
||||
|
||||
for choice in chunk.get("choices", []):
|
||||
collected_text += choice.get("text", "")
|
||||
delta = choice.get("delta", {})
|
||||
collected_text += delta.get("content", "")
|
||||
if choice.get("finish_reason"):
|
||||
finish_reason = choice["finish_reason"]
|
||||
|
||||
kv_params = chunk.get("kv_transfer_params")
|
||||
if kv_params:
|
||||
kv_params["remote_host"] = client.host
|
||||
captured_kv = kv_params
|
||||
if conversation_id:
|
||||
kv_cache.put(conversation_id, kv_params)
|
||||
|
||||
return (
|
||||
collected_text,
|
||||
finish_reason,
|
||||
captured_kv,
|
||||
response_id or request_id,
|
||||
model_name,
|
||||
created,
|
||||
)
|
||||
|
||||
|
||||
async def _stream_from_decode_sse(
|
||||
client: ServiceClient,
|
||||
endpoint: str,
|
||||
req_data: dict[str, Any],
|
||||
request_id: str,
|
||||
conversation_id: str,
|
||||
):
|
||||
"""Yield SSE chunks from D to the client, capturing kv_transfer_params."""
|
||||
payload = req_data.copy()
|
||||
payload["stream"] = True
|
||||
|
||||
async with client.client.stream(
|
||||
"POST",
|
||||
endpoint,
|
||||
json=payload,
|
||||
headers=_make_headers(request_id),
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
async for line in resp.aiter_lines():
|
||||
if not line:
|
||||
yield "\n"
|
||||
continue
|
||||
|
||||
if line.startswith("data: ") and line != "data: [DONE]":
|
||||
try:
|
||||
chunk = json.loads(line[6:])
|
||||
kv_params = chunk.get("kv_transfer_params")
|
||||
if kv_params and conversation_id:
|
||||
kv_params["remote_host"] = client.host
|
||||
kv_cache.put(conversation_id, kv_params)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
yield line + "\n"
|
||||
|
||||
|
||||
# FastAPI application
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Initialize HTTP clients for P and D instances."""
|
||||
app.state.prefill_clients: list[ServiceClient] = []
|
||||
app.state.decode_clients: list[ServiceClient] = []
|
||||
|
||||
for i, (host, port) in enumerate(global_args.prefiller_instances):
|
||||
app.state.prefill_clients.append(
|
||||
ServiceClient(
|
||||
client=httpx.AsyncClient(
|
||||
timeout=None,
|
||||
base_url=f"http://{host}:{port}/v1",
|
||||
),
|
||||
host=host,
|
||||
port=port,
|
||||
id=i,
|
||||
)
|
||||
)
|
||||
|
||||
for i, (host, port) in enumerate(global_args.decoder_instances):
|
||||
app.state.decode_clients.append(
|
||||
ServiceClient(
|
||||
client=httpx.AsyncClient(
|
||||
timeout=None,
|
||||
base_url=f"http://{host}:{port}/v1",
|
||||
),
|
||||
host=host,
|
||||
port=port,
|
||||
id=i,
|
||||
)
|
||||
)
|
||||
|
||||
app.state.prefill_iter = itertools.cycle(range(len(app.state.prefill_clients)))
|
||||
app.state.decode_iter = itertools.cycle(range(len(app.state.decode_clients)))
|
||||
|
||||
logger.info(
|
||||
"Ready: %d prefill, %d decode instances",
|
||||
len(app.state.prefill_clients),
|
||||
len(app.state.decode_clients),
|
||||
)
|
||||
yield
|
||||
|
||||
for sc in app.state.prefill_clients + app.state.decode_clients:
|
||||
await sc.client.aclose()
|
||||
|
||||
|
||||
app = FastAPI(title="Disaggregated P/D Proxy (Multi-turn)", lifespan=lifespan)
|
||||
|
||||
|
||||
def _next_client(app_state, role: str) -> ServiceClient:
|
||||
if role == "prefill":
|
||||
return app_state.prefill_clients[next(app_state.prefill_iter)]
|
||||
return app_state.decode_clients[next(app_state.decode_iter)]
|
||||
|
||||
|
||||
# Request handler
|
||||
async def _handle_request(api_path: str, request: Request):
|
||||
"""Core request handler for both /v1/chat/completions and /v1/completions."""
|
||||
req_data = await request.json()
|
||||
request_id = str(uuid.uuid4())
|
||||
conversation_id: str = req_data.pop("conversation_id", "")
|
||||
client_wants_stream = req_data.get("stream", False)
|
||||
|
||||
if not conversation_id:
|
||||
logger.warning(
|
||||
"[%s] No conversation_id provided — KV cache reuse disabled "
|
||||
"for this request. Add a 'conversation_id' field to enable "
|
||||
"cross-turn KV sharing.",
|
||||
request_id,
|
||||
)
|
||||
|
||||
# Step 1: Look up cached D blocks from the previous turn
|
||||
cached_kv = kv_cache.get(conversation_id) if conversation_id else None
|
||||
|
||||
if cached_kv:
|
||||
# Tell P to read D's blocks (bidirectional transfer)
|
||||
cached_kv["do_remote_decode"] = True
|
||||
cached_kv["do_remote_prefill"] = False
|
||||
req_data["kv_transfer_params"] = cached_kv
|
||||
logger.info(
|
||||
"[%s] conv=%s: sending D's cached blocks to P (remote_request_id=%s)",
|
||||
request_id,
|
||||
conversation_id,
|
||||
cached_kv.get("remote_request_id"),
|
||||
)
|
||||
else:
|
||||
# No cached blocks — P recomputes from scratch
|
||||
req_data["kv_transfer_params"] = {
|
||||
"do_remote_decode": True,
|
||||
"do_remote_prefill": False,
|
||||
"remote_engine_id": None,
|
||||
"remote_block_ids": None,
|
||||
"remote_host": None,
|
||||
"remote_port": None,
|
||||
}
|
||||
logger.info("[%s] conv=%s: cache MISS", request_id, conversation_id)
|
||||
|
||||
# Step 2: Send to Prefill node (non-streaming, max_tokens=1)
|
||||
prefill_client = _next_client(request.app.state, "prefill")
|
||||
t0 = time.time()
|
||||
prefill_resp = await _send_to_prefill(
|
||||
prefill_client,
|
||||
api_path,
|
||||
req_data,
|
||||
request_id,
|
||||
)
|
||||
logger.info(
|
||||
"[%s] Prefill done in %.0fms",
|
||||
request_id,
|
||||
(time.time() - t0) * 1000,
|
||||
)
|
||||
|
||||
# Attach P's kv_transfer_params for D to read P's blocks
|
||||
p_kv_params = prefill_resp.get("kv_transfer_params", {})
|
||||
if p_kv_params:
|
||||
p_kv_params["remote_host"] = prefill_client.host
|
||||
req_data["kv_transfer_params"] = p_kv_params
|
||||
|
||||
# Step 3: Stream from Decode node, capturing kv_transfer_params
|
||||
decode_client = _next_client(request.app.state, "decode")
|
||||
|
||||
if client_wants_stream:
|
||||
return StreamingResponse(
|
||||
_stream_from_decode_sse(
|
||||
decode_client,
|
||||
api_path,
|
||||
req_data,
|
||||
request_id,
|
||||
conversation_id,
|
||||
),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
text, finish_reason, _, resp_id, model, created = await _stream_from_decode(
|
||||
decode_client,
|
||||
api_path,
|
||||
req_data,
|
||||
request_id,
|
||||
conversation_id,
|
||||
)
|
||||
|
||||
# Build OpenAI-compatible response
|
||||
is_chat = "messages" in req_data
|
||||
if is_chat:
|
||||
body = {
|
||||
"id": resp_id,
|
||||
"object": "chat.completion",
|
||||
"created": created or int(time.time()),
|
||||
"model": model or req_data.get("model", ""),
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": text},
|
||||
"finish_reason": finish_reason,
|
||||
}
|
||||
],
|
||||
"usage": None,
|
||||
}
|
||||
else:
|
||||
body = {
|
||||
"id": resp_id,
|
||||
"object": "text_completion",
|
||||
"created": created or int(time.time()),
|
||||
"model": model or req_data.get("model", ""),
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"text": text,
|
||||
"logprobs": None,
|
||||
"finish_reason": finish_reason,
|
||||
}
|
||||
],
|
||||
"usage": None,
|
||||
}
|
||||
return JSONResponse(content=body)
|
||||
|
||||
|
||||
# Routes
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completions(request: Request):
|
||||
return await _handle_request("/chat/completions", request)
|
||||
|
||||
|
||||
@app.post("/v1/completions")
|
||||
async def completions(request: Request):
|
||||
return await _handle_request("/completions", request)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
evicted = kv_cache.evict_stale()
|
||||
return {
|
||||
"status": "ok",
|
||||
"cached_conversations": kv_cache.size,
|
||||
"evicted_stale": evicted,
|
||||
}
|
||||
|
||||
|
||||
# CLI
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Disaggregated P/D proxy with bidirectional KV transfer",
|
||||
)
|
||||
p.add_argument("--host", default="0.0.0.0")
|
||||
p.add_argument("--port", type=int, default=8000)
|
||||
p.add_argument(
|
||||
"--prefiller-host",
|
||||
"--prefiller-hosts",
|
||||
dest="prefiller_hosts",
|
||||
nargs="+",
|
||||
default=["localhost"],
|
||||
)
|
||||
p.add_argument(
|
||||
"--prefiller-port",
|
||||
"--prefiller-ports",
|
||||
dest="prefiller_ports",
|
||||
type=int,
|
||||
nargs="+",
|
||||
default=[8100],
|
||||
)
|
||||
p.add_argument(
|
||||
"--decoder-host",
|
||||
"--decoder-hosts",
|
||||
dest="decoder_hosts",
|
||||
nargs="+",
|
||||
default=["localhost"],
|
||||
)
|
||||
p.add_argument(
|
||||
"--decoder-port",
|
||||
"--decoder-ports",
|
||||
dest="decoder_ports",
|
||||
type=int,
|
||||
nargs="+",
|
||||
default=[8200],
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
if len(args.prefiller_hosts) != len(args.prefiller_ports):
|
||||
p.error("Number of prefiller hosts must match ports")
|
||||
if len(args.decoder_hosts) != len(args.decoder_ports):
|
||||
p.error("Number of decoder hosts must match ports")
|
||||
|
||||
args.prefiller_instances = list(zip(args.prefiller_hosts, args.prefiller_ports))
|
||||
args.decoder_instances = list(zip(args.decoder_hosts, args.decoder_ports))
|
||||
return args
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
global global_args
|
||||
global_args = parse_args()
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host=global_args.host, port=global_args.port)
|
||||
@@ -1,9 +1,9 @@
|
||||
{%- macro format_parameters(properties, required) -%}
|
||||
{%- macro format_parameters(properties, required, filter_keys=false) -%}
|
||||
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
|
||||
{%- set ns = namespace(found_first=false) -%}
|
||||
{%- for key, value in properties | dictsort -%}
|
||||
{%- set add_comma = false -%}
|
||||
{%- if key not in standard_keys -%}
|
||||
{%- if not filter_keys or key not in standard_keys -%}
|
||||
{%- if ns.found_first %},{% endif -%}
|
||||
{%- set ns.found_first = true -%}
|
||||
{{ key }}:{
|
||||
@@ -11,34 +11,15 @@
|
||||
description:<|"|>{{ value['description'] }}<|"|>
|
||||
{%- set add_comma = true -%}
|
||||
{%- endif -%}
|
||||
{%- if value['nullable'] %}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
nullable:true
|
||||
{%- endif -%}
|
||||
{%- if value['type'] | upper == 'STRING' -%}
|
||||
{%- if value['enum'] -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
enum:{{ format_argument(value['enum']) }}
|
||||
{%- endif -%}
|
||||
{%- elif value['type'] | upper == 'OBJECT' -%}
|
||||
,properties:{
|
||||
{%- if value['properties'] is defined and value['properties'] is mapping -%}
|
||||
{{- format_parameters(value['properties'], value['required'] | default([])) -}}
|
||||
{%- elif value is mapping -%}
|
||||
{{- format_parameters(value, value['required'] | default([])) -}}
|
||||
{%- endif -%}
|
||||
}
|
||||
{%- if value['required'] -%}
|
||||
,required:[
|
||||
{%- for item in value['required'] | default([]) -%}
|
||||
<|"|>{{- item -}}<|"|>
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
{%- endif -%}
|
||||
{%- elif value['type'] | upper == 'ARRAY' -%}
|
||||
{%- if value['items'] is mapping and value['items'] -%}
|
||||
,items:{
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
items:{
|
||||
{%- set ns_items = namespace(found_first=false) -%}
|
||||
{%- for item_key, item_value in value['items'] | dictsort -%}
|
||||
{%- if item_value is not none -%}
|
||||
@@ -71,6 +52,32 @@
|
||||
}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if value['nullable'] %}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
nullable:true
|
||||
{%- endif -%}
|
||||
{%- if value['type'] | upper == 'OBJECT' -%}
|
||||
{%- if value['properties'] is defined and value['properties'] is mapping -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
properties:{
|
||||
{{- format_parameters(value['properties'], value['required'] | default([])) -}}
|
||||
}
|
||||
{%- elif value is mapping -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
properties:{
|
||||
{{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}}
|
||||
}
|
||||
{%- endif -%}
|
||||
{%- if value['required'] -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
required:[
|
||||
{%- for item in value['required'] | default([]) -%}
|
||||
<|"|>{{- item -}}<|"|>
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
type:<|"|>{{ value['type'] | upper }}<|"|>}
|
||||
{%- endif -%}
|
||||
@@ -167,20 +174,25 @@
|
||||
|
||||
{%- set ns = namespace(prev_message_type=None) -%}
|
||||
{%- set loop_messages = messages -%}
|
||||
{{ bos_token }}
|
||||
{{- bos_token -}}
|
||||
{#- Handle System/Tool Definitions Block -#}
|
||||
{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
|
||||
{{- '<|turn>system\n' -}}
|
||||
|
||||
{#- Inject Thinking token at the very top of the FIRST system turn -#}
|
||||
{%- if enable_thinking is defined and enable_thinking -%}
|
||||
{{- '<|think|>' -}}
|
||||
{{- '<|think|>\n' -}}
|
||||
{%- set ns.prev_message_type = 'think' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if messages[0]['role'] in ['system', 'developer'] -%}
|
||||
{{- messages[0]['content'] | trim -}}
|
||||
{%- if messages[0]['content'] is string -%}
|
||||
{{- messages[0]['content'] | trim -}}
|
||||
{%- elif messages[0]['content'] is sequence -%}
|
||||
{%- for item in messages[0]['content'] -%}
|
||||
{{- item['text'] | trim + ' '-}}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- set loop_messages = messages[1:] -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if tools -%}
|
||||
{%- for tool in tools %}
|
||||
{{- '<|tool>' -}}
|
||||
@@ -189,10 +201,10 @@
|
||||
{%- endfor %}
|
||||
{%- set ns.prev_message_type = 'tool' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{{- '<turn|>\n' -}}
|
||||
{%- endif %}
|
||||
|
||||
{#- Pre-scan: find last user message index for reasoning guard -#}
|
||||
{%- set ns_turn = namespace(last_user_idx=-1) -%}
|
||||
{%- for i in range(loop_messages | length) -%}
|
||||
{%- if loop_messages[i]['role'] == 'user' -%}
|
||||
@@ -200,12 +212,12 @@
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{#- Loop through messages -#}
|
||||
{%- for message in loop_messages -%}
|
||||
{%- if message['role'] != 'tool' -%}
|
||||
{%- set ns.prev_message_type = None -%}
|
||||
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
|
||||
{#- OpenAI may emit multiple assistant messages in one tool loop (user → asst → tool → asst → tool).
|
||||
Only the first of those should open <|turn>model; later ones continue the same model turn. -#}
|
||||
{#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
|
||||
{%- set prev_nt = namespace(role=None, found=false) -%}
|
||||
{%- if loop.index0 > 0 -%}
|
||||
{%- for j in range(loop.index0 - 1, -1, -1) -%}
|
||||
@@ -222,8 +234,10 @@
|
||||
{{- '<|turn>' + role + '\n' }}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if message.get('reasoning') and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
|
||||
{{- '<|channel>thought\n' + message['reasoning'] + '\n<channel|>'}}
|
||||
{#- Render reasoning/reasoning_content as thinking channel -#}
|
||||
{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
|
||||
{%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
|
||||
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if message['tool_calls'] -%}
|
||||
@@ -247,14 +261,14 @@
|
||||
|
||||
{%- set ns_tr_out = namespace(flag=false) -%}
|
||||
{%- if message.get('tool_responses') -%}
|
||||
{#- Legacy: tool_responses embedded on the assistant message -#}
|
||||
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
|
||||
{%- for tool_response in message['tool_responses'] -%}
|
||||
{{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
|
||||
{%- set ns_tr_out.flag = true -%}
|
||||
{%- set ns.prev_message_type = 'tool_response' -%}
|
||||
{%- endfor -%}
|
||||
{%- elif message.get('tool_calls') -%}
|
||||
{#- OpenAI Chat Completions: consecutive following messages with role "tool" (no break/continue; range scan) -#}
|
||||
{#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
|
||||
{%- set ns_tool_scan = namespace(stopped=false) -%}
|
||||
{%- for k in range(loop.index0 + 1, loop_messages | length) -%}
|
||||
{%- if ns_tool_scan.stopped -%}
|
||||
@@ -262,12 +276,14 @@
|
||||
{%- set ns_tool_scan.stopped = true -%}
|
||||
{%- else -%}
|
||||
{%- set follow = loop_messages[k] -%}
|
||||
{#- Resolve tool_call_id to function name -#}
|
||||
{%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
|
||||
{%- for tc in message['tool_calls'] -%}
|
||||
{%- if tc.get('id') == follow.get('tool_call_id') -%}
|
||||
{%- set ns_tname.name = tc['function']['name'] -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{#- Handle content as string or content-parts array -#}
|
||||
{%- set tool_body = follow.get('content') -%}
|
||||
{%- if tool_body is string -%}
|
||||
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
||||
@@ -288,6 +304,7 @@
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- set captured_content -%}
|
||||
{%- if message['content'] is string -%}
|
||||
{%- if role == 'model' -%}
|
||||
{{- strip_thinking(message['content']) -}}
|
||||
@@ -303,29 +320,35 @@
|
||||
{{- item['text'] | trim -}}
|
||||
{%- endif -%}
|
||||
{%- elif item['type'] == 'image' -%}
|
||||
{{- '\n\n<|image|>\n\n' -}}
|
||||
{{- '<|image|>' -}}
|
||||
{%- set ns.prev_message_type = 'image' -%}
|
||||
{%- elif item['type'] == 'audio' -%}
|
||||
{{- '<|audio|>' -}}
|
||||
{%- set ns.prev_message_type = 'audio' -%}
|
||||
{%- elif item['type'] == 'video' -%}
|
||||
{{- '\n\n<|video|>\n\n' -}}
|
||||
{{- '<|video|>' -}}
|
||||
{%- set ns.prev_message_type = 'video' -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- endset -%}
|
||||
|
||||
{%- if not (ns_tr_out.flag and not message.get('content')) -%}
|
||||
{{- captured_content -}}
|
||||
{%- set has_content = captured_content | trim | length > 0 -%}
|
||||
|
||||
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
|
||||
{{- '<|tool_response>' -}}
|
||||
{%- elif not (ns_tr_out.flag and not has_content) -%}
|
||||
{{- '<turn|>\n' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- if add_generation_prompt -%}
|
||||
{%- if ns.prev_message_type != 'tool_response' -%}
|
||||
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
|
||||
{{- '<|turn>model\n' -}}
|
||||
{%- if not enable_thinking | default(false) -%}
|
||||
{{- '<|channel>thought\n<channel|>' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if not enable_thinking | default(false) -%}
|
||||
{{- '<|channel>thought\n<channel|>' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
@@ -24,7 +24,7 @@ outlines_core == 0.2.14
|
||||
# required for outlines backend disk cache
|
||||
diskcache == 5.6.3
|
||||
lark == 1.2.2
|
||||
xgrammar >= 0.1.32, < 1.0.0; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "arm64" or platform_machine == "s390x" or platform_machine == "ppc64le"
|
||||
xgrammar >= 0.2.0, < 1.0.0; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "arm64" or platform_machine == "s390x" or platform_machine == "ppc64le"
|
||||
typing_extensions >= 4.10
|
||||
filelock >= 3.16.1 # need to contain https://github.com/tox-dev/filelock/pull/317
|
||||
partial-json-parser # used for parsing partial JSON outputs
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,7 +61,11 @@ fastsafetensors>=0.2.2 # 0.2.2 contains important fixes for multi-GPU mem usage
|
||||
instanttensor>=0.1.5
|
||||
pydantic>=2.12 # 2.11 leads to error on python 3.13
|
||||
decord==0.6.0; platform_machine == "x86_64"
|
||||
terratorch >= 1.2.2 # Required for Prithvi tests
|
||||
# terratorch is temporarily disabled while PyPI has the `lightning` package
|
||||
# in `quarantined` status (every published terratorch version transitively
|
||||
# requires `lightning`, so the resolver fails with "no versions of lightning").
|
||||
# Re-enable once PyPI lifts the quarantine. Tracked in #41376.
|
||||
# terratorch >= 1.2.2 # Required for Prithvi tests
|
||||
imagehash # Required for Prithvi tests
|
||||
segmentation-models-pytorch > 0.4.0 # Required for Prithvi tests
|
||||
|
||||
|
||||
+11
-274
@@ -1,15 +1,9 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements/test/cuda.in -c requirements/cuda.txt -o requirements/test/cuda.txt --index-strategy unsafe-best-match --torch-backend cu130 --python-platform x86_64-manylinux_2_28 --python-version 3.12
|
||||
absl-py==2.1.0
|
||||
# via
|
||||
# rouge-score
|
||||
# tensorboard
|
||||
# via rouge-score
|
||||
accelerate==1.13.0
|
||||
# via peft
|
||||
aenum==3.1.16
|
||||
# via lightly
|
||||
affine==2.4.0
|
||||
# via rasterio
|
||||
aiohappyeyeballs==2.6.1
|
||||
# via aiohttp
|
||||
aiohttp==3.13.3
|
||||
@@ -25,22 +19,14 @@ aiohttp-cors==0.8.1
|
||||
# via ray
|
||||
aiosignal==1.4.0
|
||||
# via aiohttp
|
||||
albucore==0.0.16
|
||||
# via terratorch
|
||||
albumentations==1.4.6
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# terratorch
|
||||
# via -r requirements/test/cuda.in
|
||||
alembic==1.16.4
|
||||
# via optuna
|
||||
annotated-doc==0.0.4
|
||||
# via fastapi
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
antlr4-python3-runtime==4.9.3
|
||||
# via
|
||||
# hydra-core
|
||||
# omegaconf
|
||||
anyio==4.6.2.post1
|
||||
# via
|
||||
# httpx
|
||||
@@ -54,12 +40,10 @@ arrow==1.3.0
|
||||
attrs==24.2.0
|
||||
# via
|
||||
# aiohttp
|
||||
# fiona
|
||||
# hypothesis
|
||||
# jsonlines
|
||||
# jsonschema
|
||||
# pytest-subtests
|
||||
# rasterio
|
||||
# referencing
|
||||
audioread==3.0.1
|
||||
# via librosa
|
||||
@@ -78,9 +62,7 @@ backoff==2.2.1
|
||||
# -r requirements/test/cuda.in
|
||||
# schemathesis
|
||||
bitsandbytes==0.49.2
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# lightning
|
||||
# via -r requirements/test/cuda.in
|
||||
black==24.10.0
|
||||
# via datamodel-code-generator
|
||||
blobfile==3.0.0
|
||||
@@ -103,15 +85,9 @@ cachetools==5.5.2
|
||||
# via google-auth
|
||||
certifi==2024.8.30
|
||||
# via
|
||||
# fiona
|
||||
# httpcore
|
||||
# httpx
|
||||
# lightly
|
||||
# pyogrio
|
||||
# pyproj
|
||||
# rasterio
|
||||
# requests
|
||||
# sentry-sdk
|
||||
cffi==2.0.0
|
||||
# via
|
||||
# cryptography
|
||||
@@ -125,25 +101,12 @@ chz==0.3.0
|
||||
click==8.1.7
|
||||
# via
|
||||
# black
|
||||
# click-plugins
|
||||
# cligj
|
||||
# fiona
|
||||
# jiwer
|
||||
# nltk
|
||||
# rasterio
|
||||
# ray
|
||||
# schemathesis
|
||||
# typer
|
||||
# uvicorn
|
||||
# wandb
|
||||
click-plugins==1.1.1.2
|
||||
# via
|
||||
# fiona
|
||||
# rasterio
|
||||
cligj==0.7.2
|
||||
# via
|
||||
# fiona
|
||||
# rasterio
|
||||
colorama==0.4.6
|
||||
# via
|
||||
# perceptron
|
||||
@@ -191,8 +154,6 @@ decorator==5.1.1
|
||||
# via librosa
|
||||
decord==0.6.0
|
||||
# via -r requirements/test/cuda.in
|
||||
diffusers==0.36.0
|
||||
# via terratorch
|
||||
dill==0.3.8
|
||||
# via
|
||||
# datasets
|
||||
@@ -207,14 +168,10 @@ docker==7.1.0
|
||||
# via gpt-oss
|
||||
docopt==0.6.2
|
||||
# via num2words
|
||||
docstring-parser==0.17.0
|
||||
# via jsonargparse
|
||||
einops==0.8.1
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# encodec
|
||||
# terratorch
|
||||
# torchgeo
|
||||
# vector-quantize-pytorch
|
||||
# vocos
|
||||
einx==0.3.0
|
||||
@@ -244,13 +201,10 @@ filelock==3.16.1
|
||||
# -c requirements/common.txt
|
||||
# blobfile
|
||||
# datasets
|
||||
# diffusers
|
||||
# huggingface-hub
|
||||
# ray
|
||||
# torch
|
||||
# virtualenv
|
||||
fiona==1.10.1
|
||||
# via torchgeo
|
||||
fonttools==4.55.0
|
||||
# via matplotlib
|
||||
fqdn==1.5.1
|
||||
@@ -267,9 +221,6 @@ fsspec==2024.12.0
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# huggingface-hub
|
||||
# lightning
|
||||
# pytorch-lightning
|
||||
# tacoreader
|
||||
# torch
|
||||
ftfy==6.3.1
|
||||
# via open-clip-torch
|
||||
@@ -277,12 +228,6 @@ genai-perf==0.0.16
|
||||
# via -r requirements/test/cuda.in
|
||||
genson==1.3.0
|
||||
# via datamodel-code-generator
|
||||
geopandas==1.0.1
|
||||
# via terratorch
|
||||
gitdb==4.0.12
|
||||
# via gitpython
|
||||
gitpython==3.1.44
|
||||
# via wandb
|
||||
google-api-core==2.24.2
|
||||
# via
|
||||
# google-cloud-core
|
||||
@@ -317,7 +262,6 @@ grpcio==1.78.0
|
||||
# -r requirements/test/cuda.in
|
||||
# grpcio-reflection
|
||||
# ray
|
||||
# tensorboard
|
||||
grpcio-reflection==1.78.0
|
||||
# via -r requirements/test/cuda.in
|
||||
h11==0.14.0
|
||||
@@ -326,8 +270,6 @@ h11==0.14.0
|
||||
# uvicorn
|
||||
h2==4.3.0
|
||||
# via httpx
|
||||
h5py==3.13.0
|
||||
# via terratorch
|
||||
harfile==0.3.0
|
||||
# via schemathesis
|
||||
hf-xet==1.4.3
|
||||
@@ -343,7 +285,6 @@ httpcore==1.0.6
|
||||
httpx==0.27.2
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# diffusers
|
||||
# huggingface-hub
|
||||
# perceptron
|
||||
# schemathesis
|
||||
@@ -351,23 +292,17 @@ huggingface-hub==1.10.2
|
||||
# via
|
||||
# accelerate
|
||||
# datasets
|
||||
# diffusers
|
||||
# evaluate
|
||||
# open-clip-torch
|
||||
# peft
|
||||
# segmentation-models-pytorch
|
||||
# sentence-transformers
|
||||
# terratorch
|
||||
# timm
|
||||
# tokenizers
|
||||
# transformers
|
||||
# vocos
|
||||
humanize==4.11.0
|
||||
# via runai-model-streamer
|
||||
hydra-core==1.3.2
|
||||
# via
|
||||
# lightly
|
||||
# lightning
|
||||
hyperframe==6.1.0
|
||||
# via h2
|
||||
hypothesis==6.131.0
|
||||
@@ -392,11 +327,7 @@ imagehash==4.3.2
|
||||
imageio==2.37.0
|
||||
# via scikit-image
|
||||
importlib-metadata==8.7.0
|
||||
# via
|
||||
# diffusers
|
||||
# opentelemetry-api
|
||||
importlib-resources==6.5.2
|
||||
# via typeshed-client
|
||||
# via opentelemetry-api
|
||||
inflect==5.6.2
|
||||
# via datamodel-code-generator
|
||||
iniconfig==2.0.0
|
||||
@@ -426,14 +357,8 @@ joblib==1.4.2
|
||||
# librosa
|
||||
# nltk
|
||||
# scikit-learn
|
||||
jsonargparse==4.46.0
|
||||
# via
|
||||
# lightning
|
||||
# terratorch
|
||||
jsonlines==4.0.0
|
||||
# via lm-eval
|
||||
jsonnet==0.21.0
|
||||
# via jsonargparse
|
||||
jsonpointer==3.0.0
|
||||
# via jsonschema
|
||||
jsonschema==4.23.0
|
||||
@@ -452,10 +377,6 @@ kaleido==0.2.1
|
||||
# via genai-perf
|
||||
kiwisolver==1.4.7
|
||||
# via matplotlib
|
||||
kornia==0.8.1
|
||||
# via torchgeo
|
||||
kornia-rs==0.1.9
|
||||
# via kornia
|
||||
lazy-loader==0.4
|
||||
# via
|
||||
# librosa
|
||||
@@ -464,21 +385,6 @@ libnacl==2.1.0
|
||||
# via tensorizer
|
||||
librosa==0.10.2.post1
|
||||
# via -r requirements/test/cuda.in
|
||||
lightly==1.5.22
|
||||
# via
|
||||
# terratorch
|
||||
# torchgeo
|
||||
lightly-utils==0.0.2
|
||||
# via lightly
|
||||
lightning==2.6.1
|
||||
# via
|
||||
# terratorch
|
||||
# torchgeo
|
||||
lightning-utilities==0.14.3
|
||||
# via
|
||||
# lightning
|
||||
# pytorch-lightning
|
||||
# torchmetrics
|
||||
llvmlite==0.47.0
|
||||
# via numba
|
||||
lm-eval==0.4.11
|
||||
@@ -490,8 +396,6 @@ lxml==5.3.0
|
||||
# sacrebleu
|
||||
mako==1.3.10
|
||||
# via alembic
|
||||
markdown==3.8.2
|
||||
# via tensorboard
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==3.0.1
|
||||
@@ -500,11 +404,7 @@ markupsafe==3.0.1
|
||||
# mako
|
||||
# werkzeug
|
||||
matplotlib==3.9.2
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# lightning
|
||||
# pycocotools
|
||||
# torchgeo
|
||||
# via -r requirements/test/cuda.in
|
||||
mbstrdecoder==1.1.3
|
||||
# via
|
||||
# dataproperty
|
||||
@@ -559,7 +459,6 @@ numpy==2.2.6
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# accelerate
|
||||
# albucore
|
||||
# albumentations
|
||||
# bitsandbytes
|
||||
# bm25s
|
||||
@@ -567,19 +466,14 @@ numpy==2.2.6
|
||||
# cupy-cuda12x
|
||||
# datasets
|
||||
# decord
|
||||
# diffusers
|
||||
# einx
|
||||
# encodec
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# genai-perf
|
||||
# geopandas
|
||||
# h5py
|
||||
# imagehash
|
||||
# imageio
|
||||
# librosa
|
||||
# lightly
|
||||
# lightly-utils
|
||||
# lm-eval
|
||||
# matplotlib
|
||||
# mistral-common
|
||||
@@ -591,11 +485,7 @@ numpy==2.2.6
|
||||
# patsy
|
||||
# peft
|
||||
# perceptron
|
||||
# pycocotools
|
||||
# pyogrio
|
||||
# pywavelets
|
||||
# rasterio
|
||||
# rioxarray
|
||||
# rouge-score
|
||||
# runai-model-streamer
|
||||
# sacrebleu
|
||||
@@ -603,21 +493,14 @@ numpy==2.2.6
|
||||
# scikit-learn
|
||||
# scipy
|
||||
# segmentation-models-pytorch
|
||||
# shapely
|
||||
# soxr
|
||||
# statsmodels
|
||||
# tensorboard
|
||||
# tensorboardx
|
||||
# tensorizer
|
||||
# terratorch
|
||||
# tifffile
|
||||
# torchgeo
|
||||
# torchmetrics
|
||||
# torchvision
|
||||
# transformers
|
||||
# tritonclient
|
||||
# vocos
|
||||
# xarray
|
||||
nvidia-cublas==13.1.0.3
|
||||
# via
|
||||
# cuda-toolkit
|
||||
@@ -657,10 +540,6 @@ nvidia-nvshmem-cu13==3.4.5
|
||||
# via torch
|
||||
nvidia-nvtx==13.0.85
|
||||
# via cuda-toolkit
|
||||
omegaconf==2.3.0
|
||||
# via
|
||||
# hydra-core
|
||||
# lightning
|
||||
open-clip-torch==2.32.0
|
||||
# via -r requirements/test/cuda.in
|
||||
openai-harmony==0.0.4
|
||||
@@ -675,7 +554,6 @@ opencv-python-headless==4.13.0.90
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/cuda.in
|
||||
# albucore
|
||||
# albumentations
|
||||
# mistral-common
|
||||
openpyxl==3.1.5
|
||||
@@ -710,44 +588,27 @@ packaging==24.2
|
||||
# datasets
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# geopandas
|
||||
# huggingface-hub
|
||||
# hydra-core
|
||||
# kornia
|
||||
# lazy-loader
|
||||
# lightning
|
||||
# lightning-utilities
|
||||
# matplotlib
|
||||
# optuna
|
||||
# peft
|
||||
# plotly
|
||||
# pooch
|
||||
# pyogrio
|
||||
# pytest
|
||||
# pytest-rerunfailures
|
||||
# pytorch-lightning
|
||||
# ray
|
||||
# rioxarray
|
||||
# scikit-image
|
||||
# statsmodels
|
||||
# tensorboard
|
||||
# tensorboardx
|
||||
# torchmetrics
|
||||
# transformers
|
||||
# typepy
|
||||
# wandb
|
||||
# xarray
|
||||
pandas==2.2.3
|
||||
# via
|
||||
# datasets
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# genai-perf
|
||||
# geopandas
|
||||
# statsmodels
|
||||
# tacoreader
|
||||
# torchgeo
|
||||
# xarray
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
pathvalidate==3.2.1
|
||||
@@ -762,25 +623,20 @@ perf-analyzer==0.1.0
|
||||
# via genai-perf
|
||||
pillow==10.4.0
|
||||
# via
|
||||
# diffusers
|
||||
# genai-perf
|
||||
# imagehash
|
||||
# imageio
|
||||
# lightly-utils
|
||||
# matplotlib
|
||||
# mistral-common
|
||||
# perceptron
|
||||
# scikit-image
|
||||
# segmentation-models-pytorch
|
||||
# tensorboard
|
||||
# torchgeo
|
||||
# torchvision
|
||||
platformdirs==4.3.6
|
||||
# via
|
||||
# black
|
||||
# pooch
|
||||
# virtualenv
|
||||
# wandb
|
||||
plotly==5.24.1
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
@@ -817,10 +673,7 @@ protobuf==6.33.6
|
||||
# opentelemetry-proto
|
||||
# proto-plus
|
||||
# ray
|
||||
# tensorboard
|
||||
# tensorboardx
|
||||
# tensorizer
|
||||
# wandb
|
||||
psutil==6.1.0
|
||||
# via
|
||||
# accelerate
|
||||
@@ -834,16 +687,12 @@ pyarrow==23.0.0
|
||||
# via
|
||||
# datasets
|
||||
# genai-perf
|
||||
# tacoreader
|
||||
# terratorch
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# rsa
|
||||
pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pycocotools==2.0.8
|
||||
# via terratorch
|
||||
pycountry==24.6.1
|
||||
# via pydantic-extra-types
|
||||
pycparser==2.22
|
||||
@@ -858,13 +707,11 @@ pydantic==2.12.0
|
||||
# datamodel-code-generator
|
||||
# fastapi
|
||||
# gpt-oss
|
||||
# lightly
|
||||
# mistral-common
|
||||
# mteb
|
||||
# openai-harmony
|
||||
# pydantic-extra-types
|
||||
# ray
|
||||
# wandb
|
||||
pydantic-core==2.41.1
|
||||
# via pydantic
|
||||
pydantic-extra-types==2.10.5
|
||||
@@ -873,17 +720,8 @@ pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.11.0
|
||||
# via msal
|
||||
pyogrio==0.11.0
|
||||
# via geopandas
|
||||
pyparsing==3.2.0
|
||||
# via
|
||||
# matplotlib
|
||||
# rasterio
|
||||
pyproj==3.7.1
|
||||
# via
|
||||
# geopandas
|
||||
# rioxarray
|
||||
# torchgeo
|
||||
# via matplotlib
|
||||
pyrate-limiter==3.7.0
|
||||
# via schemathesis
|
||||
pystemmer==3.0.0
|
||||
@@ -920,22 +758,15 @@ pytest-subtests==0.14.1
|
||||
# via schemathesis
|
||||
pytest-timeout==2.3.1
|
||||
# via -r requirements/test/cuda.in
|
||||
python-box==7.3.2
|
||||
# via terratorch
|
||||
python-dateutil==2.9.0.post0
|
||||
# via
|
||||
# arrow
|
||||
# botocore
|
||||
# lightly
|
||||
# matplotlib
|
||||
# pandas
|
||||
# typepy
|
||||
python-rapidjson==1.20
|
||||
# via tritonclient
|
||||
pytorch-lightning==2.5.2
|
||||
# via
|
||||
# lightly
|
||||
# lightning
|
||||
pytrec-eval-terrier==0.5.7
|
||||
# via mteb
|
||||
pytz==2024.2
|
||||
@@ -952,26 +783,16 @@ pyyaml==6.0.2
|
||||
# datasets
|
||||
# genai-perf
|
||||
# huggingface-hub
|
||||
# jsonargparse
|
||||
# lightning
|
||||
# omegaconf
|
||||
# optuna
|
||||
# peft
|
||||
# pytorch-lightning
|
||||
# ray
|
||||
# responses
|
||||
# schemathesis
|
||||
# timm
|
||||
# transformers
|
||||
# vocos
|
||||
# wandb
|
||||
rapidfuzz==3.12.1
|
||||
# via jiwer
|
||||
rasterio==1.4.3
|
||||
# via
|
||||
# rioxarray
|
||||
# terratorch
|
||||
# torchgeo
|
||||
ray==2.48.0
|
||||
# via -r requirements/test/cuda.in
|
||||
redis==5.2.0
|
||||
@@ -982,7 +803,6 @@ referencing==0.35.1
|
||||
# jsonschema-specifications
|
||||
regex==2026.2.28
|
||||
# via
|
||||
# diffusers
|
||||
# nltk
|
||||
# open-clip-torch
|
||||
# sacrebleu
|
||||
@@ -994,13 +814,11 @@ requests==2.32.3
|
||||
# azure-core
|
||||
# buildkite-test-collector
|
||||
# datasets
|
||||
# diffusers
|
||||
# docker
|
||||
# evaluate
|
||||
# google-api-core
|
||||
# google-cloud-storage
|
||||
# gpt-oss
|
||||
# lightly
|
||||
# lm-eval
|
||||
# mistral-common
|
||||
# msal
|
||||
@@ -1010,9 +828,7 @@ requests==2.32.3
|
||||
# responses
|
||||
# schemathesis
|
||||
# starlette-testclient
|
||||
# tacoreader
|
||||
# tiktoken
|
||||
# wandb
|
||||
responses==0.25.3
|
||||
# via genai-perf
|
||||
rfc3339-validator==0.1.4
|
||||
@@ -1022,13 +838,9 @@ rfc3987==1.3.8
|
||||
rich==13.9.4
|
||||
# via
|
||||
# genai-perf
|
||||
# lightning
|
||||
# mteb
|
||||
# perceptron
|
||||
# terratorch
|
||||
# typer
|
||||
rioxarray==0.19.0
|
||||
# via terratorch
|
||||
rouge-score==0.1.2
|
||||
# via lm-eval
|
||||
rpds-py==0.20.1
|
||||
@@ -1037,8 +849,6 @@ rpds-py==0.20.1
|
||||
# referencing
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
rtree==1.4.0
|
||||
# via torchgeo
|
||||
runai-model-streamer==0.15.7
|
||||
# via -r requirements/test/cuda.in
|
||||
runai-model-streamer-azure==0.15.7
|
||||
@@ -1054,7 +864,6 @@ sacrebleu==2.4.3
|
||||
safetensors==0.4.5
|
||||
# via
|
||||
# accelerate
|
||||
# diffusers
|
||||
# open-clip-torch
|
||||
# peft
|
||||
# segmentation-models-pytorch
|
||||
@@ -1063,9 +872,7 @@ safetensors==0.4.5
|
||||
schemathesis==3.39.15
|
||||
# via -r requirements/test/cuda.in
|
||||
scikit-image==0.25.2
|
||||
# via
|
||||
# albumentations
|
||||
# terratorch
|
||||
# via albumentations
|
||||
scikit-learn==1.5.2
|
||||
# via
|
||||
# albumentations
|
||||
@@ -1073,7 +880,6 @@ scikit-learn==1.5.2
|
||||
# lm-eval
|
||||
# mteb
|
||||
# sentence-transformers
|
||||
# terratorch
|
||||
scipy==1.13.1
|
||||
# via
|
||||
# albumentations
|
||||
@@ -1087,27 +893,16 @@ scipy==1.13.1
|
||||
# statsmodels
|
||||
# vocos
|
||||
segmentation-models-pytorch==0.5.0
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# terratorch
|
||||
# torchgeo
|
||||
# via -r requirements/test/cuda.in
|
||||
sentence-transformers==5.2.0
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# mteb
|
||||
sentry-sdk==2.52.0
|
||||
# via wandb
|
||||
setuptools==77.0.3
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
# lightning-utilities
|
||||
# pytablewriter
|
||||
# tensorboard
|
||||
# torch
|
||||
shapely==2.1.1
|
||||
# via
|
||||
# geopandas
|
||||
# torchgeo
|
||||
shellingham==1.5.4
|
||||
# via
|
||||
# perceptron
|
||||
@@ -1116,15 +911,12 @@ six==1.16.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
# junit-xml
|
||||
# lightly
|
||||
# opencensus
|
||||
# python-dateutil
|
||||
# rfc3339-validator
|
||||
# rouge-score
|
||||
smart-open==7.1.0
|
||||
# via ray
|
||||
smmap==5.0.2
|
||||
# via gitdb
|
||||
sniffio==1.3.1
|
||||
# via
|
||||
# anyio
|
||||
@@ -1166,8 +958,6 @@ tabledata==1.3.3
|
||||
# via pytablewriter
|
||||
tabulate==0.9.0
|
||||
# via sacrebleu
|
||||
tacoreader==0.5.6
|
||||
# via terratorch
|
||||
tblib==3.1.0
|
||||
# via -r requirements/test/cuda.in
|
||||
tcolorpy==0.1.6
|
||||
@@ -1177,26 +967,14 @@ tenacity==9.1.2
|
||||
# gpt-oss
|
||||
# lm-eval
|
||||
# plotly
|
||||
tensorboard==2.20.0
|
||||
# via terratorch
|
||||
tensorboard-data-server==0.7.2
|
||||
# via tensorboard
|
||||
tensorboardx==2.6.4
|
||||
# via lightning
|
||||
tensorizer==2.10.1
|
||||
# via -r requirements/test/cuda.in
|
||||
termcolor==3.1.0
|
||||
# via
|
||||
# gpt-oss
|
||||
# terratorch
|
||||
terratorch==1.2.2
|
||||
# via -r requirements/test/cuda.in
|
||||
# via gpt-oss
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tifffile==2025.3.30
|
||||
# via
|
||||
# scikit-image
|
||||
# terratorch
|
||||
# via scikit-image
|
||||
tiktoken==0.12.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -1208,8 +986,6 @@ timm==1.0.17
|
||||
# -r requirements/test/cuda.in
|
||||
# open-clip-torch
|
||||
# segmentation-models-pytorch
|
||||
# terratorch
|
||||
# torchgeo
|
||||
tokenizers==0.22.2
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -1227,21 +1003,14 @@ torch==2.11.0+cu130
|
||||
# bitsandbytes
|
||||
# encodec
|
||||
# instanttensor
|
||||
# kornia
|
||||
# lightly
|
||||
# lightning
|
||||
# mteb
|
||||
# open-clip-torch
|
||||
# peft
|
||||
# pytorch-lightning
|
||||
# runai-model-streamer
|
||||
# segmentation-models-pytorch
|
||||
# sentence-transformers
|
||||
# tensorizer
|
||||
# terratorch
|
||||
# timm
|
||||
# torchgeo
|
||||
# torchmetrics
|
||||
# torchvision
|
||||
# vector-quantize-pytorch
|
||||
# vocos
|
||||
@@ -1251,31 +1020,18 @@ torchaudio==2.11.0+cu130
|
||||
# -r requirements/test/cuda.in
|
||||
# encodec
|
||||
# vocos
|
||||
torchgeo==0.7.0
|
||||
# via terratorch
|
||||
torchmetrics==1.7.4
|
||||
# via
|
||||
# lightning
|
||||
# pytorch-lightning
|
||||
# terratorch
|
||||
# torchgeo
|
||||
torchvision==0.26.0+cu130
|
||||
# via
|
||||
# -c requirements/cuda.txt
|
||||
# -r requirements/test/cuda.in
|
||||
# lightly
|
||||
# open-clip-torch
|
||||
# segmentation-models-pytorch
|
||||
# terratorch
|
||||
# timm
|
||||
# torchgeo
|
||||
tqdm==4.67.3
|
||||
# via
|
||||
# datasets
|
||||
# evaluate
|
||||
# huggingface-hub
|
||||
# lightly
|
||||
# lightning
|
||||
# lm-eval
|
||||
# mteb
|
||||
# nltk
|
||||
@@ -1283,11 +1039,8 @@ tqdm==4.67.3
|
||||
# optuna
|
||||
# peft
|
||||
# pqdm
|
||||
# pytorch-lightning
|
||||
# segmentation-models-pytorch
|
||||
# sentence-transformers
|
||||
# tacoreader
|
||||
# terratorch
|
||||
# transformers
|
||||
transformers==5.5.3
|
||||
# via
|
||||
@@ -1316,8 +1069,6 @@ typer==0.15.2
|
||||
# transformers
|
||||
types-python-dateutil==2.9.0.20241206
|
||||
# via arrow
|
||||
typeshed-client==2.8.2
|
||||
# via jsonargparse
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -1332,8 +1083,6 @@ typing-extensions==4.15.0
|
||||
# grpcio
|
||||
# huggingface-hub
|
||||
# librosa
|
||||
# lightning
|
||||
# lightning-utilities
|
||||
# lm-eval
|
||||
# mistral-common
|
||||
# mteb
|
||||
@@ -1344,16 +1093,12 @@ typing-extensions==4.15.0
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# pydantic-extra-types
|
||||
# pytorch-lightning
|
||||
# sentence-transformers
|
||||
# sqlalchemy
|
||||
# starlette
|
||||
# torch
|
||||
# torchgeo
|
||||
# typer
|
||||
# typeshed-client
|
||||
# typing-inspection
|
||||
# wandb
|
||||
typing-inspection==0.4.2
|
||||
# via pydantic
|
||||
tzdata==2024.2
|
||||
@@ -1365,10 +1110,8 @@ urllib3==2.2.3
|
||||
# blobfile
|
||||
# botocore
|
||||
# docker
|
||||
# lightly
|
||||
# requests
|
||||
# responses
|
||||
# sentry-sdk
|
||||
# tritonclient
|
||||
uvicorn==0.35.0
|
||||
# via gpt-oss
|
||||
@@ -1378,22 +1121,16 @@ virtualenv==20.31.2
|
||||
# via ray
|
||||
vocos==0.1.0
|
||||
# via -r requirements/test/cuda.in
|
||||
wandb==0.24.2
|
||||
# via terratorch
|
||||
wcwidth==0.2.13
|
||||
# via ftfy
|
||||
webcolors==24.11.1
|
||||
# via jsonschema
|
||||
werkzeug==3.1.3
|
||||
# via
|
||||
# schemathesis
|
||||
# tensorboard
|
||||
# via schemathesis
|
||||
word2number==1.1
|
||||
# via lm-eval
|
||||
wrapt==1.17.2
|
||||
# via smart-open
|
||||
xarray==2025.7.1
|
||||
# via rioxarray
|
||||
xxhash==3.5.0
|
||||
# via
|
||||
# datasets
|
||||
|
||||
@@ -61,7 +61,11 @@ pydantic>=2.12 # 2.11 leads to error on python 3.13
|
||||
decord==0.6.0
|
||||
|
||||
# Prithvi tests
|
||||
terratorch>=1.2.2
|
||||
# terratorch is temporarily disabled while PyPI has the `lightning` package
|
||||
# in `quarantined` status (every published terratorch version transitively
|
||||
# requires `lightning`, so the resolver fails with "no versions of lightning").
|
||||
# Re-enable once PyPI lifts the quarantine. Tracked in #41376.
|
||||
# terratorch>=1.2.2
|
||||
imagehash # Required for Prithvi tests
|
||||
segmentation-models-pytorch>0.4.0 # Required for Prithvi tests
|
||||
|
||||
@@ -79,5 +83,7 @@ plotly # required for perf comparison html report
|
||||
|
||||
# ROCm-specific extras (not in CUDA cuda.in)
|
||||
rapidfuzz
|
||||
torchgeo==0.7.0
|
||||
# torchgeo also pulled in `lightning` transitively; disabled for the same
|
||||
# quarantine reason as terratorch above. Restore once the quarantine clears.
|
||||
# torchgeo==0.7.0
|
||||
multiprocess==0.70.16
|
||||
|
||||
+17
-266
@@ -1,15 +1,9 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements/test/rocm.in -c requirements/rocm.txt -o requirements/test/rocm.txt --index-strategy unsafe-best-match --python-platform x86_64-manylinux_2_28 --python-version 3.12 --no-emit-package torch --no-emit-package torchvision --no-emit-package torchaudio --no-emit-package triton --no-emit-package cuda-bindings --no-emit-package cuda-pathfinder --no-emit-package cuda-toolkit --no-emit-package cupy-cuda12x --no-emit-package nvidia-cublas --no-emit-package nvidia-cuda-cupti --no-emit-package nvidia-cuda-nvrtc --no-emit-package nvidia-cuda-runtime --no-emit-package nvidia-cudnn --no-emit-package nvidia-cufft --no-emit-package nvidia-cufile --no-emit-package nvidia-curand --no-emit-package nvidia-cusolver --no-emit-package nvidia-cusparse --no-emit-package nvidia-cusparselt --no-emit-package nvidia-nccl --no-emit-package nvidia-nvjitlink --no-emit-package nvidia-nvshmem --no-emit-package nvidia-nvtx --no-emit-package nvidia-cublas-cu12 --no-emit-package nvidia-cuda-cupti-cu12 --no-emit-package nvidia-cuda-nvrtc-cu12 --no-emit-package nvidia-cuda-runtime-cu12 --no-emit-package nvidia-cudnn-cu12 --no-emit-package nvidia-cufft-cu12 --no-emit-package nvidia-cufile-cu12 --no-emit-package nvidia-curand-cu12 --no-emit-package nvidia-cusolver-cu12 --no-emit-package nvidia-cusparse-cu12 --no-emit-package nvidia-cusparselt-cu12 --no-emit-package nvidia-nccl-cu12 --no-emit-package nvidia-nvjitlink-cu12 --no-emit-package nvidia-nvshmem-cu12 --no-emit-package nvidia-nvtx-cu12 --no-emit-package nvidia-cublas-cu13 --no-emit-package nvidia-cuda-cupti-cu13 --no-emit-package nvidia-cuda-nvrtc-cu13 --no-emit-package nvidia-cuda-runtime-cu13 --no-emit-package nvidia-cudnn-cu13 --no-emit-package nvidia-cufft-cu13 --no-emit-package nvidia-cufile-cu13 --no-emit-package nvidia-curand-cu13 --no-emit-package nvidia-cusolver-cu13 --no-emit-package nvidia-cusparse-cu13 --no-emit-package nvidia-cusparselt-cu13 --no-emit-package nvidia-nccl-cu13 --no-emit-package nvidia-nvjitlink-cu13 --no-emit-package nvidia-nvshmem-cu13 --no-emit-package nvidia-nvtx-cu13
|
||||
absl-py==2.4.0
|
||||
# via
|
||||
# rouge-score
|
||||
# tensorboard
|
||||
# via rouge-score
|
||||
accelerate==1.13.0
|
||||
# via peft
|
||||
aenum==3.1.17
|
||||
# via lightly
|
||||
affine==2.4.0
|
||||
# via rasterio
|
||||
aiohappyeyeballs==2.6.1
|
||||
# via aiohttp
|
||||
aiohttp==3.13.3
|
||||
@@ -25,12 +19,8 @@ aiohttp-cors==0.8.1
|
||||
# via ray
|
||||
aiosignal==1.4.0
|
||||
# via aiohttp
|
||||
albucore==0.1.2
|
||||
# via terratorch
|
||||
albumentations==1.4.6
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
# terratorch
|
||||
# via -r requirements/test/rocm.in
|
||||
alembic==1.18.4
|
||||
# via optuna
|
||||
annotated-doc==0.0.4
|
||||
@@ -43,10 +33,6 @@ anthropic==0.93.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/../common.txt
|
||||
antlr4-python3-runtime==4.9.3
|
||||
# via
|
||||
# hydra-core
|
||||
# omegaconf
|
||||
anyio==4.13.0
|
||||
# via
|
||||
# anthropic
|
||||
@@ -56,6 +42,8 @@ anyio==4.13.0
|
||||
# sse-starlette
|
||||
# starlette
|
||||
# watchfiles
|
||||
apache-tvm-ffi==0.1.10
|
||||
# via xgrammar
|
||||
arctic-inference==0.1.1
|
||||
# via -r requirements/test/rocm.in
|
||||
argcomplete==3.6.3
|
||||
@@ -67,11 +55,9 @@ astor==0.8.1
|
||||
attrs==26.1.0
|
||||
# via
|
||||
# aiohttp
|
||||
# fiona
|
||||
# jsonlines
|
||||
# jsonschema
|
||||
# pytest-subtests
|
||||
# rasterio
|
||||
# referencing
|
||||
audioread==3.0.1
|
||||
# via librosa
|
||||
@@ -90,9 +76,7 @@ backoff==2.2.1
|
||||
# -r requirements/test/rocm.in
|
||||
# schemathesis
|
||||
bitsandbytes==0.49.2
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
# lightning
|
||||
# via -r requirements/test/rocm.in
|
||||
black==26.3.1
|
||||
# via datamodel-code-generator
|
||||
blake3==1.0.8
|
||||
@@ -119,13 +103,8 @@ cbor2==5.9.0
|
||||
# via -r requirements/test/../common.txt
|
||||
certifi==2026.2.25
|
||||
# via
|
||||
# fiona
|
||||
# httpcore
|
||||
# httpx
|
||||
# lightly
|
||||
# pyogrio
|
||||
# pyproj
|
||||
# rasterio
|
||||
# requests
|
||||
# sentry-sdk
|
||||
cffi==1.17.1
|
||||
@@ -143,24 +122,13 @@ chz==0.4.0
|
||||
click==8.3.1
|
||||
# via
|
||||
# black
|
||||
# click-plugins
|
||||
# cligj
|
||||
# fiona
|
||||
# jiwer
|
||||
# nltk
|
||||
# rasterio
|
||||
# ray
|
||||
# rich-toolkit
|
||||
# schemathesis
|
||||
# typer
|
||||
# uvicorn
|
||||
# wandb
|
||||
click-plugins==1.1.1.2
|
||||
# via fiona
|
||||
cligj==0.7.2
|
||||
# via
|
||||
# fiona
|
||||
# rasterio
|
||||
cloudpickle==3.1.2
|
||||
# via -r requirements/test/../common.txt
|
||||
colorama==0.4.6
|
||||
@@ -211,8 +179,6 @@ depyf==0.20.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/../common.txt
|
||||
diffusers==0.37.0
|
||||
# via terratorch
|
||||
dill==0.3.8
|
||||
# via
|
||||
# datasets
|
||||
@@ -237,16 +203,12 @@ docker==7.1.0
|
||||
docopt==0.6.2
|
||||
# via num2words
|
||||
docstring-parser==0.17.0
|
||||
# via
|
||||
# anthropic
|
||||
# jsonargparse
|
||||
# via anthropic
|
||||
einops==0.8.2
|
||||
# via
|
||||
# -r requirements/test/../common.txt
|
||||
# -r requirements/test/rocm.in
|
||||
# encodec
|
||||
# terratorch
|
||||
# torchgeo
|
||||
# vector-quantize-pytorch
|
||||
# vocos
|
||||
einx==0.4.2
|
||||
@@ -283,14 +245,11 @@ filelock==3.25.2
|
||||
# -r requirements/test/../common.txt
|
||||
# blobfile
|
||||
# datasets
|
||||
# diffusers
|
||||
# huggingface-hub
|
||||
# python-discovery
|
||||
# ray
|
||||
# torch
|
||||
# virtualenv
|
||||
fiona==1.10.1
|
||||
# via torchgeo
|
||||
fonttools==4.62.1
|
||||
# via matplotlib
|
||||
fqdn==1.5.1
|
||||
@@ -307,9 +266,6 @@ fsspec==2025.3.0
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# huggingface-hub
|
||||
# lightning
|
||||
# pytorch-lightning
|
||||
# tacoreader
|
||||
# torch
|
||||
ftfy==6.3.1
|
||||
# via open-clip-torch
|
||||
@@ -317,16 +273,10 @@ genai-perf==0.0.16
|
||||
# via -r requirements/test/rocm.in
|
||||
genson==1.3.0
|
||||
# via datamodel-code-generator
|
||||
geopandas==1.1.3
|
||||
# via terratorch
|
||||
gguf==0.18.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/../common.txt
|
||||
gitdb==4.0.12
|
||||
# via gitpython
|
||||
gitpython==3.1.46
|
||||
# via wandb
|
||||
google-api-core==2.30.0
|
||||
# via
|
||||
# google-cloud-core
|
||||
@@ -366,7 +316,6 @@ grpcio==1.78.0
|
||||
# grpcio-reflection
|
||||
# opentelemetry-exporter-otlp-proto-grpc
|
||||
# ray
|
||||
# tensorboard
|
||||
grpcio-reflection==1.78.0
|
||||
# via
|
||||
# -c requirements/rocm.txt
|
||||
@@ -377,8 +326,6 @@ h11==0.16.0
|
||||
# uvicorn
|
||||
h2==4.3.0
|
||||
# via httpx
|
||||
h5py==3.16.0
|
||||
# via terratorch
|
||||
harfile==0.4.0
|
||||
# via schemathesis
|
||||
hf-xet==1.4.3
|
||||
@@ -397,7 +344,6 @@ httpx==0.27.2
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
# anthropic
|
||||
# diffusers
|
||||
# fastapi
|
||||
# fastapi-cloud-cli
|
||||
# huggingface-hub
|
||||
@@ -412,23 +358,17 @@ huggingface-hub==1.10.2
|
||||
# via
|
||||
# accelerate
|
||||
# datasets
|
||||
# diffusers
|
||||
# evaluate
|
||||
# open-clip-torch
|
||||
# peft
|
||||
# segmentation-models-pytorch
|
||||
# sentence-transformers
|
||||
# terratorch
|
||||
# timm
|
||||
# tokenizers
|
||||
# transformers
|
||||
# vocos
|
||||
humanize==4.15.0
|
||||
# via runai-model-streamer
|
||||
hydra-core==1.3.2
|
||||
# via
|
||||
# lightly
|
||||
# lightning
|
||||
hyperframe==6.1.0
|
||||
# via h2
|
||||
hypothesis==6.151.9
|
||||
@@ -455,11 +395,7 @@ imagehash==4.3.2
|
||||
imageio==2.37.3
|
||||
# via scikit-image
|
||||
importlib-metadata==8.7.1
|
||||
# via
|
||||
# diffusers
|
||||
# opentelemetry-api
|
||||
importlib-resources==6.5.2
|
||||
# via typeshed-client
|
||||
# via opentelemetry-api
|
||||
inflect==7.5.0
|
||||
# via datamodel-code-generator
|
||||
iniconfig==2.3.0
|
||||
@@ -497,14 +433,8 @@ joblib==1.5.3
|
||||
# librosa
|
||||
# nltk
|
||||
# scikit-learn
|
||||
jsonargparse==4.47.0
|
||||
# via
|
||||
# lightning
|
||||
# terratorch
|
||||
jsonlines==4.0.0
|
||||
# via lm-eval
|
||||
jsonnet==0.21.0
|
||||
# via jsonargparse
|
||||
jsonpointer==3.1.0
|
||||
# via jsonschema
|
||||
jsonschema==4.26.0
|
||||
@@ -524,10 +454,6 @@ kaleido==1.0.0
|
||||
# via genai-perf
|
||||
kiwisolver==1.5.0
|
||||
# via matplotlib
|
||||
kornia==0.8.2
|
||||
# via torchgeo
|
||||
kornia-rs==0.1.10
|
||||
# via kornia
|
||||
lark==1.2.2
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -540,21 +466,6 @@ libnacl==2.1.0
|
||||
# via tensorizer
|
||||
librosa==0.10.2.post1
|
||||
# via -r requirements/test/rocm.in
|
||||
lightly==1.5.22
|
||||
# via
|
||||
# terratorch
|
||||
# torchgeo
|
||||
lightly-utils==0.0.2
|
||||
# via lightly
|
||||
lightning==2.6.1
|
||||
# via
|
||||
# terratorch
|
||||
# torchgeo
|
||||
lightning-utilities==0.15.3
|
||||
# via
|
||||
# lightning
|
||||
# pytorch-lightning
|
||||
# torchmetrics
|
||||
llguidance==1.3.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -580,8 +491,6 @@ lxml==6.0.2
|
||||
# sacrebleu
|
||||
mako==1.3.10
|
||||
# via alembic
|
||||
markdown==3.10.2
|
||||
# via tensorboard
|
||||
markdown-it-py==4.0.0
|
||||
# via rich
|
||||
markupsafe==3.0.3
|
||||
@@ -590,10 +499,7 @@ markupsafe==3.0.3
|
||||
# mako
|
||||
# werkzeug
|
||||
matplotlib==3.10.8
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
# lightning
|
||||
# torchgeo
|
||||
# via -r requirements/test/rocm.in
|
||||
mbstrdecoder==1.1.4
|
||||
# via
|
||||
# dataproperty
|
||||
@@ -660,14 +566,11 @@ numba==0.65.0
|
||||
# -c requirements/rocm.txt
|
||||
# -r requirements/test/rocm.in
|
||||
# librosa
|
||||
numkong==7.1.1
|
||||
# via albucore
|
||||
numpy==2.2.6
|
||||
# via
|
||||
# -r requirements/test/../common.txt
|
||||
# -r requirements/test/rocm.in
|
||||
# accelerate
|
||||
# albucore
|
||||
# albumentations
|
||||
# bitsandbytes
|
||||
# bm25s
|
||||
@@ -675,20 +578,15 @@ numpy==2.2.6
|
||||
# cupy-cuda12x
|
||||
# datasets
|
||||
# decord
|
||||
# diffusers
|
||||
# einx
|
||||
# encodec
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# genai-perf
|
||||
# geopandas
|
||||
# gguf
|
||||
# h5py
|
||||
# imagehash
|
||||
# imageio
|
||||
# librosa
|
||||
# lightly
|
||||
# lightly-utils
|
||||
# lm-eval
|
||||
# matplotlib
|
||||
# mistral-common
|
||||
@@ -700,12 +598,8 @@ numpy==2.2.6
|
||||
# patsy
|
||||
# peft
|
||||
# perceptron
|
||||
# pycocotools
|
||||
# pyogrio
|
||||
# pytrec-eval-terrier
|
||||
# pywavelets
|
||||
# rasterio
|
||||
# rioxarray
|
||||
# rouge-score
|
||||
# runai-model-streamer
|
||||
# sacrebleu
|
||||
@@ -714,27 +608,16 @@ numpy==2.2.6
|
||||
# scipy
|
||||
# segmentation-models-pytorch
|
||||
# sentence-transformers
|
||||
# shapely
|
||||
# soundfile
|
||||
# soxr
|
||||
# statsmodels
|
||||
# tensorboard
|
||||
# tensorboardx
|
||||
# tensorizer
|
||||
# terratorch
|
||||
# tifffile
|
||||
# torchgeo
|
||||
# torchmetrics
|
||||
# torchvision
|
||||
# transformers
|
||||
# tritonclient
|
||||
# vocos
|
||||
# xarray
|
||||
# xgrammar
|
||||
omegaconf==2.3.0
|
||||
# via
|
||||
# hydra-core
|
||||
# lightning
|
||||
open-clip-torch==2.32.0
|
||||
# via -r requirements/test/rocm.in
|
||||
openai==2.31.0
|
||||
@@ -824,46 +707,29 @@ packaging==26.0
|
||||
# datasets
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# geopandas
|
||||
# huggingface-hub
|
||||
# hydra-core
|
||||
# kaleido
|
||||
# kornia
|
||||
# lazy-loader
|
||||
# lightning
|
||||
# lightning-utilities
|
||||
# lm-format-enforcer
|
||||
# matplotlib
|
||||
# optuna
|
||||
# peft
|
||||
# plotly
|
||||
# pooch
|
||||
# pyogrio
|
||||
# pytest
|
||||
# pytest-rerunfailures
|
||||
# pytorch-lightning
|
||||
# ray
|
||||
# rioxarray
|
||||
# scikit-image
|
||||
# statsmodels
|
||||
# tensorboard
|
||||
# tensorboardx
|
||||
# torchmetrics
|
||||
# transformers
|
||||
# typepy
|
||||
# wandb
|
||||
# xarray
|
||||
pandas==3.0.1
|
||||
# via
|
||||
# datasets
|
||||
# evaluate
|
||||
# fastparquet
|
||||
# genai-perf
|
||||
# geopandas
|
||||
# statsmodels
|
||||
# tacoreader
|
||||
# torchgeo
|
||||
# xarray
|
||||
partial-json-parser==0.2.1.1.post7
|
||||
# via -r requirements/test/../common.txt
|
||||
pathspec==1.0.4
|
||||
@@ -881,18 +747,14 @@ perf-analyzer==0.1.0
|
||||
pillow==12.1.1
|
||||
# via
|
||||
# -r requirements/test/../common.txt
|
||||
# diffusers
|
||||
# genai-perf
|
||||
# imagehash
|
||||
# imageio
|
||||
# lightly-utils
|
||||
# matplotlib
|
||||
# mistral-common
|
||||
# perceptron
|
||||
# scikit-image
|
||||
# segmentation-models-pytorch
|
||||
# tensorboard
|
||||
# torchgeo
|
||||
# torchvision
|
||||
platformdirs==4.3.6
|
||||
# via
|
||||
@@ -900,7 +762,6 @@ platformdirs==4.3.6
|
||||
# pooch
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
# wandb
|
||||
plotly==6.6.0
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
@@ -946,10 +807,7 @@ protobuf==6.33.6
|
||||
# opentelemetry-proto
|
||||
# proto-plus
|
||||
# ray
|
||||
# tensorboard
|
||||
# tensorboardx
|
||||
# tensorizer
|
||||
# wandb
|
||||
psutil==7.2.2
|
||||
# via
|
||||
# -r requirements/test/../common.txt
|
||||
@@ -966,16 +824,12 @@ pyarrow==23.0.1
|
||||
# via
|
||||
# datasets
|
||||
# genai-perf
|
||||
# tacoreader
|
||||
# terratorch
|
||||
pyasn1==0.6.3
|
||||
# via pyasn1-modules
|
||||
pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pybase64==1.4.3
|
||||
# via -r requirements/test/../common.txt
|
||||
pycocotools==2.0.11
|
||||
# via terratorch
|
||||
pycountry==26.2.16
|
||||
# via pydantic-extra-types
|
||||
pycparser==3.0
|
||||
@@ -994,7 +848,6 @@ pydantic==2.12.5
|
||||
# fastapi
|
||||
# fastapi-cloud-cli
|
||||
# gpt-oss
|
||||
# lightly
|
||||
# lm-format-enforcer
|
||||
# mcp
|
||||
# mistral-common
|
||||
@@ -1005,7 +858,6 @@ pydantic==2.12.5
|
||||
# pydantic-extra-types
|
||||
# pydantic-settings
|
||||
# ray
|
||||
# wandb
|
||||
# xgrammar
|
||||
pydantic-core==2.41.5
|
||||
# via pydantic
|
||||
@@ -1023,17 +875,8 @@ pyjwt==2.12.1
|
||||
# via
|
||||
# mcp
|
||||
# msal
|
||||
pyogrio==0.12.1
|
||||
# via geopandas
|
||||
pyparsing==3.3.2
|
||||
# via
|
||||
# matplotlib
|
||||
# rasterio
|
||||
pyproj==3.7.2
|
||||
# via
|
||||
# geopandas
|
||||
# rioxarray
|
||||
# torchgeo
|
||||
# via matplotlib
|
||||
pyrate-limiter==3.9.0
|
||||
# via schemathesis
|
||||
pystemmer==3.0.0
|
||||
@@ -1070,13 +913,10 @@ pytest-subtests==0.14.2
|
||||
# via schemathesis
|
||||
pytest-timeout==2.3.1
|
||||
# via -r requirements/test/rocm.in
|
||||
python-box==7.4.1
|
||||
# via terratorch
|
||||
python-dateutil==2.9.0.post0
|
||||
# via
|
||||
# arrow
|
||||
# botocore
|
||||
# lightly
|
||||
# matplotlib
|
||||
# pandas
|
||||
# typepy
|
||||
@@ -1096,10 +936,6 @@ python-rapidjson==1.23
|
||||
# via tritonclient
|
||||
pytokens==0.4.1
|
||||
# via black
|
||||
pytorch-lightning==2.6.1
|
||||
# via
|
||||
# lightly
|
||||
# lightning
|
||||
pytrec-eval-terrier==0.5.10
|
||||
# via mteb
|
||||
pytz==2026.1.post1
|
||||
@@ -1116,13 +952,9 @@ pyyaml==6.0.3
|
||||
# genai-perf
|
||||
# gguf
|
||||
# huggingface-hub
|
||||
# jsonargparse
|
||||
# lightning
|
||||
# lm-format-enforcer
|
||||
# omegaconf
|
||||
# optuna
|
||||
# peft
|
||||
# pytorch-lightning
|
||||
# ray
|
||||
# responses
|
||||
# schemathesis
|
||||
@@ -1130,7 +962,6 @@ pyyaml==6.0.3
|
||||
# transformers
|
||||
# uvicorn
|
||||
# vocos
|
||||
# wandb
|
||||
pyzmq==27.1.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -1139,11 +970,6 @@ rapidfuzz==3.12.1
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
# jiwer
|
||||
rasterio==1.5.0
|
||||
# via
|
||||
# rioxarray
|
||||
# terratorch
|
||||
# torchgeo
|
||||
ray==2.54.0
|
||||
# via -r requirements/test/rocm.in
|
||||
redis==7.3.0
|
||||
@@ -1155,7 +981,6 @@ referencing==0.37.0
|
||||
regex==2026.2.28
|
||||
# via
|
||||
# -r requirements/test/../common.txt
|
||||
# diffusers
|
||||
# nltk
|
||||
# open-clip-torch
|
||||
# sacrebleu
|
||||
@@ -1168,14 +993,12 @@ requests==2.32.5
|
||||
# azure-core
|
||||
# buildkite-test-collector
|
||||
# datasets
|
||||
# diffusers
|
||||
# docker
|
||||
# evaluate
|
||||
# gguf
|
||||
# google-api-core
|
||||
# google-cloud-storage
|
||||
# gpt-oss
|
||||
# lightly
|
||||
# lm-eval
|
||||
# mistral-common
|
||||
# msal
|
||||
@@ -1186,9 +1009,7 @@ requests==2.32.5
|
||||
# responses
|
||||
# schemathesis
|
||||
# starlette-testclient
|
||||
# tacoreader
|
||||
# tiktoken
|
||||
# wandb
|
||||
responses==0.26.0
|
||||
# via genai-perf
|
||||
rfc3339-validator==0.1.4
|
||||
@@ -1198,11 +1019,9 @@ rfc3987==1.3.8
|
||||
rich==14.3.3
|
||||
# via
|
||||
# genai-perf
|
||||
# lightning
|
||||
# mteb
|
||||
# perceptron
|
||||
# rich-toolkit
|
||||
# terratorch
|
||||
# typer
|
||||
rich-toolkit==0.19.7
|
||||
# via
|
||||
@@ -1210,16 +1029,12 @@ rich-toolkit==0.19.7
|
||||
# fastapi-cloud-cli
|
||||
rignore==0.7.6
|
||||
# via fastapi-cloud-cli
|
||||
rioxarray==0.22.0
|
||||
# via terratorch
|
||||
rouge-score==0.1.2
|
||||
# via lm-eval
|
||||
rpds-py==0.30.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
rtree==1.4.1
|
||||
# via torchgeo
|
||||
runai-model-streamer==0.15.7
|
||||
# via
|
||||
# -c requirements/rocm.txt
|
||||
@@ -1237,7 +1052,6 @@ sacrebleu==2.6.0
|
||||
safetensors==0.7.0
|
||||
# via
|
||||
# accelerate
|
||||
# diffusers
|
||||
# open-clip-torch
|
||||
# peft
|
||||
# segmentation-models-pytorch
|
||||
@@ -1246,9 +1060,7 @@ safetensors==0.7.0
|
||||
schemathesis==3.39.15
|
||||
# via -r requirements/test/rocm.in
|
||||
scikit-image==0.26.0
|
||||
# via
|
||||
# albumentations
|
||||
# terratorch
|
||||
# via albumentations
|
||||
scikit-learn==1.8.0
|
||||
# via
|
||||
# albumentations
|
||||
@@ -1256,7 +1068,6 @@ scikit-learn==1.8.0
|
||||
# lm-eval
|
||||
# mteb
|
||||
# sentence-transformers
|
||||
# terratorch
|
||||
scipy==1.17.1
|
||||
# via
|
||||
# albumentations
|
||||
@@ -1271,10 +1082,7 @@ scipy==1.17.1
|
||||
# statsmodels
|
||||
# vocos
|
||||
segmentation-models-pytorch==0.5.0
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
# terratorch
|
||||
# torchgeo
|
||||
# via -r requirements/test/rocm.in
|
||||
sentence-transformers==5.3.0
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
@@ -1282,9 +1090,7 @@ sentence-transformers==5.3.0
|
||||
sentencepiece==0.2.1
|
||||
# via -r requirements/test/../common.txt
|
||||
sentry-sdk==2.55.0
|
||||
# via
|
||||
# fastapi-cloud-cli
|
||||
# wandb
|
||||
# via fastapi-cloud-cli
|
||||
setproctitle==1.3.7
|
||||
# via -r requirements/test/../common.txt
|
||||
setuptools==79.0.1
|
||||
@@ -1294,12 +1100,7 @@ setuptools==79.0.1
|
||||
# -r requirements/test/../common.txt
|
||||
# model-hosting-container-standards
|
||||
# pytablewriter
|
||||
# tensorboard
|
||||
# torch
|
||||
shapely==2.1.2
|
||||
# via
|
||||
# geopandas
|
||||
# torchgeo
|
||||
shellingham==1.5.4
|
||||
# via
|
||||
# perceptron
|
||||
@@ -1311,15 +1112,12 @@ six==1.17.0
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/../common.txt
|
||||
# junit-xml
|
||||
# lightly
|
||||
# opencensus
|
||||
# python-dateutil
|
||||
# rfc3339-validator
|
||||
# rouge-score
|
||||
smart-open==7.5.1
|
||||
# via ray
|
||||
smmap==5.0.3
|
||||
# via gitdb
|
||||
sniffio==1.3.1
|
||||
# via
|
||||
# anthropic
|
||||
@@ -1358,8 +1156,6 @@ starlette-testclient==0.4.1
|
||||
# via schemathesis
|
||||
statsmodels==0.14.6
|
||||
# via genai-perf
|
||||
stringzilla==4.6.0
|
||||
# via albucore
|
||||
structlog==25.5.0
|
||||
# via gpt-oss
|
||||
supervisor==4.3.0
|
||||
@@ -1372,8 +1168,6 @@ tabledata==1.3.4
|
||||
# via pytablewriter
|
||||
tabulate==0.10.0
|
||||
# via sacrebleu
|
||||
tacoreader==0.5.6
|
||||
# via terratorch
|
||||
tblib==3.1.0
|
||||
# via -r requirements/test/rocm.in
|
||||
tcolorpy==0.1.7
|
||||
@@ -1382,28 +1176,16 @@ tenacity==9.1.4
|
||||
# via
|
||||
# gpt-oss
|
||||
# lm-eval
|
||||
tensorboard==2.20.0
|
||||
# via terratorch
|
||||
tensorboard-data-server==0.7.2
|
||||
# via tensorboard
|
||||
tensorboardx==2.6.4
|
||||
# via lightning
|
||||
tensorizer==2.10.1
|
||||
# via
|
||||
# -c requirements/rocm.txt
|
||||
# -r requirements/test/rocm.in
|
||||
termcolor==3.3.0
|
||||
# via
|
||||
# gpt-oss
|
||||
# terratorch
|
||||
terratorch==1.2.2
|
||||
# via -r requirements/test/rocm.in
|
||||
# via gpt-oss
|
||||
threadpoolctl==3.6.0
|
||||
# via scikit-learn
|
||||
tifffile==2026.3.3
|
||||
# via
|
||||
# scikit-image
|
||||
# terratorch
|
||||
# via scikit-image
|
||||
tiktoken==0.12.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -1417,8 +1199,6 @@ timm==1.0.17
|
||||
# -r requirements/test/rocm.in
|
||||
# open-clip-torch
|
||||
# segmentation-models-pytorch
|
||||
# terratorch
|
||||
# torchgeo
|
||||
tokenizers==0.22.2
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -1429,16 +1209,6 @@ tomli==2.4.0
|
||||
# via schemathesis
|
||||
tomli-w==1.2.0
|
||||
# via schemathesis
|
||||
torchgeo==0.7.0
|
||||
# via
|
||||
# -r requirements/test/rocm.in
|
||||
# terratorch
|
||||
torchmetrics==1.9.0
|
||||
# via
|
||||
# lightning
|
||||
# pytorch-lightning
|
||||
# terratorch
|
||||
# torchgeo
|
||||
tqdm==4.67.3
|
||||
# via
|
||||
# -r requirements/test/../common.txt
|
||||
@@ -1446,8 +1216,6 @@ tqdm==4.67.3
|
||||
# evaluate
|
||||
# gguf
|
||||
# huggingface-hub
|
||||
# lightly
|
||||
# lightning
|
||||
# lm-eval
|
||||
# mteb
|
||||
# nltk
|
||||
@@ -1456,11 +1224,8 @@ tqdm==4.67.3
|
||||
# optuna
|
||||
# peft
|
||||
# pqdm
|
||||
# pytorch-lightning
|
||||
# segmentation-models-pytorch
|
||||
# sentence-transformers
|
||||
# tacoreader
|
||||
# terratorch
|
||||
# transformers
|
||||
transformers==5.5.3
|
||||
# via
|
||||
@@ -1492,8 +1257,6 @@ typer==0.24.1
|
||||
# huggingface-hub
|
||||
# perceptron
|
||||
# transformers
|
||||
typeshed-client==2.9.0
|
||||
# via jsonargparse
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -1503,6 +1266,7 @@ typing-extensions==4.15.0
|
||||
# alembic
|
||||
# anthropic
|
||||
# anyio
|
||||
# apache-tvm-ffi
|
||||
# azure-core
|
||||
# azure-identity
|
||||
# azure-storage-blob
|
||||
@@ -1511,8 +1275,6 @@ typing-extensions==4.15.0
|
||||
# grpcio
|
||||
# huggingface-hub
|
||||
# librosa
|
||||
# lightning
|
||||
# lightning-utilities
|
||||
# lm-eval
|
||||
# mcp
|
||||
# mistral-common
|
||||
@@ -1527,18 +1289,14 @@ typing-extensions==4.15.0
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# pydantic-extra-types
|
||||
# pytorch-lightning
|
||||
# referencing
|
||||
# rich-toolkit
|
||||
# sentence-transformers
|
||||
# sqlalchemy
|
||||
# starlette
|
||||
# torch
|
||||
# torchgeo
|
||||
# typeguard
|
||||
# typeshed-client
|
||||
# typing-inspection
|
||||
# wandb
|
||||
# xgrammar
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
@@ -1555,7 +1313,6 @@ urllib3==2.6.3
|
||||
# blobfile
|
||||
# botocore
|
||||
# docker
|
||||
# lightly
|
||||
# requests
|
||||
# responses
|
||||
# sentry-sdk
|
||||
@@ -1575,8 +1332,6 @@ virtualenv==21.2.0
|
||||
# via ray
|
||||
vocos==0.1.0
|
||||
# via -r requirements/test/rocm.in
|
||||
wandb==0.25.1
|
||||
# via terratorch
|
||||
watchfiles==1.1.1
|
||||
# via
|
||||
# -r requirements/test/../common.txt
|
||||
@@ -1588,16 +1343,12 @@ webcolors==25.10.0
|
||||
websockets==16.0
|
||||
# via uvicorn
|
||||
werkzeug==3.1.6
|
||||
# via
|
||||
# schemathesis
|
||||
# tensorboard
|
||||
# via schemathesis
|
||||
word2number==1.1
|
||||
# via lm-eval
|
||||
wrapt==2.1.2
|
||||
# via smart-open
|
||||
xarray==2026.2.0
|
||||
# via rioxarray
|
||||
xgrammar==0.1.33
|
||||
xgrammar==0.2.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/../common.txt
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from transformers import AutoTokenizer, PreTrainedTokenizerBase
|
||||
|
||||
from vllm.benchmarks.datasets import get_samples
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def hf_tokenizer() -> PreTrainedTokenizerBase:
|
||||
return AutoTokenizer.from_pretrained("gpt2")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, n_rows: int) -> None:
|
||||
with path.open("w") as f:
|
||||
for i in range(n_rows):
|
||||
f.write(json.dumps({"prompt": f"row {i}: unique prompt content."}) + "\n")
|
||||
|
||||
|
||||
def _args_for_custom(dataset_path: str, seed: int) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
dataset_name="custom",
|
||||
dataset_path=dataset_path,
|
||||
disable_shuffle=False,
|
||||
num_prompts=30,
|
||||
custom_output_len=32,
|
||||
skip_chat_template=True,
|
||||
no_oversample=False,
|
||||
seed=seed,
|
||||
request_id_prefix="",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_custom_dataset_seed_propagates(
|
||||
hf_tokenizer: PreTrainedTokenizerBase, tmp_path: Path
|
||||
) -> None:
|
||||
"""--seed must control the CustomDataset shuffle used by get_samples.
|
||||
|
||||
Without the fix, CustomDataset was instantiated without random_seed,
|
||||
so its load-time shuffle always used DEFAULT_SEED=0 regardless of
|
||||
args.seed, causing every run with --dataset-name custom to pick the
|
||||
same subset of rows from a larger file.
|
||||
"""
|
||||
jsonl = tmp_path / "data.jsonl"
|
||||
_write_jsonl(jsonl, n_rows=60)
|
||||
|
||||
samples_a = get_samples(_args_for_custom(str(jsonl), seed=0), hf_tokenizer)
|
||||
samples_b = get_samples(_args_for_custom(str(jsonl), seed=42), hf_tokenizer)
|
||||
|
||||
prompts_a = {s.prompt for s in samples_a}
|
||||
prompts_b = {s.prompt for s in samples_b}
|
||||
|
||||
assert len(prompts_a) == 30
|
||||
assert len(prompts_b) == 30
|
||||
assert prompts_a != prompts_b
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_custom_dataset_same_seed_is_deterministic(
|
||||
hf_tokenizer: PreTrainedTokenizerBase, tmp_path: Path
|
||||
) -> None:
|
||||
"""Same --seed must yield the same CustomDataset subset."""
|
||||
jsonl = tmp_path / "data.jsonl"
|
||||
_write_jsonl(jsonl, n_rows=60)
|
||||
|
||||
samples_a = get_samples(_args_for_custom(str(jsonl), seed=7), hf_tokenizer)
|
||||
samples_b = get_samples(_args_for_custom(str(jsonl), seed=7), hf_tokenizer)
|
||||
|
||||
prompts_a = [s.prompt for s in samples_a]
|
||||
prompts_b = [s.prompt for s in samples_b]
|
||||
|
||||
assert prompts_a == prompts_b
|
||||
@@ -12,10 +12,17 @@ from torch._ops import OpOverload, OpOverloadPacket
|
||||
from torch.fx._utils import lazy_format_graph_code
|
||||
|
||||
from vllm.compilation.passes.fx_utils import find_op_nodes
|
||||
from vllm.compilation.passes.inductor_pass import InductorPass
|
||||
from vllm.compilation.passes.inductor_pass import (
|
||||
InductorPass,
|
||||
pass_context,
|
||||
)
|
||||
from vllm.compilation.passes.ir.inplace_functionalization import (
|
||||
VllmIRInplaceFunctionalizationPass,
|
||||
)
|
||||
from vllm.compilation.passes.pass_manager import with_pattern_match_debug
|
||||
from vllm.compilation.passes.vllm_inductor_pass import VllmInductorPass
|
||||
from vllm.config import VllmConfig, get_current_vllm_config
|
||||
from vllm.config.utils import Range
|
||||
from vllm.logger import init_logger
|
||||
|
||||
logger = init_logger("vllm.tests.compile.backend")
|
||||
@@ -53,11 +60,17 @@ class TestBackend:
|
||||
self.custom_passes = list(passes)
|
||||
vllm_config = get_current_vllm_config()
|
||||
compile_config = vllm_config.compilation_config
|
||||
self.range = Range(1, vllm_config.scheduler_config.max_num_batched_tokens)
|
||||
# Deepcopy to allow multiple TestBackend instances to use the same VllmConfig
|
||||
self.inductor_config = deepcopy(compile_config.inductor_compile_config)
|
||||
self.inductor_config["force_disable_caches"] = True
|
||||
self.inductor_config["post_grad_custom_post_pass"] = self.post_pass
|
||||
|
||||
# Add VllmIRInplaceFunctionalizationPass as pre-grad pass by default
|
||||
self.inductor_config["pre_grad_custom_pass"] = (
|
||||
VllmIRInplaceFunctionalizationPass(vllm_config)
|
||||
)
|
||||
|
||||
if debug_dump_path := vllm_config.compile_debug_dump_path():
|
||||
logger.debug("Dumping depyf output to %s", debug_dump_path)
|
||||
self.debug_ctx = depyf.prepare_debug(debug_dump_path.as_posix())
|
||||
@@ -68,7 +81,7 @@ class TestBackend:
|
||||
self.graph_pre_compile = deepcopy(graph)
|
||||
from torch._inductor.compile_fx import compile_fx
|
||||
|
||||
with self.debug_ctx:
|
||||
with self.debug_ctx, pass_context(self.range):
|
||||
return compile_fx(
|
||||
graph, example_inputs, config_patches=self.inductor_config
|
||||
)
|
||||
|
||||
@@ -24,10 +24,24 @@ def mock_cuda_platform():
|
||||
def _mock_platform(is_cuda: bool = True, capability: tuple[int, int] | None = None):
|
||||
mock_platform = MagicMock()
|
||||
mock_platform.is_cuda.return_value = is_cuda
|
||||
if capability is not None:
|
||||
mock_platform.get_device_capability.return_value = DeviceCapability(
|
||||
*capability
|
||||
device_capability = (
|
||||
DeviceCapability(*capability) if capability is not None else None
|
||||
)
|
||||
mock_platform.get_device_capability.return_value = device_capability
|
||||
|
||||
def is_device_capability_family(
|
||||
requested_capability: int, device_id: int = 0
|
||||
) -> bool:
|
||||
current_capability = mock_platform.get_device_capability(
|
||||
device_id=device_id
|
||||
)
|
||||
if current_capability is None:
|
||||
return False
|
||||
return current_capability.major == (requested_capability // 10)
|
||||
|
||||
mock_platform.is_device_capability_family.side_effect = (
|
||||
is_device_capability_family
|
||||
)
|
||||
with patch("vllm.platforms.current_platform", mock_platform):
|
||||
yield mock_platform
|
||||
|
||||
|
||||
@@ -97,6 +97,12 @@ def run_e2e_fusion_test(monkeypatch, caplog_mp_spawn):
|
||||
f"attention backend '{attn_backend.backend.name}'"
|
||||
)
|
||||
|
||||
if attn_backend.backend.name == "FLASHINFER":
|
||||
from vllm.utils.flashinfer import supports_trtllm_attention
|
||||
|
||||
if not supports_trtllm_attention():
|
||||
matches = matches._replace(attn_quant_fusion=0)
|
||||
|
||||
# TODO: remove this after finishing migration from envs to model kwargs
|
||||
if model_name == "openai/gpt-oss-20b":
|
||||
from .common import is_blackwell
|
||||
|
||||
@@ -19,6 +19,8 @@ from .models import (
|
||||
FLASHINFER_ATTN,
|
||||
FLASHINFER_MLA_ATTN,
|
||||
FLASHMLA_SPARSE_ATTN,
|
||||
ROCM_AITER_UNIFIED_ATTN,
|
||||
ROCM_ATTN,
|
||||
TRITON_ATTN,
|
||||
deepseek_coder_v2_lite_fp8,
|
||||
deepseek_r1_fp4,
|
||||
@@ -34,7 +36,9 @@ from .models import (
|
||||
qwen3_a3b_fp8,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.skipif(not current_platform.is_cuda(), reason="Only test CUDA")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(), reason="Only test CUDA/ROCm"
|
||||
)
|
||||
|
||||
|
||||
@multi_gpu_test(num_gpus=2)
|
||||
@@ -55,6 +59,7 @@ pytestmark = pytest.mark.skipif(not current_platform.is_cuda(), reason="Only tes
|
||||
@pytest.mark.parametrize("n_layers", [4])
|
||||
@pytest.mark.parametrize("custom_ops", custom_ops_combos("quant_fp8", "rms_norm"))
|
||||
@pytest.mark.parametrize("inductor_graph_partition", INDUCTOR_GRAPH_PARTITION)
|
||||
@pytest.mark.skipif(not current_platform.is_cuda(), reason="Only test CUDA")
|
||||
def test_tp2_ar_rms_fp8_fusions(
|
||||
model_name: str,
|
||||
matches_fn: Callable[[int], Matches],
|
||||
@@ -124,6 +129,7 @@ def test_tp2_ar_rms_fp8_fusions(
|
||||
@pytest.mark.parametrize("custom_ops", custom_ops_combos("rms_norm"))
|
||||
@pytest.mark.parametrize("inductor_graph_partition", INDUCTOR_GRAPH_PARTITION)
|
||||
@pytest.mark.skipif(not is_blackwell(), reason="Blackwell required for fp4")
|
||||
@pytest.mark.skipif(not current_platform.is_cuda(), reason="Only test CUDA")
|
||||
def test_tp2_ar_rms_fp4_fusions(
|
||||
model_name: str,
|
||||
matches_fn: Callable[[int], Matches],
|
||||
@@ -176,10 +182,19 @@ def test_tp2_ar_rms_fp4_fusions(
|
||||
"model_name, matches_fn, model_kwargs, hf_overrides",
|
||||
[llama3_8b, qwen3_a3b, gpt_oss_20b],
|
||||
)
|
||||
@pytest.mark.parametrize("attn_backend", [TRITON_ATTN])
|
||||
@pytest.mark.parametrize(
|
||||
"attn_backend",
|
||||
[
|
||||
TRITON_ATTN,
|
||||
FLASHINFER_ATTN,
|
||||
ROCM_ATTN,
|
||||
ROCM_AITER_UNIFIED_ATTN,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("n_layers", [4])
|
||||
@pytest.mark.parametrize("custom_ops", custom_ops_combos("rms_norm"))
|
||||
@pytest.mark.parametrize("custom_ops", tuple(custom_ops_combos("rms_norm")))
|
||||
@pytest.mark.parametrize("inductor_graph_partition", INDUCTOR_GRAPH_PARTITION)
|
||||
@pytest.mark.skipif(not current_platform.is_cuda_alike(), reason="Only test CUDA/ROCm")
|
||||
def test_tp2_ar_rms_fusions(
|
||||
model_name: str,
|
||||
matches_fn: Callable[[int], Matches],
|
||||
@@ -221,4 +236,5 @@ def test_tp2_ar_rms_fusions(
|
||||
compilation_config,
|
||||
matches_check,
|
||||
tp_size=2,
|
||||
use_aiter=current_platform.is_rocm(),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ from .common import (
|
||||
AttentionBackendCase,
|
||||
Matches,
|
||||
custom_ops_combos,
|
||||
is_blackwell,
|
||||
)
|
||||
from .models import (
|
||||
FLASHINFER_ATTN,
|
||||
@@ -46,14 +45,9 @@ def test_tp2_async_tp_fp8_fusions(
|
||||
custom_ops: str,
|
||||
inductor_graph_partition: bool,
|
||||
run_e2e_fusion_test,
|
||||
monkeypatch,
|
||||
):
|
||||
matches = matches_fn(n_layers)
|
||||
|
||||
if is_blackwell():
|
||||
# Disable FlashInfer scaled_mm FP8 as it's not supported in async tp patterns
|
||||
monkeypatch.setenv("VLLM_DISABLED_KERNELS", "FlashInferFP8ScaledMMLinearKernel")
|
||||
|
||||
# Reduce size of model and skip weight loading time
|
||||
model_kwargs["hf_overrides"] = hf_overrides(n_layers)
|
||||
model_kwargs["load_format"] = "dummy"
|
||||
@@ -173,14 +167,9 @@ def test_tp2_sp_ar_rms_fp8_fusions(
|
||||
custom_ops: str,
|
||||
inductor_graph_partition: bool,
|
||||
run_e2e_fusion_test,
|
||||
monkeypatch,
|
||||
):
|
||||
matches = matches_fn(n_layers)
|
||||
|
||||
if is_blackwell():
|
||||
# Disable FlashInfer scaled_mm FP8 as it's not supported in async tp patterns
|
||||
monkeypatch.setenv("VLLM_DISABLED_KERNELS", "FlashInferFP8ScaledMMLinearKernel")
|
||||
|
||||
# Reduce size of model and skip weight loading time
|
||||
model_kwargs["hf_overrides"] = hf_overrides(n_layers)
|
||||
model_kwargs["load_format"] = "dummy"
|
||||
|
||||
@@ -8,8 +8,12 @@ import torch
|
||||
import vllm.envs as envs
|
||||
from tests.compile.backend import TestBackend
|
||||
from tests.utils import TestFP8Layer, has_module_attribute, multi_gpu_test
|
||||
from vllm._aiter_ops import IS_AITER_FOUND, rocm_aiter_ops
|
||||
from vllm._custom_ops import cutlass_scaled_fp4_mm, scaled_fp4_quant
|
||||
from vllm.compilation.passes.fusion.allreduce_rms_fusion import AllReduceFusionPass
|
||||
from vllm.compilation.passes.fusion.allreduce_rms_fusion import (
|
||||
AllReduceFusionPass,
|
||||
RocmAiterAllReduceFusionPass,
|
||||
)
|
||||
from vllm.compilation.passes.utility.fix_functionalization import (
|
||||
FixFunctionalizationPass,
|
||||
)
|
||||
@@ -42,13 +46,19 @@ DEVICE_TYPE = current_platform.device_type
|
||||
|
||||
class TestAllReduceRMSNormModel(torch.nn.Module):
|
||||
def __init__(
|
||||
self, hidden_size=16, token_num=16, eps=1e-6, dtype: torch.dtype = torch.float16
|
||||
self,
|
||||
hidden_size=16,
|
||||
token_num=16,
|
||||
eps=1e-6,
|
||||
dtype: torch.dtype = torch.float16,
|
||||
use_aiter: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.norm = [RMSNorm(hidden_size, eps) for i in range(4)]
|
||||
self.w = [torch.rand(hidden_size, hidden_size) for _ in range(3)]
|
||||
self.use_aiter = use_aiter
|
||||
|
||||
def forward(self, x):
|
||||
# avoid having graph input be an arg to a pattern directly
|
||||
@@ -76,6 +86,8 @@ class TestAllReduceRMSNormModel(torch.nn.Module):
|
||||
return [torch.ops.vllm.all_reduce.default]
|
||||
|
||||
def ops_in_model_after(self):
|
||||
if self.use_aiter:
|
||||
return [rocm_aiter_ops.get_fused_allreduce_rmsnorm_op()]
|
||||
return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
|
||||
|
||||
|
||||
@@ -194,12 +206,36 @@ class TestAllReduceFusedAddRMSNormStaticQuantFP4Model(torch.nn.Module):
|
||||
|
||||
@multi_gpu_test(num_gpus=2)
|
||||
@pytest.mark.parametrize(
|
||||
"test_model, enable_quant_fp8_custom_op",
|
||||
"test_model, enable_quant_fp8_custom_op, use_aiter",
|
||||
[
|
||||
(TestAllReduceRMSNormModel, False),
|
||||
(TestAllReduceRMSNormStaticQuantFP8Model, True),
|
||||
(TestAllReduceRMSNormStaticQuantFP8Model, False),
|
||||
(TestAllReduceFusedAddRMSNormStaticQuantFP4Model, False),
|
||||
(TestAllReduceRMSNormModel, False, IS_AITER_FOUND),
|
||||
pytest.param(
|
||||
TestAllReduceRMSNormStaticQuantFP8Model,
|
||||
True,
|
||||
False,
|
||||
marks=pytest.mark.skipif(
|
||||
current_platform.is_rocm(),
|
||||
reason="Not supported on ROCm platform",
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
TestAllReduceRMSNormStaticQuantFP8Model,
|
||||
False,
|
||||
False,
|
||||
marks=pytest.mark.skipif(
|
||||
current_platform.is_rocm(),
|
||||
reason="Not supported on ROCm platform",
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
TestAllReduceFusedAddRMSNormStaticQuantFP4Model,
|
||||
False,
|
||||
False,
|
||||
marks=pytest.mark.skipif(
|
||||
current_platform.is_rocm(),
|
||||
reason="Not supported on ROCm platform",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("batch_size", [8])
|
||||
@@ -210,9 +246,18 @@ class TestAllReduceFusedAddRMSNormStaticQuantFP4Model(torch.nn.Module):
|
||||
@pytest.mark.parametrize("flashinfer_allreduce_backend", ["trtllm", "mnnvl"])
|
||||
@pytest.mark.skipif(envs.VLLM_TARGET_DEVICE not in ["cuda"], reason="Only test on CUDA")
|
||||
@pytest.mark.skipif(
|
||||
not find_spec("flashinfer")
|
||||
or not has_module_attribute("flashinfer.comm", "allreduce_fusion")
|
||||
or not has_module_attribute("flashinfer.comm", "create_allreduce_fusion_workspace"),
|
||||
current_platform.is_rocm() and not IS_AITER_FOUND,
|
||||
reason="aiter is not found",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
current_platform.is_cuda()
|
||||
and (
|
||||
not find_spec("flashinfer")
|
||||
or not has_module_attribute("flashinfer.comm", "allreduce_fusion")
|
||||
or not has_module_attribute(
|
||||
"flashinfer.comm", "create_allreduce_fusion_workspace"
|
||||
)
|
||||
),
|
||||
reason="flashinfer is not found or flashinfer "
|
||||
"is not compiled with allreduce_fusion",
|
||||
)
|
||||
@@ -225,7 +270,14 @@ def test_all_reduce_fusion_pass_replace(
|
||||
enable_rms_norm_custom_op,
|
||||
enable_quant_fp8_custom_op,
|
||||
flashinfer_allreduce_backend,
|
||||
use_aiter: bool,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
if use_aiter:
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("VLLM_ROCM_USE_AITER", str(use_aiter))
|
||||
rocm_aiter_ops.refresh_env_variables()
|
||||
|
||||
num_processes = 2
|
||||
if (
|
||||
test_model == TestAllReduceFusedAddRMSNormStaticQuantFP4Model
|
||||
@@ -249,6 +301,8 @@ def test_all_reduce_fusion_pass_replace(
|
||||
enable_rms_norm_custom_op,
|
||||
enable_quant_fp8_custom_op,
|
||||
flashinfer_allreduce_backend,
|
||||
use_aiter,
|
||||
monkeypatch,
|
||||
),
|
||||
nprocs=nprocs,
|
||||
)
|
||||
@@ -267,6 +321,8 @@ def all_reduce_fusion_pass_on_test_model(
|
||||
enable_rms_norm_custom_op,
|
||||
enable_quant_fp8_custom_op,
|
||||
flashinfer_allreduce_backend,
|
||||
use_aiter: bool,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
set_random_seed(0)
|
||||
|
||||
@@ -313,7 +369,11 @@ def all_reduce_fusion_pass_on_test_model(
|
||||
)
|
||||
with set_current_vllm_config(vllm_config):
|
||||
initialize_model_parallel(tensor_model_parallel_size=world_size)
|
||||
all_reduce_fusion_pass = AllReduceFusionPass(vllm_config)
|
||||
all_reduce_fusion_pass = (
|
||||
RocmAiterAllReduceFusionPass(vllm_config)
|
||||
if use_aiter
|
||||
else AllReduceFusionPass(vllm_config)
|
||||
)
|
||||
noop_pass = NoOpEliminationPass(vllm_config)
|
||||
func_pass = FixFunctionalizationPass(vllm_config)
|
||||
cleanup_pass = PostCleanupPass(vllm_config)
|
||||
@@ -323,7 +383,12 @@ def all_reduce_fusion_pass_on_test_model(
|
||||
)
|
||||
|
||||
token_num = batch_size * seq_len
|
||||
model = test_model_cls(hidden_size, token_num, dtype=dtype)
|
||||
if test_model_cls is TestAllReduceRMSNormModel:
|
||||
model = test_model_cls(
|
||||
hidden_size, token_num, dtype=dtype, use_aiter=use_aiter
|
||||
)
|
||||
else:
|
||||
model = test_model_cls(hidden_size, token_num, dtype=dtype)
|
||||
|
||||
hidden_states = torch.randn((token_num, hidden_size), requires_grad=False)
|
||||
|
||||
|
||||
@@ -88,14 +88,10 @@ class TestAllReduceRMSNormModel(torch.nn.Module):
|
||||
]
|
||||
|
||||
def ops_in_model(self):
|
||||
return (
|
||||
[torch.ops.vllm_ir.rms_norm]
|
||||
+ [
|
||||
torch.ops._C.fused_add_rms_norm.default,
|
||||
]
|
||||
if RMSNorm.enabled()
|
||||
else []
|
||||
)
|
||||
return [
|
||||
torch.ops.vllm_ir.rms_norm,
|
||||
torch.ops.vllm_ir.fused_add_rms_norm,
|
||||
]
|
||||
|
||||
|
||||
class TestAllReduceRMSNormStaticQuantFP8Model(torch.nn.Module):
|
||||
@@ -152,16 +148,17 @@ class TestAllReduceRMSNormStaticQuantFP8Model(torch.nn.Module):
|
||||
def ops_in_model(self):
|
||||
if self.vllm_config.compilation_config.pass_config.fuse_norm_quant:
|
||||
return [torch.ops._C.fused_add_rms_norm_static_fp8_quant.default]
|
||||
elif RMSNorm.enabled():
|
||||
return [
|
||||
torch.ops._C.fused_add_rms_norm.default,
|
||||
]
|
||||
elif any(layer.is_quant_fp8_enabled() for layer in self.fp8_linear_layers):
|
||||
return [
|
||||
torch.ops._C.static_scaled_fp8_quant.default,
|
||||
]
|
||||
else:
|
||||
return []
|
||||
quant_ops = (
|
||||
[torch.ops._C.static_scaled_fp8_quant.default]
|
||||
if any(layer.is_quant_fp8_enabled() for layer in self.fp8_linear_layers)
|
||||
else [torch.ops.aten.reciprocal]
|
||||
)
|
||||
return [
|
||||
torch.ops.vllm_ir.rms_norm,
|
||||
torch.ops.vllm_ir.fused_add_rms_norm,
|
||||
*quant_ops,
|
||||
]
|
||||
|
||||
|
||||
@multi_gpu_test(num_gpus=2)
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Comprehensive tests for UnsafeCloneEliminationPass.
|
||||
|
||||
This test suite exercises all possible valid FX graph patterns involving clones:
|
||||
1. Clone with no users (dead code)
|
||||
2. Clone with read-only users
|
||||
3. Clone with mutation users
|
||||
4. Clone of graph input
|
||||
5. Clone with original used after mutation
|
||||
6. Clone chains
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from torch import fx
|
||||
from torch.fx.experimental.proxy_tensor import make_fx
|
||||
|
||||
from vllm.compilation.passes.fx_utils import find_op_nodes
|
||||
from vllm.compilation.passes.inductor_pass import get_pass_context, pass_context
|
||||
from vllm.compilation.passes.ir.clone_elimination import (
|
||||
UnsafeCloneEliminationPass,
|
||||
user_writes_to_node,
|
||||
)
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.config.utils import Range
|
||||
|
||||
|
||||
def count_clones(graph: fx.Graph) -> int:
|
||||
"""Count clone nodes in a graph."""
|
||||
return len(list(find_op_nodes(torch.ops.aten.clone.default, graph)))
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def clone_cleanup_pass():
|
||||
return UnsafeCloneEliminationPass(VllmConfig())
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_pass_context():
|
||||
"""Set up pass context for each test."""
|
||||
with pass_context(compile_range=Range(1, 8192)):
|
||||
yield
|
||||
|
||||
|
||||
class TestCloneCleanup:
|
||||
"""Test UnsafeCloneEliminationPass behavior on various graph patterns."""
|
||||
|
||||
def test_remove_clone_readonly_users(self, clone_cleanup_pass):
|
||||
"""Clone with only read-only users should be removed."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
x_clone = x.clone()
|
||||
return x_clone + 1
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
expected = graph_module(inp)
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
def test_keep_clone_with_mutation_and_original_used_after(self, clone_cleanup_pass):
|
||||
"""Clone must be kept if it's mutated AND original is used after mutation."""
|
||||
|
||||
def f(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
x = x.relu() # not a graph param
|
||||
x_clone = x.clone()
|
||||
x_clone.add_(1)
|
||||
return x, x_clone
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
expected = graph_module(inp)
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
|
||||
# Clone should be KEPT because original is used after mutation
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
torch.testing.assert_close(actual[0], expected[0])
|
||||
torch.testing.assert_close(actual[1], expected[1])
|
||||
|
||||
def test_remove_clone_with_mutation_no_original_use(self, clone_cleanup_pass):
|
||||
"""Clone can be removed if it's mutated but original is not used after."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
x = x.relu() # not a graph param
|
||||
x_clone = x.clone()
|
||||
x_clone.add_(1)
|
||||
return x_clone
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
expected = graph_module(inp)
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
def test_clone_chain(self, clone_cleanup_pass):
|
||||
"""Test handling of clone chains: x -> clone1 -> clone2."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
x = x.relu() # not a graph param
|
||||
x1 = x.clone()
|
||||
x2 = x1.clone()
|
||||
return x2 + 1
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 2
|
||||
|
||||
expected = graph_module(inp)
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
|
||||
# Both clones should be removed
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
def test_multiple_clones_of_same_input(self, clone_cleanup_pass):
|
||||
"""Test multiple independent clones of the same input."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
x1 = x.clone()
|
||||
x2 = x.clone()
|
||||
return x1 + x2
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 2
|
||||
|
||||
expected = graph_module(inp)
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
|
||||
# Both clones should be removed (only readonly uses)
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
def test_no_clones_in_graph(self, clone_cleanup_pass):
|
||||
"""Test pass behavior when graph has no clones."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
return x + 1
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
|
||||
expected = graph_module(inp)
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
def test_multiple_passes(self, clone_cleanup_pass):
|
||||
"""Test running the pass multiple times (should be idempotent)."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
x1 = x.clone()
|
||||
return x1 + 1
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
expected = graph_module(inp)
|
||||
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
graph_module.recompile()
|
||||
actual = graph_module(inp)
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
def test_output_node_no_write(self):
|
||||
"""Output nodes never write to their inputs."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
return x
|
||||
|
||||
graph_module = make_fx(f)(torch.randn(2, 3))
|
||||
x_node = [n for n in graph_module.graph.nodes if n.op == "placeholder"][0]
|
||||
output_node = [n for n in graph_module.graph.nodes if n.op == "output"][0]
|
||||
|
||||
assert not user_writes_to_node(output_node, x_node)
|
||||
|
||||
def test_readonly_op_no_write(self):
|
||||
"""Readonly operations don't write to inputs."""
|
||||
|
||||
def f(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
return x + y
|
||||
|
||||
graph_module = make_fx(f)(torch.randn(2, 3), torch.randn(2, 3))
|
||||
placeholders = [n for n in graph_module.graph.nodes if n.op == "placeholder"]
|
||||
add_node = [
|
||||
n
|
||||
for n in graph_module.graph.nodes
|
||||
if n.op == "call_function" and n.target == torch.ops.aten.add.Tensor
|
||||
][0]
|
||||
|
||||
assert not user_writes_to_node(add_node, placeholders[0])
|
||||
assert not user_writes_to_node(add_node, placeholders[1])
|
||||
|
||||
def test_inplace_op_writes(self):
|
||||
"""Inplace operations write to first argument."""
|
||||
|
||||
def f(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
x.add_(y)
|
||||
return x
|
||||
|
||||
graph_module = make_fx(f)(torch.randn(2, 3), torch.randn(2, 3))
|
||||
placeholders = [n for n in graph_module.graph.nodes if n.op == "placeholder"]
|
||||
add_node = [
|
||||
n
|
||||
for n in graph_module.graph.nodes
|
||||
if n.op == "call_function" and "add_" in str(n.target)
|
||||
][0]
|
||||
|
||||
# add_ writes to first arg but not second
|
||||
assert user_writes_to_node(add_node, placeholders[0])
|
||||
assert not user_writes_to_node(add_node, placeholders[1])
|
||||
|
||||
def test_copy_writes(self):
|
||||
"""copy_ operation writes to first argument."""
|
||||
|
||||
def f(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
x.copy_(y)
|
||||
return x
|
||||
|
||||
graph_module = make_fx(f)(torch.randn(2, 3), torch.randn(2, 3))
|
||||
placeholders = [n for n in graph_module.graph.nodes if n.op == "placeholder"]
|
||||
copy_node = [
|
||||
n
|
||||
for n in graph_module.graph.nodes
|
||||
if n.op == "call_function" and "copy_" in str(n.target)
|
||||
][0]
|
||||
|
||||
assert user_writes_to_node(copy_node, placeholders[0])
|
||||
assert not user_writes_to_node(copy_node, placeholders[1])
|
||||
|
||||
def test_auto_functionalized_not_a_write(self):
|
||||
"""auto_functionalized ops are follow-up uses, not writes."""
|
||||
from torch._higher_order_ops.auto_functionalize import auto_functionalized
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
return x
|
||||
|
||||
graph_module = make_fx(f)(torch.randn(2, 3))
|
||||
x_node = [n for n in graph_module.graph.nodes if n.op == "placeholder"][0]
|
||||
|
||||
# Create an auto_functionalized node in the graph
|
||||
with graph_module.graph.inserting_before(None):
|
||||
af_node = graph_module.graph.call_function(
|
||||
auto_functionalized, kwargs={"input": x_node}
|
||||
)
|
||||
|
||||
# auto_functionalized should not be treated as a write
|
||||
assert not user_writes_to_node(af_node, x_node)
|
||||
|
||||
def test_higher_order_op_conservatively_writes(self):
|
||||
"""Other higher-order operators are conservatively treated as writes."""
|
||||
from torch._ops import HigherOrderOperator
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
return x
|
||||
|
||||
graph_module = make_fx(f)(torch.randn(2, 3))
|
||||
x_node = [n for n in graph_module.graph.nodes if n.op == "placeholder"][0]
|
||||
|
||||
# Create a concrete higher-order operator subclass
|
||||
class MockHigherOrderOp(HigherOrderOperator):
|
||||
def __call__(self, *args, **kwargs):
|
||||
return args[0] if args else None
|
||||
|
||||
mock_hoo = MockHigherOrderOp("mock_higher_order_op")
|
||||
|
||||
with graph_module.graph.inserting_before(None):
|
||||
hoo_node = graph_module.graph.call_function(mock_hoo, args=(x_node,))
|
||||
|
||||
# Should be conservative and assume it could write
|
||||
assert user_writes_to_node(hoo_node, x_node)
|
||||
|
||||
|
||||
class TestCloneCleanupWithDonatedInputs:
|
||||
"""Test UnsafeCloneEliminationPass with donated input tracking via PassContext."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_pass_context(self):
|
||||
"""Set up pass context for each test."""
|
||||
with pass_context(compile_range=Range(1, 8192)):
|
||||
yield
|
||||
|
||||
def test_donated_input_clone_removed(self, clone_cleanup_pass):
|
||||
"""Clone of donated input should be removed."""
|
||||
|
||||
def f(x: torch.Tensor) -> torch.Tensor:
|
||||
x_clone = x.clone()
|
||||
x_clone.add_(1)
|
||||
return x_clone
|
||||
|
||||
inp = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp)
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
# Mark first parameter as donated
|
||||
get_pass_context().donated_input_ids = {0}
|
||||
|
||||
expected = graph_module(inp.clone())
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
|
||||
# Clone should be removed since input is donated
|
||||
assert count_clones(graph_module.graph) == 0
|
||||
|
||||
# Input can be mutated (donated)
|
||||
inp_copy = inp.clone()
|
||||
actual = graph_module(inp_copy)
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
def test_non_donated_input_clone_kept(self, clone_cleanup_pass):
|
||||
"""Clone of non-donated input with mutation should be kept."""
|
||||
|
||||
def f(x: torch.Tensor, y: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
x_clone = x.clone()
|
||||
x_clone.add_(1)
|
||||
return x, x_clone
|
||||
|
||||
inp_x = torch.randn(2, 3)
|
||||
inp_y = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp_x, inp_y)
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
# No donated inputs
|
||||
get_pass_context().donated_input_ids = set()
|
||||
|
||||
expected = graph_module(inp_x.clone(), inp_y.clone())
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
|
||||
# Clone should be kept since input is not donated and original is used
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
# Verify inputs are not mutated
|
||||
inp_x_before = inp_x.clone()
|
||||
inp_y_before = inp_y.clone()
|
||||
actual = graph_module(inp_x, inp_y)
|
||||
torch.testing.assert_close(
|
||||
inp_x, inp_x_before, msg="Input x should not be mutated"
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
inp_y, inp_y_before, msg="Input y should not be mutated"
|
||||
)
|
||||
torch.testing.assert_close(actual[0], expected[0])
|
||||
torch.testing.assert_close(actual[1], expected[1])
|
||||
|
||||
def test_mixed_donated_inputs(self, clone_cleanup_pass):
|
||||
"""Test with some inputs donated and some not."""
|
||||
|
||||
def f(x: torch.Tensor, y: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
x_clone = x.clone()
|
||||
x_clone.add_(1)
|
||||
y_clone = y.clone()
|
||||
y_clone.add_(2)
|
||||
return x_clone, y_clone
|
||||
|
||||
inp_x = torch.randn(2, 3)
|
||||
inp_y = torch.randn(2, 3)
|
||||
graph_module = make_fx(f)(inp_x, inp_y)
|
||||
assert count_clones(graph_module.graph) == 2
|
||||
|
||||
# Only x is donated
|
||||
get_pass_context().donated_input_ids = {0}
|
||||
|
||||
expected = graph_module(inp_x.clone(), inp_y.clone())
|
||||
clone_cleanup_pass(graph_module.graph)
|
||||
graph_module.recompile()
|
||||
|
||||
# x_clone removed (x is donated), y_clone kept (y is not donated)
|
||||
assert count_clones(graph_module.graph) == 1
|
||||
|
||||
# Verify y is not mutated (x can be mutated since it's donated)
|
||||
inp_y_before = inp_y.clone()
|
||||
actual = graph_module(inp_x.clone(), inp_y)
|
||||
torch.testing.assert_close(
|
||||
inp_y, inp_y_before, msg="Input y should not be mutated"
|
||||
)
|
||||
torch.testing.assert_close(actual[0], expected[0])
|
||||
torch.testing.assert_close(actual[1], expected[1])
|
||||
@@ -0,0 +1,465 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Tests for IR inplace functionalization pass integration.
|
||||
|
||||
This test suite verifies that the inplace functionalization pass, lowering pass,
|
||||
and clone cleanup pass work together correctly with donated buffer tracking.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch._dynamo.exc
|
||||
from torch import nn
|
||||
|
||||
import vllm.kernels # noqa: F401 to register kernels
|
||||
from vllm.compilation.passes.inductor_pass import InductorPass, get_pass_context
|
||||
from vllm.compilation.passes.ir.clone_elimination import (
|
||||
UnsafeCloneEliminationPass,
|
||||
)
|
||||
from vllm.compilation.passes.ir.inplace_functionalization import (
|
||||
VllmIRInplaceFunctionalizationPass,
|
||||
)
|
||||
from vllm.compilation.passes.ir.lowering_pass import VllmIRLoweringPass
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.ir import ops
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.triton_utils import HAS_TRITON, tl, triton
|
||||
|
||||
from ...backend import TestBackend
|
||||
|
||||
|
||||
class StoreDonationInfoPass(InductorPass):
|
||||
def __init__(self):
|
||||
self.donated_input_ids_sets: list[set[int]] = []
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
ctx = get_pass_context()
|
||||
self.donated_input_ids_sets += [ctx.donated_input_ids]
|
||||
|
||||
|
||||
class MaybeInplaceModel(nn.Module):
|
||||
"""Model using only maybe_inplace variants."""
|
||||
|
||||
def __init__(self, hidden_size=16):
|
||||
super().__init__()
|
||||
self.weight1 = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
self.weight2 = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, residual1: torch.Tensor, residual2: torch.Tensor
|
||||
):
|
||||
# First maybe_inplace - x & residual1 are donated
|
||||
x_normed1, residual_out1 = ops.fused_add_rms_norm.maybe_inplace(
|
||||
x, residual1, self.weight1, 1e-5
|
||||
)
|
||||
# Second maybe_inplace - residual2 is donated
|
||||
x_normed2, residual_out2 = ops.fused_add_rms_norm.maybe_inplace(
|
||||
x_normed1, residual2, self.weight2, 1e-5
|
||||
)
|
||||
return x_normed2, residual_out1, residual_out2
|
||||
|
||||
|
||||
class FunctionalModel(nn.Module):
|
||||
"""Model using only functional (default) variants."""
|
||||
|
||||
def __init__(self, hidden_size=16):
|
||||
super().__init__()
|
||||
self.weight1 = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
self.weight2 = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, residual1: torch.Tensor, residual2: torch.Tensor
|
||||
):
|
||||
# First functional - no donation
|
||||
x_normed1, residual_out1 = ops.fused_add_rms_norm(
|
||||
x, residual1, self.weight1, 1e-5
|
||||
)
|
||||
# Second functional - no donation
|
||||
x_normed2, residual_out2 = ops.fused_add_rms_norm(
|
||||
x_normed1, residual2, self.weight2, 1e-5
|
||||
)
|
||||
return x_normed2, residual_out1, residual_out2
|
||||
|
||||
|
||||
class MixedModel(nn.Module):
|
||||
"""Model mixing maybe_inplace and functional variants."""
|
||||
|
||||
def __init__(self, hidden_size=16):
|
||||
super().__init__()
|
||||
self.weight1 = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
self.weight2 = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, residual1: torch.Tensor, residual2: torch.Tensor
|
||||
):
|
||||
# First maybe_inplace - x & residual1 are donated
|
||||
x_normed1, residual_out1 = ops.fused_add_rms_norm.maybe_inplace(
|
||||
x, residual1, self.weight1, 1e-5
|
||||
)
|
||||
# Second functional - no donation, x_normed1 must be preserved as it's returned
|
||||
x_normed2, residual_out2 = ops.fused_add_rms_norm(
|
||||
x_normed1, residual2, self.weight2, 1e-5
|
||||
)
|
||||
# Return both to prevent x_normed1 from being optimized away
|
||||
return x_normed1, x_normed2, residual_out1, residual_out2
|
||||
|
||||
|
||||
class ModelWithTritonAfterMaybeInplace(nn.Module):
|
||||
"""
|
||||
Model using maybe_inplace followed by a Triton kernel.
|
||||
Test clone elimination can handle Triton in the graph
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size=16):
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
|
||||
@triton.jit
|
||||
def _triton_add_kernel(
|
||||
x_ptr,
|
||||
y_ptr,
|
||||
n_elements,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(axis=0)
|
||||
block_start = pid * BLOCK_SIZE
|
||||
offsets = block_start + tl.arange(0, BLOCK_SIZE)
|
||||
mask = offsets < n_elements
|
||||
x = tl.load(x_ptr + offsets, mask=mask)
|
||||
y = x + 0.1
|
||||
tl.store(y_ptr + offsets, y, mask=mask)
|
||||
|
||||
def triton_add(x: torch.Tensor) -> torch.Tensor:
|
||||
"""Simple Triton add kernel."""
|
||||
y = torch.empty_like(x)
|
||||
n_elements = x.numel()
|
||||
grid = (triton.cdiv(n_elements, 256),)
|
||||
_triton_add_kernel[grid](x, y, n_elements, BLOCK_SIZE=256)
|
||||
return y
|
||||
|
||||
self.triton_add = triton_add
|
||||
|
||||
def forward(self, x: torch.Tensor, residual: torch.Tensor, residual2: torch.Tensor):
|
||||
x_normed, residual_out = ops.fused_add_rms_norm.maybe_inplace(
|
||||
x, residual, self.weight, 1e-5
|
||||
)
|
||||
|
||||
x_processed = self.triton_add(x_normed)
|
||||
|
||||
# x_processed does not need to be cloned, residual2 does
|
||||
x_normed2, residual_out2 = ops.fused_add_rms_norm(
|
||||
x_processed, residual2, self.weight, 1e-5
|
||||
)
|
||||
return x_normed2, residual_out2
|
||||
|
||||
|
||||
skipif_no_triton = pytest.mark.skipif(not HAS_TRITON, reason="Requires Triton")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="Only test on cuda and rocm platform",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"model_class,expected_functionalized,expected_donated,expected_clones",
|
||||
[
|
||||
# 2 inplace calls, all activations donated, all clones eliminated
|
||||
(MaybeInplaceModel, 2, 3, 0),
|
||||
# No inplace calls, no donations, 3 clones (one eliminated)
|
||||
(FunctionalModel, 0, 0, 3),
|
||||
# One inplace call, two donated activations, 2 clones
|
||||
(MixedModel, 1, 2, 2),
|
||||
# One inplace call, two donated, 1 clone remaining
|
||||
pytest.param(ModelWithTritonAfterMaybeInplace, 1, 2, 1, marks=skipif_no_triton),
|
||||
],
|
||||
)
|
||||
def test_inplace_functionalization(
|
||||
default_vllm_config: VllmConfig,
|
||||
model_class,
|
||||
expected_functionalized: int,
|
||||
expected_clones: int,
|
||||
expected_donated: int,
|
||||
):
|
||||
"""Test inplace functionalization, lowering, and clone cleanup."""
|
||||
torch.set_default_device(current_platform.device_type)
|
||||
|
||||
# Use vllm_c so inplace path is triggered
|
||||
default_vllm_config.kernel_config.ir_op_priority.fused_add_rms_norm = [
|
||||
"vllm_c",
|
||||
"native",
|
||||
]
|
||||
|
||||
# Create passes in order they run during compilation
|
||||
functionalization_pass = VllmIRInplaceFunctionalizationPass(default_vllm_config)
|
||||
lowering_pass = VllmIRLoweringPass(default_vllm_config)
|
||||
donated_info_pass = StoreDonationInfoPass()
|
||||
cleanup_pass = UnsafeCloneEliminationPass(default_vllm_config)
|
||||
|
||||
# Set up backend with pre-grad pass
|
||||
backend = TestBackend(lowering_pass, donated_info_pass, cleanup_pass)
|
||||
backend.inductor_config["pre_grad_custom_pass"] = functionalization_pass
|
||||
|
||||
model = model_class()
|
||||
x = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
residual1 = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
residual2 = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
|
||||
with default_vllm_config.kernel_config.ir_op_priority.set_priority():
|
||||
# Reference output without optimization
|
||||
ref_output = model(x.clone(), residual1.clone(), residual2.clone())
|
||||
|
||||
# Compile with inplace optimization
|
||||
compiled_model = torch.compile(model, backend=backend, fullgraph=True)
|
||||
output = compiled_model(x.clone(), residual1.clone(), residual2.clone())
|
||||
|
||||
# Verify correctness (relaxed tolerance for bfloat16)
|
||||
for i in range(len(ref_output)):
|
||||
torch.testing.assert_close(output[i], ref_output[i], rtol=1e-2, atol=1e-2)
|
||||
|
||||
# Verify expected number of ops were functionalized
|
||||
func_ops = functionalization_pass.functionalized_ops
|
||||
assert len(func_ops) == int(bool(expected_functionalized))
|
||||
if expected_functionalized > 0:
|
||||
assert "fused_add_rms_norm" in func_ops
|
||||
assert func_ops["fused_add_rms_norm"] == expected_functionalized
|
||||
|
||||
# Verify lowering happened (2 ops in all cases)
|
||||
assert "fused_add_rms_norm" in lowering_pass.selected_impls
|
||||
assert len(lowering_pass.selected_impls["fused_add_rms_norm"]) == 2
|
||||
assert all(
|
||||
provider == "vllm_c"
|
||||
for node, provider in lowering_pass.selected_impls["fused_add_rms_norm"].items()
|
||||
), lowering_pass.selected_impls
|
||||
|
||||
# Verify correct number of donated IDs
|
||||
assert len(donated_info_pass.donated_input_ids_sets) == 1
|
||||
assert len(donated_info_pass.donated_input_ids_sets[0]) == expected_donated
|
||||
|
||||
# Verify expected number of clones after cleanup
|
||||
actual_clones = backend.op_count(torch.ops.aten.clone.default, before=False)
|
||||
assert actual_clones == expected_clones, (
|
||||
f"Expected {expected_clones} clones, got {actual_clones}:"
|
||||
f"{backend.print_graphs()}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="Only test on cuda and rocm platform",
|
||||
)
|
||||
def test_donated_buffer_context_propagation(default_vllm_config):
|
||||
"""Test that donated_input_ids propagates correctly through pass_context."""
|
||||
torch.set_default_device(current_platform.device_type)
|
||||
|
||||
# Create a custom backend that inspects pass_context in cleanup pass
|
||||
functionalization_pass = VllmIRInplaceFunctionalizationPass(default_vllm_config)
|
||||
lowering_pass = VllmIRLoweringPass(default_vllm_config)
|
||||
|
||||
donation_info_pass = StoreDonationInfoPass()
|
||||
cleanup_pass = UnsafeCloneEliminationPass(default_vllm_config)
|
||||
|
||||
backend = TestBackend(lowering_pass, donation_info_pass, cleanup_pass)
|
||||
backend.inductor_config["pre_grad_custom_pass"] = functionalization_pass
|
||||
|
||||
model = MaybeInplaceModel()
|
||||
x = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
residual1 = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
residual2 = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
|
||||
compiled_model = torch.compile(model, backend=backend, fullgraph=True)
|
||||
compiled_model(x.clone(), residual1.clone(), residual2.clone())
|
||||
|
||||
donated_ids_seen = donation_info_pass.donated_input_ids_sets
|
||||
# Verify donated_input_ids was set and propagated
|
||||
assert len(donated_ids_seen) == 1
|
||||
# Should have donated inputs (exact indices depend on AOTAutograd)
|
||||
assert len(donated_ids_seen[0]) == 3
|
||||
# All donated ids should be valid non-negative integers
|
||||
for idx in donated_ids_seen[0]:
|
||||
assert isinstance(idx, int) and idx >= 0, f"Invalid donated index: {idx}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="Only test on cuda and rocm platform",
|
||||
)
|
||||
def test_maybe_inplace_reuse_error(default_vllm_config):
|
||||
"""Test that reusing a donated activation input raises ValueError."""
|
||||
torch.set_default_device(current_platform.device_type)
|
||||
|
||||
class ReuseModel(nn.Module):
|
||||
"""Model that incorrectly reuses a donated activation input."""
|
||||
|
||||
def __init__(self, hidden_size=16):
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
|
||||
def forward(self, x: torch.Tensor, residual: torch.Tensor):
|
||||
# x is donated to maybe_inplace
|
||||
x_normed, residual_out = ops.fused_add_rms_norm.maybe_inplace(
|
||||
x, residual, self.weight, 1e-5
|
||||
)
|
||||
# ERROR: x is used again after being donated
|
||||
return x_normed + x # This should raise ValueError
|
||||
|
||||
functionalization_pass = VllmIRInplaceFunctionalizationPass(default_vllm_config)
|
||||
lowering_pass = VllmIRLoweringPass(default_vllm_config)
|
||||
cleanup_pass = UnsafeCloneEliminationPass(default_vllm_config)
|
||||
|
||||
backend = TestBackend(lowering_pass, cleanup_pass)
|
||||
backend.inductor_config["pre_grad_custom_pass"] = functionalization_pass
|
||||
|
||||
model = ReuseModel()
|
||||
x = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
residual = torch.randn(8, 16, dtype=torch.bfloat16)
|
||||
|
||||
# Compilation should raise BackendCompilerFailed wrapping ValueError
|
||||
with pytest.raises(
|
||||
torch._dynamo.exc.BackendCompilerFailed,
|
||||
match="is used again after the node",
|
||||
):
|
||||
compiled_model = torch.compile(model, backend=backend, fullgraph=True)
|
||||
compiled_model(x.clone(), residual.clone())
|
||||
|
||||
|
||||
# Piecewise compilation tests with graph splitting
|
||||
|
||||
|
||||
@torch.library.custom_op("vllm::test_split_marker", mutates_args=())
|
||||
def test_split_marker(x: torch.Tensor) -> torch.Tensor:
|
||||
"""Identity op that marks a split point for piecewise compilation."""
|
||||
return x.clone()
|
||||
|
||||
|
||||
@test_split_marker.register_fake
|
||||
def _fake_split_marker(x: torch.Tensor) -> torch.Tensor:
|
||||
return torch.empty_like(x)
|
||||
|
||||
|
||||
class TransformerBlockWithSplits(nn.Module):
|
||||
"""Transformer block with explicit split points for piecewise compilation."""
|
||||
|
||||
def __init__(self, hidden_size=32, intermediate_size=128):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.intermediate_size = intermediate_size
|
||||
|
||||
# Attention-like projection
|
||||
self.attn_proj = nn.Linear(
|
||||
hidden_size, hidden_size, bias=False, dtype=torch.bfloat16
|
||||
)
|
||||
|
||||
# Post-attention norm
|
||||
self.post_attn_norm = nn.Parameter(
|
||||
torch.ones(hidden_size, dtype=torch.bfloat16)
|
||||
)
|
||||
|
||||
# MLP
|
||||
self.gate_proj = nn.Linear(
|
||||
hidden_size, intermediate_size, bias=False, dtype=torch.bfloat16
|
||||
)
|
||||
self.up_proj = nn.Linear(
|
||||
hidden_size, intermediate_size, bias=False, dtype=torch.bfloat16
|
||||
)
|
||||
self.down_proj = nn.Linear(
|
||||
intermediate_size, hidden_size, bias=False, dtype=torch.bfloat16
|
||||
)
|
||||
|
||||
# Post-MLP norm
|
||||
self.post_mlp_norm = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Attention block with residual
|
||||
residual1 = x
|
||||
attn_out = self.attn_proj(x)
|
||||
|
||||
# Fused add + norm (maybe_inplace: residual1 is donated)
|
||||
normed1, residual1 = ops.fused_add_rms_norm.maybe_inplace(
|
||||
attn_out, residual1, self.post_attn_norm, 1e-5
|
||||
)
|
||||
|
||||
# Force a graph split here
|
||||
normed1 = torch.ops.vllm.test_split_marker(normed1)
|
||||
|
||||
# MLP block
|
||||
gate = self.gate_proj(normed1)
|
||||
up = self.up_proj(normed1)
|
||||
mlp_out = self.down_proj(gate * torch.nn.functional.silu(up))
|
||||
|
||||
# Fused add + norm (maybe_inplace: residual1 is donated)
|
||||
normed2, residual2 = ops.fused_add_rms_norm.maybe_inplace(
|
||||
mlp_out, residual1, self.post_mlp_norm, 1e-5
|
||||
)
|
||||
|
||||
return normed2, residual2
|
||||
|
||||
|
||||
def with_dyn_arg(fn: Callable, arg_index: int, dim_index: int):
|
||||
def inner(*args):
|
||||
torch._dynamo.mark_dynamic(args[arg_index], dim_index)
|
||||
return fn(*args)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="Only test on cuda and rocm platform",
|
||||
)
|
||||
def test_piecewise_compilation_with_donated_buffers(monkeypatch, fresh_vllm_cache):
|
||||
"""
|
||||
Test piecewise compilation with donated buffers across graph splits.
|
||||
Utilizes a custom splitting op. Uses fresh cache to avoid compilation caching.
|
||||
"""
|
||||
torch.set_default_device(current_platform.device_type)
|
||||
|
||||
# Disable compilation cache to avoid serialization issues
|
||||
monkeypatch.setenv("VLLM_DISABLE_COMPILE_CACHE", "1")
|
||||
|
||||
from vllm.compilation.backends import VllmBackend
|
||||
from vllm.config import CompilationConfig, VllmConfig
|
||||
|
||||
# Create config with custom splitting op
|
||||
store_donation_info = StoreDonationInfoPass()
|
||||
vllm_config = VllmConfig(
|
||||
compilation_config=CompilationConfig(
|
||||
custom_ops=["all"],
|
||||
splitting_ops=["vllm::test_split_marker"],
|
||||
inductor_compile_config={"post_grad_custom_post_pass": store_donation_info},
|
||||
)
|
||||
)
|
||||
|
||||
backend = VllmBackend(vllm_config)
|
||||
|
||||
model = TransformerBlockWithSplits()
|
||||
x = torch.randn(8, 32, dtype=torch.bfloat16)
|
||||
|
||||
# Reference output
|
||||
ref_output = with_dyn_arg(model, 0, 0)(x.clone())
|
||||
|
||||
# Compile with piecewise compilation (graph will split at split_marker)
|
||||
compiled_model = torch.compile(model, backend=backend, fullgraph=False)
|
||||
output = with_dyn_arg(compiled_model, 0, 0)(x.clone())
|
||||
|
||||
# Verify correctness (relaxed tolerance for bfloat16)
|
||||
torch.testing.assert_close(output[0], ref_output[0], rtol=1e-2, atol=1e-2)
|
||||
torch.testing.assert_close(output[1], ref_output[1], rtol=1e-2, atol=1e-2)
|
||||
|
||||
# Verify the model was split into multiple submodules
|
||||
assert hasattr(backend, "split_gm"), "Backend should have split graph module"
|
||||
|
||||
# Should have at least 2 submodules (split by test_split_marker op)
|
||||
submodules = list(backend.split_gm.named_children())
|
||||
num_submodules = len(submodules)
|
||||
assert num_submodules >= 2, (
|
||||
f"Expected at least 2 submodules (split), got {num_submodules}"
|
||||
)
|
||||
|
||||
# Check that donation info was propagated correctly
|
||||
donated_inputs_sets = store_donation_info.donated_input_ids_sets
|
||||
assert len(donated_inputs_sets) == 2
|
||||
assert len(donated_inputs_sets[0]) == 1
|
||||
assert len(donated_inputs_sets[1]) == 1
|
||||
@@ -126,7 +126,7 @@ class TestFusedAddRMSNorm(torch.nn.Module):
|
||||
if TEST_FP8 and do_fusion:
|
||||
return [torch.ops._C.fused_add_rms_norm_static_fp8_quant.default]
|
||||
else:
|
||||
return [torch.ops._C.fused_add_rms_norm.default]
|
||||
return []
|
||||
|
||||
def ops_not_in_model(self):
|
||||
return []
|
||||
|
||||
@@ -59,7 +59,7 @@ class TestModel(torch.nn.Module):
|
||||
|
||||
def ops_in_model_before(self):
|
||||
return [
|
||||
rocm_aiter_ops.get_rmsnorm_fused_add_op(),
|
||||
torch.ops.vllm_ir.fused_add_rms_norm,
|
||||
torch.ops.aten.constant_pad_nd,
|
||||
]
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from vllm.compilation.passes.fusion.rms_quant_fusion import (
|
||||
FusedRMSQuantKey,
|
||||
RMSNormQuantFusionPass,
|
||||
)
|
||||
from vllm.compilation.passes.fx_utils import find_op_nodes
|
||||
from vllm.compilation.passes.utility.noop_elimination import NoOpEliminationPass
|
||||
from vllm.compilation.passes.utility.post_cleanup import PostCleanupPass
|
||||
from vllm.config import (
|
||||
@@ -243,9 +242,10 @@ class TestModel(torch.nn.Module):
|
||||
]
|
||||
|
||||
def ops_in_model_before_partial(self):
|
||||
return [torch.ops.vllm_ir.rms_norm] + (
|
||||
[RMS_ADD_OP] if self.enable_rms_norm_custom_op else [torch.ops.aten.rsqrt]
|
||||
)
|
||||
return [
|
||||
torch.ops.vllm_ir.rms_norm,
|
||||
torch.ops.vllm_ir.fused_add_rms_norm.default,
|
||||
]
|
||||
|
||||
|
||||
def _run_fusion_test(
|
||||
@@ -383,17 +383,6 @@ def test_fusion_rmsnorm_quant(
|
||||
model.ops_in_model_before_partial(), fully_replaced=False
|
||||
)
|
||||
|
||||
# If RMSNorm custom op is disabled (native/torch impl used),
|
||||
# there's a risk that the fused add doesn't get included in the
|
||||
# replacement and only the rms part gets fused with quant.
|
||||
# Hence, we check only 2 add nodes are left (final fused rmsnorm add).
|
||||
if not enable_rms_norm_custom_op:
|
||||
n_add_nodes = lambda g: sum(1 for _ in find_op_nodes(torch.ops.aten.add, g))
|
||||
# rms_norm is IR, not included
|
||||
# 6 = 3x2 (3xRMS_ADD, 2 each)
|
||||
assert n_add_nodes(backend.graph_pre_pass) == 6
|
||||
assert n_add_nodes(backend.graph_post_pass) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("hidden_size", [256])
|
||||
|
||||
+33
-8
@@ -371,6 +371,8 @@ class HfRunner:
|
||||
is_cross_encoder: bool = False,
|
||||
skip_tokenizer_init: bool = False,
|
||||
auto_cls: type[_BaseAutoModelClass] = AutoModelForCausalLM,
|
||||
tokenizer_name: str | None = None,
|
||||
processor: Any | None = None,
|
||||
# Set this to avoid hanging issue
|
||||
default_torch_num_threads: int | None = None,
|
||||
) -> None:
|
||||
@@ -391,6 +393,8 @@ class HfRunner:
|
||||
is_cross_encoder=is_cross_encoder,
|
||||
skip_tokenizer_init=skip_tokenizer_init,
|
||||
auto_cls=auto_cls,
|
||||
tokenizer_name=tokenizer_name,
|
||||
processor=processor,
|
||||
)
|
||||
|
||||
def _init(
|
||||
@@ -405,6 +409,8 @@ class HfRunner:
|
||||
is_cross_encoder: bool = False,
|
||||
skip_tokenizer_init: bool = False,
|
||||
auto_cls: type[_BaseAutoModelClass] = AutoModelForCausalLM,
|
||||
tokenizer_name: str | None = None,
|
||||
processor: Any | None = None,
|
||||
) -> None:
|
||||
model_name = maybe_model_redirect(model_name)
|
||||
self.model_name = model_name
|
||||
@@ -484,20 +490,27 @@ class HfRunner:
|
||||
if not skip_tokenizer_init:
|
||||
self.tokenizer: "PreTrainedTokenizer | PreTrainedTokenizerFast" = (
|
||||
AutoTokenizer.from_pretrained(
|
||||
model_name,
|
||||
tokenizer_name or model_name,
|
||||
trust_remote_code=trust_remote_code,
|
||||
)
|
||||
)
|
||||
|
||||
# don't put this import at the top level
|
||||
# it will call torch.accelerator.device_count()
|
||||
from transformers import AutoProcessor
|
||||
if processor is not None:
|
||||
self.processor = processor
|
||||
else:
|
||||
# don't put this import at the top level
|
||||
# it will call torch.accelerator.device_count()
|
||||
from transformers import AutoProcessor
|
||||
|
||||
self.processor = AutoProcessor.from_pretrained(
|
||||
model_name,
|
||||
trust_remote_code=trust_remote_code,
|
||||
)
|
||||
self.processor = AutoProcessor.from_pretrained(
|
||||
model_name,
|
||||
trust_remote_code=trust_remote_code,
|
||||
)
|
||||
if skip_tokenizer_init:
|
||||
if self.processor is None:
|
||||
raise ValueError(
|
||||
"skip_tokenizer_init=True requires processor initialization."
|
||||
)
|
||||
self.tokenizer = self.processor.tokenizer
|
||||
|
||||
def get_inputs(
|
||||
@@ -520,6 +533,12 @@ class HfRunner:
|
||||
all_inputs: list[BatchFeature | BatchEncoding | dict[str, torch.Tensor]] = []
|
||||
for i, prompt in enumerate(prompts):
|
||||
if isinstance(prompt, str):
|
||||
if self.processor is None:
|
||||
raise RuntimeError(
|
||||
"HfRunner.processor is not initialized. "
|
||||
"Pass processor=... to HfRunner or set "
|
||||
"hf_model.processor before generation."
|
||||
)
|
||||
# Create a copy to avoid modifying the original dict
|
||||
processor_kwargs = (
|
||||
tokenization_kwargs.copy()
|
||||
@@ -617,6 +636,10 @@ class HfRunner:
|
||||
use_cache=True,
|
||||
**kwargs,
|
||||
)
|
||||
if self.processor is None:
|
||||
raise RuntimeError(
|
||||
"HfRunner.processor is not initialized; cannot decode output."
|
||||
)
|
||||
output_str = self.processor.batch_decode(
|
||||
output_ids,
|
||||
skip_special_tokens=True,
|
||||
@@ -973,6 +996,8 @@ class VllmRunner:
|
||||
req_sample_output_ids: list[list[int]] = []
|
||||
req_sample_output_strs: list[str] = []
|
||||
req_logprobs = []
|
||||
if req_output.prompt_logprobs:
|
||||
req_logprobs.extend(req_output.prompt_logprobs)
|
||||
for sample in req_output.outputs:
|
||||
output_str = sample.text
|
||||
output_ids = list(sample.token_ids)
|
||||
|
||||
@@ -10,10 +10,95 @@ Tests cover:
|
||||
|
||||
import math
|
||||
|
||||
import multiprocess as mp
|
||||
import pytest
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from vllm.config.parallel import ParallelConfig
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
from vllm.utils.system_utils import update_environment_variables
|
||||
|
||||
mp.set_start_method("spawn", force=True)
|
||||
|
||||
|
||||
class _FakeCPGroup:
|
||||
def __init__(self, world_size: int, device_group: dist.ProcessGroup):
|
||||
self.world_size = world_size
|
||||
self.device_group = device_group
|
||||
|
||||
|
||||
def _dtype_from_name(dtype_name: str) -> torch.dtype:
|
||||
return {
|
||||
"float16": torch.float16,
|
||||
"bfloat16": torch.bfloat16,
|
||||
"float32": torch.float32,
|
||||
}[dtype_name]
|
||||
|
||||
|
||||
def _packed_a2a_reference(
|
||||
cp_attn_out: torch.Tensor,
|
||||
cp_attn_lse: torch.Tensor,
|
||||
world_size: int,
|
||||
h_per_rank: int,
|
||||
is_lse_base_on_e: bool,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
from vllm.v1.attention.ops.dcp_alltoall import _lse_weighted_combine
|
||||
|
||||
B, _H, D = cp_attn_out.shape
|
||||
outputs = (
|
||||
cp_attn_out.view(B, world_size, h_per_rank, D)
|
||||
.permute(1, 0, 2, 3)
|
||||
.contiguous()
|
||||
.float()
|
||||
)
|
||||
lses = cp_attn_lse.view(B, world_size, h_per_rank).permute(1, 0, 2).contiguous()
|
||||
return _lse_weighted_combine(
|
||||
outputs,
|
||||
lses,
|
||||
return_lse=True,
|
||||
is_lse_base_on_e=is_lse_base_on_e,
|
||||
)
|
||||
|
||||
|
||||
def _assert_packed_a2a_close(
|
||||
actual: torch.Tensor,
|
||||
expected: torch.Tensor,
|
||||
dtype: torch.dtype,
|
||||
) -> None:
|
||||
if dtype == torch.float32:
|
||||
torch.testing.assert_close(actual, expected, rtol=1e-5, atol=1e-5)
|
||||
else:
|
||||
torch.testing.assert_close(
|
||||
actual.float(), expected.float(), rtol=3e-2, atol=3e-2
|
||||
)
|
||||
|
||||
|
||||
def _distributed_run(fn, world_size: int, extra_env: dict[str, str]) -> None:
|
||||
port = str(get_open_port())
|
||||
processes: list[mp.Process] = []
|
||||
for rank in range(world_size):
|
||||
env = {
|
||||
"RANK": str(rank),
|
||||
"LOCAL_RANK": str(rank),
|
||||
"WORLD_SIZE": str(world_size),
|
||||
"LOCAL_WORLD_SIZE": str(world_size),
|
||||
"MASTER_ADDR": "localhost",
|
||||
"MASTER_PORT": port,
|
||||
**extra_env,
|
||||
}
|
||||
process = mp.Process(target=fn, args=(env,))
|
||||
processes.append(process)
|
||||
process.start()
|
||||
|
||||
for process in processes:
|
||||
process.join(timeout=120)
|
||||
|
||||
for process in processes:
|
||||
if process.is_alive():
|
||||
process.kill()
|
||||
process.join()
|
||||
assert process.exitcode == 0
|
||||
|
||||
|
||||
class TestDCPCommBackendConfig:
|
||||
@@ -38,14 +123,14 @@ class TestDCPCommBackendConfig:
|
||||
"""A2A backend is valid when DCP > 1."""
|
||||
config = ParallelConfig(
|
||||
dcp_comm_backend="a2a",
|
||||
tensor_parallel_size=8,
|
||||
tensor_parallel_size=4,
|
||||
decode_context_parallel_size=4,
|
||||
)
|
||||
assert config.dcp_comm_backend == "a2a"
|
||||
|
||||
def test_invalid_backend_rejected(self):
|
||||
"""Invalid backend values are rejected."""
|
||||
with pytest.raises(ValueError, match="must be one of"):
|
||||
with pytest.raises(ValueError, match="must be one of|Input should be"):
|
||||
ParallelConfig(
|
||||
dcp_comm_backend="invalid",
|
||||
)
|
||||
@@ -134,7 +219,7 @@ class TestLSEWeightedCombine:
|
||||
result = _lse_weighted_combine(outputs, lses)
|
||||
|
||||
assert result.shape == (B, H, D)
|
||||
torch.testing.assert_close(result, outputs[1].squeeze(0), atol=1e-5, rtol=1e-5)
|
||||
torch.testing.assert_close(result, outputs[1], atol=1e-5, rtol=1e-5)
|
||||
|
||||
def test_mathematically_correct(self):
|
||||
"""Verify mathematical correctness of LSE combination."""
|
||||
@@ -187,6 +272,224 @@ class TestLSEWeightedCombine:
|
||||
assert global_lse.shape == (B, H)
|
||||
assert abs(global_lse.item() - expected_global_lse) < 1e-5
|
||||
|
||||
def test_base2_return_lse(self):
|
||||
"""Base-2 LSE mode returns log2-sum-exp2 global LSE."""
|
||||
from vllm.v1.attention.ops.dcp_alltoall import _lse_weighted_combine
|
||||
|
||||
outputs = torch.tensor(
|
||||
[
|
||||
[[[1.0, 2.0]]],
|
||||
[[[3.0, 4.0]]],
|
||||
]
|
||||
)
|
||||
lses = torch.tensor(
|
||||
[
|
||||
[[1.0]],
|
||||
[[2.0]],
|
||||
]
|
||||
)
|
||||
|
||||
result, global_lse = _lse_weighted_combine(
|
||||
outputs,
|
||||
lses,
|
||||
return_lse=True,
|
||||
is_lse_base_on_e=False,
|
||||
)
|
||||
|
||||
expected_global_lse = math.log2(2**1 + 2**2)
|
||||
w0 = 2**1 / (2**1 + 2**2)
|
||||
w1 = 2**2 / (2**1 + 2**2)
|
||||
expected = torch.tensor([[[w0 * 1.0 + w1 * 3.0, w0 * 2.0 + w1 * 4.0]]])
|
||||
|
||||
torch.testing.assert_close(result, expected, rtol=1e-5, atol=1e-5)
|
||||
torch.testing.assert_close(
|
||||
global_lse,
|
||||
torch.tensor([[expected_global_lse]]),
|
||||
rtol=1e-5,
|
||||
atol=1e-5,
|
||||
)
|
||||
|
||||
def test_lse_pack_dim(self):
|
||||
"""Packed A2A stores one fp32 LSE in output-dtype lanes."""
|
||||
from vllm.v1.attention.ops.dcp_alltoall import _dcp_a2a_lse_pack_dim
|
||||
|
||||
assert _dcp_a2a_lse_pack_dim(torch.bfloat16) == 2
|
||||
assert _dcp_a2a_lse_pack_dim(torch.float16) == 2
|
||||
assert _dcp_a2a_lse_pack_dim(torch.float32) == 1
|
||||
|
||||
|
||||
class TestPackedA2AKernels:
|
||||
@pytest.mark.skipif(
|
||||
torch.accelerator.device_count() < 1, reason="CUDA is required."
|
||||
)
|
||||
@pytest.mark.parametrize("dtype_name", ["float16", "bfloat16", "float32"])
|
||||
@pytest.mark.parametrize("return_lse", [False, True])
|
||||
@pytest.mark.parametrize("is_lse_base_on_e", [False, True])
|
||||
def test_pack_unpack_combine_matches_reference(
|
||||
self,
|
||||
dtype_name: str,
|
||||
return_lse: bool,
|
||||
is_lse_base_on_e: bool,
|
||||
):
|
||||
from vllm.v1.attention.ops.dcp_alltoall import (
|
||||
_dcp_a2a_lse_pack_dim,
|
||||
_dcp_a2a_pack_send,
|
||||
_dcp_a2a_unpack_combine,
|
||||
)
|
||||
|
||||
torch.manual_seed(0)
|
||||
dtype = _dtype_from_name(dtype_name)
|
||||
device = torch.device("cuda")
|
||||
world_size, B, h_per_rank, D = 4, 7, 2, 32
|
||||
H = world_size * h_per_rank
|
||||
cp_attn_out = torch.randn(B, H, D, device=device, dtype=dtype)
|
||||
cp_attn_lse = torch.randn(B, H, device=device, dtype=torch.float32)
|
||||
lse_pack_dim = _dcp_a2a_lse_pack_dim(dtype)
|
||||
send_buffer = torch.empty(
|
||||
(world_size, B, h_per_rank, D + lse_pack_dim),
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
_dcp_a2a_pack_send(
|
||||
cp_attn_out,
|
||||
cp_attn_lse,
|
||||
send_buffer,
|
||||
world_size,
|
||||
h_per_rank,
|
||||
D,
|
||||
lse_pack_dim,
|
||||
)
|
||||
actual = _dcp_a2a_unpack_combine(
|
||||
send_buffer, D, lse_pack_dim, return_lse, is_lse_base_on_e
|
||||
)
|
||||
expected_out, expected_lse = _packed_a2a_reference(
|
||||
cp_attn_out, cp_attn_lse, world_size, h_per_rank, is_lse_base_on_e
|
||||
)
|
||||
|
||||
if return_lse:
|
||||
actual_out, actual_lse = actual
|
||||
_assert_packed_a2a_close(actual_out, expected_out, dtype)
|
||||
torch.testing.assert_close(actual_lse, expected_lse, rtol=1e-4, atol=1e-4)
|
||||
else:
|
||||
_assert_packed_a2a_close(actual, expected_out, dtype)
|
||||
|
||||
|
||||
def _distributed_packed_a2a_worker(env: dict[str, str]) -> None:
|
||||
update_environment_variables(env)
|
||||
local_rank = int(env["LOCAL_RANK"])
|
||||
torch.accelerator.set_device_index(local_rank)
|
||||
dist.init_process_group(backend="nccl")
|
||||
use_workspace = env.get("USE_WORKSPACE") == "1"
|
||||
if use_workspace:
|
||||
from vllm.v1.worker.workspace import init_workspace_manager
|
||||
|
||||
init_workspace_manager(torch.device(f"cuda:{local_rank}"))
|
||||
try:
|
||||
from vllm.v1.attention.ops.dcp_alltoall import dcp_a2a_lse_reduce
|
||||
|
||||
dtype = _dtype_from_name(env["TEST_DTYPE"])
|
||||
return_lse = env["RETURN_LSE"] == "1"
|
||||
is_lse_base_on_e = env["LSE_BASE_E"] == "1"
|
||||
rank = dist.get_rank()
|
||||
world_size = dist.get_world_size()
|
||||
B, h_per_rank, D = 5, 2, 32
|
||||
H = world_size * h_per_rank
|
||||
|
||||
generator = torch.Generator(device=f"cuda:{local_rank}")
|
||||
generator.manual_seed(1234 + rank)
|
||||
cp_attn_out = torch.randn(
|
||||
B,
|
||||
H,
|
||||
D,
|
||||
device=f"cuda:{local_rank}",
|
||||
dtype=dtype,
|
||||
generator=generator,
|
||||
)
|
||||
cp_attn_lse = torch.randn(
|
||||
B,
|
||||
H,
|
||||
device=f"cuda:{local_rank}",
|
||||
dtype=torch.float32,
|
||||
generator=generator,
|
||||
)
|
||||
actual = dcp_a2a_lse_reduce(
|
||||
cp_attn_out,
|
||||
cp_attn_lse,
|
||||
_FakeCPGroup(world_size, dist.group.WORLD),
|
||||
return_lse=return_lse,
|
||||
is_lse_base_on_e=is_lse_base_on_e,
|
||||
)
|
||||
|
||||
gathered_out = [torch.empty_like(cp_attn_out) for _ in range(world_size)]
|
||||
gathered_lse = [torch.empty_like(cp_attn_lse) for _ in range(world_size)]
|
||||
dist.all_gather(gathered_out, cp_attn_out)
|
||||
dist.all_gather(gathered_lse, cp_attn_lse)
|
||||
outputs = torch.stack(
|
||||
[
|
||||
t[:, rank * h_per_rank : (rank + 1) * h_per_rank, :]
|
||||
for t in gathered_out
|
||||
],
|
||||
dim=0,
|
||||
).float()
|
||||
lses = torch.stack(
|
||||
[t[:, rank * h_per_rank : (rank + 1) * h_per_rank] for t in gathered_lse],
|
||||
dim=0,
|
||||
)
|
||||
from vllm.v1.attention.ops.dcp_alltoall import _lse_weighted_combine
|
||||
|
||||
expected_out, expected_lse = _lse_weighted_combine(
|
||||
outputs,
|
||||
lses,
|
||||
return_lse=True,
|
||||
is_lse_base_on_e=is_lse_base_on_e,
|
||||
)
|
||||
|
||||
if return_lse:
|
||||
actual_out, actual_lse = actual
|
||||
_assert_packed_a2a_close(actual_out, expected_out, dtype)
|
||||
torch.testing.assert_close(actual_lse, expected_lse, rtol=1e-4, atol=1e-4)
|
||||
else:
|
||||
_assert_packed_a2a_close(actual, expected_out, dtype)
|
||||
finally:
|
||||
if use_workspace:
|
||||
from vllm.v1.worker.workspace import reset_workspace_manager
|
||||
|
||||
reset_workspace_manager()
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
torch.accelerator.device_count() < 4, reason="Need at least 4 GPUs."
|
||||
)
|
||||
@pytest.mark.parametrize("dtype_name", ["float16", "bfloat16", "float32"])
|
||||
def test_distributed_packed_a2a_matches_reference(dtype_name: str):
|
||||
_distributed_run(
|
||||
_distributed_packed_a2a_worker,
|
||||
world_size=4,
|
||||
extra_env={
|
||||
"TEST_DTYPE": dtype_name,
|
||||
"RETURN_LSE": "1",
|
||||
"LSE_BASE_E": "1",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
torch.accelerator.device_count() < 4, reason="Need at least 4 GPUs."
|
||||
)
|
||||
def test_distributed_packed_a2a_with_workspace_matches_reference():
|
||||
_distributed_run(
|
||||
_distributed_packed_a2a_worker,
|
||||
world_size=4,
|
||||
extra_env={
|
||||
"TEST_DTYPE": "bfloat16",
|
||||
"RETURN_LSE": "1",
|
||||
"LSE_BASE_E": "1",
|
||||
"USE_WORKSPACE": "1",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
@@ -333,8 +333,6 @@ def test_attention_config():
|
||||
"true",
|
||||
"--attention-config.flash_attn_max_num_splits_for_cuda_graph",
|
||||
"16",
|
||||
"--attention-config.use_cudnn_prefill",
|
||||
"true",
|
||||
"--attention-config.use_trtllm_ragged_deepseek_prefill",
|
||||
"true",
|
||||
"--attention-config.use_trtllm_attention",
|
||||
@@ -352,7 +350,6 @@ def test_attention_config():
|
||||
assert engine_args.attention_config.flash_attn_version == 3
|
||||
assert engine_args.attention_config.use_prefill_decode_attention is True
|
||||
assert engine_args.attention_config.flash_attn_max_num_splits_for_cuda_graph == 16
|
||||
assert engine_args.attention_config.use_cudnn_prefill is True
|
||||
assert engine_args.attention_config.use_trtllm_ragged_deepseek_prefill is True
|
||||
assert engine_args.attention_config.use_trtllm_attention is True
|
||||
assert engine_args.attention_config.disable_flashinfer_prefill is True
|
||||
|
||||
@@ -11,7 +11,9 @@ from vllm import LLM, SamplingParams
|
||||
|
||||
def _make_mock_llm() -> LLM:
|
||||
llm = object.__new__(LLM)
|
||||
llm.model_config = SimpleNamespace(runner_type="generate")
|
||||
llm.model_config = SimpleNamespace(
|
||||
runner_type="generate", enable_prompt_embeds=False
|
||||
)
|
||||
return llm
|
||||
|
||||
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""E2E test for mixing `prompt_embeds` with `audio_embeds` in a single
|
||||
Chat Completions request."""
|
||||
|
||||
import json
|
||||
|
||||
import openai
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import safetensors
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from huggingface_hub import hf_hub_download
|
||||
from transformers import AutoConfig, AutoTokenizer
|
||||
|
||||
from tests.utils import RemoteOpenAIServer
|
||||
from vllm.utils.serial_utils import tensor2base64
|
||||
|
||||
QWEN2AUDIO_MODEL = "Qwen/Qwen2-Audio-7B-Instruct"
|
||||
|
||||
# Use the model's native dtype to avoid an implicit cast inside
|
||||
# `safe_load_prompt_embeds` (mismatched floating-point dtypes are cast to the
|
||||
# model's dtype automatically, matching here just skips the conversion).
|
||||
QWEN2AUDIO_DTYPE = torch.bfloat16
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def qwen2audio_server_args() -> list[str]:
|
||||
return [
|
||||
"--dtype",
|
||||
"bfloat16",
|
||||
"--max-model-len",
|
||||
"2048",
|
||||
"--max-num-seqs",
|
||||
"4",
|
||||
"--enforce-eager",
|
||||
"--trust-remote-code",
|
||||
"--gpu-memory-utilization",
|
||||
"0.85",
|
||||
"--limit-mm-per-prompt",
|
||||
json.dumps({"audio": 1}),
|
||||
"--enable-prompt-embeds",
|
||||
"--enable-mm-embeds",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def qwen2audio_server(qwen2audio_server_args):
|
||||
with RemoteOpenAIServer(
|
||||
QWEN2AUDIO_MODEL,
|
||||
qwen2audio_server_args,
|
||||
max_wait_seconds=600,
|
||||
) as remote_server:
|
||||
yield remote_server
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def qwen2audio_client(qwen2audio_server):
|
||||
async with qwen2audio_server.get_async_client() as async_client:
|
||||
yield async_client
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def qwen2audio_hidden_size() -> int:
|
||||
config = AutoConfig.from_pretrained(QWEN2AUDIO_MODEL, trust_remote_code=True)
|
||||
return config.text_config.hidden_size
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def qwen2audio_prompt_embeds_b64(qwen2audio_hidden_size: int) -> str:
|
||||
tensor = torch.randn(4, qwen2audio_hidden_size, dtype=QWEN2AUDIO_DTYPE)
|
||||
return tensor2base64(tensor)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def qwen2audio_audio_embeds_b64(qwen2audio_hidden_size: int) -> str:
|
||||
# Shape matches the `audio_embeds` unit-test fixture.
|
||||
torch.manual_seed(0)
|
||||
tensor = torch.randn(1, 128, qwen2audio_hidden_size, dtype=QWEN2AUDIO_DTYPE)
|
||||
return tensor2base64(tensor)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_embeds_plus_audio_embeds(
|
||||
qwen2audio_client: openai.AsyncOpenAI,
|
||||
qwen2audio_prompt_embeds_b64: str,
|
||||
qwen2audio_audio_embeds_b64: str,
|
||||
):
|
||||
"""Single user message carrying both prompt_embeds and audio_embeds parts."""
|
||||
chat = await qwen2audio_client.chat.completions.create(
|
||||
model=QWEN2AUDIO_MODEL,
|
||||
max_tokens=5,
|
||||
temperature=0.0,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "prompt_embeds",
|
||||
"data": qwen2audio_prompt_embeds_b64,
|
||||
},
|
||||
{
|
||||
"type": "audio_embeds",
|
||||
"audio_embeds": qwen2audio_audio_embeds_b64,
|
||||
},
|
||||
{"type": "text", "text": "Continue."},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
assert chat.choices[0].message.content is not None
|
||||
assert len(chat.choices[0].message.content) > 0
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def qwen2audio_aligned_content_and_embeds_b64() -> tuple[str, str]:
|
||||
"""Return `(content, base64_embeds)` where the embeddings are the model's
|
||||
embedding of `content` tokenized WITHOUT special tokens.
|
||||
|
||||
Loads only the `embed_tokens` shard from disk on CPU (~1.1 GB of host
|
||||
RAM) instead of the full 7B model on GPU.
|
||||
"""
|
||||
content = "Describe this audio."
|
||||
tokenizer = AutoTokenizer.from_pretrained(QWEN2AUDIO_MODEL, trust_remote_code=True)
|
||||
|
||||
index_path = hf_hub_download(QWEN2AUDIO_MODEL, "model.safetensors.index.json")
|
||||
with open(index_path) as f:
|
||||
weight_map = json.load(f)["weight_map"]
|
||||
embed_key = next(k for k in weight_map if k.endswith("embed_tokens.weight"))
|
||||
shard_path = hf_hub_download(QWEN2AUDIO_MODEL, weight_map[embed_key])
|
||||
with safetensors.safe_open(shard_path, framework="pt", device="cpu") as f:
|
||||
embed_weight = f.get_tensor(embed_key)
|
||||
embed_layer = nn.Embedding.from_pretrained(embed_weight.to(QWEN2AUDIO_DTYPE))
|
||||
|
||||
ids = tokenizer(content, add_special_tokens=False, return_tensors="pt").input_ids
|
||||
embeds = embed_layer(ids).squeeze(0)
|
||||
return content, tensor2base64(embeds)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"audio_first",
|
||||
[True, False],
|
||||
ids=["audio_embeds-then-text", "text-then-audio_embeds"],
|
||||
)
|
||||
async def test_text_content_and_prompt_embeds_match_with_audio_embeds(
|
||||
qwen2audio_client: openai.AsyncOpenAI,
|
||||
qwen2audio_audio_embeds_b64: str,
|
||||
qwen2audio_aligned_content_and_embeds_b64: tuple[str, str],
|
||||
audio_first: bool,
|
||||
):
|
||||
"""Same content as text vs `prompt_embeds` should yield identical Chat
|
||||
Completions output when mixed with `audio_embeds` in the same message.
|
||||
"""
|
||||
content, encoded_text_embeds = qwen2audio_aligned_content_and_embeds_b64
|
||||
|
||||
audio_part = {
|
||||
"type": "audio_embeds",
|
||||
"audio_embeds": qwen2audio_audio_embeds_b64,
|
||||
}
|
||||
text_part = {"type": "text", "text": content}
|
||||
embeds_part = {"type": "prompt_embeds", "data": encoded_text_embeds}
|
||||
|
||||
if audio_first:
|
||||
text_content = [audio_part, text_part]
|
||||
embeds_content = [audio_part, embeds_part]
|
||||
else:
|
||||
text_content = [text_part, audio_part]
|
||||
embeds_content = [embeds_part, audio_part]
|
||||
|
||||
text_resp = await qwen2audio_client.chat.completions.create(
|
||||
model=QWEN2AUDIO_MODEL,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[{"role": "user", "content": text_content}],
|
||||
)
|
||||
embeds_resp = await qwen2audio_client.chat.completions.create(
|
||||
model=QWEN2AUDIO_MODEL,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[{"role": "user", "content": embeds_content}],
|
||||
)
|
||||
|
||||
text_out = text_resp.choices[0].message.content
|
||||
embeds_out = embeds_resp.choices[0].message.content
|
||||
assert text_out is not None and len(text_out) > 0
|
||||
assert embeds_out is not None and len(embeds_out) > 0
|
||||
assert text_out == embeds_out
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""E2E tests for mixing `prompt_embeds` with image content parts in a single
|
||||
Chat Completions request.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import openai
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import safetensors
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from huggingface_hub import hf_hub_download
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from tests.utils import RemoteOpenAIServer
|
||||
from vllm.assets.image import ImageAsset
|
||||
from vllm.multimodal.utils import encode_image_url
|
||||
from vllm.utils.serial_utils import tensor2base64
|
||||
|
||||
MODEL_NAME = "Qwen/Qwen2-VL-2B-Instruct"
|
||||
|
||||
# Use the model's native dtype to skip the implicit cast inside
|
||||
# `safe_load_prompt_embeds` (mismatched floating-point dtypes are cast to the
|
||||
# model's dtype automatically).
|
||||
MODEL_DTYPE = torch.bfloat16
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def server_args() -> list[str]:
|
||||
return [
|
||||
"--dtype",
|
||||
"bfloat16",
|
||||
"--max-model-len",
|
||||
"2048",
|
||||
"--max-num-seqs",
|
||||
"4",
|
||||
"--enforce-eager",
|
||||
"--gpu-memory-utilization",
|
||||
"0.4",
|
||||
"--limit-mm-per-prompt",
|
||||
json.dumps({"image": 1}),
|
||||
"--enable-prompt-embeds",
|
||||
"--enable-mm-embeds",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def server(server_args):
|
||||
with RemoteOpenAIServer(
|
||||
MODEL_NAME,
|
||||
server_args,
|
||||
max_wait_seconds=600,
|
||||
) as remote_server:
|
||||
yield remote_server
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(server):
|
||||
async with server.get_async_client() as async_client:
|
||||
yield async_client
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def image_url() -> str:
|
||||
"""Stable real image as a data URL, kept identical across both the
|
||||
text and prompt_embeds requests so any output difference must come from
|
||||
how the text content is delivered."""
|
||||
return encode_image_url(ImageAsset("stop_sign").pil_image)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def aligned_content_and_embeds_b64() -> tuple[str, str]:
|
||||
"""`(content, base64_embeds)` where the embeddings are the model's
|
||||
embedding of `content` tokenized WITHOUT special tokens.
|
||||
|
||||
Loads only the `embed_tokens` shard from disk on CPU instead of the full
|
||||
model on GPU, so the fixture has zero VRAM footprint and won't contend
|
||||
with the running vLLM server.
|
||||
"""
|
||||
content = "Describe this image."
|
||||
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
|
||||
|
||||
index_path = hf_hub_download(MODEL_NAME, "model.safetensors.index.json")
|
||||
with open(index_path) as f:
|
||||
weight_map = json.load(f)["weight_map"]
|
||||
embed_key = next(k for k in weight_map if k.endswith("embed_tokens.weight"))
|
||||
shard_path = hf_hub_download(MODEL_NAME, weight_map[embed_key])
|
||||
with safetensors.safe_open(shard_path, framework="pt", device="cpu") as f:
|
||||
embed_weight = f.get_tensor(embed_key)
|
||||
embed_layer = nn.Embedding.from_pretrained(embed_weight.to(MODEL_DTYPE))
|
||||
|
||||
ids = tokenizer(content, add_special_tokens=False, return_tensors="pt").input_ids
|
||||
embeds = embed_layer(ids).squeeze(0)
|
||||
return content, tensor2base64(embeds)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"image_first",
|
||||
[True, False],
|
||||
ids=["image_url-then-text", "text-then-image_url"],
|
||||
)
|
||||
async def test_text_content_and_prompt_embeds_match_with_image_url(
|
||||
client: openai.AsyncOpenAI,
|
||||
image_url: str,
|
||||
aligned_content_and_embeds_b64: tuple[str, str],
|
||||
image_first: bool,
|
||||
):
|
||||
"""Same content as text vs `prompt_embeds` should yield identical Chat
|
||||
Completions output when mixed with an `image_url` part in the same
|
||||
message under greedy decoding.
|
||||
"""
|
||||
content, encoded_text_embeds = aligned_content_and_embeds_b64
|
||||
|
||||
image_part = {"type": "image_url", "image_url": {"url": image_url}}
|
||||
text_part = {"type": "text", "text": content}
|
||||
embeds_part = {"type": "prompt_embeds", "data": encoded_text_embeds}
|
||||
|
||||
if image_first:
|
||||
text_content = [image_part, text_part]
|
||||
embeds_content = [image_part, embeds_part]
|
||||
else:
|
||||
text_content = [text_part, image_part]
|
||||
embeds_content = [embeds_part, image_part]
|
||||
|
||||
text_resp = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[{"role": "user", "content": text_content}],
|
||||
)
|
||||
embeds_resp = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[{"role": "user", "content": embeds_content}],
|
||||
)
|
||||
|
||||
text_out = text_resp.choices[0].message.content
|
||||
embeds_out = embeds_resp.choices[0].message.content
|
||||
assert text_out is not None and len(text_out) > 0
|
||||
assert embeds_out is not None and len(embeds_out) > 0
|
||||
assert text_out == embeds_out
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def image_embeds_b64() -> dict[str, str]:
|
||||
"""Synthetic but stable `image_embeds` for Qwen2-VL."""
|
||||
grid = (1, 4, 4)
|
||||
spatial_merge_size = 2
|
||||
num_patches = (grid[1] // spatial_merge_size) * (grid[2] // spatial_merge_size)
|
||||
text_hidden_size = 1536 # Qwen2-VL-2B
|
||||
torch.manual_seed(0)
|
||||
return {
|
||||
"image_embeds": tensor2base64(
|
||||
torch.randn(num_patches, text_hidden_size, dtype=MODEL_DTYPE)
|
||||
),
|
||||
"image_grid_thw": tensor2base64(torch.tensor(grid)),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"image_first",
|
||||
[True, False],
|
||||
ids=["image_embeds-then-text", "text-then-image_embeds"],
|
||||
)
|
||||
async def test_text_content_and_prompt_embeds_match_with_image_embeds(
|
||||
client: openai.AsyncOpenAI,
|
||||
image_embeds_b64: dict[str, str],
|
||||
aligned_content_and_embeds_b64: tuple[str, str],
|
||||
image_first: bool,
|
||||
):
|
||||
"""Same content as text vs `prompt_embeds` should yield identical Chat
|
||||
Completions output when mixed with a precomputed `image_embeds` part in
|
||||
the same message under greedy decoding.
|
||||
"""
|
||||
content, encoded_text_embeds = aligned_content_and_embeds_b64
|
||||
|
||||
image_part = {"type": "image_embeds", "image_embeds": image_embeds_b64}
|
||||
text_part = {"type": "text", "text": content}
|
||||
embeds_part = {"type": "prompt_embeds", "data": encoded_text_embeds}
|
||||
|
||||
if image_first:
|
||||
text_content = [image_part, text_part]
|
||||
embeds_content = [image_part, embeds_part]
|
||||
else:
|
||||
text_content = [text_part, image_part]
|
||||
embeds_content = [embeds_part, image_part]
|
||||
|
||||
text_resp = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[{"role": "user", "content": text_content}],
|
||||
)
|
||||
embeds_resp = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[{"role": "user", "content": embeds_content}],
|
||||
)
|
||||
|
||||
text_out = text_resp.choices[0].message.content
|
||||
embeds_out = embeds_resp.choices[0].message.content
|
||||
assert text_out is not None and len(text_out) > 0
|
||||
assert embeds_out is not None and len(embeds_out) > 0
|
||||
assert text_out == embeds_out
|
||||
@@ -0,0 +1,293 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""E2E tests for `prompt_embeds` content parts in the Chat Completions API."""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
|
||||
import openai
|
||||
import pybase64 as base64
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import torch
|
||||
from openai import BadRequestError
|
||||
|
||||
from tests.utils import VLLM_PATH, RemoteOpenAIServer
|
||||
|
||||
MODEL_NAME = "facebook/opt-125m"
|
||||
CHAT_TEMPLATE = VLLM_PATH / "examples/template_chatml.jinja"
|
||||
# Matches `--dtype` in `server_args` to avoid an implicit cast in
|
||||
# `safe_load_prompt_embeds` (mismatched floating-point dtypes are cast to the
|
||||
# model's dtype automatically, we match here just to skip the conversion).
|
||||
SERVER_DTYPE: torch.dtype = torch.bfloat16
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def server_args() -> list[str]:
|
||||
return [
|
||||
"--dtype",
|
||||
"bfloat16",
|
||||
"--max-model-len",
|
||||
"2048",
|
||||
"--max-num-seqs",
|
||||
"128",
|
||||
"--enforce-eager",
|
||||
"--chat-template",
|
||||
str(CHAT_TEMPLATE),
|
||||
# Prompt Embeds server args
|
||||
"--enable-prompt-embeds",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def server(server_args):
|
||||
with RemoteOpenAIServer(MODEL_NAME, server_args) as remote_server:
|
||||
yield remote_server
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(server):
|
||||
async with server.get_async_client() as async_client:
|
||||
yield async_client
|
||||
|
||||
|
||||
def _encode_embeds(embeds: torch.Tensor) -> str:
|
||||
buf = io.BytesIO()
|
||||
torch.save(embeds, buf)
|
||||
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prompt_embeds_b64(hf_runner) -> list[str]:
|
||||
"""Pre-compute embeddings for two short prompts and return as base64."""
|
||||
prompts = ["Hello, my name is", "What is an LLM?"]
|
||||
with hf_runner(MODEL_NAME) as hf_model:
|
||||
embeddings = hf_model.get_prompt_embeddings(prompts)
|
||||
# Cast to the server's dtype so `safe_load_prompt_embeds` doesn't need to
|
||||
# convert on its own, the function accepts any floating-point dtype and
|
||||
# will cast to the model's dtype, but matching up front skips the work.
|
||||
return [_encode_embeds(e.to(SERVER_DTYPE)) for e in embeddings]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_prompt_embeds_part(
|
||||
client: openai.AsyncOpenAI,
|
||||
prompt_embeds_b64: list[str],
|
||||
):
|
||||
"""A user message with one prompt_embeds part + text."""
|
||||
b64 = prompt_embeds_b64[0]
|
||||
chat = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=5,
|
||||
temperature=0.0,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "prompt_embeds", "data": b64},
|
||||
{"type": "text", "text": "Continue:"},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
assert chat.choices[0].message.content is not None
|
||||
assert len(chat.choices[0].message.content) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_prompt_embeds_parts(
|
||||
client: openai.AsyncOpenAI,
|
||||
prompt_embeds_b64: list[str],
|
||||
):
|
||||
"""Multiple prompt_embeds parts in a single message."""
|
||||
b64_a, b64_b = prompt_embeds_b64
|
||||
chat = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=5,
|
||||
temperature=0.0,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "prompt_embeds", "data": b64_a},
|
||||
{"type": "text", "text": " and "},
|
||||
{"type": "prompt_embeds", "data": b64_b},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
assert chat.choices[0].message.content is not None
|
||||
assert len(chat.choices[0].message.content) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_message_conversation(
|
||||
client: openai.AsyncOpenAI,
|
||||
prompt_embeds_b64: list[str],
|
||||
):
|
||||
"""prompt_embeds in both system and user messages."""
|
||||
b64_sys, b64_usr = prompt_embeds_b64
|
||||
chat = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=5,
|
||||
temperature=0.0,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"type": "text", "text": "You are helpful."},
|
||||
{"type": "prompt_embeds", "data": b64_sys},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "prompt_embeds", "data": b64_usr},
|
||||
{"type": "text", "text": "Summarize."},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
assert chat.choices[0].message.content is not None
|
||||
assert len(chat.choices[0].message.content) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming(
|
||||
client: openai.AsyncOpenAI,
|
||||
prompt_embeds_b64: list[str],
|
||||
):
|
||||
"""Streaming chat completion with prompt_embeds."""
|
||||
b64 = prompt_embeds_b64[0]
|
||||
|
||||
# Non-streaming baseline.
|
||||
baseline = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=5,
|
||||
temperature=0.0,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "prompt_embeds", "data": b64},
|
||||
{"type": "text", "text": "Continue:"},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
expected = baseline.choices[0].message.content
|
||||
|
||||
# Streaming.
|
||||
stream = await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=5,
|
||||
temperature=0.0,
|
||||
stream=True,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "prompt_embeds", "data": b64},
|
||||
{"type": "text", "text": "Continue:"},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
chunks: list[str] = []
|
||||
async for chunk in stream:
|
||||
delta = chunk.choices[0].delta.content
|
||||
if delta:
|
||||
chunks.append(delta)
|
||||
assert "".join(chunks) == expected
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def aligned_content_and_embeds_b64(hf_runner) -> tuple[str, str]:
|
||||
"""Return `(content, base64_embeds)` where the embeddings are the model's
|
||||
embedding of `content` tokenized WITHOUT special tokens.
|
||||
"""
|
||||
content = "Hello, my name is"
|
||||
with hf_runner(MODEL_NAME) as hf_model:
|
||||
ids = hf_model.tokenizer(
|
||||
content, add_special_tokens=False, return_tensors="pt"
|
||||
).input_ids
|
||||
ids = hf_model.wrap_device({"input_ids": ids})["input_ids"]
|
||||
embed_layer = hf_model.model.get_input_embeddings()
|
||||
embeds = embed_layer(ids).squeeze(0).to(SERVER_DTYPE).cpu()
|
||||
return content, _encode_embeds(embeds)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_text_content_and_prompt_embeds_match(
|
||||
client: openai.AsyncOpenAI,
|
||||
aligned_content_and_embeds_b64: tuple[str, str],
|
||||
):
|
||||
"""Equal content in text and `prompt_embeds` should yield identical
|
||||
Chat Completions output under greedy decoding.
|
||||
"""
|
||||
content, encoded_embeds = aligned_content_and_embeds_b64
|
||||
|
||||
text_resp, embeds_resp = await asyncio.gather(
|
||||
client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[{"role": "user", "content": content}],
|
||||
),
|
||||
client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=10,
|
||||
temperature=0.0,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "prompt_embeds", "data": encoded_embeds}],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
text_out = text_resp.choices[0].message.content
|
||||
embeds_out = embeds_resp.choices[0].message.content
|
||||
assert text_out is not None and len(text_out) > 0
|
||||
assert embeds_out is not None and len(embeds_out) > 0
|
||||
assert text_out == embeds_out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_data_field(
|
||||
client: openai.AsyncOpenAI,
|
||||
):
|
||||
"""A prompt_embeds part without `data` should return a clear error."""
|
||||
with pytest.raises(BadRequestError):
|
||||
await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=5,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "prompt_embeds"}],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_base64(
|
||||
client: openai.AsyncOpenAI,
|
||||
):
|
||||
"""Invalid base64 in the `data` field should return a clear error."""
|
||||
with pytest.raises(BadRequestError):
|
||||
await client.chat.completions.create(
|
||||
model=MODEL_NAME,
|
||||
max_tokens=5,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "prompt_embeds", "data": "not_valid_base64!!"},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
@@ -538,6 +538,7 @@ class MockModelConfig:
|
||||
is_encoder_decoder: bool = False
|
||||
is_multimodal_model: bool = False
|
||||
renderer_num_workers: int = 1
|
||||
enable_prompt_embeds: bool = False
|
||||
|
||||
def get_diff_sampling_param(self):
|
||||
return self.diff_sampling_param or {}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import importlib.util
|
||||
|
||||
import numpy as np
|
||||
import pybase64 as base64
|
||||
import pytest
|
||||
@@ -10,7 +12,16 @@ import torch
|
||||
from tests.utils import RemoteOpenAIServer
|
||||
from vllm.utils.serial_utils import tensor2base64
|
||||
|
||||
# Prithvi requires terratorch, which is temporarily unavailable while PyPI has
|
||||
# `lightning` quarantined (#41376). Skip just the Prithvi case; leave the
|
||||
# Qwen3-VL case in the same file untouched.
|
||||
_TERRATORCH_AVAILABLE = importlib.util.find_spec("terratorch") is not None
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _TERRATORCH_AVAILABLE,
|
||||
reason="terratorch unavailable while PyPI has `lightning` quarantined; see #41376",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"model_name", ["ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL-Sen1Floods11"]
|
||||
)
|
||||
|
||||
@@ -62,6 +62,8 @@ def test_load_prompt_embeds(
|
||||
):
|
||||
model_config = Mock(spec=ModelConfig)
|
||||
model_config.enable_prompt_embeds = True
|
||||
model_config.get_hidden_size.return_value = hidden_size
|
||||
model_config.dtype = dtype
|
||||
|
||||
# construct arbitrary tensors of various dtypes, layouts, and sizes.
|
||||
# We need to check against different layouts to make sure that if a user
|
||||
|
||||
@@ -26,6 +26,10 @@ from vllm.tokenizers import get_tokenizer
|
||||
from ....models.registry import HF_EXAMPLE_MODELS
|
||||
from ....utils import RemoteOpenAIServer
|
||||
|
||||
# Tuned to prevent OOM on 18GB GPUs in transcription correctness tests.
|
||||
MAX_SEQS_FOR_TRANSCRIPTION_TEST = 8
|
||||
GPU_UTIL_FOR_TRANSCRIPTION_TEST = 0.5
|
||||
|
||||
|
||||
def to_bytes(y, sr):
|
||||
buffer = io.BytesIO()
|
||||
@@ -167,9 +171,8 @@ def run_evaluation(
|
||||
"model_config",
|
||||
[
|
||||
("openai/whisper-large-v3", 12.744980),
|
||||
# TODO (ekagra): turn on after asr release
|
||||
# CohereASR is used to test the variable encoder length code paths
|
||||
# ("CohereLabs/cohere-transcribe-03-2026", 11.92),
|
||||
("CohereLabs/cohere-transcribe-03-2026", 11.92),
|
||||
],
|
||||
)
|
||||
# Original dataset is 20GB+ in size, hence we use a pre-filtered slice.
|
||||
@@ -185,6 +188,8 @@ def test_wer_correctness(
|
||||
server_args = [
|
||||
"--enforce-eager",
|
||||
f"--tokenizer_mode={model_info.tokenizer_mode}",
|
||||
f"--max_num_seqs={MAX_SEQS_FOR_TRANSCRIPTION_TEST}",
|
||||
f"--gpu_memory_utilization={GPU_UTIL_FOR_TRANSCRIPTION_TEST}",
|
||||
]
|
||||
if model_info.trust_remote_code:
|
||||
server_args.append("--trust-remote-code")
|
||||
|
||||
@@ -6,7 +6,9 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import vllm.envs as envs
|
||||
from vllm.entrypoints.openai.engine.serving import GenerationError, OpenAIServing
|
||||
from vllm.envs import disable_envs_cache
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -60,3 +62,35 @@ async def test_convert_generation_error_to_streaming_response():
|
||||
assert isinstance(error_json, str)
|
||||
assert "Internal server error" in error_json
|
||||
assert "InternalServerError" in error_json
|
||||
|
||||
|
||||
def test_is_model_supported_skip_name_validation_env(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When VLLM_SKIP_MODEL_NAME_VALIDATION is set, accept any model id."""
|
||||
disable_envs_cache()
|
||||
monkeypatch.delenv("VLLM_SKIP_MODEL_NAME_VALIDATION", raising=False)
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.model_config = MagicMock()
|
||||
mock_engine.model_config.max_model_len = 100
|
||||
mock_models = MagicMock()
|
||||
mock_models.is_base_model.return_value = False
|
||||
|
||||
serving = OpenAIServing(
|
||||
engine_client=mock_engine,
|
||||
models=mock_models,
|
||||
request_logger=None,
|
||||
)
|
||||
|
||||
assert serving._is_model_supported("not-a-registered-model") is False
|
||||
|
||||
monkeypatch.setenv("VLLM_SKIP_MODEL_NAME_VALIDATION", "1")
|
||||
disable_envs_cache()
|
||||
assert envs.VLLM_SKIP_MODEL_NAME_VALIDATION is True
|
||||
assert serving._is_model_supported("not-a-registered-model") is True
|
||||
|
||||
monkeypatch.setenv("VLLM_SKIP_MODEL_NAME_VALIDATION", "true")
|
||||
disable_envs_cache()
|
||||
assert envs.VLLM_SKIP_MODEL_NAME_VALIDATION is True
|
||||
assert serving._is_model_supported("another-alias") is True
|
||||
|
||||
@@ -38,6 +38,15 @@ BACKEND_TOL: dict[str, float] = {
|
||||
"FLEX_ATTENTION": 0.045, # gfx950:~3.25%, gfx942:~1.10%
|
||||
}
|
||||
|
||||
# ROCm 7.2/gfx950 shows small absolute drift on the low text-vs-text
|
||||
# probability even though larger scores remain well inside the relative
|
||||
# tolerance. Keep the relative tolerances tight and add only a small floor.
|
||||
BACKEND_ABS_TOL: dict[str, float] = {
|
||||
"default": 0.0,
|
||||
"ROCM_AITER_FA": 0.005,
|
||||
"FLEX_ATTENTION": 0.006,
|
||||
}
|
||||
|
||||
# ROCm: disable skinny GEMM to avoid non-deterministic results from
|
||||
# atomic reductions in wvSplitKrc kernel.
|
||||
# See: https://github.com/vllm-project/vllm/pull/33493#issuecomment-3906083975
|
||||
@@ -57,18 +66,23 @@ def get_tol(backend: str) -> float:
|
||||
return BACKEND_TOL.get(backend, BACKEND_TOL["default"])
|
||||
|
||||
|
||||
def get_abs_tol(backend: str) -> float:
|
||||
return BACKEND_ABS_TOL.get(backend, BACKEND_ABS_TOL["default"])
|
||||
|
||||
|
||||
def assert_score(actual: float, expected: float, backend: str, label: str):
|
||||
tol = get_tol(backend)
|
||||
abs_tol = get_abs_tol(backend)
|
||||
diff = abs(actual - expected)
|
||||
rel_diff = diff / abs(expected) if expected != 0 else diff
|
||||
print(
|
||||
f"[{backend}] {label}: actual={actual:.6f} expected={expected:.6f} "
|
||||
f"diff={diff:.6f} rel_diff={rel_diff:.4f} tol={tol}"
|
||||
f"diff={diff:.6f} rel_diff={rel_diff:.4f} tol={tol} abs_tol={abs_tol}"
|
||||
)
|
||||
assert actual == pytest.approx(expected, rel=tol), (
|
||||
assert actual == pytest.approx(expected, rel=tol, abs=abs_tol), (
|
||||
f"[{backend}] {label}: score mismatch — "
|
||||
f"actual={actual:.6f}, expected={expected:.6f}, "
|
||||
f"rel_diff={rel_diff:.4f}, tol={tol}"
|
||||
f"rel_diff={rel_diff:.4f}, tol={tol}, abs_tol={abs_tol}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# MRCR Long-Context Accuracy Evaluation
|
||||
|
||||
Smoke test for long-context behavior using OpenAI's public [`openai/mrcr`](https://huggingface.co/datasets/openai/mrcr) dataset. The model sees a long chat with several near-duplicate "needles" and must reproduce a specific earlier assistant turn verbatim, prepended with a random anti-guessing string.
|
||||
|
||||
**Scoring:** if the response doesn't start with `random_string_to_prepend`, score is 0; otherwise the prefix is stripped and the mean `SequenceMatcher.ratio()` against the reference answer is reported.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Pytest (spawns the server)
|
||||
pytest -s -v tests/evals/mrcr/test_mrcr_correctness.py \
|
||||
--config-list-file=configs/models-small.txt
|
||||
|
||||
# Standalone (server already running; model and context auto-discovered)
|
||||
vllm serve Qwen/Qwen3-0.6B --reasoning-parser qwen3 --port 8000
|
||||
python tests/evals/mrcr/mrcr_eval.py --port 8000
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
model_name: "Qwen/Qwen3-0.6B"
|
||||
# Per-needle thresholds catch bucket-specific regressions (sliding window,
|
||||
# chunked prefill, prefix cache) that an aggregate can hide. A scalar
|
||||
# (e.g. `match_ratio_threshold: 0.20`) is also accepted and checked against
|
||||
# the mean match ratio.
|
||||
match_ratio_threshold:
|
||||
2: 0.30
|
||||
4: 0.15
|
||||
8: 0.10
|
||||
num_samples: 30
|
||||
needles: [2, 4, 8]
|
||||
# max_prompt_tokens: 32768 # Optional; defaults to server max_model_len - max_tokens - 256
|
||||
max_tokens: 2048
|
||||
concurrency: 8
|
||||
server_args: "--max-model-len 32768 --reasoning-parser qwen3"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Samples stream from three parquet shards (`{N}needle/{N}needle_0.parquet`); only the first few row groups are fetched, not the full 1.4 GB repo.
|
||||
- `max_prompt_tokens` defaults to `max_model_len - max_tokens - 256`, i.e. fills whatever context the server advertises. Set `--max-model-len` on the server to control the smoke-test context length; override `--max-prompt-tokens` on the client to cap below that.
|
||||
- Sample length is pre-filtered by `n_chars × 4 ≤ max_prompt_tokens`, then verified via the server's `/tokenize` endpoint under the actual chat template.
|
||||
- Reasoning models: start the server with `--reasoning-parser <name>` (e.g. `qwen3`, `deepseek_r1`) so `<think>` goes to `message.reasoning_content` and doesn't contaminate the scored answer.
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,7 @@
|
||||
model_name: "Qwen/Qwen3.5-4B"
|
||||
needles: [2, 4, 8]
|
||||
match_ratio_threshold:
|
||||
2: 0.99
|
||||
4: 0.84
|
||||
8: 0.76
|
||||
server_args: "--max-model-len 128K --reasoning-parser qwen3"
|
||||
@@ -0,0 +1 @@
|
||||
Qwen3.5-4B.yaml
|
||||
@@ -0,0 +1,54 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Add custom command line options."""
|
||||
parser.addoption(
|
||||
"--config-list-file",
|
||||
default="configs/models-small.txt",
|
||||
help="File containing list of config files to test",
|
||||
)
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
"""Generate test parameters from config files."""
|
||||
if "config_filename" in metafunc.fixturenames:
|
||||
config_list_file = metafunc.config.getoption("--config-list-file")
|
||||
|
||||
config_list_path = Path(config_list_file)
|
||||
if not config_list_path.is_absolute():
|
||||
test_dir_path = Path(__file__).parent / config_list_file
|
||||
if test_dir_path.exists():
|
||||
config_list_path = test_dir_path
|
||||
else:
|
||||
config_list_path = Path.cwd() / config_list_file
|
||||
|
||||
print(f"Looking for config list at: {config_list_path}")
|
||||
|
||||
config_files = []
|
||||
if config_list_path.exists():
|
||||
config_dir = config_list_path.parent
|
||||
with open(config_list_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
config_path = config_dir / line
|
||||
if config_path.exists():
|
||||
config_files.append(config_path)
|
||||
print(f" ✓ Found: {config_path}")
|
||||
else:
|
||||
print(f" ✗ Missing: {config_path}")
|
||||
else:
|
||||
print(f"Config list file not found: {config_list_path}")
|
||||
|
||||
if config_files:
|
||||
metafunc.parametrize(
|
||||
"config_filename",
|
||||
config_files,
|
||||
ids=[config_file.stem for config_file in config_files],
|
||||
)
|
||||
else:
|
||||
print("No config files found, test will be skipped")
|
||||
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""MRCR long-context evaluation for vLLM's OpenAI-compatible server.
|
||||
|
||||
Streams samples from `openai/mrcr` on HuggingFace, sends chat completions to
|
||||
the server, and scores each response with a prefix-gated SequenceMatcher ratio
|
||||
against the reference answer.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
import aiohttp
|
||||
import numpy as np
|
||||
import requests
|
||||
from tqdm.asyncio import tqdm
|
||||
|
||||
DATASET_REPO = "openai/mrcr"
|
||||
NEEDLE_SHARDS = {
|
||||
2: "2needle/2needle_0.parquet",
|
||||
4: "4needle/4needle_0.parquet",
|
||||
8: "8needle/8needle_0.parquet",
|
||||
}
|
||||
# Reserve headroom for chat-template tokens on top of the messages.
|
||||
PROMPT_SAFETY_BUFFER = 256
|
||||
# Pre-filter heuristic before the authoritative /tokenize check.
|
||||
CHARS_PER_TOKEN = 4
|
||||
# Skip chain-of-thought on reasoning models; ignored by non-reasoning templates.
|
||||
DEFAULT_EXTRA_BODY: dict = {"chat_template_kwargs": {"enable_thinking": False}}
|
||||
|
||||
|
||||
def discover_server_model(base_url: str) -> tuple[str, int | None]:
|
||||
"""Return (model_id, max_model_len) from /v1/models."""
|
||||
resp = requests.get(f"{base_url}/v1/models", timeout=30)
|
||||
resp.raise_for_status()
|
||||
data = resp.json().get("data", [])
|
||||
if not data:
|
||||
raise RuntimeError(f"No models advertised at {base_url}/v1/models")
|
||||
entry = data[0]
|
||||
return entry["id"], entry.get("max_model_len")
|
||||
|
||||
|
||||
def count_chat_tokens(base_url: str, model: str, messages: list[dict]) -> int:
|
||||
"""Return the chat-template-rendered token count via /tokenize."""
|
||||
resp = requests.post(
|
||||
f"{base_url}/tokenize",
|
||||
json={"model": model, "messages": messages, "add_generation_prompt": True},
|
||||
timeout=120,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return int(resp.json()["count"])
|
||||
|
||||
|
||||
def _load_mrcr_samples(
|
||||
needles: list[int],
|
||||
max_prompt_tokens: int,
|
||||
num_samples: int,
|
||||
seed: int,
|
||||
base_url: str,
|
||||
model_name: str,
|
||||
) -> list[dict]:
|
||||
"""Stream MRCR samples balanced across needle buckets, token-verified."""
|
||||
try:
|
||||
from datasets import load_dataset
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"MRCR eval requires `datasets`. Install with: uv pip install datasets"
|
||||
) from e
|
||||
|
||||
max_chars = max_prompt_tokens * CHARS_PER_TOKEN
|
||||
per_bucket = num_samples // len(needles)
|
||||
leftover = num_samples - per_bucket * len(needles)
|
||||
|
||||
samples: list[dict] = []
|
||||
for idx, n in enumerate(needles):
|
||||
if n not in NEEDLE_SHARDS:
|
||||
raise ValueError(f"Unsupported needle count {n}")
|
||||
target = per_bucket + (1 if idx < leftover else 0)
|
||||
if target == 0:
|
||||
continue
|
||||
|
||||
ds = load_dataset(
|
||||
DATASET_REPO,
|
||||
data_files=NEEDLE_SHARDS[n],
|
||||
split="train",
|
||||
streaming=True,
|
||||
).shuffle(seed=seed + n, buffer_size=16)
|
||||
|
||||
taken = 0
|
||||
for row in ds:
|
||||
if int(row.get("n_chars", 0)) > max_chars:
|
||||
continue
|
||||
prompt = row["prompt"]
|
||||
messages = json.loads(prompt) if isinstance(prompt, str) else list(prompt)
|
||||
n_tokens = count_chat_tokens(base_url, model_name, messages)
|
||||
if n_tokens > max_prompt_tokens:
|
||||
continue
|
||||
samples.append(
|
||||
{
|
||||
"messages": messages,
|
||||
"answer": row["answer"],
|
||||
"random_string_to_prepend": row["random_string_to_prepend"],
|
||||
"n_needles": int(row["n_needles"]),
|
||||
"n_tokens": n_tokens,
|
||||
}
|
||||
)
|
||||
taken += 1
|
||||
if taken >= target:
|
||||
break
|
||||
|
||||
if taken < target:
|
||||
print(f"Warning: only {taken}/{target} samples for n_needles={n}")
|
||||
|
||||
if not samples:
|
||||
raise RuntimeError("No MRCR samples fit; loosen max_prompt_tokens.")
|
||||
return samples
|
||||
|
||||
|
||||
def score_mrcr(response: str, answer: str, random_prefix: str) -> float:
|
||||
"""Prefix-gated SequenceMatcher ratio; 0 if the prefix is missing."""
|
||||
if not response.startswith(random_prefix):
|
||||
return 0.0
|
||||
stripped = response[len(random_prefix) :]
|
||||
return SequenceMatcher(a=answer, b=stripped, autojunk=False).ratio()
|
||||
|
||||
|
||||
async def _call_chat(
|
||||
session: aiohttp.ClientSession,
|
||||
url: str,
|
||||
model: str,
|
||||
messages: list[dict],
|
||||
max_tokens: int,
|
||||
temperature: float,
|
||||
seed: int | None,
|
||||
extra_body: dict,
|
||||
) -> tuple[str, int]:
|
||||
data = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
**extra_body,
|
||||
}
|
||||
if seed is not None:
|
||||
data["seed"] = seed
|
||||
try:
|
||||
async with session.post(f"{url}/v1/chat/completions", json=data) as resp:
|
||||
resp.raise_for_status()
|
||||
result = await resp.json()
|
||||
text = result["choices"][0]["message"]["content"] or ""
|
||||
return text, result.get("usage", {}).get("completion_tokens", 0)
|
||||
except Exception as e:
|
||||
print(f"chat request failed: {e}")
|
||||
return "", 0
|
||||
|
||||
|
||||
def evaluate_mrcr(
|
||||
model_name: str | None = None,
|
||||
num_samples: int = 40,
|
||||
needles: list[int] | None = None,
|
||||
max_prompt_tokens: int | None = None,
|
||||
max_tokens: int = 2048,
|
||||
host: str = "http://127.0.0.1",
|
||||
port: int = 8000,
|
||||
temperature: float = 0.0,
|
||||
seed: int | None = 42,
|
||||
concurrency: int = 8,
|
||||
extra_body: dict | None = None,
|
||||
) -> dict:
|
||||
"""Run MRCR against a vLLM server; auto-discovers model and context."""
|
||||
needles = needles or [2, 4, 8]
|
||||
extra_body = DEFAULT_EXTRA_BODY if extra_body is None else extra_body
|
||||
base_url = f"{host}:{port}"
|
||||
|
||||
discovered_model, server_max_len = discover_server_model(base_url)
|
||||
if model_name is None:
|
||||
model_name = discovered_model
|
||||
if max_prompt_tokens is None:
|
||||
if server_max_len is None:
|
||||
raise RuntimeError(
|
||||
"Server did not advertise max_model_len; pass --max-prompt-tokens."
|
||||
)
|
||||
max_prompt_tokens = max(512, server_max_len - max_tokens - PROMPT_SAFETY_BUFFER)
|
||||
print(
|
||||
f"Model: {model_name} | max_prompt_tokens={max_prompt_tokens} "
|
||||
f"(server max_model_len={server_max_len}, max_tokens={max_tokens})"
|
||||
)
|
||||
|
||||
samples = _load_mrcr_samples(
|
||||
needles=needles,
|
||||
max_prompt_tokens=max_prompt_tokens,
|
||||
num_samples=num_samples,
|
||||
seed=seed or 0,
|
||||
base_url=base_url,
|
||||
model_name=model_name,
|
||||
)
|
||||
tok_counts = [s["n_tokens"] for s in samples]
|
||||
print(
|
||||
f"Loaded {len(samples)} samples (needles={needles}, "
|
||||
f"tokens={min(tok_counts)}-{max(tok_counts)})"
|
||||
)
|
||||
|
||||
async def run():
|
||||
sem = asyncio.Semaphore(concurrency)
|
||||
responses = [""] * len(samples)
|
||||
out_tokens = [0] * len(samples)
|
||||
|
||||
async def one(session, i):
|
||||
async with sem:
|
||||
text, toks = await _call_chat(
|
||||
session=session,
|
||||
url=base_url,
|
||||
model=model_name,
|
||||
messages=samples[i]["messages"],
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
seed=seed,
|
||||
extra_body=extra_body,
|
||||
)
|
||||
responses[i] = text
|
||||
out_tokens[i] = toks
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=1800)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
await tqdm.gather(
|
||||
*[one(session, i) for i in range(len(samples))], desc="MRCR"
|
||||
)
|
||||
return responses, out_tokens
|
||||
|
||||
tic = time.perf_counter()
|
||||
responses, out_tokens = asyncio.run(run())
|
||||
latency = time.perf_counter() - tic
|
||||
|
||||
scores = np.array(
|
||||
[
|
||||
score_mrcr(r, s["answer"], s["random_string_to_prepend"])
|
||||
for r, s in zip(responses, samples)
|
||||
]
|
||||
)
|
||||
prefix_hits = np.array(
|
||||
[
|
||||
r.startswith(s["random_string_to_prepend"])
|
||||
for r, s in zip(responses, samples)
|
||||
]
|
||||
)
|
||||
per_needle = {
|
||||
f"match_ratio_n{n}": float(
|
||||
scores[np.array([s["n_needles"] == n for s in samples])].mean()
|
||||
)
|
||||
for n in needles
|
||||
if any(s["n_needles"] == n for s in samples)
|
||||
}
|
||||
|
||||
total_out = int(sum(out_tokens))
|
||||
return {
|
||||
"model": model_name,
|
||||
"match_ratio": float(scores.mean()),
|
||||
"prefix_hit_rate": float(prefix_hits.mean()),
|
||||
"per_needle": per_needle,
|
||||
"num_samples": len(samples),
|
||||
"latency": latency,
|
||||
"total_output_tokens": total_out,
|
||||
"tokens_per_second": total_out / latency if latency > 0 else 0.0,
|
||||
"max_tokens": max_tokens,
|
||||
"needles": needles,
|
||||
"max_prompt_tokens": max_prompt_tokens,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description="MRCR evaluation for vLLM serve")
|
||||
p.add_argument("--model", default=None, help="Default: discovered from /v1/models")
|
||||
p.add_argument("--num-samples", type=int, default=40)
|
||||
p.add_argument(
|
||||
"--needles", type=int, nargs="+", default=[2, 4, 8], choices=[2, 4, 8]
|
||||
)
|
||||
p.add_argument(
|
||||
"--max-prompt-tokens",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Default: server max_model_len - max_tokens - buffer",
|
||||
)
|
||||
p.add_argument("--max-tokens", type=int, default=2048)
|
||||
p.add_argument("--host", default="http://127.0.0.1")
|
||||
p.add_argument("--port", type=int, default=8000)
|
||||
p.add_argument("--temperature", type=float, default=0.0)
|
||||
p.add_argument("--seed", type=int, default=42)
|
||||
p.add_argument("--concurrency", type=int, default=8)
|
||||
p.add_argument(
|
||||
"--extra-body",
|
||||
default=None,
|
||||
help="JSON merged into each request. "
|
||||
"Pass '{}' to disable the default enable_thinking=false.",
|
||||
)
|
||||
p.add_argument("--save-results", default=None)
|
||||
args = p.parse_args()
|
||||
|
||||
extra_body = json.loads(args.extra_body) if args.extra_body else None
|
||||
|
||||
result = evaluate_mrcr(
|
||||
model_name=args.model,
|
||||
num_samples=args.num_samples,
|
||||
needles=args.needles,
|
||||
max_prompt_tokens=args.max_prompt_tokens,
|
||||
max_tokens=args.max_tokens,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
temperature=args.temperature,
|
||||
seed=args.seed,
|
||||
concurrency=args.concurrency,
|
||||
extra_body=extra_body,
|
||||
)
|
||||
|
||||
print("\nResults:")
|
||||
print(f" match_ratio: {result['match_ratio']:.4f}")
|
||||
print(f" prefix_hit_rate: {result['prefix_hit_rate']:.4f}")
|
||||
for k, v in result["per_needle"].items():
|
||||
print(f" {k}: {v:.4f}")
|
||||
print(f" samples: {result['num_samples']}")
|
||||
print(f" latency: {result['latency']:.1f}s")
|
||||
print(f" output tok/s: {result['tokens_per_second']:.1f}")
|
||||
|
||||
if args.save_results:
|
||||
with open(args.save_results, "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
MRCR long-context accuracy test.
|
||||
|
||||
Usage:
|
||||
pytest -s -v tests/evals/mrcr/test_mrcr_correctness.py \
|
||||
--config-list-file=configs/models-small.txt
|
||||
"""
|
||||
|
||||
import shlex
|
||||
|
||||
import yaml
|
||||
|
||||
from tests.utils import RemoteOpenAIServer
|
||||
|
||||
from .mrcr_eval import evaluate_mrcr
|
||||
|
||||
|
||||
def _split_host_port(url: str, default_port: int = 8000) -> tuple[str, int]:
|
||||
if "://" in url:
|
||||
url = url.split("://", 1)[1]
|
||||
host_port = url.split("/", 1)[0]
|
||||
if ":" in host_port:
|
||||
host, p = host_port.split(":", 1)
|
||||
return f"http://{host}", int(p)
|
||||
return f"http://{host_port}", default_port
|
||||
|
||||
|
||||
def test_mrcr_correctness(config_filename):
|
||||
cfg = yaml.safe_load(config_filename.read_text(encoding="utf-8"))
|
||||
|
||||
server_args = shlex.split(cfg.get("server_args", ""))
|
||||
server_args += ["--trust-remote-code", "--disable-uvicorn-access-log"]
|
||||
|
||||
print(
|
||||
f"MRCR eval for {cfg['model_name']} (threshold {cfg['match_ratio_threshold']})"
|
||||
)
|
||||
|
||||
with RemoteOpenAIServer(
|
||||
cfg["model_name"],
|
||||
server_args,
|
||||
env_dict=cfg.get("env"),
|
||||
max_wait_seconds=cfg.get("startup_max_wait_seconds", 600),
|
||||
) as server:
|
||||
host, port = _split_host_port(server.url_for("v1"))
|
||||
results = evaluate_mrcr(
|
||||
model_name=cfg.get("model_name"),
|
||||
num_samples=cfg.get("num_samples", 40),
|
||||
needles=cfg.get("needles", [2, 4, 8]),
|
||||
max_prompt_tokens=cfg.get("max_prompt_tokens"),
|
||||
max_tokens=cfg.get("max_tokens", 2048),
|
||||
host=host,
|
||||
port=port,
|
||||
concurrency=cfg.get("concurrency", 8),
|
||||
extra_body=cfg.get("extra_body"),
|
||||
)
|
||||
|
||||
threshold = cfg["match_ratio_threshold"]
|
||||
tol = cfg.get("tolerance", 0.05)
|
||||
|
||||
print(f" match_ratio: {results['match_ratio']:.4f}")
|
||||
print(f" prefix_hit_rate: {results['prefix_hit_rate']:.4f}")
|
||||
for k, v in results["per_needle"].items():
|
||||
print(f" {k}: {v:.4f}")
|
||||
|
||||
failures: list[str] = []
|
||||
if isinstance(threshold, dict):
|
||||
for n, expected in threshold.items():
|
||||
key = f"match_ratio_n{int(n)}"
|
||||
measured = results["per_needle"].get(key)
|
||||
if measured is None:
|
||||
failures.append(f"{key}: no samples collected")
|
||||
elif measured < expected - tol:
|
||||
failures.append(f"{key}: {measured:.4f} < {expected:.4f} - {tol:.4f}")
|
||||
else:
|
||||
measured = results["match_ratio"]
|
||||
if measured < threshold - tol:
|
||||
failures.append(
|
||||
f"match_ratio: {measured:.4f} < {threshold:.4f} - {tol:.4f}"
|
||||
)
|
||||
|
||||
assert not failures, "MRCR thresholds failed: " + "; ".join(failures)
|
||||
@@ -0,0 +1,91 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import torch
|
||||
from torch import Tensor
|
||||
from torch.fx.experimental.proxy_tensor import make_fx
|
||||
|
||||
import vllm.ir.op
|
||||
from vllm.ir.op import IrOp, IrOpInplaceOverload
|
||||
|
||||
|
||||
@vllm.ir.register_op(allow_inplace=True)
|
||||
def _custom_mm2(x: Tensor, w: Tensor) -> Tensor:
|
||||
return x @ w
|
||||
|
||||
|
||||
@_custom_mm2.register_impl("regular")
|
||||
def _custom_mm2_regular(x: Tensor, w: Tensor) -> Tensor:
|
||||
return x @ w + 1
|
||||
|
||||
|
||||
@_custom_mm2.register_impl("inplace", inplace=True)
|
||||
def _custom_mm2_inplace(x: Tensor, w: Tensor) -> Tensor:
|
||||
x.copy_(x @ w + 2)
|
||||
return x
|
||||
|
||||
|
||||
class TestInplaceOp:
|
||||
def test_registration(self):
|
||||
# Test that the inplace op is registered correctly.
|
||||
assert "_custom_mm2" in IrOp.registry
|
||||
assert IrOp.registry["_custom_mm2"] is _custom_mm2
|
||||
assert _custom_mm2.torch_op is torch.ops.vllm_ir._custom_mm2.default
|
||||
assert isinstance(_custom_mm2.maybe_inplace, IrOpInplaceOverload)
|
||||
assert (
|
||||
_custom_mm2.maybe_inplace.torch_op
|
||||
is torch.ops.vllm_ir._custom_mm2.maybe_inplace
|
||||
)
|
||||
|
||||
def test_inplace_dispatching(self):
|
||||
# check that the correct implementation is dispatched based on priority,
|
||||
# and inplace semantics hold
|
||||
w = torch.randn(3, 3)
|
||||
x = torch.randn(2, 3)
|
||||
x1 = x.clone()
|
||||
|
||||
with _custom_mm2.set_priority(["regular"]):
|
||||
result_regular = _custom_mm2.maybe_inplace(x, w)
|
||||
|
||||
# check that the regular op does not modify x
|
||||
torch.testing.assert_close(x, x1, atol=0, rtol=0)
|
||||
|
||||
with _custom_mm2.set_priority(["inplace"]):
|
||||
result_inplace: Tensor = _custom_mm2.maybe_inplace(x, w)
|
||||
|
||||
# check that the inplace op returns x directly
|
||||
assert result_inplace.data_ptr() == x.data_ptr()
|
||||
|
||||
torch.testing.assert_close(result_inplace, x1 @ w + 2)
|
||||
torch.testing.assert_close(result_regular, x1 @ w + 1)
|
||||
|
||||
def test_default_dispatching(self):
|
||||
# check that the correct implementation is dispatched,
|
||||
# and ops do not modify inputs when using the default overload
|
||||
w = torch.randn(3, 3)
|
||||
x = torch.randn(2, 3)
|
||||
x1 = x.clone()
|
||||
|
||||
with _custom_mm2.set_priority(["regular"]):
|
||||
result_regular = _custom_mm2(x, w)
|
||||
|
||||
with _custom_mm2.set_priority(["inplace"]):
|
||||
result_inplace = _custom_mm2(x, w)
|
||||
|
||||
# check that x was not modified by either impl
|
||||
torch.testing.assert_close(x, x1, atol=0, rtol=0)
|
||||
|
||||
torch.testing.assert_close(result_inplace, x1 @ w + 2)
|
||||
torch.testing.assert_close(result_regular, x1 @ w + 1)
|
||||
|
||||
def test_trace(self):
|
||||
# Test that the inplace op can be used in a graph.
|
||||
def func(x: Tensor, y: Tensor) -> Tensor:
|
||||
return _custom_mm2.maybe_inplace(x, y)
|
||||
|
||||
x = torch.randn(2, 3)
|
||||
y = torch.randn(3, 4)
|
||||
graph = make_fx(func)(x, y)
|
||||
assert any(
|
||||
node.target == torch.ops.vllm_ir._custom_mm2.maybe_inplace
|
||||
for node in graph.graph.nodes
|
||||
)
|
||||
+27
-16
@@ -21,7 +21,7 @@ class CustomError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@vllm.ir.register_op
|
||||
@vllm.ir.register_op(allow_inplace=True)
|
||||
def _custom_add(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
return x + y
|
||||
|
||||
@@ -129,11 +129,15 @@ class TestIrOpCustomAdd:
|
||||
|
||||
@pytest.mark.parametrize("enable_torch_wrap", [True, False])
|
||||
@pytest.mark.parametrize("symbolic_trace", [True, False])
|
||||
@pytest.mark.parametrize("overload", ["default", "maybe_inplace"])
|
||||
def test_trace_sees_single_custom_op(
|
||||
self, symbolic_trace: bool, enable_torch_wrap: bool
|
||||
self, symbolic_trace: bool, enable_torch_wrap: bool, overload: str
|
||||
):
|
||||
op_fn = _custom_add if overload == "default" else _custom_add.maybe_inplace
|
||||
torch_op = getattr(torch.ops.vllm_ir._custom_add, overload)
|
||||
|
||||
def fn(x, y):
|
||||
return _custom_add(x, y)
|
||||
return op_fn(x, y)
|
||||
|
||||
def find_fn(target: Any, gm: fx.GraphModule):
|
||||
return gm.graph.find_nodes(op="call_function", target=target)
|
||||
@@ -155,7 +159,7 @@ class TestIrOpCustomAdd:
|
||||
torch.testing.assert_close(out_fx, out_eager)
|
||||
|
||||
# check that IR nodes only appear if enable_torch_wrap=True
|
||||
ir_nodes = find_fn(torch.ops.vllm_ir._custom_add.default, gm)
|
||||
ir_nodes = find_fn(torch_op, gm)
|
||||
if enable_torch_wrap:
|
||||
assert len(ir_nodes) == 1, gm.code
|
||||
else:
|
||||
@@ -167,7 +171,7 @@ class TestIrOpCustomAdd:
|
||||
else:
|
||||
gm = make_fx(fn)(torch.randn(2, 2), torch.randn(2, 2))
|
||||
|
||||
ir_nodes = find_fn(torch.ops.vllm_ir._custom_add.default, gm)
|
||||
ir_nodes = find_fn(torch_op, gm)
|
||||
assert len(ir_nodes) == 1, gm.code
|
||||
|
||||
|
||||
@@ -176,9 +180,12 @@ def impl_a(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
return x + y + 10
|
||||
|
||||
|
||||
@_custom_add.register_impl("impl_b")
|
||||
@_custom_add.register_impl("impl_b", inplace=True)
|
||||
def impl_b(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
return x + y + 20
|
||||
"""Computes x+y+20"""
|
||||
x.add_(y)
|
||||
x.add_(20)
|
||||
return x
|
||||
|
||||
|
||||
@_custom_add.register_impl("impl_even", supports_args=lambda x, y: x.size(1) % 2 == 0)
|
||||
@@ -243,19 +250,23 @@ class TestIrOpImplDispatch:
|
||||
# Restored to empty
|
||||
assert _custom_add.get_priority() == []
|
||||
|
||||
def test_dispatch_priority_order(self):
|
||||
@pytest.mark.parametrize("overload", ["default", "maybe_inplace"])
|
||||
def test_dispatch_priority_order(self, overload: str):
|
||||
op_fn = _custom_add if overload == "default" else _custom_add.maybe_inplace
|
||||
torch_op = getattr(torch.ops.vllm_ir._custom_add, overload)
|
||||
|
||||
x = torch.tensor(1, dtype=torch.int32)
|
||||
y = torch.tensor(2, dtype=torch.int32)
|
||||
|
||||
with _custom_add.set_priority(["impl_b", "impl_a"]):
|
||||
assert _custom_add.dispatch(x, y) is impl_b
|
||||
out1 = _custom_add(x, y)
|
||||
out2 = torch.ops.vllm_ir._custom_add(x, y)
|
||||
out1 = op_fn(x.clone(), y)
|
||||
out2 = torch_op(x.clone(), y)
|
||||
|
||||
with _custom_add.set_priority(["impl_a"]):
|
||||
assert _custom_add.dispatch(x, y) is impl_a
|
||||
out3 = _custom_add(x, y)
|
||||
out4 = torch.ops.vllm_ir._custom_add(x, y)
|
||||
out3 = op_fn(x.clone(), y)
|
||||
out4 = torch_op(x.clone(), y)
|
||||
|
||||
# impl_b
|
||||
assert out1.item() == 1 + 2 + 20
|
||||
@@ -265,18 +276,18 @@ class TestIrOpImplDispatch:
|
||||
assert out4.item() == 1 + 2 + 10
|
||||
|
||||
def test_unsupported_impl_filtered(self):
|
||||
@_custom_add.register_impl("unsupported", supported=False)
|
||||
def impl_bad(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
@_custom_add.register_impl("impl_unsupported", supported=False)
|
||||
def impl_unsupported(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
return x + y + 999
|
||||
|
||||
x = torch.tensor(1, dtype=torch.int32)
|
||||
y = torch.tensor(2, dtype=torch.int32)
|
||||
|
||||
with _custom_add.set_priority(["unsupported", "impl_a"]):
|
||||
with _custom_add.set_priority(["impl_unsupported", "impl_a"]):
|
||||
assert _custom_add.get_priority() == ["impl_a"]
|
||||
out = _custom_add(x, y)
|
||||
|
||||
# impl_bad skipped → impl_a
|
||||
# impl_unsupported skipped → impl_a
|
||||
assert out.item() == 1 + 2 + 10
|
||||
|
||||
def test_supports_args_runtime_dispatch_and_warning(
|
||||
|
||||
@@ -5,12 +5,17 @@ import pytest
|
||||
import torch
|
||||
|
||||
from tests.kernels.quantization.nvfp4_utils import (
|
||||
dequant_nvfp4_kv_cache,
|
||||
dequantize_nvfp4_to_dtype,
|
||||
get_nvfp4_global_scale,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.math_utils import round_up
|
||||
from vllm.utils.torch_utils import set_random_seed
|
||||
from vllm.utils.torch_utils import (
|
||||
nvfp4_kv_cache_full_dim,
|
||||
nvfp4_kv_cache_split_views,
|
||||
set_random_seed,
|
||||
)
|
||||
|
||||
if not current_platform.is_device_capability_family(100):
|
||||
pytest.skip(
|
||||
@@ -33,6 +38,117 @@ def to_float8(x, dtype=torch.float8_e4m3fn):
|
||||
return x_scl_sat.to(dtype), scale.float().reciprocal()
|
||||
|
||||
|
||||
def build_paged_kv_metadata(
|
||||
seq_lens: torch.Tensor,
|
||||
block_tables: torch.Tensor,
|
||||
block_size: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Build paged-KV indptr/indices/last_page_lens from seq_lens + block_tables."""
|
||||
kv_indptr = [0]
|
||||
kv_indices = []
|
||||
kv_last_page_lens = []
|
||||
for i in range(len(seq_lens)):
|
||||
sl = int(seq_lens[i])
|
||||
assert sl > 0
|
||||
nb = (sl + block_size - 1) // block_size
|
||||
kv_indices.extend(block_tables[i, :nb].tolist())
|
||||
kv_indptr.append(kv_indptr[-1] + nb)
|
||||
kv_last_page_lens.append(sl % block_size or block_size)
|
||||
return (
|
||||
torch.tensor(kv_indptr, dtype=torch.int32),
|
||||
torch.tensor(kv_indices, dtype=torch.int32),
|
||||
torch.tensor(kv_last_page_lens, dtype=torch.int32),
|
||||
)
|
||||
|
||||
|
||||
def make_nvfp4_kv_cache(
|
||||
kv_bf16_hnd: torch.Tensor, block_size: int, head_size: int
|
||||
) -> tuple:
|
||||
"""Quantize bf16 KV cache to nvfp4 via reshape_and_cache_flash.
|
||||
|
||||
Returns (k_data, v_data), (k_scales, v_scales), kv_scale, ref_kv_bf16.
|
||||
"""
|
||||
num_blocks, _, num_kv_heads, _, _ = kv_bf16_hnd.shape
|
||||
kv_scale_val = (kv_bf16_hnd.abs().amax() / 448.0).item()
|
||||
kv_scale_tensor = torch.tensor(
|
||||
kv_scale_val, dtype=torch.float32, device=kv_bf16_hnd.device
|
||||
)
|
||||
|
||||
# Allocate in HND physical order, permute to NHD logical order.
|
||||
# hnd_order swaps dims 2↔3; it is its own inverse.
|
||||
full_dim = nvfp4_kv_cache_full_dim(head_size)
|
||||
hnd_order = (0, 1, 3, 2, 4)
|
||||
kv_cache = torch.zeros(
|
||||
(num_blocks, 2, num_kv_heads, block_size, full_dim),
|
||||
dtype=torch.uint8,
|
||||
device=kv_bf16_hnd.device,
|
||||
).permute(*hnd_order)
|
||||
|
||||
# Flatten NHD [N, T, H, D] → token tensors [N*T, H, D] for the kernel.
|
||||
num_tokens = num_blocks * block_size
|
||||
k_tokens = (
|
||||
kv_bf16_hnd[:, 0]
|
||||
.permute(0, 2, 1, 3)
|
||||
.reshape(num_tokens, num_kv_heads, head_size)
|
||||
)
|
||||
v_tokens = (
|
||||
kv_bf16_hnd[:, 1]
|
||||
.permute(0, 2, 1, 3)
|
||||
.reshape(num_tokens, num_kv_heads, head_size)
|
||||
)
|
||||
slot_mapping = torch.arange(num_tokens, dtype=torch.long, device=kv_bf16_hnd.device)
|
||||
|
||||
# reshape_and_cache_flash: kernel receives kv_cache[:, 0] and [:, 1]
|
||||
# (full K/V buffers containing both data and scale).
|
||||
torch.ops._C_cache_ops.reshape_and_cache_flash(
|
||||
k_tokens,
|
||||
v_tokens,
|
||||
kv_cache[:, 0],
|
||||
kv_cache[:, 1],
|
||||
slot_mapping,
|
||||
"nvfp4",
|
||||
kv_scale_tensor,
|
||||
kv_scale_tensor,
|
||||
)
|
||||
|
||||
# Split in HND order for trtllm kernel (expects HND numTokensPerPage).
|
||||
kv_cache_hnd = kv_cache.permute(*hnd_order)
|
||||
(k_data, v_data), (k_scales, v_scales) = nvfp4_kv_cache_split_views(kv_cache_hnd)
|
||||
|
||||
# Dequantize for the FA2 reference baseline.
|
||||
ref_k = dequant_nvfp4_kv_cache(
|
||||
k_data, k_scales, kv_scale_val, head_size, block_size
|
||||
).to(torch.bfloat16)
|
||||
ref_v = dequant_nvfp4_kv_cache(
|
||||
v_data, v_scales, kv_scale_val, head_size, block_size
|
||||
).to(torch.bfloat16)
|
||||
ref_kv_bf16 = torch.stack([ref_k, ref_v], dim=1) # [N, 2, H, T, D]
|
||||
|
||||
return (k_data, v_data), (k_scales, v_scales), kv_scale_val, ref_kv_bf16
|
||||
|
||||
|
||||
def make_quantized_kv_cache(
|
||||
kv_cache: torch.Tensor,
|
||||
kv_quant_dtype: torch.dtype,
|
||||
block_size: int,
|
||||
head_size: int,
|
||||
) -> tuple:
|
||||
"""Quantize kv_cache based on dtype. Returns (kv_cache, kv_cache_sf,
|
||||
kv_scale, ref_kv_cache, is_nvfp4_kv)."""
|
||||
is_nvfp4_kv = kv_quant_dtype == FP4_DTYPE
|
||||
if is_nvfp4_kv:
|
||||
data, scales, kv_scale, ref = make_nvfp4_kv_cache(
|
||||
kv_cache, block_size, head_size
|
||||
)
|
||||
return data, scales, kv_scale, ref, True
|
||||
elif kv_quant_dtype == FP8_DTYPE:
|
||||
kv_fp8, kv_scale = to_float8(kv_cache)
|
||||
ref = kv_fp8.to(kv_cache.dtype) * kv_scale
|
||||
return kv_fp8, None, kv_scale, ref, False
|
||||
else:
|
||||
return kv_cache, None, 1.0, kv_cache, False
|
||||
|
||||
|
||||
DTYPE = [torch.bfloat16]
|
||||
QUANT_DTYPES = [
|
||||
# (q_quant_dtype, kv_quant_dtype, o_quant_dtype)
|
||||
@@ -41,6 +157,7 @@ QUANT_DTYPES = [
|
||||
(FP8_DTYPE, FP8_DTYPE, None),
|
||||
(FP8_DTYPE, FP8_DTYPE, FP8_DTYPE),
|
||||
(FP8_DTYPE, FP8_DTYPE, FP4_DTYPE),
|
||||
(FP8_DTYPE, FP4_DTYPE, FP8_DTYPE), # nvfp4 KV cache
|
||||
]
|
||||
BATCH_SIZE = [4, 12]
|
||||
MAX_SEQ_LENS = [(1024, 4096)]
|
||||
@@ -127,35 +244,19 @@ def test_flashinfer_trtllm_decode_with_baseline(
|
||||
max_seq_len = torch.max(seq_lens).item()
|
||||
|
||||
kv_cache = torch.randn(kv_cache_shape, dtype=dtype)
|
||||
if kv_quant_dtype == FP8_DTYPE:
|
||||
kv_cache, kv_scale = to_float8(kv_cache)
|
||||
ref_kv_cache = kv_cache.to(dtype) * kv_scale
|
||||
else:
|
||||
kv_scale = 1.0
|
||||
ref_kv_cache = kv_cache
|
||||
kv_cache, kv_cache_sf, kv_scale, ref_kv_cache, is_nvfp4_kv = (
|
||||
make_quantized_kv_cache(kv_cache, kv_quant_dtype, block_size, head_size)
|
||||
)
|
||||
|
||||
k_scale = v_scale = kv_scale
|
||||
|
||||
max_num_blocks_per_seq = (max_seq_len + block_size - 1) // block_size
|
||||
block_tables = torch.randint(
|
||||
0, NUM_BLOCKS, (batch_size, max_num_blocks_per_seq), dtype=torch.int32
|
||||
)
|
||||
kv_indptr = [0]
|
||||
kv_indices = []
|
||||
kv_last_page_lens = []
|
||||
for i in range(batch_size):
|
||||
seq_len = seq_lens[i]
|
||||
assert seq_len > 0
|
||||
num_blocks = (seq_len + block_size - 1) // block_size
|
||||
kv_indices.extend(block_tables[i, :num_blocks])
|
||||
kv_indptr.append(kv_indptr[-1] + num_blocks)
|
||||
kv_last_page_len = seq_len % block_size
|
||||
if kv_last_page_len == 0:
|
||||
kv_last_page_len = block_size
|
||||
kv_last_page_lens.append(kv_last_page_len)
|
||||
|
||||
kv_indptr = torch.tensor(kv_indptr, dtype=torch.int32)
|
||||
kv_indices = torch.tensor(kv_indices, dtype=torch.int32)
|
||||
kv_last_page_lens = torch.tensor(kv_last_page_lens, dtype=torch.int32)
|
||||
kv_indptr, kv_indices, kv_last_page_lens = build_paged_kv_metadata(
|
||||
seq_lens, block_tables, block_size
|
||||
)
|
||||
workspace_buffer = torch.zeros(128 * 1024 * 1024, dtype=torch.int8)
|
||||
|
||||
# Baseline Decode
|
||||
@@ -225,6 +326,7 @@ def test_flashinfer_trtllm_decode_with_baseline(
|
||||
sinks=sinks,
|
||||
o_sf_scale=o_sf_scale_float,
|
||||
out=output_trtllm,
|
||||
kv_cache_sf=kv_cache_sf,
|
||||
)
|
||||
if o_quant_dtype == FP8_DTYPE:
|
||||
output_trtllm = output_trtllm.to(dtype) * o_scale
|
||||
@@ -237,7 +339,9 @@ def test_flashinfer_trtllm_decode_with_baseline(
|
||||
)
|
||||
output_trtllm = output_trtllm.reshape(-1, query.shape[1], query.shape[2])
|
||||
|
||||
if q_quant_dtype == FP8_DTYPE and o_quant_dtype == FP4_DTYPE:
|
||||
if is_nvfp4_kv:
|
||||
rtol, atol = 1.0, 1.0 # nvfp4 has higher quantization error
|
||||
elif q_quant_dtype == FP8_DTYPE and o_quant_dtype == FP4_DTYPE:
|
||||
rtol, atol = 7e-2, 9e-2
|
||||
elif q_quant_dtype == FP8_DTYPE and o_quant_dtype == FP8_DTYPE:
|
||||
rtol, atol = 3e-2, 4e-2
|
||||
@@ -287,7 +391,12 @@ def test_flashinfer_trtllm_prefill_with_baseline(
|
||||
kv_quant_dtype = kv_quant_dtype or dtype
|
||||
o_quant_dtype = o_quant_dtype or dtype
|
||||
|
||||
if q_quant_dtype != kv_quant_dtype:
|
||||
# FP8 Q + nvfp4 KV is the required combination for the nvfp4 KV path.
|
||||
# All other mixed Q/KV dtype combinations are unsupported.
|
||||
is_nvfp4_kv = kv_quant_dtype == FP4_DTYPE
|
||||
if q_quant_dtype != kv_quant_dtype and not (
|
||||
q_quant_dtype == FP8_DTYPE and is_nvfp4_kv
|
||||
):
|
||||
pytest.skip("Skipped mixed QKV dtypes for prefill")
|
||||
|
||||
max_q_len, max_kv_len = max_seq_lens
|
||||
@@ -329,35 +438,19 @@ def test_flashinfer_trtllm_prefill_with_baseline(
|
||||
max_seq_len = torch.max(seq_lens).item()
|
||||
|
||||
kv_cache = torch.randn(kv_cache_shape, dtype=dtype)
|
||||
if kv_quant_dtype == FP8_DTYPE:
|
||||
kv_cache, kv_scale = to_float8(kv_cache)
|
||||
ref_kv_cache = kv_cache.to(dtype) * kv_scale
|
||||
else:
|
||||
kv_scale = 1.0
|
||||
ref_kv_cache = kv_cache
|
||||
kv_cache, kv_cache_sf, kv_scale, ref_kv_cache, is_nvfp4_kv = (
|
||||
make_quantized_kv_cache(kv_cache, kv_quant_dtype, block_size, head_size)
|
||||
)
|
||||
|
||||
k_scale = v_scale = kv_scale
|
||||
|
||||
max_num_blocks_per_seq = (max_seq_len + block_size - 1) // block_size
|
||||
block_tables = torch.randint(
|
||||
0, NUM_BLOCKS, (batch_size, max_num_blocks_per_seq), dtype=torch.int32
|
||||
)
|
||||
kv_indptr = [0]
|
||||
kv_indices = []
|
||||
kv_last_page_lens = []
|
||||
for i in range(batch_size):
|
||||
seq_len = seq_lens[i]
|
||||
assert seq_len > 0
|
||||
num_blocks = (seq_len + block_size - 1) // block_size
|
||||
kv_indices.extend(block_tables[i, :num_blocks])
|
||||
kv_indptr.append(kv_indptr[-1] + num_blocks)
|
||||
kv_last_page_len = seq_len % block_size
|
||||
if kv_last_page_len == 0:
|
||||
kv_last_page_len = block_size
|
||||
kv_last_page_lens.append(kv_last_page_len)
|
||||
|
||||
kv_indptr = torch.tensor(kv_indptr, dtype=torch.int32)
|
||||
kv_indices = torch.tensor(kv_indices, dtype=torch.int32)
|
||||
kv_last_page_lens = torch.tensor(kv_last_page_lens, dtype=torch.int32)
|
||||
kv_indptr, kv_indices, kv_last_page_lens = build_paged_kv_metadata(
|
||||
seq_lens, block_tables, block_size
|
||||
)
|
||||
workspace_buffer = torch.zeros(128 * 1024 * 1024, dtype=torch.int8)
|
||||
|
||||
# Baseline Prefill
|
||||
@@ -431,6 +524,7 @@ def test_flashinfer_trtllm_prefill_with_baseline(
|
||||
sinks=sinks,
|
||||
o_sf_scale=o_sf_scale_float,
|
||||
out=output_trtllm,
|
||||
kv_cache_sf=kv_cache_sf,
|
||||
)
|
||||
if o_quant_dtype == FP8_DTYPE:
|
||||
output_trtllm = output_trtllm.to(dtype) * o_scale
|
||||
@@ -443,7 +537,9 @@ def test_flashinfer_trtllm_prefill_with_baseline(
|
||||
)
|
||||
output_trtllm = output_trtllm.reshape(-1, query.shape[1], query.shape[2])
|
||||
|
||||
if q_quant_dtype == FP8_DTYPE and o_quant_dtype == FP4_DTYPE:
|
||||
if is_nvfp4_kv:
|
||||
rtol, atol = 1.0, 1.5 # nvfp4 has higher quantization error
|
||||
elif q_quant_dtype == FP8_DTYPE and o_quant_dtype == FP4_DTYPE:
|
||||
rtol, atol = 3e-1, 4e-1
|
||||
elif q_quant_dtype == FP8_DTYPE and o_quant_dtype == FP8_DTYPE:
|
||||
rtol, atol = 4e-2, 6e-2
|
||||
|
||||
@@ -28,7 +28,9 @@ def test_rms_norm_registration():
|
||||
"native": True,
|
||||
"vllm_c": current_platform.is_cuda_alike(),
|
||||
"aiter": current_platform.is_rocm(),
|
||||
"oink": False,
|
||||
"oink": current_platform.has_device_capability(100)
|
||||
and hasattr(torch.ops, "oink")
|
||||
and hasattr(torch.ops.oink, "rmsnorm"),
|
||||
"xpu_kernels": current_platform.is_xpu(),
|
||||
}
|
||||
|
||||
@@ -67,6 +69,14 @@ class TestRMSNorm:
|
||||
out2 = rms_norm_native(x * 2.0, weight, epsilon=epsilon)
|
||||
torch.testing.assert_close(out2, out, rtol=get_default_rtol(out), atol=1e-3)
|
||||
|
||||
# Mean square should be approximately 1 (ignoring epsilon and weight scaling)
|
||||
combined_norm = out.float() / weight.float()
|
||||
variance = combined_norm.pow(2).mean(dim=-1)
|
||||
# After RMS normalization, variance should be close to 1
|
||||
torch.testing.assert_close(
|
||||
variance, torch.ones_like(variance), rtol=1e-2, atol=1e-2
|
||||
)
|
||||
|
||||
# Check behavior with and without weight
|
||||
weight1 = torch.ones_like(weight)
|
||||
out3 = rms_norm_native(x, weight1, epsilon=epsilon)
|
||||
@@ -129,3 +139,197 @@ def test_aiter_rejects_unsupported_dtypes():
|
||||
num_tokens=8, hidden_size=4096, dtype=dtype, epsilon=1e-5
|
||||
)
|
||||
assert not impl.supports_args(*args), f"aiter should reject dtype={dtype}"
|
||||
|
||||
|
||||
fused_add_rms_norm_native = ir.ops.fused_add_rms_norm.impls["native"].impl_fn
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike() and not current_platform.is_xpu(),
|
||||
reason="Currently only kernels on CUDA, ROCm and XPU",
|
||||
)
|
||||
def test_fused_add_rms_norm_registration():
|
||||
expected = {
|
||||
"native": True,
|
||||
"vllm_c": current_platform.is_cuda_alike(),
|
||||
"aiter": current_platform.is_rocm(),
|
||||
"oink": current_platform.has_device_capability(100)
|
||||
and hasattr(torch.ops, "oink")
|
||||
and hasattr(torch.ops.oink, "fused_add_rms_norm"),
|
||||
"xpu_kernels": current_platform.is_xpu(),
|
||||
}
|
||||
|
||||
actual = {
|
||||
provider: impl.supported
|
||||
for provider, impl in ir.ops.fused_add_rms_norm.impls.items()
|
||||
}
|
||||
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
|
||||
@pytest.mark.parametrize("n_tokens", NUM_TOKENS)
|
||||
@pytest.mark.parametrize("hidden_size", COMMON_HIDDEN_SIZES)
|
||||
@pytest.mark.parametrize("epsilon", [1e-6, 1e-5])
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike() and not current_platform.is_xpu(),
|
||||
reason="Currently only kernels on CUDA, ROCm and XPU",
|
||||
)
|
||||
class TestFusedAddRMSNorm:
|
||||
@classmethod
|
||||
def setup_class(cls, **kwargs):
|
||||
torch.set_default_device(current_platform.device_type)
|
||||
|
||||
def test_native_semantics(self, dtype, n_tokens, hidden_size, epsilon):
|
||||
x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
|
||||
num_tokens=4, hidden_size=8, dtype=dtype, epsilon=epsilon
|
||||
)
|
||||
out, residual_out = fused_add_rms_norm_native(x, x_residual, weight, eps)
|
||||
|
||||
# Check shape, dtype, device
|
||||
assert out.shape == x.shape
|
||||
assert out.dtype == x.dtype
|
||||
assert out.device == x.device
|
||||
assert residual_out.shape == x_residual.shape
|
||||
assert residual_out.dtype == x_residual.dtype
|
||||
assert residual_out.device == x_residual.device
|
||||
|
||||
# Check that residual_out = x + x_residual
|
||||
expected_residual = (x.float() + x_residual.float()).to(dtype)
|
||||
torch.testing.assert_close(
|
||||
residual_out, expected_residual, rtol=1e-3, atol=1e-3
|
||||
)
|
||||
|
||||
# Verify that the output is RMS normalized version of (x + x_residual)
|
||||
expected_out = rms_norm_native(expected_residual, weight, epsilon)
|
||||
assert_close(
|
||||
ir.ops.fused_add_rms_norm,
|
||||
(out, residual_out),
|
||||
(expected_out, expected_residual),
|
||||
)
|
||||
|
||||
# Check the scaling property of rms norm
|
||||
out1, _ = fused_add_rms_norm_native(
|
||||
x, torch.zeros_like(x), weight, epsilon=epsilon
|
||||
)
|
||||
out2, _ = fused_add_rms_norm_native(
|
||||
x * 2.0, torch.zeros_like(x), weight, epsilon=epsilon
|
||||
)
|
||||
torch.testing.assert_close(out2, out1, rtol=get_default_rtol(out), atol=1e-3)
|
||||
|
||||
# Check behavior with and without weight
|
||||
weight1 = torch.ones_like(weight)
|
||||
out3, _ = fused_add_rms_norm_native(x, x_residual, weight1, eps)
|
||||
out4, _ = fused_add_rms_norm_native(x, x_residual, None, eps)
|
||||
torch.testing.assert_close(out3, out4)
|
||||
|
||||
@pytest.mark.parametrize("provider", supported_providers(ir.ops.fused_add_rms_norm))
|
||||
def test_impls(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
||||
impl = ir.ops.fused_add_rms_norm.impls[provider]
|
||||
x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
|
||||
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
||||
)
|
||||
args = (x, x_residual, weight, eps, None)
|
||||
|
||||
if not impl.supports_args(*args):
|
||||
pytest.skip(f"{provider} does not support args")
|
||||
|
||||
ref_output, ref_residual = fused_add_rms_norm_native(*clone_args(args))
|
||||
output, residual = impl.impl_fn(*clone_args(args))
|
||||
assert_close(ir.ops.fused_add_rms_norm, output, ref_output)
|
||||
assert_close(ir.ops.fused_add_rms_norm, residual, ref_residual)
|
||||
|
||||
# check that dispatched call matches direct call
|
||||
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
||||
out_dispatched, residual_dispatched = ir.ops.fused_add_rms_norm(*args[:4])
|
||||
out_direct, residual_direct = impl.impl_fn(*clone_args(args))
|
||||
torch.testing.assert_close(out_dispatched, out_direct, rtol=0.0, atol=0.0)
|
||||
torch.testing.assert_close(
|
||||
residual_dispatched, residual_direct, rtol=0.0, atol=0.0
|
||||
)
|
||||
|
||||
# none of these support variance_size override
|
||||
assert not impl.supports_args(x, x_residual, weight, epsilon, 4)
|
||||
assert not impl.supports_args(x, x_residual, weight, epsilon, variance_size=4)
|
||||
|
||||
# test weight=None behavior
|
||||
out_no_weight, residual_no_weight = impl.impl_fn(
|
||||
x.clone(), x_residual.clone(), None, epsilon
|
||||
)
|
||||
out_unit_weight, residual_unit_weight = impl.impl_fn(
|
||||
x.clone(), x_residual.clone(), torch.ones_like(weight), epsilon
|
||||
)
|
||||
assert_close(ir.ops.fused_add_rms_norm, out_no_weight, out_unit_weight)
|
||||
assert_close(
|
||||
ir.ops.fused_add_rms_norm, residual_no_weight, residual_unit_weight
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("provider", ["vllm_c"])
|
||||
def test_inplace_semantics(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
||||
"""Test that inplace implementations reuse inputs,
|
||||
for maybe_inplace overload but not for default overload."""
|
||||
impl = ir.ops.fused_add_rms_norm.impls[provider]
|
||||
if not impl.supported:
|
||||
pytest.skip(f"{provider} impl not supported on this platform")
|
||||
|
||||
x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
|
||||
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
||||
)
|
||||
|
||||
# Test default overload - should NOT modify inputs even with inplace impl
|
||||
x_default = x.clone()
|
||||
x_residual_default = x_residual.clone()
|
||||
x_default_ptr = x_default.data_ptr()
|
||||
x_residual_default_ptr = x_residual_default.data_ptr()
|
||||
|
||||
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
||||
out_default, residual_default = ir.ops.fused_add_rms_norm(
|
||||
x_default, x_residual_default, weight, eps
|
||||
)
|
||||
|
||||
# Default should NOT be inplace (even with inplace implementation)
|
||||
assert out_default.data_ptr() != x_default_ptr
|
||||
assert residual_default.data_ptr() != x_residual_default_ptr
|
||||
torch.testing.assert_close(x, x_default, rtol=0.0, atol=0.0)
|
||||
torch.testing.assert_close(x_residual, x_residual_default, rtol=0.0, atol=0.0)
|
||||
|
||||
# Test maybe_inplace overload - should modify inputs with inplace impl
|
||||
x_inplace = x.clone()
|
||||
x_residual_inplace = x_residual.clone()
|
||||
x_inplace_ptr = x_inplace.data_ptr()
|
||||
x_residual_inplace_ptr = x_residual_inplace.data_ptr()
|
||||
|
||||
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
||||
out_inplace, residual_inplace = ir.ops.fused_add_rms_norm.maybe_inplace(
|
||||
x_inplace, x_residual_inplace, weight, eps
|
||||
)
|
||||
|
||||
# maybe_inplace should be inplace
|
||||
assert out_inplace.data_ptr() == x_inplace_ptr
|
||||
assert residual_inplace.data_ptr() == x_residual_inplace_ptr
|
||||
|
||||
# Both should produce same results
|
||||
torch.testing.assert_close(out_default, out_inplace, atol=0.0, rtol=0.0)
|
||||
torch.testing.assert_close(
|
||||
residual_default, residual_inplace, atol=0.0, rtol=0.0
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("provider", supported_providers(ir.ops.fused_add_rms_norm))
|
||||
def test_torch_opcheck(self, dtype, n_tokens, hidden_size, epsilon, provider):
|
||||
args = ir.ops.fused_add_rms_norm.generate_inputs(
|
||||
num_tokens=n_tokens, hidden_size=hidden_size, dtype=dtype, epsilon=epsilon
|
||||
)
|
||||
args = args + (None,) # Add variance_size parameter
|
||||
|
||||
# When checking the torch op, we have to set priority and use dispatch
|
||||
with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
|
||||
torch.library.opcheck(torch.ops.vllm_ir.fused_add_rms_norm.default, args)
|
||||
|
||||
# Only test maybe_inplace with non-inplace implementations
|
||||
# Inplace implementations return aliases of inputs which is not allowed.
|
||||
# We break this invariant, but we also convert maybe_inplace to the default
|
||||
# overload during compilation, so maybe_inplace never reaches Inductor.
|
||||
if not ir.ops.fused_add_rms_norm.impls[provider].inplace:
|
||||
torch.library.opcheck(
|
||||
torch.ops.vllm_ir.fused_add_rms_norm.maybe_inplace, args
|
||||
)
|
||||
|
||||
@@ -28,6 +28,25 @@ HOPPER_MXFP4_BF16_AVAILABLE = (
|
||||
and has_flashinfer()
|
||||
)
|
||||
|
||||
# ROCm platform and dependencies
|
||||
ROCM_AVAILABLE = current_platform.is_rocm()
|
||||
ROCM_TRITON_KERNELS_AVAILABLE = False
|
||||
ROCM_AITER_AVAILABLE = False
|
||||
ROCM_GFX950 = False
|
||||
|
||||
if ROCM_AVAILABLE:
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
from vllm.platforms.rocm import on_gfx950
|
||||
from vllm.utils.import_utils import has_triton_kernels
|
||||
|
||||
ROCM_TRITON_KERNELS_AVAILABLE = has_triton_kernels()
|
||||
ROCM_GFX950 = on_gfx950()
|
||||
ROCM_AITER_AVAILABLE = rocm_aiter_ops.is_enabled()
|
||||
|
||||
if ROCM_AITER_AVAILABLE:
|
||||
from aiter.ops.triton.moe.quant_moe import upcast_from_mxfp
|
||||
from aiter.ops.triton.quant import dynamic_mxfp4_quant
|
||||
|
||||
if TRTLLM_GEN_MXFP4_AVAILABLE:
|
||||
from flashinfer import (
|
||||
fp4_quantize,
|
||||
@@ -111,6 +130,7 @@ def test_mxfp4_loading_and_execution_moe(vllm_runner, model_case: ModelCase):
|
||||
|
||||
def swiglu(x, alpha: float = 1.702, beta: float = 1.0, limit: float | None = None):
|
||||
# Note we add an extra bias of 1 to the linear layer
|
||||
# Uses chunked layout: first half is gate, second half is up
|
||||
x_glu, x_linear = torch.chunk(x, 2, dim=-1)
|
||||
if limit is not None:
|
||||
x_glu = x_glu.clamp(max=limit)
|
||||
@@ -119,6 +139,16 @@ def swiglu(x, alpha: float = 1.702, beta: float = 1.0, limit: float | None = Non
|
||||
return out_glu * (x_linear + beta)
|
||||
|
||||
|
||||
def swigluoai(x, alpha: float = 1.702, limit: float = 7.0):
|
||||
# OAI swiglu uses interleaved layout: gate/up alternating
|
||||
# See SwigluOAIAndMul in vllm/model_executor/layers/activation.py
|
||||
gate, up = x[..., ::2], x[..., 1::2]
|
||||
gate = gate.clamp(max=limit)
|
||||
up = up.clamp(min=-limit, max=limit)
|
||||
glu = gate * torch.sigmoid(gate * alpha)
|
||||
return (up + 1) * glu
|
||||
|
||||
|
||||
fp4_lookup_table = [0, 0.5, 1, 1.5, 2, 3, 4, 6, -0, -0.5, -1, -1.5, -2, -3, -4, -6]
|
||||
|
||||
|
||||
@@ -168,8 +198,20 @@ def reference_moe(
|
||||
beta,
|
||||
limit,
|
||||
act_type,
|
||||
is_gated,
|
||||
activation: str = "swiglu",
|
||||
use_interleaved_layout: bool = False,
|
||||
):
|
||||
"""
|
||||
Reference MoE implementation for accuracy testing.
|
||||
|
||||
Args:
|
||||
activation: One of "swiglu", "silu", "relu2". Controls the activation
|
||||
function used after the first MLP.
|
||||
use_interleaved_layout: If True, uses interleaved gate/up layout
|
||||
(gate=x[..., ::2], up=x[..., 1::2]) as used by SWIGLUOAI.
|
||||
If False, uses chunked layout (gate, up = chunk(x, 2)) as used
|
||||
by standard swiglu/silu.
|
||||
"""
|
||||
# renormalize routing
|
||||
experts = torch.topk(roouting_logits, k=topk, dim=-1, sorted=True)
|
||||
expert_weights = torch.nn.functional.softmax(experts.values, dim=1)
|
||||
@@ -179,12 +221,21 @@ def reference_moe(
|
||||
mlp1_weight = w13[expert_indices, ...]
|
||||
mlp1_bias = bias13[expert_indices, ...]
|
||||
t = torch.einsum("beck,bk->bec", mlp1_weight, t) + mlp1_bias
|
||||
if is_gated:
|
||||
t = swiglu(t, alpha=alpha, beta=beta, limit=limit)
|
||||
else:
|
||||
|
||||
# Apply activation
|
||||
if activation in ("swiglu", "silu"):
|
||||
if use_interleaved_layout:
|
||||
# SWIGLUOAI: interleaved gate/up layout
|
||||
t = swigluoai(t, alpha=alpha, limit=limit)
|
||||
else:
|
||||
# Standard swiglu/silu: chunked layout
|
||||
t = swiglu(t, alpha=alpha, beta=beta, limit=limit)
|
||||
elif activation == "relu2":
|
||||
# RELU2_NO_MUL: relu(x)^2
|
||||
t = torch.relu(t)
|
||||
t = t * t
|
||||
else:
|
||||
raise ValueError(f"Unknown activation: {activation}")
|
||||
|
||||
if act_type == "mxfp8":
|
||||
t_quantized, t_scale = mxfp8_quantize(
|
||||
@@ -585,7 +636,8 @@ def test_trtllm_gen_mxfp4_fused_moe(
|
||||
beta,
|
||||
limit,
|
||||
act_type,
|
||||
is_gated=True,
|
||||
activation="swiglu",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
ref_result[start_idx:end_idx].copy_(chunk_result)
|
||||
|
||||
@@ -722,7 +774,8 @@ def test_flashinfer_cutlass_mxfp4_fused_moe(
|
||||
beta,
|
||||
limit,
|
||||
"bf16",
|
||||
is_gated=True,
|
||||
activation="swiglu",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
|
||||
from vllm.utils.flashinfer import flashinfer_cutlass_fused_moe
|
||||
@@ -908,7 +961,8 @@ def test_flashinfer_cutlass_mxfp4_mxfp8_fused_moe(
|
||||
beta,
|
||||
limit,
|
||||
"mxfp8",
|
||||
is_gated=True,
|
||||
activation="swiglu",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
|
||||
# Prepare inputs for FlashInfer CUTLASS fused MoE
|
||||
@@ -1080,7 +1134,8 @@ def test_trtllm_gen_mxfp8_block_scale_moe(
|
||||
beta=0.0,
|
||||
limit=None,
|
||||
act_type="mxfp8",
|
||||
is_gated=is_gated,
|
||||
activation="swiglu" if is_gated else "relu2",
|
||||
use_interleaved_layout=False,
|
||||
)
|
||||
|
||||
# Shuffle weights/scales with the same indexed layout used by TRTLLM kernels.
|
||||
@@ -1150,3 +1205,328 @@ def test_trtllm_gen_mxfp8_block_scale_moe(
|
||||
|
||||
# Block-scale MXFP8 kernels are approximate; require majority close.
|
||||
check_accuracy(ref, out, atol=0.1, rtol=0.85, percent=0.8)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ROCm Oracle-based kernel execution tests
|
||||
# -----------------------------------------------------------------------------
|
||||
# TODO: Further tighten the accuracy threshold.
|
||||
# - More accurate ref moe to include activation quantization
|
||||
# - Check aiter kernel accuracy. E.g., quant / dequant details.
|
||||
ROCM_BACKEND_CONFIGS = {
|
||||
"TRITON": {
|
||||
"activation": "SWIGLUOAI",
|
||||
"rtol": 0.3,
|
||||
"percent": 0.95,
|
||||
"requires_aiter": False,
|
||||
"requires_gfx950": False,
|
||||
},
|
||||
"TRITON_UNFUSED": {
|
||||
"activation": "SWIGLUOAI",
|
||||
"rtol": 0.3,
|
||||
"percent": 0.95,
|
||||
"requires_aiter": False,
|
||||
"requires_gfx950": False,
|
||||
},
|
||||
"AITER_MXFP4_BF16": {
|
||||
"activation": "SILU",
|
||||
"rtol": 1.0,
|
||||
"percent": 0.7,
|
||||
"requires_aiter": True,
|
||||
"requires_gfx950": True,
|
||||
},
|
||||
"AITER_MXFP4_FP8": {
|
||||
"activation": "SWIGLUOAI",
|
||||
"rtol": 0.5,
|
||||
"percent": 0.9,
|
||||
"requires_aiter": True,
|
||||
"requires_gfx950": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend_name", list(ROCM_BACKEND_CONFIGS.keys()))
|
||||
@pytest.mark.parametrize("topk", [4])
|
||||
@pytest.mark.parametrize("num_experts", [8])
|
||||
@pytest.mark.parametrize("num_tokens,hidden_size,intermediate_size", [(16, 256, 256)])
|
||||
@pytest.mark.skipif(
|
||||
not ROCM_AVAILABLE,
|
||||
reason="ROCm is required for this test",
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_rocm_mxfp4_moe_oracle(
|
||||
backend_name: str,
|
||||
topk: int,
|
||||
num_experts: int,
|
||||
num_tokens: int,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
):
|
||||
"""
|
||||
Test ROCm MXFP4 MoE using oracle functions.
|
||||
|
||||
This test validates that the oracle functions work end-to-end:
|
||||
- select_mxfp4_moe_backend() selects a valid backend
|
||||
- convert_to_mxfp4_moe_kernel_format() converts weights without error
|
||||
- make_mxfp4_moe_quant_config() builds a valid quant config
|
||||
- make_mxfp4_moe_kernel() creates a kernel that runs without error
|
||||
- The kernel output is within accuracy tolerance of reference
|
||||
"""
|
||||
config = ROCM_BACKEND_CONFIGS[backend_name]
|
||||
|
||||
# Check platform requirements
|
||||
if not ROCM_TRITON_KERNELS_AVAILABLE:
|
||||
pytest.skip("triton_kernels required for quantization")
|
||||
if config["requires_aiter"] and not ROCM_AITER_AVAILABLE:
|
||||
pytest.skip(f"Backend {backend_name} requires AITER")
|
||||
if config["requires_gfx950"] and not ROCM_GFX950:
|
||||
pytest.skip(f"Backend {backend_name} requires GFX950")
|
||||
|
||||
from vllm.config import VllmConfig, set_current_vllm_config
|
||||
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
||||
from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
|
||||
Mxfp4MoeBackend,
|
||||
backend_to_kernel_cls,
|
||||
convert_to_mxfp4_moe_kernel_format,
|
||||
make_mxfp4_moe_kernel,
|
||||
make_mxfp4_moe_quant_config,
|
||||
)
|
||||
from vllm.v1.worker.workspace import init_workspace_manager
|
||||
|
||||
# Initialize workspace manager (needed for modular kernels)
|
||||
init_workspace_manager(torch.accelerator.current_device_index())
|
||||
|
||||
# Map string to enum
|
||||
backend = Mxfp4MoeBackend[backend_name]
|
||||
|
||||
# Get experts class from oracle
|
||||
experts_cls_list = backend_to_kernel_cls(backend)
|
||||
if experts_cls_list is None or len(experts_cls_list) == 0:
|
||||
pytest.skip(f"Backend {backend_name} not available")
|
||||
|
||||
# Use first experts class
|
||||
experts_cls = experts_cls_list[0]
|
||||
|
||||
torch.manual_seed(42)
|
||||
dtype = torch.bfloat16
|
||||
device = "cuda:0"
|
||||
|
||||
# Create MoE config with Renormalize routing (required by monolithic kernels)
|
||||
from vllm.model_executor.layers.fused_moe import FusedMoEConfig
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
FusedMoEParallelConfig,
|
||||
RoutingMethodType,
|
||||
)
|
||||
|
||||
moe_config = FusedMoEConfig(
|
||||
num_experts=num_experts,
|
||||
experts_per_token=topk,
|
||||
hidden_dim=hidden_size,
|
||||
intermediate_size_per_partition=intermediate_size,
|
||||
num_local_experts=num_experts,
|
||||
num_logical_experts=num_experts,
|
||||
moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
|
||||
activation=MoEActivation[config["activation"]],
|
||||
in_dtype=dtype,
|
||||
device="cuda",
|
||||
routing_method=RoutingMethodType.Renormalize,
|
||||
)
|
||||
|
||||
# Create float weights in checkpoint format:
|
||||
# w13: [num_experts, 2*intermediate_size, hidden_size]
|
||||
# w2: [num_experts, hidden_size, intermediate_size]
|
||||
w13_float = torch.randn(
|
||||
num_experts, 2 * intermediate_size, hidden_size, dtype=dtype, device=device
|
||||
)
|
||||
w2_float = torch.randn(
|
||||
num_experts, hidden_size, intermediate_size, dtype=dtype, device=device
|
||||
)
|
||||
|
||||
# dynamic_mxfp4_quant expects 2D input, so reshape 3D weights
|
||||
# w13: [E, 2*I, H] -> [E*2*I, H] -> quantize -> [E, 2*I, H//2]
|
||||
# w2: [E, H, I] -> [E*H, I] -> quantize -> [E, H, I//2]
|
||||
w13_2d = w13_float.reshape(-1, hidden_size)
|
||||
w13_quant_2d, w13_scale_2d = dynamic_mxfp4_quant(w13_2d)
|
||||
w13_quant = w13_quant_2d.reshape(num_experts, 2 * intermediate_size, -1)
|
||||
w13_scale = w13_scale_2d.reshape(num_experts, 2 * intermediate_size, -1)
|
||||
|
||||
w2_2d = w2_float.reshape(-1, intermediate_size)
|
||||
w2_quant_2d, w2_scale_2d = dynamic_mxfp4_quant(w2_2d)
|
||||
w2_quant = w2_quant_2d.reshape(num_experts, hidden_size, -1)
|
||||
w2_scale = w2_scale_2d.reshape(num_experts, hidden_size, -1)
|
||||
|
||||
w13_bias = torch.randn(
|
||||
num_experts, 2 * intermediate_size, dtype=dtype, device=device
|
||||
)
|
||||
w2_bias = torch.randn(num_experts, hidden_size, dtype=dtype, device=device)
|
||||
|
||||
# Create static input scales for W4A8 backend (AITER_MXFP4_FP8)
|
||||
w13_input_scale: torch.Tensor | None = None
|
||||
w2_input_scale: torch.Tensor | None = None
|
||||
if backend_name == "AITER_MXFP4_FP8":
|
||||
# Static FP8 scales: one scale per expert
|
||||
w13_input_scale = torch.ones(num_experts, dtype=torch.float32, device=device)
|
||||
w2_input_scale = torch.ones(num_experts, dtype=torch.float32, device=device)
|
||||
|
||||
# Create mock layer for oracle functions
|
||||
class MockLayer:
|
||||
w13_weight: torch.Tensor
|
||||
w2_weight: torch.Tensor
|
||||
w13_weight_scale: torch.Tensor
|
||||
w2_weight_scale: torch.Tensor
|
||||
w13_input_scale: torch.Tensor | None
|
||||
w2_input_scale: torch.Tensor | None
|
||||
|
||||
layer = MockLayer()
|
||||
layer.w13_weight = w13_quant
|
||||
layer.w2_weight = w2_quant
|
||||
layer.w13_weight_scale = w13_scale
|
||||
layer.w2_weight_scale = w2_scale
|
||||
layer.w13_input_scale = w13_input_scale
|
||||
layer.w2_input_scale = w2_input_scale
|
||||
|
||||
# Convert weights using oracle
|
||||
w13_conv, w2_conv, w13_scale_conv, w2_scale_conv, w13_bias_conv, w2_bias_conv = (
|
||||
convert_to_mxfp4_moe_kernel_format(
|
||||
mxfp4_backend=backend,
|
||||
layer=layer, # type: ignore[arg-type]
|
||||
w13_weight=w13_quant,
|
||||
w2_weight=w2_quant,
|
||||
w13_weight_scale=w13_scale,
|
||||
w2_weight_scale=w2_scale,
|
||||
w13_bias=w13_bias,
|
||||
w2_bias=w2_bias,
|
||||
)
|
||||
)
|
||||
|
||||
# Build quant config using oracle
|
||||
quant_config = make_mxfp4_moe_quant_config(
|
||||
mxfp4_backend=backend,
|
||||
w1_scale=w13_scale_conv,
|
||||
w2_scale=w2_scale_conv,
|
||||
w1_bias=w13_bias_conv,
|
||||
w2_bias=w2_bias_conv,
|
||||
a1_scale=w13_input_scale,
|
||||
a2_scale=w2_input_scale,
|
||||
)
|
||||
|
||||
# Select activation based on backend
|
||||
activation_name = str(config["activation"])
|
||||
activation = MoEActivation[activation_name]
|
||||
|
||||
# Build kernel using oracle
|
||||
assert quant_config is not None, "Failed to create quant config"
|
||||
with set_current_vllm_config(VllmConfig()):
|
||||
kernel = make_mxfp4_moe_kernel(
|
||||
moe_quant_config=quant_config,
|
||||
moe_config=moe_config,
|
||||
mxfp4_backend=backend,
|
||||
experts_cls=experts_cls,
|
||||
routing_tables=None,
|
||||
shared_experts=None,
|
||||
)
|
||||
|
||||
# Create inputs
|
||||
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
|
||||
router_logits = torch.randn(
|
||||
num_tokens, num_experts, dtype=torch.float32, device=device
|
||||
)
|
||||
topk_weights, topk_ids = torch.topk(router_logits, k=topk, dim=-1, sorted=True)
|
||||
topk_weights = torch.nn.functional.softmax(topk_weights, dim=-1)
|
||||
|
||||
# Run kernel - use appropriate method based on impl type
|
||||
if kernel.is_monolithic:
|
||||
# Monolithic impl uses router_logits
|
||||
out = kernel.apply_monolithic(
|
||||
hidden_states=x,
|
||||
w1=w13_conv,
|
||||
w2=w2_conv,
|
||||
router_logits=router_logits,
|
||||
activation=activation,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=None,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
else:
|
||||
# Modular impl uses topk_weights and topk_ids
|
||||
out = kernel.apply(
|
||||
hidden_states=x,
|
||||
w1=w13_conv,
|
||||
w2=w2_conv,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
activation=activation,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=None,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
|
||||
# Verify output is valid (no NaN/Inf) and has expected shape
|
||||
assert out.shape == (num_tokens, hidden_size), f"Unexpected shape: {out.shape}"
|
||||
assert not torch.any(torch.isnan(out)), "Output contains NaN"
|
||||
assert not torch.any(torch.isinf(out)), "Output contains Inf"
|
||||
|
||||
# Verify output has reasonable magnitude (not all zeros)
|
||||
assert out.abs().max() > 0.01, "Output is effectively zero"
|
||||
|
||||
# Dequantize weights for reference computation
|
||||
w13_dq = upcast_from_mxfp(
|
||||
w13_quant.view(torch.uint8), w13_scale, torch.bfloat16, axis=-1
|
||||
)
|
||||
w2_dq = upcast_from_mxfp(
|
||||
w2_quant.view(torch.uint8), w2_scale, torch.bfloat16, axis=-1
|
||||
)
|
||||
|
||||
# Determine activation type and layout
|
||||
# SWIGLUOAI uses interleaved layout (gate/up alternating)
|
||||
# SILU uses chunked layout (first half gate, second half up)
|
||||
use_interleaved = activation == MoEActivation.SWIGLUOAI
|
||||
if activation in [MoEActivation.SWIGLUOAI, MoEActivation.SILU]:
|
||||
act_name = "swiglu"
|
||||
else:
|
||||
act_name = "relu2"
|
||||
|
||||
ref = reference_moe(
|
||||
router_logits,
|
||||
topk,
|
||||
num_experts,
|
||||
x.to(torch.float32),
|
||||
w13_dq.to(torch.float32),
|
||||
w13_bias.to(torch.float32),
|
||||
w2_dq.to(torch.float32),
|
||||
w2_bias.to(torch.float32),
|
||||
alpha=1.702 if activation == MoEActivation.SWIGLUOAI else 1.0,
|
||||
beta=1.0 if activation == MoEActivation.SWIGLUOAI else 0.0,
|
||||
limit=7.0 if activation == MoEActivation.SWIGLUOAI else None,
|
||||
act_type="bf16",
|
||||
activation=act_name,
|
||||
use_interleaved_layout=use_interleaved,
|
||||
)
|
||||
|
||||
# Compute and print accuracy statistics
|
||||
diff = (ref.float() - out.float()).abs()
|
||||
rel_diff = diff / (ref.float().abs() + 1e-6)
|
||||
|
||||
print(f"\n[{backend_name}] Accuracy statistics:")
|
||||
print(
|
||||
f" Reference: min={ref.min():.4f}, max={ref.max():.4f}, mean={ref.mean():.4f}"
|
||||
)
|
||||
print(
|
||||
f" Output: min={out.min():.4f}, max={out.max():.4f}, mean={out.mean():.4f}"
|
||||
)
|
||||
print(
|
||||
f" Abs diff: min={diff.min():.4f}, max={diff.max():.4f}, "
|
||||
f"mean={diff.mean():.4f}"
|
||||
)
|
||||
print(
|
||||
f" Rel diff: min={rel_diff.min():.4f}, max={rel_diff.max():.4f}, "
|
||||
f"mean={rel_diff.mean():.4f}"
|
||||
)
|
||||
|
||||
# Check what percentage of values are within various tolerances
|
||||
for rtol in [0.1, 0.5, 1.0, 2.0]:
|
||||
within_tol = (diff <= rtol * out.float().abs()).float().mean()
|
||||
print(f" Within rtol={rtol}: {within_tol * 100:.1f}%")
|
||||
|
||||
# Check accuracy using per-backend thresholds
|
||||
check_accuracy(ref, out, atol=0.1, rtol=config["rtol"], percent=config["percent"])
|
||||
|
||||
@@ -70,7 +70,8 @@ def test_sqrtsoftplus_bias_uses_deepseek_v4_routing_method():
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(), reason="This test is skipped on non-CUDA platform."
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="This test is skipped on non-CUDA platform.",
|
||||
)
|
||||
@pytest.mark.parametrize("num_tokens", [1, 33, 128])
|
||||
@pytest.mark.parametrize("hidden_size", [1024, 2048])
|
||||
@@ -125,7 +126,8 @@ def test_fused_topk_softplus_sqrt(
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(), reason="This test is skipped on non-CUDA platform."
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="This test is skipped on non-CUDA platform.",
|
||||
)
|
||||
@pytest.mark.parametrize("num_tokens", [1, 33, 128])
|
||||
@pytest.mark.parametrize("hidden_size", [1024, 2048])
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Tests for the FlashInfer TRTLLM NvFP4 MoE backend
|
||||
(`TrtLlmNvFp4ExpertsModular`).
|
||||
|
||||
Covers the activations the wrapper claims to support — SiLU, RELU^2 (non-gated),
|
||||
and GELU — including a Gemma4-shaped case (128 experts, top-k 8,
|
||||
intermediate_size 704) that exercises the non-256-aligned padding path.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
|
||||
from tests.kernels.moe.utils import make_test_quant_config
|
||||
from tests.kernels.quantization.nvfp4_utils import (
|
||||
FLOAT4_E2M1_MAX,
|
||||
FLOAT8_E4M3_MAX,
|
||||
dequantize_nvfp4_to_dtype,
|
||||
)
|
||||
from tests.kernels.utils import torch_moe
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
|
||||
from vllm.model_executor.layers.fused_moe import fused_topk
|
||||
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
||||
from vllm.model_executor.layers.fused_moe.all2all_utils import (
|
||||
maybe_make_prepare_finalize,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
FusedMoEConfig,
|
||||
FusedMoEParallelConfig,
|
||||
RoutingMethodType,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe import (
|
||||
TrtLlmNvFp4ExpertsModular,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.flashinfer import has_flashinfer_trtllm_fused_moe
|
||||
from vllm.utils.math_utils import next_power_of_2
|
||||
from vllm.utils.torch_utils import set_random_seed
|
||||
|
||||
if pytest and (
|
||||
not has_flashinfer_trtllm_fused_moe()
|
||||
or not current_platform.has_device_capability(100)
|
||||
):
|
||||
pytest.skip(
|
||||
"Requires flashinfer TRTLLM fused MoE and NvFP4 (SM100)",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
# (m, n, k) = (tokens, intermediate_size_per_partition, hidden_dim).
|
||||
# The (64, 704, 4096) row matches Gemma4's MoE shape and exercises the
|
||||
# non-256-aligned intermediate (padded inside the wrapper).
|
||||
MNK_FACTORS = [
|
||||
(2, 1024, 1024),
|
||||
(64, 2048, 1536),
|
||||
(64, 704, 4096),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m,n,k", MNK_FACTORS)
|
||||
@pytest.mark.parametrize("e", [128])
|
||||
@pytest.mark.parametrize("topk", [8])
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize(
|
||||
"activation",
|
||||
[MoEActivation.SILU, MoEActivation.RELU2_NO_MUL, MoEActivation.GELU],
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_trtllm_fp4_moe_no_graph(
|
||||
m: int,
|
||||
n: int,
|
||||
k: int,
|
||||
e: int,
|
||||
topk: int,
|
||||
dtype: torch.dtype,
|
||||
activation: MoEActivation,
|
||||
workspace_init,
|
||||
):
|
||||
# FlashInfer's trtllm_batched_gemm_runner has no precompiled tile
|
||||
# config for non-gated RELU^2 at non-256-aligned intermediate_size
|
||||
# (e.g. Gemma4's 704). Other activations (SiLU/GELU) work at the
|
||||
# same shape. Tracked upstream in FlashInfer; unrelated to this
|
||||
# PR's GELU enablement (Gemma4 uses GeGLU, not non-gated RELU^2).
|
||||
if activation == MoEActivation.RELU2_NO_MUL and (m, n, k) == (64, 704, 4096):
|
||||
pytest.skip(
|
||||
"FlashInfer trtllm_batched_gemm_runner: no valid tile config "
|
||||
"for non-gated RELU^2 at intermediate_size=704 "
|
||||
"(getValidConfigIndices throws). Tracked upstream."
|
||||
)
|
||||
|
||||
set_random_seed(7)
|
||||
with set_current_vllm_config(
|
||||
VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1))
|
||||
):
|
||||
a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
|
||||
|
||||
quant_blocksize = 16
|
||||
is_gated_act = activation.is_gated
|
||||
|
||||
w1_q, w2_q, quant_config = make_test_quant_config(
|
||||
e,
|
||||
n,
|
||||
k,
|
||||
in_dtype=dtype,
|
||||
quant_dtype="nvfp4",
|
||||
block_shape=None,
|
||||
per_act_token_quant=False,
|
||||
make_gate=is_gated_act,
|
||||
# The TRT-LLM FP4 MoE kernel rejects swizzled (padded) activation
|
||||
# scales — its numel-based vec_size check requires numel == M*K/16.
|
||||
# Match what oracle/nvfp4.py does for this backend.
|
||||
is_nvfp4_scale_swizzled=False,
|
||||
)
|
||||
|
||||
score = torch.randn((m, e), device="cuda", dtype=dtype)
|
||||
topk_weights, topk_ids, _ = fused_topk(a, score, topk, renormalize=False)
|
||||
|
||||
moe_config = FusedMoEConfig(
|
||||
num_experts=e,
|
||||
experts_per_token=topk,
|
||||
hidden_dim=k,
|
||||
intermediate_size_per_partition=n,
|
||||
num_local_experts=e,
|
||||
num_logical_experts=e,
|
||||
activation=activation,
|
||||
device="cuda",
|
||||
moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
|
||||
in_dtype=dtype,
|
||||
is_act_and_mul=is_gated_act,
|
||||
routing_method=RoutingMethodType.TopK,
|
||||
max_num_tokens=next_power_of_2(m),
|
||||
)
|
||||
|
||||
trtllm_experts = mk.FusedMoEKernel(
|
||||
maybe_make_prepare_finalize(
|
||||
moe=moe_config,
|
||||
quant_config=quant_config,
|
||||
allow_new_interface=True,
|
||||
use_monolithic=False,
|
||||
),
|
||||
TrtLlmNvFp4ExpertsModular(moe_config=moe_config, quant_config=quant_config),
|
||||
inplace=False,
|
||||
)
|
||||
|
||||
trtllm_output = trtllm_experts.apply(
|
||||
hidden_states=a,
|
||||
w1=w1_q,
|
||||
w2=w2_q,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
activation=activation,
|
||||
global_num_experts=e,
|
||||
expert_map=None,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
|
||||
# Reference: round-trip activations and weights through FP4
|
||||
# quant/dequant so the comparison isolates kernel/activation behavior
|
||||
# from quantization error.
|
||||
a_global_scale = ((FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX) / a.abs().max()).to(
|
||||
torch.float32
|
||||
)
|
||||
a_fp4, a_scale_interleaved = ops.scaled_fp4_quant(a, a_global_scale)
|
||||
a_in_dtype = dequantize_nvfp4_to_dtype(
|
||||
a_fp4,
|
||||
a_scale_interleaved,
|
||||
a_global_scale,
|
||||
dtype=a.dtype,
|
||||
device=a.device,
|
||||
block_size=quant_blocksize,
|
||||
)
|
||||
|
||||
w1_d = torch.empty(
|
||||
(e, (2 if is_gated_act else 1) * n, k), device="cuda", dtype=dtype
|
||||
)
|
||||
w2_d = torch.empty((e, k, n), device="cuda", dtype=dtype)
|
||||
for idx in range(e):
|
||||
w1_d[idx] = dequantize_nvfp4_to_dtype(
|
||||
w1_q[idx],
|
||||
quant_config.w1_scale[idx],
|
||||
(1 / quant_config.g1_alphas[idx]),
|
||||
dtype=dtype,
|
||||
device=w1_q.device,
|
||||
block_size=quant_blocksize,
|
||||
)
|
||||
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
||||
w2_q[idx],
|
||||
quant_config.w2_scale[idx],
|
||||
(1 / quant_config.g2_alphas[idx]),
|
||||
dtype=dtype,
|
||||
device=w2_q.device,
|
||||
block_size=quant_blocksize,
|
||||
)
|
||||
|
||||
torch_output = torch_moe(
|
||||
a_in_dtype, w1_d, w2_d, score, topk, activation=activation
|
||||
)
|
||||
|
||||
torch.testing.assert_close(torch_output, trtllm_output, atol=2e-1, rtol=2e-1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_trtllm_fp4_moe_no_graph(
|
||||
64, 704, 4096, 128, 8, torch.bfloat16, MoEActivation.GELU, None
|
||||
)
|
||||
@@ -402,6 +402,7 @@ def make_test_quant_config(
|
||||
per_act_token_quant: bool = False,
|
||||
block_shape: list[int] | None = None,
|
||||
make_gate: bool = True,
|
||||
is_nvfp4_scale_swizzled: bool = True,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, FusedMoEQuantConfig]:
|
||||
(_, w1, w1_s, w1_gs), (_, w2, w2_s, w2_gs) = make_test_weights(
|
||||
e,
|
||||
@@ -442,6 +443,7 @@ def make_test_quant_config(
|
||||
# TODO: make sure this is handled properly
|
||||
g1_alphas=(1 / w1_gs) if w1_gs is not None else None,
|
||||
g2_alphas=(1 / w2_gs) if w2_gs is not None else None,
|
||||
is_nvfp4_scale_swizzled=is_nvfp4_scale_swizzled,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import huggingface_hub
|
||||
import pytest
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
|
||||
from vllm.model_executor.layers.quantization.utils import (
|
||||
nvfp4_emulation_utils,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
|
||||
dequantize_to_dtype,
|
||||
ref_nvfp4_quant_dequant,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.triton_utils import triton
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="Triton NVFP4 kernel requires CUDA.",
|
||||
)
|
||||
def test_triton_dequantize_nvfp4(monkeypatch) -> None:
|
||||
"""Test the Triton dequantization kernel against the CPU reference
|
||||
using real NVFP4 weights from a checkpoint.
|
||||
|
||||
Tests both 2D (attention projection) and 3D (stacked MoE experts).
|
||||
"""
|
||||
checkpoint_path = huggingface_hub.snapshot_download(
|
||||
"nvidia/Qwen3-30B-A3B-NVFP4",
|
||||
allow_patterns=["model-00001-of-00004.safetensors"],
|
||||
)
|
||||
shard_path = f"{checkpoint_path}/model-00001-of-00004.safetensors"
|
||||
block_size = 16
|
||||
|
||||
with safe_open(shard_path, framework="pt", device="cpu") as f:
|
||||
all_keys = list(f.keys())
|
||||
|
||||
# 2D case: attention projection
|
||||
tensor_fp4_2d = f.get_tensor("model.layers.9.self_attn.k_proj.weight")
|
||||
tensor_sf_2d = f.get_tensor("model.layers.9.self_attn.k_proj.weight_scale")
|
||||
global_scale_2d = f.get_tensor("model.layers.9.self_attn.k_proj.weight_scale_2")
|
||||
|
||||
# 3D case: stack ALL experts for layer 9 up_proj
|
||||
expert_prefix = "model.layers.9.mlp.experts."
|
||||
expert_indices = sorted(
|
||||
int(key.split(".")[5])
|
||||
for key in all_keys
|
||||
if key.startswith(expert_prefix) and key.endswith(".up_proj.weight")
|
||||
)
|
||||
assert len(expert_indices) > 0
|
||||
|
||||
all_fp4 = []
|
||||
all_sf = []
|
||||
all_global_scale = []
|
||||
for index in expert_indices:
|
||||
name = f"{expert_prefix}{index}.up_proj"
|
||||
all_fp4.append(f.get_tensor(f"{name}.weight"))
|
||||
all_sf.append(f.get_tensor(f"{name}.weight_scale"))
|
||||
all_global_scale.append(f.get_tensor(f"{name}.weight_scale_2"))
|
||||
|
||||
tensor_fp4_3d = torch.stack(all_fp4)
|
||||
tensor_sf_3d = torch.stack(all_sf)
|
||||
global_scale_3d = torch.stack(all_global_scale)
|
||||
|
||||
test_cases = [
|
||||
("2D base", tensor_fp4_2d, tensor_sf_2d, global_scale_2d),
|
||||
(
|
||||
"2D 2x rows",
|
||||
tensor_fp4_2d.repeat(2, 1),
|
||||
tensor_sf_2d.repeat(2, 1),
|
||||
global_scale_2d,
|
||||
),
|
||||
(
|
||||
"2D 4x rows",
|
||||
tensor_fp4_2d.repeat(4, 1),
|
||||
tensor_sf_2d.repeat(4, 1),
|
||||
global_scale_2d,
|
||||
),
|
||||
(
|
||||
"2D 2x cols",
|
||||
tensor_fp4_2d.repeat(1, 2),
|
||||
tensor_sf_2d.repeat(1, 2),
|
||||
global_scale_2d,
|
||||
),
|
||||
("3D base", tensor_fp4_3d, tensor_sf_3d, global_scale_3d),
|
||||
(
|
||||
"3D 2x experts",
|
||||
tensor_fp4_3d.repeat(2, 1, 1),
|
||||
tensor_sf_3d.repeat(2, 1, 1),
|
||||
global_scale_3d.repeat(2),
|
||||
),
|
||||
(
|
||||
"3D 2x rows",
|
||||
tensor_fp4_3d.repeat(1, 2, 1),
|
||||
tensor_sf_3d.repeat(1, 2, 1),
|
||||
global_scale_3d,
|
||||
),
|
||||
(
|
||||
"3D 2x cols",
|
||||
tensor_fp4_3d.repeat(1, 1, 2),
|
||||
tensor_sf_3d.repeat(1, 1, 2),
|
||||
global_scale_3d,
|
||||
),
|
||||
]
|
||||
|
||||
quantiles = [0.5, 0.001, 0.999]
|
||||
|
||||
# Move the E2M1 lookup table to CUDA ahead of time, as would normally
|
||||
# happen during model loading (process_weights_after_loading). Both the
|
||||
# Triton and PyTorch reference paths run on CUDA.
|
||||
nvfp4_emulation_utils.kE2M1ToFloat_handle.val = (
|
||||
nvfp4_emulation_utils.kE2M1ToFloat_handle.val.cuda()
|
||||
)
|
||||
|
||||
for label, tensor_fp4, tensor_sf, global_scale in test_cases:
|
||||
fp4_cuda = tensor_fp4.cuda()
|
||||
sf_cuda = tensor_sf.cuda()
|
||||
gs_cuda = global_scale.cuda()
|
||||
|
||||
# Triton path
|
||||
triton_result = dequantize_to_dtype(
|
||||
fp4_cuda,
|
||||
sf_cuda,
|
||||
gs_cuda,
|
||||
torch.bfloat16,
|
||||
block_size,
|
||||
swizzle=False,
|
||||
)
|
||||
|
||||
# Reference path (PyTorch ops on CUDA, Triton dispatch disabled)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(
|
||||
nvfp4_emulation_utils.current_platform,
|
||||
"is_cuda_alike",
|
||||
lambda: False,
|
||||
)
|
||||
reference = dequantize_to_dtype(
|
||||
fp4_cuda,
|
||||
sf_cuda,
|
||||
gs_cuda,
|
||||
torch.bfloat16,
|
||||
block_size,
|
||||
swizzle=False,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(triton_result, reference, atol=0, rtol=0)
|
||||
|
||||
# Benchmark
|
||||
shape = list(tensor_fp4.shape)
|
||||
|
||||
def _triton_bench(
|
||||
fp4_cuda=fp4_cuda,
|
||||
scale_cuda=sf_cuda,
|
||||
global_scale_cuda=gs_cuda,
|
||||
block_size=block_size,
|
||||
):
|
||||
return dequantize_to_dtype(
|
||||
fp4_cuda,
|
||||
scale_cuda,
|
||||
global_scale_cuda,
|
||||
torch.bfloat16,
|
||||
block_size,
|
||||
swizzle=False,
|
||||
)
|
||||
|
||||
triton_ms, triton_min, triton_max = triton.testing.do_bench(
|
||||
_triton_bench, quantiles=quantiles
|
||||
)
|
||||
|
||||
def _reference_bench(
|
||||
fp4_cuda=fp4_cuda,
|
||||
scale_cuda=sf_cuda,
|
||||
global_scale_cuda=gs_cuda,
|
||||
block_size=block_size,
|
||||
):
|
||||
with monkeypatch.context() as m2:
|
||||
m2.setattr(
|
||||
nvfp4_emulation_utils.current_platform,
|
||||
"is_cuda_alike",
|
||||
lambda: False,
|
||||
)
|
||||
dequantize_to_dtype(
|
||||
fp4_cuda,
|
||||
scale_cuda,
|
||||
global_scale_cuda,
|
||||
torch.bfloat16,
|
||||
block_size,
|
||||
swizzle=False,
|
||||
)
|
||||
|
||||
ref_ms, ref_min, ref_max = triton.testing.do_bench(
|
||||
_reference_bench, quantiles=quantiles
|
||||
)
|
||||
|
||||
speedup = ref_ms / triton_ms if triton_ms > 0 else float("inf")
|
||||
print(f" dequantize {label} {shape}:")
|
||||
print(
|
||||
f" triton: median={triton_ms:.3f}ms, "
|
||||
f"min={triton_min:.3f}ms, max={triton_max:.3f}ms"
|
||||
)
|
||||
print(
|
||||
f" reference: median={ref_ms:.3f}ms, "
|
||||
f"min={ref_min:.3f}ms, max={ref_max:.3f}ms"
|
||||
)
|
||||
print(f" speedup: {speedup:.2f}x")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="Triton NVFP4 kernel requires CUDA.",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"m, k",
|
||||
[
|
||||
(1, 16),
|
||||
(1, 4096),
|
||||
(2, 4096),
|
||||
(4, 4096),
|
||||
(8, 4096),
|
||||
(16, 4096),
|
||||
(24, 4096),
|
||||
(32, 4096),
|
||||
(1, 8192),
|
||||
(2, 8192),
|
||||
(4, 8192),
|
||||
(8, 8192),
|
||||
(16, 8192),
|
||||
(24, 8192),
|
||||
(32, 8192),
|
||||
(1, 32),
|
||||
(2, 48),
|
||||
(7, 64),
|
||||
(16, 128),
|
||||
(33, 160),
|
||||
(128, 256),
|
||||
(256, 512),
|
||||
(1024, 1024),
|
||||
(5120, 2048),
|
||||
(2048, 4096),
|
||||
(4096, 7168),
|
||||
(8192, 8192),
|
||||
(128, 16384),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("global_scale_value", [0.5, 1.0, 0.001])
|
||||
def test_triton_nvfp4_quant_dequant(
|
||||
monkeypatch, m: int, k: int, global_scale_value: float
|
||||
) -> None:
|
||||
"""Test the Triton quant-dequant kernel against the CPU reference."""
|
||||
block_size = 16
|
||||
x = torch.randn(m, k, dtype=torch.bfloat16, device="cuda")
|
||||
global_scale = torch.tensor(global_scale_value, dtype=torch.float32, device="cuda")
|
||||
|
||||
# Triton path
|
||||
triton_result = ref_nvfp4_quant_dequant(x, global_scale, block_size)
|
||||
|
||||
# CPU reference path
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(
|
||||
nvfp4_emulation_utils.current_platform,
|
||||
"is_cuda_alike",
|
||||
lambda: False,
|
||||
)
|
||||
reference = ref_nvfp4_quant_dequant(x.cpu(), global_scale.cpu(), block_size)
|
||||
|
||||
torch.testing.assert_close(triton_result.cpu(), reference, atol=0, rtol=0)
|
||||
|
||||
# Benchmark (both paths on CUDA tensors for fair comparison)
|
||||
quantiles = [0.5, 0.001, 0.999]
|
||||
|
||||
def _triton_bench(
|
||||
input_tensor=x, input_global_scale=global_scale, input_block_size=block_size
|
||||
):
|
||||
return ref_nvfp4_quant_dequant(
|
||||
input_tensor, input_global_scale, input_block_size
|
||||
)
|
||||
|
||||
triton_ms, triton_min, triton_max = triton.testing.do_bench(
|
||||
_triton_bench, quantiles=quantiles
|
||||
)
|
||||
|
||||
def _reference_bench(
|
||||
input_tensor=x, input_global_scale=global_scale, input_block_size=block_size
|
||||
):
|
||||
with monkeypatch.context() as mp2:
|
||||
mp2.setattr(
|
||||
nvfp4_emulation_utils.current_platform,
|
||||
"is_cuda_alike",
|
||||
lambda: False,
|
||||
)
|
||||
ref_nvfp4_quant_dequant(input_tensor, input_global_scale, input_block_size)
|
||||
|
||||
ref_ms, ref_min, ref_max = triton.testing.do_bench(
|
||||
_reference_bench, quantiles=quantiles
|
||||
)
|
||||
|
||||
speedup = ref_ms / triton_ms if triton_ms > 0 else float("inf")
|
||||
print(f" quant_dequant [{m}x{k}] gs={global_scale_value}:")
|
||||
print(
|
||||
f" triton: median={triton_ms:.3f}ms, "
|
||||
f"min={triton_min:.3f}ms, max={triton_max:.3f}ms"
|
||||
)
|
||||
print(
|
||||
f" reference: median={ref_ms:.3f}ms, "
|
||||
f"min={ref_min:.3f}ms, max={ref_max:.3f}ms"
|
||||
)
|
||||
print(f" speedup: {speedup:.2f}x")
|
||||
@@ -73,11 +73,6 @@ def test_per_token_group_quant_fp8(
|
||||
# Larger shapes with padding
|
||||
(127, 7168, 128),
|
||||
(253, 640, 128),
|
||||
# Non-power-of-2 group size
|
||||
(4, 768, 96), # 768/96=8 groups, no padding
|
||||
(3, 768, 96), # 768/96=8 groups, MN padding
|
||||
(4, 480, 96), # 480/96=5 groups, K padding
|
||||
(1, 480, 96), # both MN and K padding
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("poisoned_scales", [False, True])
|
||||
@@ -161,6 +156,188 @@ def test_per_token_group_quant_fp8_packed(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(), reason="DeepGEMM not available on this platform"
|
||||
)
|
||||
def test_per_token_group_quant_fp8_packed_all_zero():
|
||||
"""All-zero input must produce well-defined UE8M0 scale bytes via the eps
|
||||
floor in the kernel's UE8M0 path. Locks down the all-zero behavior before
|
||||
optimization.
|
||||
|
||||
The CUDA kernel computes:
|
||||
y_s = eps / fp8_max
|
||||
y_s = exp2(ceil(log2(fmax(y_s, 1e-10))))
|
||||
For all-zero input, eps/fp8_max < 1e-10, so the inner fmax clamps back to
|
||||
1e-10, giving exp2(ceil(log2(1e-10))) = exp2(-33) => UE8M0 byte 0x5E (94).
|
||||
"""
|
||||
|
||||
device = "cuda"
|
||||
num_tokens, hidden_dim, group_size = 4, 7168, 128
|
||||
x = torch.zeros((num_tokens, hidden_dim), device=device, dtype=torch.bfloat16)
|
||||
|
||||
out_q, out_s_packed = fp8_utils.per_token_group_quant_fp8_packed_for_deepgemm(
|
||||
x,
|
||||
group_size=group_size,
|
||||
use_ue8m0=True,
|
||||
)
|
||||
|
||||
# Quantized values must be all zero.
|
||||
assert torch.equal(
|
||||
out_q.view(torch.uint8),
|
||||
torch.zeros_like(out_q, dtype=torch.uint8),
|
||||
), "All-zero input should produce all-zero FP8 output"
|
||||
|
||||
# UE8M0 byte produced by the kernel for all-zero input.
|
||||
# The kernel's inner fmax(y_s, 1e-10) clamps eps/fp8_max back to 1e-10.
|
||||
# 1e-10 as float32 has biased exponent 0x5D and a non-zero mantissa, so
|
||||
# the kernel's bit-twiddle (exp_bits + (mant_bits != 0)) rounds up to
|
||||
# 0x5E. This matches exp2(ceil(log2(1e-10))) = exp2(-33).
|
||||
expected_exp_byte = 0x5E
|
||||
|
||||
mn = num_tokens
|
||||
groups_per_row = hidden_dim // group_size
|
||||
k_num_packed = (groups_per_row + 3) // 4
|
||||
tma_aligned_mn = ((mn + 3) // 4) * 4
|
||||
num_scale_elems = mn + (k_num_packed - 1) * tma_aligned_mn
|
||||
|
||||
# All valid scale slots must contain the expected packed value.
|
||||
# Padding slots must be zero.
|
||||
actual = torch.as_strided(out_s_packed, (num_scale_elems,), (1,)).cpu()
|
||||
|
||||
expected = torch.zeros(num_scale_elems, dtype=torch.int32, device="cpu")
|
||||
for row in range(mn):
|
||||
for g in range(groups_per_row):
|
||||
pack_col = g // 4
|
||||
pos = g % 4
|
||||
idx = pack_col * tma_aligned_mn + row
|
||||
expected[idx] |= expected_exp_byte << (pos * 8)
|
||||
|
||||
assert torch.equal(actual, expected), "All-zero scale bytes mismatch"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(), reason="DeepGEMM not available on this platform"
|
||||
)
|
||||
def test_per_token_group_quant_fp8_packed_mantissa_rounds_up():
|
||||
"""Inputs whose absmax/max_8bit produces a non-power-of-2 force the
|
||||
mantissa-rounding-up branch (exp_byte += 1). Locks down this behavior
|
||||
before optimization."""
|
||||
|
||||
device = "cuda"
|
||||
num_tokens, hidden_dim, group_size = 4, 7168, 128
|
||||
|
||||
# Build a tensor whose per-group absmax = 1.5 * fp8_max * 2^k for various k.
|
||||
# fp8_max = torch.finfo(torch.float8_e4m3fn).max = 448.0.
|
||||
# Then absmax/fp8_max = 1.5 * 2^k -> non-zero mantissa, triggers ceil
|
||||
# rounding to 2^(k+1). Use k=0 for simplicity; the bf16 representation of
|
||||
# 1.5*448=672.0 is exact.
|
||||
x = torch.full(
|
||||
(num_tokens, hidden_dim),
|
||||
672.0,
|
||||
device=device,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
|
||||
out_q, out_s_packed = fp8_utils.per_token_group_quant_fp8_packed_for_deepgemm(
|
||||
x,
|
||||
group_size=group_size,
|
||||
use_ue8m0=True,
|
||||
)
|
||||
|
||||
with patch("vllm.platforms.current_platform.is_cuda", return_value=False):
|
||||
ref_q, ref_s = fp8_utils.per_token_group_quant_fp8(
|
||||
x,
|
||||
group_size,
|
||||
use_ue8m0=True,
|
||||
)
|
||||
|
||||
assert torch.equal(out_q, ref_q), "Quantized output mismatch"
|
||||
|
||||
mn = num_tokens
|
||||
groups_per_row = hidden_dim // group_size
|
||||
k_num_packed = (groups_per_row + 3) // 4
|
||||
tma_aligned_mn = ((mn + 3) // 4) * 4
|
||||
num_scale_elems = mn + (k_num_packed - 1) * tma_aligned_mn
|
||||
|
||||
ref_s_flat = ref_s.reshape(mn, groups_per_row)
|
||||
ref_exponents = (ref_s_flat.view(torch.int32) >> 23) & 0xFF
|
||||
expected = torch.zeros(num_scale_elems, dtype=torch.int32, device="cpu")
|
||||
for row in range(mn):
|
||||
for g in range(groups_per_row):
|
||||
pack_col = g // 4
|
||||
pos = g % 4
|
||||
idx = pack_col * tma_aligned_mn + row
|
||||
expected[idx] |= int(ref_exponents[row, g].item()) << (pos * 8)
|
||||
|
||||
actual = torch.as_strided(out_s_packed, (num_scale_elems,), (1,)).cpu()
|
||||
assert torch.equal(actual, expected), "Scale bytes mismatch"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens,hidden_dim",
|
||||
[
|
||||
(1, 7168), # mn padded 1 -> 4
|
||||
(2, 7168), # mn padded 2 -> 4
|
||||
(3, 7168), # mn padded 3 -> 4
|
||||
(5, 7168), # mn padded 5 -> 8
|
||||
(127, 7168), # mn padded 127 -> 128
|
||||
(253, 640), # both mn and groups padded
|
||||
(1, 384), # extreme: 1 group, 1 mn row -> both axes padded
|
||||
],
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(), reason="DeepGEMM not available on this platform"
|
||||
)
|
||||
def test_per_token_group_quant_fp8_packed_zero_fills_padded_output_q(
|
||||
num_tokens, hidden_dim
|
||||
):
|
||||
"""When output_q is allocated with shape (tma_aligned_mn, k) instead of
|
||||
(mn, k), the kernel must overwrite the padded mn rows with zeros so
|
||||
callers can use ``torch.empty`` instead of ``torch.zeros``."""
|
||||
|
||||
device = "cuda"
|
||||
group_size = 128
|
||||
torch.manual_seed(42)
|
||||
x = torch.randn((num_tokens, hidden_dim), device=device, dtype=torch.bfloat16) * 8
|
||||
|
||||
mn = num_tokens
|
||||
groups_per_row = hidden_dim // group_size
|
||||
k_num_packed = (groups_per_row + 3) // 4
|
||||
tma_aligned_mn = ((mn + 3) // 4) * 4
|
||||
|
||||
fp8_dtype = torch.float8_e4m3fn
|
||||
finfo = torch.finfo(fp8_dtype)
|
||||
# Allocate output_q with the padded mn extent and pre-fill with 0xFF
|
||||
# so the kernel cannot rely on a clean buffer.
|
||||
out_q = torch.empty((tma_aligned_mn, hidden_dim), device=device, dtype=fp8_dtype)
|
||||
out_q.view(torch.uint8).fill_(0xFF)
|
||||
|
||||
out_s_packed = torch.empty_strided(
|
||||
(mn, k_num_packed),
|
||||
(1, tma_aligned_mn),
|
||||
device=device,
|
||||
dtype=torch.int32,
|
||||
)
|
||||
|
||||
torch.ops._C.per_token_group_fp8_quant_packed(
|
||||
x, out_q, out_s_packed, group_size, 1e-10, finfo.min, finfo.max
|
||||
)
|
||||
|
||||
# Live rows must match the Triton reference.
|
||||
with patch("vllm.platforms.current_platform.is_cuda", return_value=False):
|
||||
ref_q, _ = fp8_utils.per_token_group_quant_fp8(x, group_size, use_ue8m0=True)
|
||||
assert torch.equal(out_q[:mn], ref_q), "Live region mismatch"
|
||||
|
||||
# Padded rows must be all-zero; without this, downstream TMA loads would
|
||||
# see uninitialised data.
|
||||
if tma_aligned_mn > mn:
|
||||
padded_bytes = out_q[mn:tma_aligned_mn].view(torch.uint8)
|
||||
assert padded_bytes.eq(0).all(), (
|
||||
f"Padded rows [{mn}, {tma_aligned_mn}) not zeroed; "
|
||||
f"{padded_bytes.ne(0).sum().item()} non-zero bytes"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(32, 128), (64, 256), (16, 512)])
|
||||
@pytest.mark.parametrize("group_size", [64, 128])
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Precision tests for vllm's chunk_kda Triton operator.
|
||||
|
||||
Compares chunk_kda against a naive recurrent reference (float32).
|
||||
Uses torch.rand for q/k/v to match FLA's test pattern.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from vllm.model_executor.layers.fla.ops.kda import chunk_kda
|
||||
from vllm.model_executor.layers.fla.ops.l2norm import l2norm_fwd
|
||||
|
||||
DEVICE = "cuda"
|
||||
|
||||
|
||||
def naive_recurrent_kda(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
g: torch.Tensor,
|
||||
beta: torch.Tensor,
|
||||
scale: float | None = None,
|
||||
initial_state: torch.Tensor | None = None,
|
||||
output_final_state: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
"""Naive recurrent KDA reference, ported from FLA's naive.py."""
|
||||
dtype = v.dtype
|
||||
B, T, H, K = q.shape
|
||||
V = v.shape[-1]
|
||||
if scale is None:
|
||||
scale = K**-0.5
|
||||
|
||||
q, k, v, g, beta = (x.to(torch.float) for x in [q, k, v, g, beta])
|
||||
q = q * scale
|
||||
|
||||
S = k.new_zeros(B, H, K, V).to(q)
|
||||
if initial_state is not None:
|
||||
S += initial_state
|
||||
o = torch.zeros_like(v)
|
||||
for i in range(T):
|
||||
q_i, k_i, v_i, g_i, b_i = q[:, i], k[:, i], v[:, i], g[:, i], beta[:, i]
|
||||
S = S * g_i[..., None].exp()
|
||||
S = S + torch.einsum(
|
||||
"bhk,bhv->bhkv",
|
||||
b_i[..., None] * k_i,
|
||||
v_i - (k_i[..., None] * S).sum(-2),
|
||||
)
|
||||
o[:, i] = torch.einsum("bhk,bhkv->bhv", q_i, S)
|
||||
if not output_final_state:
|
||||
S = None
|
||||
return o.to(dtype), S
|
||||
|
||||
|
||||
def assert_close(
|
||||
name: str,
|
||||
ref: torch.Tensor,
|
||||
tri: torch.Tensor,
|
||||
ratio: float,
|
||||
err_atol: float = 1e-6,
|
||||
):
|
||||
"""RMSE-based relative error comparison."""
|
||||
abs_err = (ref.detach() - tri.detach()).flatten().abs().max().item()
|
||||
rmse_diff = (ref.detach() - tri.detach()).flatten().square().mean().sqrt().item()
|
||||
rmse_base = ref.detach().flatten().square().mean().sqrt().item()
|
||||
rel_err = rmse_diff / (rmse_base + 1e-8)
|
||||
print(f"{name:>4} | abs={abs_err:.6f} | rmse={rel_err:.6f} | thr={ratio}")
|
||||
if abs_err <= err_atol:
|
||||
return
|
||||
assert not torch.isnan(ref).any(), f"{name}: NaN detected in ref"
|
||||
assert not torch.isnan(tri).any(), f"{name}: NaN detected in tri"
|
||||
assert rel_err < ratio, (
|
||||
f"{name}: max abs err {abs_err:.6f}, rmse ratio {rel_err:.6f} >= {ratio}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("H", "D", "cu_seqlens", "dtype"),
|
||||
[
|
||||
pytest.param(
|
||||
*test,
|
||||
id="H{}-D{}-cu{}-{}".format(*test),
|
||||
)
|
||||
for test in [
|
||||
(32, 128, [0, 64], torch.float16),
|
||||
(32, 128, [0, 1024], torch.float16),
|
||||
(32, 128, [0, 15], torch.float16),
|
||||
(32, 128, [0, 256, 512, 768, 1024], torch.float16),
|
||||
(32, 128, [0, 15, 100, 300, 1200], torch.float16),
|
||||
(64, 128, [0, 256, 500, 1000], torch.float16),
|
||||
(32, 128, [0, 8192], torch.float16),
|
||||
(32, 128, [0, 256, 500, 1000], torch.bfloat16),
|
||||
]
|
||||
],
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_chunk_kda(
|
||||
H: int,
|
||||
D: int,
|
||||
cu_seqlens: list[int],
|
||||
dtype: torch.dtype,
|
||||
):
|
||||
T = cu_seqlens[-1]
|
||||
torch.manual_seed(42)
|
||||
B = 1
|
||||
cu_seqlens_t = torch.LongTensor(cu_seqlens).to(DEVICE)
|
||||
N = len(cu_seqlens) - 1
|
||||
|
||||
q = torch.rand(B, T, H, D, dtype=dtype, device=DEVICE)
|
||||
k = torch.rand(B, T, H, D, dtype=dtype, device=DEVICE)
|
||||
v = torch.rand(B, T, H, D, dtype=dtype, device=DEVICE)
|
||||
g = F.logsigmoid(torch.randn(B, T, H, D, dtype=torch.float32, device=DEVICE)).to(
|
||||
dtype
|
||||
)
|
||||
beta = torch.rand(B, T, H, dtype=dtype, device=DEVICE).sigmoid()
|
||||
h0 = torch.randn(N, H, D, D, dtype=torch.float32, device=DEVICE)
|
||||
|
||||
# Naive reference with l2norm_fwd (same kernel as chunk_kda)
|
||||
ref_outputs = []
|
||||
ref_states = []
|
||||
for i in range(N):
|
||||
s, e = cu_seqlens[i], cu_seqlens[i + 1]
|
||||
q_i = l2norm_fwd(q[:, s:e].contiguous())
|
||||
k_i = l2norm_fwd(k[:, s:e].contiguous())
|
||||
o_i, ht_i = naive_recurrent_kda(
|
||||
q_i,
|
||||
k_i,
|
||||
v[:, s:e],
|
||||
g[:, s:e],
|
||||
beta[:, s:e],
|
||||
initial_state=h0[i],
|
||||
output_final_state=True,
|
||||
)
|
||||
ref_outputs.append(o_i)
|
||||
ref_states.append(ht_i)
|
||||
ref_o = torch.cat(ref_outputs, dim=1)
|
||||
ref_ht = torch.cat(ref_states, dim=0)
|
||||
|
||||
# h0 transposed to (V, K) layout for the kernel; naive uses (K, V)
|
||||
tri_o, tri_ht = chunk_kda(
|
||||
q=q.clone(),
|
||||
k=k.clone(),
|
||||
v=v.clone(),
|
||||
g=g.clone(),
|
||||
beta=beta.clone(),
|
||||
initial_state=h0.transpose(-1, -2).contiguous().clone(),
|
||||
output_final_state=True,
|
||||
cu_seqlens=cu_seqlens_t,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
)
|
||||
|
||||
assert not torch.isnan(tri_o).any(), "Triton output o contains NaN"
|
||||
assert not torch.isnan(tri_ht).any(), "Triton output ht contains NaN"
|
||||
assert_close("o", ref_o, tri_o, 0.005)
|
||||
assert_close("ht", ref_ht, tri_ht.transpose(-1, -2).contiguous(), 0.005)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user