[Feature] Support batch invariance on ROCm (#52231)

Signed-off-by: Matthew Wong <Matthew.Wong2@amd.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Andreas Karatzas <akaratza@amd.com>
This commit is contained in:
Matt
2026-10-12 10:53:26 -05:00
committed by GitHub
co-authored by Claude Opus 5.5 Andreas Karatzas
parent 50b21a2667
commit e0317b28ce
31 changed files with 1106 additions and 122 deletions
+87 -2
View File
@@ -273,7 +273,7 @@ steps:
#------------------------------------------------------------ mi250 · v1 -------------------------------------------------------------# #------------------------------------------------------------ mi250 · v1 -------------------------------------------------------------#
- label: ":amd: (MI250) Batch Invariance" # TBD - label: ":amd: (MI250) Batch Invariance" # TBD
timeout_in_minutes: 35 timeout_in_minutes: 60
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
dind: false dind: false
agent_pool: mi250_1 agent_pool: mi250_1
@@ -282,14 +282,18 @@ steps:
source_file_dependencies: source_file_dependencies:
- vllm/v1/attention - vllm/v1/attention
- vllm/model_executor/layers - vllm/model_executor/layers
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/ - tests/v1/determinism/
- vllm/_aiter_ops.py - vllm/_aiter_ops.py
- vllm/platforms/rocm.py - vllm/platforms/rocm.py
commands: commands:
- export VLLM_GPU_MEMORY_UTILIZATION=0.8
- export VLLM_TEST_CLEAN_GPU_MEMORY=1
- export VLLM_WORKER_MULTIPROC_METHOD=spawn - export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pip install pytest-timeout pytest-forked - pip install pytest-timeout pytest-forked
- pytest -v -s v1/determinism/test_batch_invariance.py - pytest -v -s v1/determinism/test_batch_invariance.py
- pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA
- label: ":amd: (MI250) CUDAGraph" # TBD - label: ":amd: (MI250) CUDAGraph" # TBD
timeout_in_minutes: 55 timeout_in_minutes: 55
@@ -2443,6 +2447,56 @@ steps:
#------------------------------------------------------------ mi300 · v1 -------------------------------------------------------------# #------------------------------------------------------------ mi300 · v1 -------------------------------------------------------------#
- label: ":amd: (MI300) Batch Invariance" # TBD
timeout_in_minutes: 60
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
dind: false
agent_pool: mi300_1
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/v1/attention
- vllm/model_executor/layers
- vllm/lora/ops/triton_ops
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/
- vllm/_aiter_ops.py
- vllm/platforms/rocm.py
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pip install pytest-timeout pytest-forked
- pytest -v -s v1/determinism/test_batch_invariance.py
- pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py
- pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py
- pytest -v -s v1/determinism/test_cutlass_batch_invariance.py
- pytest -v -s v1/determinism/test_matmul_batch_invariant.py
- VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA
- VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN
- label: ":amd: (MI300) Batch Invariance (Distributed)" # TBD
timeout_in_minutes: 45
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
dind: false
agent_pool: mi300_4
num_gpus: 4
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/distributed/device_communicators/
- vllm/model_executor/determinism
- vllm/model_executor/layers
- vllm/v1/attention
- tests/v1/determinism/
- vllm/platforms/rocm.py
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_TEST_CLEAN_GPU_MEMORY=1
- VLLM_TEST_TP_SIZE=4 pytest -v -s v1/determinism/test_batch_invariance.py -k "bs1_vs_bsN or without_batch_invariance_should_fail"
- VLLM_TEST_TP_SIZE=4 VLLM_TEST_DISABLE_CUSTOM_AR=1 pytest -v -s v1/determinism/test_batch_invariance.py -k bs1_vs_bsN
- VLLM_TP_SIZE=4 VLLM_NEEDLE_TRIALS=2 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN
- VLLM_TEST_MODEL=allenai/OLMoE-1B-7B-0924 VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py
- label: ":amd: (MI300) Spec Decode Speculators + MTP" # TBD - label: ":amd: (MI300) Spec Decode Speculators + MTP" # TBD
timeout_in_minutes: 75 timeout_in_minutes: 75
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
@@ -5215,6 +5269,37 @@ steps:
#------------------------------------------------------------ mi355 · v1 -------------------------------------------------------------# #------------------------------------------------------------ mi355 · v1 -------------------------------------------------------------#
- label: ":amd: (MI355) Batch Invariance" # TBD
timeout_in_minutes: 60
mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355]
dind: false
agent_pool: mi355_1
optional: true
working_dir: "/vllm-workspace/tests"
source_file_dependencies:
- vllm/v1/attention
- vllm/model_executor/layers
- vllm/lora/ops/triton_ops
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/
- vllm/_aiter_ops.py
- vllm/platforms/rocm.py
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pip install pytest-timeout pytest-forked
- pytest -v -s v1/determinism/test_batch_invariance.py
- pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py
- pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py
- pytest -v -s v1/determinism/test_batch_invariance_vlm.py
- VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA
- VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN
- pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py
- pytest -v -s v1/determinism/test_matmul_batch_invariant.py
- pytest -v -s v1/determinism/test_cutlass_batch_invariance.py
- pytest -v -s v1/determinism/test_online_batch_invariance.py
- pytest -v -s v1/determinism/test_mx_linear_batch_invariant.py
- label: ":amd: (MI355) Spec Decode AL DFlash2 Nightly" - label: ":amd: (MI355) Spec Decode AL DFlash2 Nightly"
timeout_in_minutes: 60 timeout_in_minutes: 60
mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355]
+109 -3
View File
@@ -644,12 +644,33 @@ steps:
source_file_dependencies: source_file_dependencies:
- vllm/v1/attention - vllm/v1/attention
- vllm/model_executor/layers - vllm/model_executor/layers
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/ - tests/v1/determinism/
commands: commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn - export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pip install pytest-timeout pytest-forked - pip install pytest-timeout pytest-forked
- pytest -v -s v1/determinism/test_batch_invariance.py - pytest -v -s v1/determinism/test_batch_invariance.py
- VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA
mirror:
amd:
label: ":amd: (MI250) Batch Invariance"
dind: false
device: mi250_1
timeout_in_minutes: 60
env:
VLLM_GPU_MEMORY_UTILIZATION: "0.8"
VLLM_TEST_CLEAN_GPU_MEMORY: "1"
depends_on:
- image-build-amd
source_file_dependencies:
- vllm/v1/attention
- vllm/model_executor/layers
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/
- vllm/_aiter_ops.py
- vllm/platforms/rocm.py
- label: ":nvidia: (H100) Batch Invariance" - label: ":nvidia: (H100) Batch Invariance"
key: batch-invariance-h100 key: batch-invariance-h100
@@ -660,6 +681,8 @@ steps:
- vllm/v1/attention - vllm/v1/attention
- vllm/model_executor/layers - vllm/model_executor/layers
- vllm/lora/ops/triton_ops - vllm/lora/ops/triton_ops
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/ - tests/v1/determinism/
commands: commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn - export VLLM_WORKER_MULTIPROC_METHOD=spawn
@@ -671,15 +694,18 @@ steps:
- VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN
mirror: mirror:
amd: amd:
label: ":amd: (MI250) Batch Invariance" label: ":amd: (MI300) Batch Invariance"
dind: false dind: false
device: mi250_1 device: mi300_1
timeout_in_minutes: 35 timeout_in_minutes: 60
depends_on: depends_on:
- image-build-amd - image-build-amd
source_file_dependencies: source_file_dependencies:
- vllm/v1/attention - vllm/v1/attention
- vllm/model_executor/layers - vllm/model_executor/layers
- vllm/lora/ops/triton_ops
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/ - tests/v1/determinism/
- vllm/_aiter_ops.py - vllm/_aiter_ops.py
- vllm/platforms/rocm.py - vllm/platforms/rocm.py
@@ -688,6 +714,11 @@ steps:
- pip install pytest-timeout pytest-forked - pip install pytest-timeout pytest-forked
- pytest -v -s v1/determinism/test_batch_invariance.py - pytest -v -s v1/determinism/test_batch_invariance.py
- pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py
- pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py
- pytest -v -s v1/determinism/test_cutlass_batch_invariance.py
- pytest -v -s v1/determinism/test_matmul_batch_invariant.py
- VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA
- VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN
- label: ":nvidia: (B200) Batch Invariance" - label: ":nvidia: (B200) Batch Invariance"
key: batch-invariance-b200 key: batch-invariance-b200
@@ -697,6 +728,8 @@ steps:
- vllm/v1/attention - vllm/v1/attention
- vllm/model_executor/layers - vllm/model_executor/layers
- vllm/lora/ops/triton_ops - vllm/lora/ops/triton_ops
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/ - tests/v1/determinism/
commands: commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn - export VLLM_WORKER_MULTIPROC_METHOD=spawn
@@ -711,6 +744,79 @@ steps:
- pytest -v -s v1/determinism/test_matmul_batch_invariant.py - pytest -v -s v1/determinism/test_matmul_batch_invariant.py
- pytest -v -s v1/determinism/test_cutlass_batch_invariance.py - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py
- pytest -v -s v1/determinism/test_online_batch_invariance.py - pytest -v -s v1/determinism/test_online_batch_invariance.py
mirror:
amd:
label: ":amd: (MI355) Batch Invariance"
dind: false
device: mi355_1
timeout_in_minutes: 60
depends_on:
- image-build-amd
source_file_dependencies:
- vllm/v1/attention
- vllm/model_executor/layers
- vllm/lora/ops/triton_ops
- vllm/model_executor/kernels/linear
- vllm/model_executor/determinism
- tests/v1/determinism/
- vllm/_aiter_ops.py
- vllm/platforms/rocm.py
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pip install pytest-timeout pytest-forked
- pytest -v -s v1/determinism/test_batch_invariance.py
- pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py
- pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py
- pytest -v -s v1/determinism/test_batch_invariance_vlm.py
- VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA
- VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN
- pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py
- pytest -v -s v1/determinism/test_matmul_batch_invariant.py
- pytest -v -s v1/determinism/test_cutlass_batch_invariance.py
- pytest -v -s v1/determinism/test_online_batch_invariance.py
- pytest -v -s v1/determinism/test_mx_linear_batch_invariant.py
- label: ":nvidia: (H100) Batch Invariance (Distributed)"
key: batch-invariance-distributed-4xh100
timeout_in_minutes: 45
device: h100
optional: true
num_devices: 4
source_file_dependencies:
- vllm/distributed/device_communicators/
- vllm/model_executor/determinism
- vllm/model_executor/layers
- vllm/v1/attention
- tests/v1/determinism/
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_TEST_CLEAN_GPU_MEMORY=1
- VLLM_TEST_TP_SIZE=4 pytest -v -s v1/determinism/test_batch_invariance.py -k "bs1_vs_bsN or without_batch_invariance_should_fail"
- VLLM_TEST_TP_SIZE=4 VLLM_TEST_DISABLE_CUSTOM_AR=1 pytest -v -s v1/determinism/test_batch_invariance.py -k bs1_vs_bsN
- VLLM_TP_SIZE=4 VLLM_NEEDLE_TRIALS=2 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN
- VLLM_TEST_MODEL=allenai/OLMoE-1B-7B-0924 VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py
mirror:
amd:
label: ":amd: (MI300) Batch Invariance (Distributed)"
dind: false
device: mi300_4
timeout_in_minutes: 45
depends_on:
- image-build-amd
source_file_dependencies:
- vllm/distributed/device_communicators/
- vllm/model_executor/determinism
- vllm/model_executor/layers
- vllm/v1/attention
- tests/v1/determinism/
- vllm/platforms/rocm.py
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_TEST_CLEAN_GPU_MEMORY=1
- VLLM_TEST_TP_SIZE=4 pytest -v -s v1/determinism/test_batch_invariance.py -k "bs1_vs_bsN or without_batch_invariance_should_fail"
- VLLM_TEST_TP_SIZE=4 VLLM_TEST_DISABLE_CUSTOM_AR=1 pytest -v -s v1/determinism/test_batch_invariance.py -k bs1_vs_bsN
- VLLM_TP_SIZE=4 VLLM_NEEDLE_TRIALS=2 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN
- VLLM_TEST_MODEL=allenai/OLMoE-1B-7B-0924 VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py
- label: ":nvidia: (H200 MIG 35GB) Acceptance Length (Large Models)" - label: ":nvidia: (H200 MIG 35GB) Acceptance Length (Large Models)"
device: h200_35gb device: h200_35gb
+1
View File
@@ -20,6 +20,7 @@ Batch invariance is crucial for several use cases:
Batch invariance is supported on the following platforms: Batch invariance is supported on the following platforms:
- NVIDIA GPUs with compute capability 8.0 or higher. - NVIDIA GPUs with compute capability 8.0 or higher.
- AMD Instinct GPUs (gfx90a, gfx942, gfx950).
- Intel XPUs with Triton support. - Intel XPUs with Triton support.
### Attention Backend Selection for XPU ### Attention Backend Selection for XPU
@@ -32,11 +32,9 @@ def torch_scaled_mm(
out = torch.mm(a.to(torch.float32), b.to(torch.float32)) out = torch.mm(a.to(torch.float32), b.to(torch.float32))
out = scale_a * out out = scale_a * out
out = scale_b.T * out out = scale_b.T * out
out = out.to(out_dtype)
if bias is not None: if bias is not None:
out = out + bias out = out + bias.float()
return out.to(out_dtype)
return out
def get_8bit_types(): def get_8bit_types():
@@ -125,6 +123,25 @@ def test_scaled_mm(
torch.testing.assert_close(c_check, c_actual, rtol=1e-1, atol=1e-1) torch.testing.assert_close(c_check, c_actual, rtol=1e-1, atol=1e-1)
@pytest.mark.parametrize("in_dtype", get_8bit_types())
@pytest.mark.parametrize("use_heuristic", [True, False])
def test_scaled_mm_adds_bias_before_rounding(in_dtype, use_heuristic):
# The accumulator is 1 + 3 * 2**-10 and the bias 2**-8. Rounding to bf16
# first gives 1.0 (1 + 2**-8 is a tie, to even); adding first, 1 + 2**-7.
a = torch.ones((1, 32), device=device).to(in_dtype)
col = torch.tensor([64.0] * 16 + [2.0, 1.0] + [0.0] * 14, device=device)
b = col[:, None].expand(32, 16).to(in_dtype).contiguous()
scale_a = torch.ones((1, 1), device=device)
scale_b = torch.full((1, 1), 2**-10, device=device)
bias = torch.full((16,), 2**-8, device=device, dtype=torch.bfloat16)
args = (a, b, scale_a, scale_b, torch.bfloat16, bias)
out = triton_scaled_mm(*args, use_heuristic=use_heuristic)
expected = torch_scaled_mm(*args)
assert (expected == 1 + 2**-7).all()
torch.testing.assert_close(out, expected, rtol=0, atol=0)
# TD operand loads must be bit-exact vs the plain masked-load path. # TD operand loads must be bit-exact vs the plain masked-load path.
@pytest.mark.skipif( @pytest.mark.skipif(
not (current_platform.is_cuda_alike() or current_platform.is_xpu()), not (current_platform.is_cuda_alike() or current_platform.is_xpu()),
@@ -180,6 +180,7 @@ def test_logprobs_bitwise_batch_invariance_bs1_vs_bsN(
llm = LLM( llm = LLM(
model=TEST_MODEL, model=TEST_MODEL,
tensor_parallel_size=tp_size, tensor_parallel_size=tp_size,
disable_custom_all_reduce=os.getenv("VLLM_TEST_DISABLE_CUSTOM_AR") == "1",
max_num_seqs=128, max_num_seqs=128,
max_model_len=8192, max_model_len=8192,
dtype="auto", # not everything is supported dtype="auto", # not everything is supported
@@ -9,7 +9,7 @@ import numpy as np
import pytest import pytest
import torch import torch
from PIL import Image from PIL import Image
from utils import _extract_step_logprobs, skip_if_not_cuda from utils import _extract_step_logprobs, skip_if_not_cuda_alike
from vllm import LLM, SamplingParams from vllm import LLM, SamplingParams
from vllm.inputs import TextPrompt from vllm.inputs import TextPrompt
@@ -89,7 +89,7 @@ def _assert_batch_invariant(llm: LLM, inputs: list[TextPrompt], sampling) -> Non
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("input_type", ["image", "video"]) @pytest.mark.parametrize("input_type", ["image", "video"])
@pytest.mark.parametrize("mm_encoder_attn_backend", ["FLASH_ATTN", "TORCH_SDPA"]) @pytest.mark.parametrize("mm_encoder_attn_backend", ["FLASH_ATTN", "TORCH_SDPA"])
def test_vlm_batch_invariance_bs1_vs_bsN( def test_vlm_batch_invariance_bs1_vs_bsN(
@@ -101,7 +101,7 @@ def test_vlm_batch_invariance_bs1_vs_bsN(
_run_vlm_batch_invariance(input_type, mm_encoder_attn_backend, enforce_eager=True) _run_vlm_batch_invariance(input_type, mm_encoder_attn_backend, enforce_eager=True)
@skip_if_not_cuda @skip_if_not_cuda_alike
def test_vlm_batch_invariance_default_execution(monkeypatch: pytest.MonkeyPatch): def test_vlm_batch_invariance_default_execution(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("VLLM_USE_FLASHINFER_SAMPLER", "0") monkeypatch.setenv("VLLM_USE_FLASHINFER_SAMPLER", "0")
_run_vlm_batch_invariance("image", "FLASH_ATTN", enforce_eager=False) _run_vlm_batch_invariance("image", "FLASH_ATTN", enforce_eager=False)
@@ -19,8 +19,10 @@ from tests.kernels.moe.utils import make_dummy_moe_config, make_test_quant_confi
from tests.kernels.quantization.nvfp4_utils import get_nvfp4_global_scale from tests.kernels.quantization.nvfp4_utils import get_nvfp4_global_scale
from tests.utils import TestFP8Layer, requires_fp8 from tests.utils import TestFP8Layer, requires_fp8
from vllm import _custom_ops as ops from vllm import _custom_ops as ops
from vllm.model_executor.kernels.linear.scaled_mm.cutlass import ( from vllm.model_executor.kernels.linear import fp8_scaled_mm_is_batch_invariant
from vllm.model_executor.kernels.linear.scaled_mm import (
CutlassFP8ScaledMMLinearKernel, CutlassFP8ScaledMMLinearKernel,
TritonFP8ScaledMMLinearKernel,
) )
from vllm.model_executor.layers.fused_moe import fused_topk 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.activation import MoEActivation
@@ -31,8 +33,11 @@ from vllm.model_executor.layers.fused_moe.experts.cutlass_moe import (
CutlassExpertsFp4, CutlassExpertsFp4,
) )
from vllm.model_executor.layers.quantization.utils.quant_utils import ( from vllm.model_executor.layers.quantization.utils.quant_utils import (
kFp8DynamicTensorSym,
kFp8DynamicTokenSym, kFp8DynamicTokenSym,
kFp8StaticChannelSym,
kFp8StaticTensorSym, kFp8StaticTensorSym,
kFp8StaticTokenSym,
) )
from vllm.platforms import current_platform from vllm.platforms import current_platform
from vllm.utils.torch_utils import set_random_seed from vllm.utils.torch_utils import set_random_seed
@@ -87,16 +92,42 @@ _NVFP4_MOE_BATCH_INVARIANT_CASES = (
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_cuda(): def setup_cuda():
if not current_platform.is_cuda(): if not current_platform.is_cuda_alike():
pytest.skip("CUTLASS FP8 kernels require CUDA.") pytest.skip("Requires CUDA or ROCm.")
torch.set_default_device("cuda") torch.set_default_device("cuda")
@requires_fp8 @requires_fp8
@pytest.mark.parametrize(
"kernel",
[
pytest.param(
CutlassFP8ScaledMMLinearKernel,
marks=pytest.mark.skipif(
not current_platform.is_cuda(), reason="CUTLASS requires CUDA."
),
),
TritonFP8ScaledMMLinearKernel,
],
)
@pytest.mark.parametrize(
"act_key,weight_key",
[
(kFp8DynamicTokenSym, kFp8StaticTensorSym),
(kFp8DynamicTokenSym, kFp8StaticChannelSym),
(kFp8DynamicTokenSym, kFp8StaticTokenSym),
(kFp8StaticTensorSym, kFp8StaticTensorSym),
# An amax over the whole batch: the GEMM is invariant, the layer is not.
(kFp8DynamicTensorSym, kFp8StaticTensorSym),
],
)
@pytest.mark.parametrize("weight_shape", [(1024, 2048), (4608, 4096)]) @pytest.mark.parametrize("weight_shape", [(1024, 2048), (4608, 4096)])
@pytest.mark.parametrize("batch_size", [1, 16, 17, 32, 64, 65, 256, 257]) @pytest.mark.parametrize("batch_size", [1, 16, 17, 32, 64, 65, 256, 257])
@torch.inference_mode() @torch.inference_mode()
def test_cutlass_fp8_batch_invariant_fixed_config( def test_fp8_scaled_mm_batch_invariant_fixed_config(
kernel: type,
act_key,
weight_key,
weight_shape: tuple[int, int], weight_shape: tuple[int, int],
batch_size: int, batch_size: int,
default_vllm_config, default_vllm_config,
@@ -108,14 +139,16 @@ def test_cutlass_fp8_batch_invariant_fixed_config(
torch.manual_seed(0) torch.manual_seed(0)
layer = TestFP8Layer( layer = TestFP8Layer(
weight_shape=weight_shape, weight_shape=weight_shape,
activation_quant_key=kFp8DynamicTokenSym, activation_quant_key=act_key,
weight_quant_key=kFp8StaticTensorSym, weight_quant_key=weight_key,
input_dtype=torch.bfloat16, input_dtype=torch.bfloat16,
out_dtype=torch.bfloat16, out_dtype=torch.bfloat16,
device=torch.device("cuda"), device=torch.device("cuda"),
force_kernel=CutlassFP8ScaledMMLinearKernel, force_kernel=kernel,
) )
assert isinstance(layer.kernel, CutlassFP8ScaledMMLinearKernel) assert isinstance(layer.kernel, kernel)
dynamic_per_tensor = act_key == kFp8DynamicTensorSym
assert fp8_scaled_mm_is_batch_invariant(layer.kernel) != dynamic_per_tensor
in_features = weight_shape[1] in_features = weight_shape[1]
needle = torch.randn((1, in_features), device="cuda", dtype=torch.bfloat16) needle = torch.randn((1, in_features), device="cuda", dtype=torch.bfloat16)
@@ -124,6 +157,12 @@ def test_cutlass_fp8_batch_invariant_fixed_config(
filler = torch.randn( filler = torch.randn(
(max(batch_size - 1, 0), in_features), device="cuda", dtype=torch.bfloat16 (max(batch_size - 1, 0), in_features), device="cuda", dtype=torch.bfloat16
) )
if dynamic_per_tensor:
if batch_size == 1:
pytest.skip("no other rows to move the scale")
front_output = layer(torch.cat([needle, 100 * filler], dim=0))[0]
assert not torch.equal(front_output, baseline)
return
front_batch = torch.cat([needle, filler], dim=0) front_batch = torch.cat([needle, filler], dim=0)
back_batch = torch.cat([filler, needle], dim=0) back_batch = torch.cat([filler, needle], dim=0)
@@ -135,6 +174,69 @@ def test_cutlass_fp8_batch_invariant_fixed_config(
torch.testing.assert_close(back_output, baseline, rtol=0, atol=0) torch.testing.assert_close(back_output, baseline, rtol=0, atol=0)
@requires_fp8
@pytest.mark.parametrize("scheme", ["fp8_per_tensor", "fp8_per_channel"])
@torch.inference_mode()
def test_online_fp8_linear_batch_invariant(
scheme, monkeypatch, default_vllm_config, dist_init
):
from vllm.config.model import ModelConfig
from vllm.config.quantization import resolve_quantization_config
from vllm.model_executor.layers.linear import ReplicatedLinear
from vllm.model_executor.layers.quantization.online.base import (
OnlineQuantizationConfig,
)
from vllm.utils.torch_utils import set_default_torch_dtype
monkeypatch.setattr(envs, "VLLM_BATCH_INVARIANT", True)
default_vllm_config.model_config = ModelConfig(dtype="bfloat16")
quant_config = OnlineQuantizationConfig(resolve_quantization_config(scheme, None))
set_random_seed(0)
with set_default_torch_dtype(torch.bfloat16), torch.device("cuda"):
layer = ReplicatedLinear(
1024, 768, bias=False, quant_config=quant_config, prefix="mlp.down_proj"
)
layer.weight.weight_loader(layer.weight, torch.randn(768, 1024))
needle = torch.randn(1, 1024)
filler = 100 * torch.randn(63, 1024)
baseline = layer(needle)[0]
assert torch.equal(layer(torch.cat([needle, filler]))[0][:1], baseline)
assert torch.equal(layer(torch.cat([filler, needle]))[0][-1:], baseline)
@requires_fp8
@torch.inference_mode()
def test_block_fp8_batch_invariant_across_tuned_block_size_k(monkeypatch):
"""The block-FP8 kernel applies the block scales once per K tile, so a tuned
BLOCK_SIZE_K below the scale group size reorders the sum."""
from vllm.model_executor.layers.quantization.utils import fp8_utils
base = dict(
BLOCK_SIZE_M=64, BLOCK_SIZE_N=128, GROUP_SIZE_M=1, num_warps=4, num_stages=2
)
table = {256: {**base, "BLOCK_SIZE_K": 128}, 512: {**base, "BLOCK_SIZE_K": 64}}
monkeypatch.setattr(fp8_utils, "get_w8a8_block_fp8_configs", lambda *_: table)
monkeypatch.setattr(envs, "VLLM_BATCH_INVARIANT", True)
N, K, block = 12288, 2048, [128, 128]
set_random_seed(0)
fp8 = current_platform.fp8_dtype()
A = torch.randn(512, K).to(fp8)
B = torch.randn(N, K).to(fp8)
As = torch.rand(512, K // 128) / 100
Bs = torch.rand(N // 128, K // 128) / 100
def run(m):
return fp8_utils.w8a8_triton_block_scaled_mm(
A[:m], B, As[:m], Bs, block, torch.bfloat16
)[:256]
assert torch.equal(run(256), run(512))
monkeypatch.setattr(envs, "VLLM_BATCH_INVARIANT", False)
assert not torch.equal(run(256), run(512))
@_NVFP4_REQUIRES_SM100 @_NVFP4_REQUIRES_SM100
@pytest.mark.parametrize("dtype", _NVFP4_LINEAR_DTYPES) @pytest.mark.parametrize("dtype", _NVFP4_LINEAR_DTYPES)
@pytest.mark.parametrize("shape", _NVFP4_LINEAR_CONSISTENCY_SHAPES) @pytest.mark.parametrize("shape", _NVFP4_LINEAR_CONSISTENCY_SHAPES)
@@ -4,7 +4,7 @@
import pytest import pytest
import torch import torch
from utils import skip_if_not_cuda from utils import skip_if_not_cuda_alike
import vllm.lora.ops.triton_ops.lora_shrink_op as shrink_op import vllm.lora.ops.triton_ops.lora_shrink_op as shrink_op
import vllm.lora.ops.triton_ops.utils as lora_utils import vllm.lora.ops.triton_ops.utils as lora_utils
@@ -39,7 +39,7 @@ def _shrink(inputs, weights, mapping, scaling):
return out return out
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize( @pytest.mark.parametrize(
"hidden_size,rank,nslices,dtype", "hidden_size,rank,nslices,dtype",
[ [
@@ -68,7 +68,7 @@ def test_lora_shrink_matches_reference(hidden_size, rank, nslices, dtype):
torch.testing.assert_close(out, ref, rtol=5e-3, atol=5e-3) torch.testing.assert_close(out, ref, rtol=5e-3, atol=5e-3)
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("nslices", [1, 3]) @pytest.mark.parametrize("nslices", [1, 3])
def test_lora_shrink_invariant_across_batch_sizes(monkeypatch, nslices): def test_lora_shrink_invariant_across_batch_sizes(monkeypatch, nslices):
reduce_kernel, reduce_grids = shrink_op._lora_shrink_reduce_kernel, [] reduce_kernel, reduce_grids = shrink_op._lora_shrink_reduce_kernel, []
@@ -10,7 +10,11 @@ import pytest
import torch import torch
from utils import skip_unsupported from utils import skip_unsupported
from vllm.model_executor.determinism.batch_invariant import matmul_batch_invariant import vllm.model_executor.determinism.batch_invariant as bi
from vllm.model_executor.determinism.batch_invariant import (
enable_batch_invariant_mode,
matmul_batch_invariant,
)
from vllm.model_executor.determinism.batch_invariant_configs import ( from vllm.model_executor.determinism.batch_invariant_configs import (
_BATCH_INVARIANT_MATMUL_TUNED_CONFIGS, _BATCH_INVARIANT_MATMUL_TUNED_CONFIGS,
_get_tuned_matmul_arch_family, _get_tuned_matmul_arch_family,
@@ -129,3 +133,49 @@ def test_matmul_batch_invariance_across_tuned_m_buckets(m, transpose_b):
batch_output = matmul_batch_invariant(a, b) batch_output = matmul_batch_invariant(a, b)
assert torch.equal(single_output[0], batch_output[0]) assert torch.equal(single_output[0], batch_output[0])
@pytest.fixture
def batch_invariant_mode():
# The overrides are process-global: remove them so later tests compare
# against the stock aten kernels again.
enable_batch_invariant_mode()
yield
assert bi._batch_invariant_LIB is not None
bi._batch_invariant_LIB._destroy()
bi._batch_invariant_MODE = False
@skip_unsupported
@pytest.mark.usefixtures("batch_invariant_mode")
@pytest.mark.parametrize("op", ["mm", "addmm", "bmm"])
def test_compiled_matmul_reaches_batch_invariant_out_overloads(op):
"""Inductor lowers mm/addmm/bmm to ``extern_kernels.<op>(..., out=buf)``,
i.e. the ``.out`` overload, which eager never dispatches."""
if (
op != "bmm"
and current_platform.is_cuda()
and not current_platform.is_device_capability_family(80)
):
pytest.skip("mm/addmm are not overridden on SM90+")
torch.manual_seed(0)
dtype, k, n, needle = torch.bfloat16, 1024, 768, 37
w = torch.randn(4, k, n, device=DEVICE_TYPE, dtype=dtype)
bias = torch.randn(n, device=DEVICE_TYPE, dtype=dtype)
fn = {
"mm": lambda x: torch.mm(x[0], w[0]),
# Bare addmm: a following pointwise op would make Inductor unfuse the bias.
"addmm": lambda x: torch.addmm(bias, x[0], w[0]),
"bmm": lambda x: torch.bmm(x, w),
}[op]
compiled = torch.compile(fn, fullgraph=True)
x = torch.randn(4, 256, k, device=DEVICE_TYPE, dtype=dtype)
row = None
for m in (64, 256, 128):
xs = x[:, :m].clone()
# With dynamic=True, a specialization on M would silently recompile.
torch._dynamo.mark_dynamic(xs, 1)
out = compiled(xs)
assert torch.equal(out, fn(xs)), f"compiled {op} bypassed the BI kernel (M={m})"
row = out[..., needle, :] if row is None else row
assert torch.equal(out[..., needle, :], row)
@@ -0,0 +1,165 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""The ROCm native MX linear kernels must not depend on the number of rows."""
import pytest
import torch
from utils import requires_mx
from vllm.utils.torch_utils import set_random_seed
SEED = 0
# E8M0 exponent half-width. With 4-bit operands and a narrow spread the fp32
# partial sums are exact, so a reordering of them would be invisible.
SCALE_SPREAD = 15
# (N, K, use_asm_gemm)
MXFP4_CASES = [
(4096, 2048, False),
(2048, 6144, False),
(4608, 7168, False),
(8192, 2048, True),
(8192, 1024, True),
(8192, 14336, True),
]
MXFP8_CASES = [(4096, 2048), (2048, 6144), (1024, 768), (1536, 1024)]
# The untuned AITER MXFP4 configs switch at M = 9/33/65/129/257/513.
MXFP4_TOKEN_COUNTS = [
1,
2,
8,
9,
16,
32,
33,
64,
65,
128,
129,
256,
257,
512,
513,
1024,
2048,
]
MXFP8_TOKEN_COUNTS = [1, 32, 64, 65, 128, 129, 256, 257, 512, 1024, 1025, 2048]
# Row 0 lands in the first tile of every decomposition, so it can stay
# invariant when the rest of the output does not.
CHECK_ROWS = [0, 1, 2, 3, 7, 15, 31]
def _probe(out: torch.Tensor) -> torch.Tensor:
"""The leading rows of an output, kept so the sweep does not hold every GEMM."""
return out[: CHECK_ROWS[-1] + 1].clone()
def _variant_rows(probes: dict[int, torch.Tensor]) -> list[str]:
"""Rows whose contents changed with the number of rows in the launch."""
failures = []
for row in CHECK_ROWS:
counts = [n for n in sorted(probes) if n > row]
if not counts:
continue
reference = probes[counts[0]][row]
variant = [n for n in counts if not torch.equal(probes[n][row], reference)]
if variant:
failures.append(f"row {row} changed at row counts {variant}")
return failures
def _mxfp4_weights(n: int, k: int, use_asm_gemm: bool):
"""Weights laid out as AiterMxfp4LinearKernel.process_weights_after_loading."""
weight = torch.randint(0, 255, (n, k // 2), dtype=torch.uint8, device="cuda")
weight_scale = torch.randint(
127 - SCALE_SPREAD,
128 + SCALE_SPREAD,
(n, k // 32),
dtype=torch.uint8,
device="cuda",
)
if not use_asm_gemm:
return weight, weight_scale.T.contiguous()
from aiter.ops.shuffle import shuffle_weight
sm, sn = weight_scale.shape
weight_scale = (
weight_scale.view(sm // 32, 2, 16, sn // 8, 2, 4, 1)
.permute(0, 3, 5, 2, 4, 1, 6)
.contiguous()
.view(sm, sn)
)
return shuffle_weight(weight, layout=(16, 16)), weight_scale
def _mxfp8_weights(n: int, k: int):
"""E4M3 weights with E8M0 block scales, as the MXFP8 layer stores them."""
weight = (torch.randn(n, k, device="cuda") / 8).to(torch.float8_e4m3fn)
weight_scale = torch.randint(
127 - SCALE_SPREAD,
128 + SCALE_SPREAD,
(n, k // 32),
dtype=torch.uint8,
device="cuda",
)
return weight, weight_scale
@requires_mx
@pytest.mark.parametrize("n,k,use_asm_gemm", MXFP4_CASES)
def test_mxfp4_linear_is_batch_invariant(n: int, k: int, use_asm_gemm: bool):
pytest.importorskip("aiter")
# Importing the module registers torch.ops.vllm.gemm_with_dynamic_quant.
import vllm.model_executor.kernels.linear.mxfp4.aiter # noqa: F401
set_random_seed(SEED)
weight, weight_scale = _mxfp4_weights(n, k, use_asm_gemm)
x = torch.randn(max(MXFP4_TOKEN_COUNTS), k, device="cuda", dtype=torch.bfloat16)
probes = {
num_tokens: _probe(
torch.ops.vllm.gemm_with_dynamic_quant(
x[:num_tokens], weight, weight_scale, use_asm_gemm, torch.bfloat16
)
)
for num_tokens in MXFP4_TOKEN_COUNTS
}
failures = _variant_rows(probes)
assert not failures, (
f"MXFP4 linear depends on the row count "
f"(N={n}, K={k}, asm={use_asm_gemm}):\n " + "\n ".join(failures)
)
@requires_mx
@pytest.mark.parametrize("n,k", MXFP8_CASES)
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
def test_mxfp8_linear_is_batch_invariant(n: int, k: int, dtype: torch.dtype):
from vllm.model_executor.kernels.linear.mxfp8.rocm_native import (
_mxfp8_dot_scaled_linear,
)
set_random_seed(SEED)
weight, weight_scale = _mxfp8_weights(n, k)
x = torch.randn(max(MXFP8_TOKEN_COUNTS), k, device="cuda", dtype=dtype)
probes = {
num_tokens: _probe(
_mxfp8_dot_scaled_linear(x[:num_tokens], weight, weight_scale)
)
for num_tokens in MXFP8_TOKEN_COUNTS
}
failures = _variant_rows(probes)
assert not failures, (
f"MXFP8 linear depends on the row count (N={n}, K={k}, {dtype}):\n "
+ "\n ".join(failures)
)
@@ -9,10 +9,14 @@ import torch
from utils import ( from utils import (
_extract_step_logprobs, _extract_step_logprobs,
_random_prompt, _random_prompt,
skip_if_not_cuda, skip_if_not_cuda_alike,
) )
from vllm import LLM, SamplingParams from vllm import LLM, SamplingParams
from vllm.platforms import current_platform
# FLASH_ATTN is CUDA-only.
E2E_BACKENDS = ["TRITON_ATTN"] if current_platform.is_rocm() else ["FLASH_ATTN"]
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(
not hasattr(torch, "float8_e4m3fn"), not hasattr(torch, "float8_e4m3fn"),
@@ -40,8 +44,8 @@ def _make_llm(max_num_seqs: int, backend: str) -> LLM:
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("backend", ["FLASH_ATTN"]) @pytest.mark.parametrize("backend", E2E_BACKENDS)
def test_dense_nvfp4_generation_is_deterministic_across_batch_sizes_e2e(backend): def test_dense_nvfp4_generation_is_deterministic_across_batch_sizes_e2e(backend):
seed = int(os.getenv("VLLM_TEST_SEED", "12345")) seed = int(os.getenv("VLLM_TEST_SEED", "12345"))
random.seed(seed) random.seed(seed)
@@ -6,6 +6,7 @@ vLLM server and compare BS=1 vs BS=N results (tokens and per-step logprobs).
Environment variables: Environment variables:
- VLLM_TEST_MODEL: served model name (e.g., Qwen/Qwen3-1.7B / DeepSeek-R1) - VLLM_TEST_MODEL: served model name (e.g., Qwen/Qwen3-1.7B / DeepSeek-R1)
- VLLM_TP_SIZE: tensor parallelism size (e.g., 4) - VLLM_TP_SIZE: tensor parallelism size (e.g., 4)
- VLLM_TEST_SERVER_ARGS: extra server args (e.g., "--data-parallel-size 4")
""" """
@@ -16,7 +17,7 @@ from typing import Any
import openai import openai
import pytest import pytest
from utils import BACKENDS, TEST_MODEL, _random_prompt, skip_if_not_cuda from utils import BACKENDS, TEST_MODEL, _random_prompt, skip_if_not_cuda_alike
from tests.utils import RemoteOpenAIServer from tests.utils import RemoteOpenAIServer
@@ -132,7 +133,7 @@ def _compare_bs1_vs_bsn_single_process(
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("backend", BACKENDS) @pytest.mark.parametrize("backend", BACKENDS)
def test_logprobs_bitwise_batch_invariance_bs1_vs_bsN( def test_logprobs_bitwise_batch_invariance_bs1_vs_bsN(
backend: str, backend: str,
@@ -156,6 +157,7 @@ def test_logprobs_bitwise_batch_invariance_bs1_vs_bsN(
] ]
if tp_size: if tp_size:
server_args += ["-tp", tp_size] server_args += ["-tp", tp_size]
server_args += os.getenv("VLLM_TEST_SERVER_ARGS", "").split()
with RemoteOpenAIServer(TEST_MODEL, server_args) as server: with RemoteOpenAIServer(TEST_MODEL, server_args) as server:
client = server.get_client() client = server.get_client()
@@ -4,7 +4,7 @@
import pytest import pytest
import torch import torch
from utils import skip_if_not_cuda, skip_unsupported from utils import skip_if_not_cuda_alike, skip_unsupported
from vllm.model_executor.determinism.batch_invariant import ( from vllm.model_executor.determinism.batch_invariant import (
rms_norm_batch_invariant, rms_norm_batch_invariant,
@@ -27,7 +27,7 @@ def _rms_norm_reference(
return (output * weight.float()).to(input_tensor.dtype) return (output * weight.float()).to(input_tensor.dtype)
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("batch_size", [1, 4, 64, 300]) @pytest.mark.parametrize("batch_size", [1, 4, 64, 300])
@pytest.mark.parametrize("hidden_size", [512, 2048, 4096, 8192]) @pytest.mark.parametrize("hidden_size", [512, 2048, 4096, 8192])
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16]) @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@@ -76,7 +76,7 @@ def test_rms_norm_batch_invariant_vs_reference(
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("hidden_size", [512, 4096]) @pytest.mark.parametrize("hidden_size", [512, 4096])
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16]) @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@pytest.mark.parametrize("eps", [1e-6]) @pytest.mark.parametrize("eps", [1e-6])
@@ -189,7 +189,7 @@ def _assert_rows_bit_identical(small, large, msg):
torch.testing.assert_close(small, large, rtol=0.0, atol=0.0, msg=msg) torch.testing.assert_close(small, large, rtol=0.0, atol=0.0, msg=msg)
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("hidden_size", [512, 4096]) @pytest.mark.parametrize("hidden_size", [512, 4096])
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16]) @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@pytest.mark.parametrize("seed", list(range(4))) @pytest.mark.parametrize("seed", list(range(4)))
@@ -221,7 +221,7 @@ def test_rms_norm_batch_invariant_nonresidual_kernel(
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("hidden_size", [512, 4096]) @pytest.mark.parametrize("hidden_size", [512, 4096])
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16]) @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@pytest.mark.parametrize("seed", list(range(4))) @pytest.mark.parametrize("seed", list(range(4)))
@@ -267,7 +267,7 @@ def test_rms_norm_static_fp8_quant_batch_invariant(
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("hidden_size", [512, 4096]) @pytest.mark.parametrize("hidden_size", [512, 4096])
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16]) @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@pytest.mark.parametrize("seed", list(range(4))) @pytest.mark.parametrize("seed", list(range(4)))
@@ -303,7 +303,7 @@ def test_rms_norm_per_block_quant_batch_invariant(
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("batch_size", [1, 16, 128]) @pytest.mark.parametrize("batch_size", [1, 16, 128])
@pytest.mark.parametrize("seq_len", [1, 32, 512]) @pytest.mark.parametrize("seq_len", [1, 32, 512])
@pytest.mark.parametrize("hidden_size", [2048, 4096]) @pytest.mark.parametrize("hidden_size", [2048, 4096])
@@ -343,7 +343,7 @@ def test_rms_norm_3d_input(
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
def test_rms_norm_numerical_stability(default_vllm_config): def test_rms_norm_numerical_stability(default_vllm_config):
"""Test RMS norm numerical stability with extreme values. """Test RMS norm numerical stability with extreme values.
@@ -431,7 +431,7 @@ def test_rms_norm_formula(default_vllm_config):
) )
@skip_if_not_cuda @skip_if_not_cuda_alike
@pytest.mark.parametrize("hidden_size", [128, 1024, 4096, 16384]) @pytest.mark.parametrize("hidden_size", [128, 1024, 4096, 16384])
def test_rms_norm_different_hidden_sizes(default_vllm_config, hidden_size: int): def test_rms_norm_different_hidden_sizes(default_vllm_config, hidden_size: int):
"""Test RMS norm with various hidden sizes to ensure block size handling. """Test RMS norm with various hidden sizes to ensure block size handling.
+20 -2
View File
@@ -34,12 +34,16 @@ DEVICE_BACKENDS: dict[str, DeviceConfig] = {
available=current_platform.is_xpu() and HAS_TRITON, available=current_platform.is_xpu() and HAS_TRITON,
backends=["TRITON_ATTN"], backends=["TRITON_ATTN"],
), ),
"rocm": DeviceConfig(
available=current_platform.is_rocm() and HAS_TRITON,
backends=["TRITON_ATTN"],
),
} }
DEFAULT_MODEL = "Qwen/Qwen3-1.7B" DEFAULT_MODEL = "Qwen/Qwen3-1.7B"
TEST_MODEL = os.getenv("VLLM_TEST_MODEL", DEFAULT_MODEL) TEST_MODEL = os.getenv("VLLM_TEST_MODEL", DEFAULT_MODEL)
# Override backends for MLA models (MLA only supported on CUDA). # Override backends for MLA models.
if os.getenv("VLLM_TEST_MODEL"): if os.getenv("VLLM_TEST_MODEL"):
config = get_config(TEST_MODEL, trust_remote_code=False) config = get_config(TEST_MODEL, trust_remote_code=False)
if ModelArchConfigConvertorBase(config, config.get_text_config()).is_deepseek_mla(): if ModelArchConfigConvertorBase(config, config.get_text_config()).is_deepseek_mla():
@@ -52,6 +56,10 @@ if os.getenv("VLLM_TEST_MODEL"):
available=DEVICE_BACKENDS["xpu"].available, available=DEVICE_BACKENDS["xpu"].available,
backends=[], backends=[],
) )
DEVICE_BACKENDS["rocm"] = DeviceConfig(
available=DEVICE_BACKENDS["rocm"].available,
backends=["TRITON_MLA"],
)
# Only include backends for devices that are actually available. # Only include backends for devices that are actually available.
BACKENDS: list[str] = sorted( BACKENDS: list[str] = sorted(
@@ -60,7 +68,7 @@ BACKENDS: list[str] = sorted(
skip_unsupported = pytest.mark.skipif( skip_unsupported = pytest.mark.skipif(
not any(cfg.available for cfg in DEVICE_BACKENDS.values()), not any(cfg.available for cfg in DEVICE_BACKENDS.values()),
reason="Requires CUDA >= Ampere (SM80) or Intel XPU with Triton", reason="Requires CUDA >= Ampere (SM80), ROCm, or Intel XPU with Triton",
) )
skip_if_not_cuda = pytest.mark.skipif( skip_if_not_cuda = pytest.mark.skipif(
@@ -68,6 +76,16 @@ skip_if_not_cuda = pytest.mark.skipif(
reason="Requires CUDA >= Ampere (SM80)", reason="Requires CUDA >= Ampere (SM80)",
) )
skip_if_not_cuda_alike = pytest.mark.skipif(
not (DEVICE_BACKENDS["cuda"].available or DEVICE_BACKENDS["rocm"].available),
reason="Requires CUDA >= Ampere (SM80) or ROCm",
)
requires_mx = pytest.mark.skipif(
not (current_platform.is_rocm() and current_platform.supports_mx()),
reason="requires a ROCm device with native MX support (gfx95x)",
)
def _random_prompt(min_words: int = 1024, max_words: int = 1024 * 2) -> str: def _random_prompt(min_words: int = 1024, max_words: int = 1024 * 2) -> str:
# Generate more realistic prompts that will actually produce varied tokens # Generate more realistic prompts that will actually produce varied tokens
@@ -413,6 +413,17 @@ class CudaCommunicator(DeviceCommunicatorBase):
out = ca_comm.custom_all_reduce(input_) out = ca_comm.custom_all_reduce(input_)
assert out is not None assert out is not None
return out return out
if (
envs.VLLM_BATCH_INVARIANT
and current_platform.is_rocm()
and input_.is_floating_point()
):
# Integer sums are exact in any order and stay on RCCL.
from vllm.model_executor.determinism.batch_invariant import (
all_reduce_batch_invariant,
)
return all_reduce_batch_invariant(input_, self.device_group)
symm_mem_comm = self.symm_mem_comm symm_mem_comm = self.symm_mem_comm
if symm_mem_comm is not None and symm_mem_comm.should_use_symm_mem(input_): if symm_mem_comm is not None and symm_mem_comm.should_use_symm_mem(input_):
out = symm_mem_comm.all_reduce(input_) out = symm_mem_comm.all_reduce(input_)
@@ -498,7 +509,17 @@ class CudaCommunicator(DeviceCommunicatorBase):
chunk_size = input_tensor.shape[0] // world_size chunk_size = input_tensor.shape[0] // world_size
output_shape = (chunk_size,) + input_tensor.shape[1:] output_shape = (chunk_size,) + input_tensor.shape[1:]
if should_nccl_symm_mem_ag_rs(): if (
envs.VLLM_BATCH_INVARIANT
and current_platform.is_rocm()
and input_.is_floating_point()
):
from vllm.model_executor.determinism.batch_invariant import (
reduce_scatter_batch_invariant,
)
output = reduce_scatter_batch_invariant(input_tensor, self.device_group)
elif should_nccl_symm_mem_ag_rs():
output = self._reduce_scatter_symm_mem(input_tensor) output = self._reduce_scatter_symm_mem(input_tensor)
else: else:
output = torch.empty( output = torch.empty(
@@ -563,6 +584,22 @@ class CudaCommunicator(DeviceCommunicatorBase):
assert output.device == input_tensor.device assert output.device == input_tensor.device
assert output.is_contiguous() assert output.is_contiguous()
if (
envs.VLLM_BATCH_INVARIANT
and current_platform.is_rocm()
and input_.is_floating_point()
):
from vllm.model_executor.determinism.batch_invariant import (
reduce_scatter_batch_invariant,
)
result = reduce_scatter_batch_invariant(
input_tensor, self.device_group, sizes=sizes
)
if output is not None:
return output.copy_(result)
return result.movedim(0, dim).contiguous()
if self._can_use_aiter_ag_rs(sizes): if self._can_use_aiter_ag_rs(sizes):
aiter_comm = self.aiter_ar_comm aiter_comm = self.aiter_ar_comm
assert aiter_comm is not None assert aiter_comm is not None
@@ -6,6 +6,7 @@ from collections.abc import Callable
from typing import Any from typing import Any
import torch import torch
import torch.distributed as dist
import vllm.envs as envs import vllm.envs as envs
from vllm.model_executor.determinism.batch_invariant_configs import ( from vllm.model_executor.determinism.batch_invariant_configs import (
@@ -59,6 +60,7 @@ def matmul_kernel_persistent(
stride_bn, stride_bn,
stride_cm, stride_cm,
stride_cn, stride_cn,
stride_bias,
BLOCK_SIZE_M: tl.constexpr, # BLOCK_SIZE_M: tl.constexpr, #
BLOCK_SIZE_N: tl.constexpr, # BLOCK_SIZE_N: tl.constexpr, #
BLOCK_SIZE_K: tl.constexpr, # BLOCK_SIZE_K: tl.constexpr, #
@@ -128,7 +130,7 @@ def matmul_kernel_persistent(
c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
if HAS_BIAS: if HAS_BIAS:
bias_ptrs = bias_ptr + offs_cn bias_ptrs = bias_ptr + offs_cn * stride_bias
bias = tl.load(bias_ptrs, mask=offs_cn < N, other=0.0).to(tl.float32) bias = tl.load(bias_ptrs, mask=offs_cn < N, other=0.0).to(tl.float32)
accumulator += bias accumulator += bias
c = accumulator.to(c_ptr.dtype.element_ty) c = accumulator.to(c_ptr.dtype.element_ty)
@@ -305,6 +307,8 @@ def matmul_persistent(
), ),
) )
if not _BLOCK_SIZES_RESOLVED:
_resolve_block_sizes()
configs = { configs = {
torch.bfloat16: { torch.bfloat16: {
"BLOCK_SIZE_M": 128, "BLOCK_SIZE_M": 128,
@@ -345,6 +349,7 @@ def matmul_persistent(
b.stride(1), # b.stride(1), #
c.stride(0), c.stride(0),
c.stride(1), # c.stride(1), #
bias.stride(0) if bias is not None else 1,
NUM_SMS=NUM_SMS, # NUM_SMS=NUM_SMS, #
A_LARGE=a.numel() > 2**31, A_LARGE=a.numel() > 2**31,
B_LARGE=b.numel() > 2**31, B_LARGE=b.numel() > 2**31,
@@ -489,16 +494,19 @@ def bmm_kernel(
@triton.jit @triton.jit
def _log_softmax_kernel( def _softmax_kernel(
input_ptr, input_ptr,
output_ptr, output_ptr,
input_row_stride, input_row_stride,
input_col_stride,
output_row_stride, output_row_stride,
n_cols, n_cols,
BLOCK_SIZE: tl.constexpr, BLOCK_SIZE: tl.constexpr,
LOG: tl.constexpr = False,
): ):
"""Compute log_softmax along the last dimension of a 2D tensor. """Compute softmax, or log_softmax when ``LOG``, along the last dimension of a
Each block handles one row of the input tensor. 2D tensor. Each block handles one row of the input tensor, in a fixed block
order, so the result never depends on the row count.
""" """
# Get the row index for this block # Get the row index for this block
row_idx = tl.program_id(0).to(tl.int64) row_idx = tl.program_id(0).to(tl.int64)
@@ -514,7 +522,9 @@ def _log_softmax_kernel(
mask = col_idx < n_cols mask = col_idx < n_cols
# Load values # Load values
vals = tl.load(row_start_ptr + col_idx, mask=mask, other=-float("inf")) vals = tl.load(
row_start_ptr + col_idx * input_col_stride, mask=mask, other=-float("inf")
)
# Update maximum # Update maximum
max_val = tl.max(tl.maximum(vals, max_val)) max_val = tl.max(tl.maximum(vals, max_val))
@@ -526,25 +536,26 @@ def _log_softmax_kernel(
mask = col_idx < n_cols mask = col_idx < n_cols
# Load values # Load values
vals = tl.load(row_start_ptr + col_idx, mask=mask, other=0.0) vals = tl.load(row_start_ptr + col_idx * input_col_stride, mask=mask, other=0.0)
# Compute exp(x - max_val) and accumulate # Compute exp(x - max_val) and accumulate
exp_vals = tl.exp(vals - max_val) exp_vals = tl.exp(vals - max_val)
sum_exp += tl.sum(tl.where(mask, exp_vals, 0.0)) sum_exp += tl.sum(tl.where(mask, exp_vals, 0.0))
# Compute log(sum_exp) log_sum_exp = tl.log(sum_exp) if LOG else 0.0
log_sum_exp = tl.log(sum_exp)
# Step 3: Compute final log_softmax values: x - max_val - log_sum_exp # Step 3: normalise
for col_offset in range(0, n_cols, BLOCK_SIZE): for col_offset in range(0, n_cols, BLOCK_SIZE):
col_idx = col_offset + tl.arange(0, BLOCK_SIZE) col_idx = col_offset + tl.arange(0, BLOCK_SIZE)
mask = col_idx < n_cols mask = col_idx < n_cols
# Load values # Load values
vals = tl.load(row_start_ptr + col_idx, mask=mask) vals = tl.load(row_start_ptr + col_idx * input_col_stride, mask=mask)
# Compute log_softmax if LOG:
output = vals - max_val - log_sum_exp output = vals - max_val - log_sum_exp
else:
output = tl.exp(vals - max_val) / sum_exp
# Store results # Store results
tl.store(output_row_start_ptr + col_idx, output, mask=mask) tl.store(output_row_start_ptr + col_idx, output, mask=mask)
@@ -570,30 +581,55 @@ def log_softmax(input: torch.Tensor, dim: int = -1) -> torch.Tensor:
# Flatten all dimensions except the last one # Flatten all dimensions except the last one
original_shape = input.shape original_shape = input.shape
input_2d = input.reshape(-1, input.shape[-1]) input_2d = input.reshape(-1, input.shape[-1])
input_2d = input_2d.contiguous()
n_rows, n_cols = input_2d.shape n_rows, n_cols = input_2d.shape
# Allocate output tensor # Allocate output tensor
output = torch.empty_like(input_2d) output = torch.empty((n_rows, n_cols), dtype=input_2d.dtype, device=input_2d.device)
# Choose block size based on the number of columns # Choose block size based on the number of columns
BLOCK_SIZE = 1024 BLOCK_SIZE = 1024
# Launch kernel with one block per row # Launch kernel with one block per row
grid = (n_rows,) grid = (n_rows,)
_log_softmax_kernel[grid]( _softmax_kernel[grid](
input_2d, input_2d,
output, output,
input_2d.stride(0), input_2d.stride(0),
input_2d.stride(1),
output.stride(0), output.stride(0),
n_cols, n_cols,
BLOCK_SIZE=BLOCK_SIZE, BLOCK_SIZE=BLOCK_SIZE,
LOG=True,
) )
# Reshape output back to original shape # Reshape output back to original shape
return output.reshape(original_shape) return output.reshape(original_shape)
def softmax(input: torch.Tensor, dim: int = -1) -> torch.Tensor:
"""Compute softmax along the last dimension using a Triton kernel."""
if dim != -1 and dim != input.ndim - 1:
raise ValueError(
"This implementation only supports softmax along the last dimension"
)
original_shape = input.shape
input_2d = input.reshape(-1, input.shape[-1])
n_rows, n_cols = input_2d.shape
output = torch.empty((n_rows, n_cols), dtype=input_2d.dtype, device=input_2d.device)
_softmax_kernel[(n_rows,)](
input_2d,
output,
input_2d.stride(0),
input_2d.stride(1),
output.stride(0),
n_cols,
BLOCK_SIZE=1024,
)
return output.reshape(original_shape)
@triton.jit @triton.jit
def mean_kernel( def mean_kernel(
input_ptr, input_ptr,
@@ -735,6 +771,9 @@ def mean_dim(
def mm_batch_invariant(a, b, bias=None): def mm_batch_invariant(a, b, bias=None):
if a.device.type == "xpu": if a.device.type == "xpu":
# The descriptor kernel reads the bias at unit stride.
if bias is not None:
bias = bias.contiguous()
return matmul_descriptor_persistent(a, b, bias=bias) return matmul_descriptor_persistent(a, b, bias=bias)
return matmul_persistent(a, b, bias=bias) return matmul_persistent(a, b, bias=bias)
@@ -820,6 +859,8 @@ def bmm_batch_invariant(a, b, *, out=None):
assert out.dtype == dtype and out.device == a.device, "out tensor mismatch" assert out.dtype == dtype and out.device == a.device, "out tensor mismatch"
c = out c = out
if not _BLOCK_SIZES_RESOLVED:
_resolve_block_sizes()
configs = { configs = {
torch.bfloat16: { torch.bfloat16: {
"BLOCK_SIZE_M": 128, "BLOCK_SIZE_M": 128,
@@ -877,8 +918,50 @@ def bmm_batch_invariant(a, b, *, out=None):
return c return c
def addmm_batch_invariant(bias, a, b): def _addmm_fused_bias(bias, beta, alpha, n):
return mm_batch_invariant(a, b, bias=bias) # The kernel adds bias into its fp32 accumulator indexed by output column,
# so it can only absorb a row vector of width n, unscaled by alpha.
if beta == 0 or alpha != 1:
return None
if bias.dim() != 1 or bias.shape[0] != n:
return None
# fp32 keeps the scaling exact; the kernel casts the bias to fp32 anyway.
return bias if beta == 1 else bias.float() * beta
def _addmm_impl(bias, a, b, beta, alpha, out):
fused_bias = _addmm_fused_bias(bias, beta, alpha, b.shape[1])
result = mm_batch_invariant(a, b, bias=fused_bias)
if fused_bias is None:
if alpha != 1:
result = result * alpha
if beta != 0:
result = result + (bias if beta == 1 else beta * bias)
if out is None:
return result
return out.copy_(result)
def addmm_batch_invariant(bias, a, b, *, beta=1, alpha=1):
return _addmm_impl(bias, a, b, beta, alpha, None)
# Inductor lowers mm/addmm/bmm to ``extern_kernels.<op>(..., out=buf)``, which
# dispatches the ``.out`` overload rather than the default one. Registering only
# the default leaves the compiled path on the vendor GEMM, so a compiled
# ``torch.addmm`` is batch variant while the eager one is not.
def mm_out_batch_invariant(a, b, *, out):
return matmul_batch_invariant(a, b, out=out)
def addmm_out_batch_invariant(bias, a, b, *, beta=1, alpha=1, out):
return _addmm_impl(bias, a, b, beta, alpha, out)
def bmm_out_batch_invariant(a, b, *, out):
return bmm_batch_invariant(a, b, out=out)
def _log_softmax_batch_invariant(input, dim, _half_to_float): def _log_softmax_batch_invariant(input, dim, _half_to_float):
@@ -887,10 +970,16 @@ def _log_softmax_batch_invariant(input, dim, _half_to_float):
return log_softmax(input, dim=dim) return log_softmax(input, dim=dim)
def softmax_batch_invariant(input, dim, dtype=None): def softmax_batch_invariant(input, dim, half_to_float=False):
# Compute softmax in a deterministic way if half_to_float:
# First subtract max for numerical stability (standard practice) input = input.float()
if dim == -1 or dim == input.ndim - 1:
return softmax(input, dim=-1)
# Off the last dimension this falls back to torch ops, whose reduction split
# depends on the tensor shape.
input_max = torch.amax(input, dim=dim, keepdim=True) input_max = torch.amax(input, dim=dim, keepdim=True)
# First subtract max for numerical stability (standard practice)
input = input - input_max input = input - input_max
exp_x = torch.exp(input) exp_x = torch.exp(input)
sum_exp_x = torch.sum(exp_x, dim=dim, keepdim=True) sum_exp_x = torch.sum(exp_x, dim=dim, keepdim=True)
@@ -1035,11 +1124,150 @@ def rms_norm_batch_invariant(
def linear_batch_invariant(input, weight, bias=None): def linear_batch_invariant(input, weight, bias=None):
output = matmul_batch_invariant(input, weight.t())
if bias is not None: if bias is not None:
output = output + bias bias = bias.expand(weight.shape[0])
return output output = mm_batch_invariant(
input.reshape(-1, input.shape[-1]), weight.t(), bias=bias
)
return output.view(*input.shape[:-1], output.shape[-1])
@triton.jit
def _fixed_order_sum_kernel(
src_ptr, # (WORLD_SIZE, numel) gathered contributions, rank-major
out_ptr, # (numel,)
numel,
stride_rank,
WORLD_SIZE: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
LARGE: tl.constexpr,
):
"""Sum ``WORLD_SIZE`` contributions in ascending rank order, fp32 accumulator.
An output element sums exactly ``WORLD_SIZE`` values in a compile-time
constant order and rounds once, so the result depends only on the rank
ordering and never on how many elements are being reduced.
"""
pid = tl.program_id(0)
if LARGE:
pid = pid.to(tl.int64)
stride_rank = stride_rank.to(tl.int64)
offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = offs < numel
acc = tl.zeros([BLOCK_SIZE], dtype=tl.float32)
for r in tl.static_range(WORLD_SIZE):
vals = tl.load(src_ptr + r * stride_rank + offs, mask=mask, other=0.0)
acc += vals.to(tl.float32)
tl.store(out_ptr + offs, acc.to(out_ptr.dtype.element_ty), mask=mask)
def all_reduce_batch_invariant(
input_: torch.Tensor, group: "torch.distributed.ProcessGroup | None" = None
) -> torch.Tensor:
"""Sum all-reduce whose result does not depend on the message size.
Library all-reduces (NCCL/RCCL) pick their algorithm, channel count and chunk
boundaries from the message size, so the order in which a given element's
contributions are summed changes with the number of tokens in the batch.
Instead, all-gather the contributions -- pure data movement, so bitwise
reproducible at any size -- and reduce them with ``_fixed_order_sum_kernel``.
"""
world_size = dist.get_world_size(group)
if world_size == 1:
return input_
x = input_.contiguous()
numel = x.numel()
gathered = torch.empty((world_size, numel), dtype=x.dtype, device=x.device)
dist.all_gather_into_tensor(gathered, x.view(-1), group=group)
out = torch.empty_like(x)
BLOCK_SIZE = 1024
grid = (triton.cdiv(numel, BLOCK_SIZE),)
_fixed_order_sum_kernel[grid](
gathered,
out.view(-1),
numel,
numel,
WORLD_SIZE=world_size,
BLOCK_SIZE=BLOCK_SIZE,
LARGE=gathered.numel() > 2**31,
)
return out
def reduce_scatter_batch_invariant(
input_: torch.Tensor,
group: "torch.distributed.ProcessGroup | None" = None,
sizes: list[int] | None = None,
) -> torch.Tensor:
"""Sum reduce-scatter over dim 0 whose result does not depend on the size.
``ncclReduceScatter`` picks its chunking from the message size just as
``ncclAllReduce`` does. Route around it the same way
``all_reduce_batch_invariant`` does: an all-to-all sends rank ``d`` exactly
the rows it owns from every rank -- pure data movement, so bitwise
reproducible however the library chunks it -- and lands them rank-major,
which is the layout ``_fixed_order_sum_kernel`` already consumes.
``sizes`` (the reduce-scatterv case) changes only *which* rows a rank
receives, never how the received contributions are summed, so variable
shard sizes are invariant for the same reason.
Args:
input_: Contiguous full-size input; dim 0 is the scattered axis.
group: Process group, defaults to the world group.
sizes: Per-rank row counts. Uniform split when ``None``.
Returns:
This rank's shard of the sum.
"""
world_size = dist.get_world_size(group)
if world_size == 1:
return input_
x = input_.contiguous()
if sizes is None:
assert x.shape[0] % world_size == 0
sizes = [x.shape[0] // world_size] * world_size
else:
assert len(sizes) == world_size
assert x.shape[0] == sum(sizes)
rows = sizes[dist.get_rank(group)]
out = torch.empty((rows, *x.shape[1:]), dtype=x.dtype, device=x.device)
recv = torch.empty(
(world_size * rows, *x.shape[1:]), dtype=x.dtype, device=x.device
)
# Every rank takes part even when its own shard is empty: it still has rows
# to send to the others.
dist.all_to_all_single(
recv,
x,
output_split_sizes=[rows] * world_size,
input_split_sizes=sizes,
group=group,
)
numel = out.numel()
if numel == 0:
return out
BLOCK_SIZE = 1024
grid = (triton.cdiv(numel, BLOCK_SIZE),)
_fixed_order_sum_kernel[grid](
recv.view(-1),
out.view(-1),
numel,
numel,
WORLD_SIZE=world_size,
BLOCK_SIZE=BLOCK_SIZE,
LARGE=recv.numel() > 2**31,
)
return out
_batch_invariant_MODE = False _batch_invariant_MODE = False
@@ -1047,9 +1275,33 @@ _batch_invariant_LIB = None
_fp16_block_size_n = 256 _fp16_block_size_n = 256
_fp32_block_size_n = 128 _fp32_block_size_n = 128
_fp32_num_stages = 3 _fp32_num_stages = 3
_BLOCK_SIZES_RESOLVED = False
_NUM_SMS: int = 0 _NUM_SMS: int = 0
def _resolve_block_sizes():
"""Size the tiles to the shared memory, to avoid triton OutOfResources.
Runs when the mode is enabled, or on first use by a caller that never
enables it, such as a kernel unit test.
"""
global _fp16_block_size_n, _fp32_block_size_n, _fp32_num_stages
global _BLOCK_SIZES_RESOLVED
_BLOCK_SIZES_RESOLVED = True
if not current_platform.is_cuda_alike():
return
if get_max_shared_memory_bytes() > 106496:
_fp16_block_size_n = 256
_fp32_block_size_n = 128
_fp32_num_stages = 3
else:
_fp16_block_size_n = 128
# SM89 N=1 fp32 128/stages=3 compiles to 131072B (>101376).
# Only N=1 OORs; keep 128/stages=3 for every other N.
_fp32_block_size_n = 32
_fp32_num_stages = 2
def enable_batch_invariant_mode(): def enable_batch_invariant_mode():
global _batch_invariant_MODE, _batch_invariant_LIB global _batch_invariant_MODE, _batch_invariant_LIB
global _fp16_block_size_n, _fp32_block_size_n, _fp32_num_stages global _fp16_block_size_n, _fp32_block_size_n, _fp32_num_stages
@@ -1062,12 +1314,17 @@ def enable_batch_invariant_mode():
_batch_invariant_LIB = torch.library.Library("aten", "IMPL") _batch_invariant_LIB = torch.library.Library("aten", "IMPL")
key = current_platform.dispatch_key key = current_platform.dispatch_key
if current_platform.is_cuda(): if current_platform.is_cuda_alike():
if current_platform.is_device_capability_family(80): if current_platform.is_rocm() or current_platform.is_device_capability_family(
# SM80 (Ampere) cannot rely on cuBLASLt-only determinism; install the 80
# triton persistent matmul overrides for mm/addmm/matmul/linear. ):
# Neither cuBLASLt on SM80 (Ampere) nor hipBLASLt can be made batch
# invariant; install the triton persistent matmul overrides for
# mm/addmm/matmul/linear.
_batch_invariant_LIB.impl("aten::mm", mm_batch_invariant, key) _batch_invariant_LIB.impl("aten::mm", mm_batch_invariant, key)
_batch_invariant_LIB.impl("aten::addmm", addmm_batch_invariant, key) _batch_invariant_LIB.impl("aten::addmm", addmm_batch_invariant, key)
_batch_invariant_LIB.impl("aten::mm.out", mm_out_batch_invariant, key)
_batch_invariant_LIB.impl("aten::addmm.out", addmm_out_batch_invariant, key)
_batch_invariant_LIB.impl("aten::matmul", matmul_batch_invariant, key) _batch_invariant_LIB.impl("aten::matmul", matmul_batch_invariant, key)
_batch_invariant_LIB.impl("aten::linear", linear_batch_invariant, key) _batch_invariant_LIB.impl("aten::linear", linear_batch_invariant, key)
else: else:
@@ -1077,18 +1334,7 @@ def enable_batch_invariant_mode():
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8" os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8"
os.environ["CUBLASLT_WORKSPACE_SIZE"] = "1" os.environ["CUBLASLT_WORKSPACE_SIZE"] = "1"
# Query the shared memory size and set block size _resolve_block_sizes()
# accordingly to avoid triton OutOfResources.
if get_max_shared_memory_bytes() > 106496:
_fp16_block_size_n = 256
_fp32_block_size_n = 128
_fp32_num_stages = 3
else:
_fp16_block_size_n = 128
# SM89 N=1 fp32 128/stages=3 compiles to 131072B (>101376).
# Only N=1 OORs; keep 128/stages=3 for every other N.
_fp32_block_size_n = 32
_fp32_num_stages = 2
elif current_platform.is_xpu(): elif current_platform.is_xpu():
# Tensor descriptors need a global-memory allocator; must be set outside # Tensor descriptors need a global-memory allocator; must be set outside
# torch.compile regions (triton.set_allocator modifies global state). # torch.compile regions (triton.set_allocator modifies global state).
@@ -1100,6 +1346,8 @@ def enable_batch_invariant_mode():
_NUM_SMS = num_compute_units(0) _NUM_SMS = num_compute_units(0)
_batch_invariant_LIB.impl("aten::mm", mm_batch_invariant, key) _batch_invariant_LIB.impl("aten::mm", mm_batch_invariant, key)
_batch_invariant_LIB.impl("aten::addmm", addmm_batch_invariant, key) _batch_invariant_LIB.impl("aten::addmm", addmm_batch_invariant, key)
_batch_invariant_LIB.impl("aten::mm.out", mm_out_batch_invariant, key)
_batch_invariant_LIB.impl("aten::addmm.out", addmm_out_batch_invariant, key)
_batch_invariant_LIB.impl("aten::matmul", matmul_batch_invariant, key) _batch_invariant_LIB.impl("aten::matmul", matmul_batch_invariant, key)
_batch_invariant_LIB.impl("aten::linear", linear_batch_invariant, key) _batch_invariant_LIB.impl("aten::linear", linear_batch_invariant, key)
@@ -1123,7 +1371,9 @@ def enable_batch_invariant_mode():
_batch_invariant_LIB.impl( _batch_invariant_LIB.impl(
"aten::bmm", bmm_batch_invariant, key, allow_override=True "aten::bmm", bmm_batch_invariant, key, allow_override=True
) )
torch.bmm = bmm_batch_invariant _batch_invariant_LIB.impl(
"aten::bmm.out", bmm_out_batch_invariant, key, allow_override=True
)
reduced_precision_val = ( reduced_precision_val = (
(False, False) if is_torch_equal_or_newer("2.10.0") else False (False, False) if is_torch_equal_or_newer("2.10.0") else False
@@ -1145,28 +1395,32 @@ def override_envs_for_invariance():
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8" os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
# NCCL determinism settings. NCCL keeps the launch mode process-wide, so, if current_platform.is_rocm():
# unlike the pins below, it also applies to communicators shared with other # The skinny GEMMs pick their split of K from the batch size.
# processes. os.environ["VLLM_ROCM_USE_SKINNY_GEMM"] = "0"
os.environ["NCCL_LAUNCH_MODE"] = "GROUP" else:
pin_nccl_env( # NCCL determinism settings. NCCL keeps the launch mode process-wide, so,
{ # unlike the pins below, it also applies to communicators shared with other
"NCCL_COLLNET_ENABLE": "0", # processes.
"NCCL_NVLS_ENABLE": "0", os.environ["NCCL_LAUNCH_MODE"] = "GROUP"
"NCCL_P2P_NET_DISABLE": "1", pin_nccl_env(
"NCCL_MIN_NCHANNELS": "1", {
"NCCL_MAX_NCHANNELS": "1", "NCCL_COLLNET_ENABLE": "0",
"NCCL_PROTO": "Simple", "NCCL_NVLS_ENABLE": "0",
# NCCL >= 2.31 zero-fills the algorithm table of every collective "NCCL_P2P_NET_DISABLE": "1",
# when NCCL_ALGO is set and re-enables only the named ones; together "NCCL_MIN_NCHANNELS": "1",
# with the NCCL_PROTO above, collectives not named here end up with "NCCL_MAX_NCHANNELS": "1",
# no algorithm and fail with ncclInvalidUsage. Re-enable Ring and "NCCL_PROTO": "Simple",
# Tree for all collectives, then pin AllReduce to Tree. # NCCL >= 2.31 zero-fills the algorithm table of every collective
"NCCL_ALGO": "ring,tree;allreduce:tree", # when NCCL_ALGO is set and re-enables only the named ones; together
"NCCL_NTHREADS": "1", # with the NCCL_PROTO above, collectives not named here end up with
"NCCL_SOCKET_NTHREADS": "1", # no algorithm and fail with ncclInvalidUsage. Re-enable Ring and
} # Tree for all collectives, then pin AllReduce to Tree.
) "NCCL_ALGO": "ring,tree;allreduce:tree",
"NCCL_NTHREADS": "1",
"NCCL_SOCKET_NTHREADS": "1",
}
)
# torch.compile settings # torch.compile settings
os.environ["VLLM_USE_AOT_COMPILE"] = "0" os.environ["VLLM_USE_AOT_COMPILE"] = "0"
@@ -218,6 +218,7 @@ from vllm.model_executor.kernels.linear.scaled_mm.rocm import (
) )
from vllm.model_executor.kernels.linear.scaled_mm.triton import ( from vllm.model_executor.kernels.linear.scaled_mm.triton import (
TritonFp8BlockScaledMMKernel, TritonFp8BlockScaledMMKernel,
TritonFP8ScaledMMLinearKernel,
TritonInt8ScaledMMLinearKernel, TritonInt8ScaledMMLinearKernel,
) )
from vllm.model_executor.kernels.linear.scaled_mm.xpu import ( from vllm.model_executor.kernels.linear.scaled_mm.xpu import (
@@ -253,6 +254,20 @@ def _get_linear_backend(*, quantization: str) -> str:
return config.kernel_config.linear_backend return config.kernel_config.linear_backend
def _batch_invariant_fp8_kernel(kernel, config, quantization: str):
can_implement, reason = is_supported_and_can_implement_kernel(kernel, config, None)
if not can_implement:
raise ValueError(f"Batch invariance on ROCm: {reason}")
linear_backend = _get_linear_backend(quantization=quantization)
if linear_backend not in ("auto", "triton"):
logger.warning_once(
"VLLM_BATCH_INVARIANT overrides --linear-backend=%s; "
"using the Triton backend for deterministic execution.",
linear_backend,
)
return kernel
# Kernel classes covered by each --linear-backend value. # Kernel classes covered by each --linear-backend value.
_LINEAR_BACKEND_KERNEL_MAP: dict[str, set[type]] = { _LINEAR_BACKEND_KERNEL_MAP: dict[str, set[type]] = {
"b12x": { "b12x": {
@@ -723,6 +738,13 @@ def init_fp8_linear_kernel(
) )
if activation_quant_key.scale.group_shape.is_per_group(): if activation_quant_key.scale.group_shape.is_per_group():
if envs.VLLM_BATCH_INVARIANT and current_platform.is_rocm():
force_kernel = _batch_invariant_fp8_kernel(
TritonFp8BlockScaledMMKernel,
scaled_mm_linear_kernel_config,
"fp8_block_w8a8",
)
kernel_type = choose_scaled_mm_linear_kernel( kernel_type = choose_scaled_mm_linear_kernel(
config=scaled_mm_linear_kernel_config, config=scaled_mm_linear_kernel_config,
possible_kernels=_POSSIBLE_FP8_BLOCK_KERNELS, # type: ignore[misc] possible_kernels=_POSSIBLE_FP8_BLOCK_KERNELS, # type: ignore[misc]
@@ -755,6 +777,13 @@ def init_fp8_linear_kernel(
) )
else: else:
if envs.VLLM_BATCH_INVARIANT and current_platform.is_rocm():
force_kernel = _batch_invariant_fp8_kernel(
TritonFP8ScaledMMLinearKernel,
scaled_mm_linear_kernel_config,
"fp8_w8a8",
)
kernel_type = choose_scaled_mm_linear_kernel( kernel_type = choose_scaled_mm_linear_kernel(
config=scaled_mm_linear_kernel_config, config=scaled_mm_linear_kernel_config,
possible_kernels=_POSSIBLE_FP8_KERNELS, # type: ignore[arg-type] possible_kernels=_POSSIBLE_FP8_KERNELS, # type: ignore[arg-type]
@@ -780,6 +809,19 @@ def init_fp8_linear_kernel(
) )
def fp8_scaled_mm_is_batch_invariant(
kernel: FP8ScaledMMLinearKernel | Fp8BlockScaledMMLinearKernel,
) -> bool:
"""Whether a row's output from the kernel is independent of the other rows."""
if not isinstance(
kernel, (CutlassFP8ScaledMMLinearKernel, TritonFP8ScaledMMLinearKernel)
):
return False
# A dynamic per-tensor activation scale is an amax over every row.
scale = kernel.config.activation_quant_key.scale
return scale.static or not scale.group_shape.is_per_tensor()
def init_int8_linear_kernel( def init_int8_linear_kernel(
is_channelwise: bool, is_channelwise: bool,
is_static_input_scheme: bool, is_static_input_scheme: bool,
@@ -1250,6 +1292,7 @@ def register_linear_kernel(
__all__ = [ __all__ = [
"fp8_scaled_mm_is_batch_invariant",
"init_fp8_linear_kernel", "init_fp8_linear_kernel",
"init_int8_linear_kernel", "init_int8_linear_kernel",
"init_nvfp4_linear_kernel", "init_nvfp4_linear_kernel",
@@ -62,7 +62,12 @@ if is_aiter_found_and_supported():
N = weight.shape[0] N = weight.shape[0]
K = weight.shape[1] K = weight.shape[1]
if rocm_use_aiter_fp4_asm_gemm: if rocm_use_aiter_fp4_asm_gemm:
if M <= 64 and rocm_aiter_ops.is_triton_gemm_afp4wfp4_presh_ws_tuned(N, K): # The preshuffle kernel's tuned configs split K in some M buckets.
if (
M <= 64
and not envs.VLLM_BATCH_INVARIANT
and rocm_aiter_ops.is_triton_gemm_afp4wfp4_presh_ws_tuned(N, K)
):
if x_scales is None: if x_scales is None:
# use hip quant kernel for performance # use hip quant kernel for performance
if M >= 32: if M >= 32:
@@ -116,7 +121,16 @@ if is_aiter_found_and_supported():
x_q.shape[0], weight.shape[0], device=x_q.device, dtype=out_dtype x_q.shape[0], weight.shape[0], device=x_q.device, dtype=out_dtype
) )
gemm_afp4wfp4(x_q, weight, x_s, weight_scale.T, out_dtype, y) config = None
if envs.VLLM_BATCH_INVARIANT:
# The configs split K in some M buckets (up to M <= 256 for
# tuned shapes). Keep the tile, drop the split.
from aiter.ops.triton.gemm_afp4wfp4 import _get_config
config, _ = _get_config(M, N, K)
config = dict(config, NUM_KSPLIT=1)
gemm_afp4wfp4(x_q, weight, x_s, weight_scale.T, out_dtype, y, config)
return y return y
def gemm_with_dynamic_quant_fake( def gemm_with_dynamic_quant_fake(
@@ -12,6 +12,7 @@ instead, since ``can_implement`` does not filter on K.
import torch import torch
from torch.nn.parameter import Parameter from torch.nn.parameter import Parameter
import vllm.envs as envs
from vllm.model_executor.layers.quantization.utils.mxfp8_utils import ( from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
MXFP8_BLOCK_SIZE, MXFP8_BLOCK_SIZE,
MXFP8_SCALE_DTYPE, MXFP8_SCALE_DTYPE,
@@ -246,7 +247,10 @@ class RocmDotScaledMxfp8LinearKernel(Mxfp8LinearKernel):
N, K = weight.shape N, K = weight.shape
scale_k = K // MXFP8_BLOCK_SIZE scale_k = K // MXFP8_BLOCK_SIZE
weight_scale = layer.weight_scale.data[:N, :scale_k].contiguous() weight_scale = layer.weight_scale.data[:N, :scale_k].contiguous()
block_scale = _as_block32_scale(weight_scale) # rocm_mxfp8_block32_gemm's split-K varies with M.
block_scale = (
None if envs.VLLM_BATCH_INVARIANT else _as_block32_scale(weight_scale)
)
if block_scale is not None: if block_scale is not None:
# Checkpoints with 32x32 scale blocks (DeepSeek V4/V4.1) get them # Checkpoints with 32x32 scale blocks (DeepSeek V4/V4.1) get them
# back from the loader expanded per row; keep the blocks and use # back from the loader expanded per row; keep the blocks and use
@@ -39,6 +39,7 @@ from vllm.model_executor.kernels.linear.scaled_mm.ScaledMMLinearKernel import (
ScaledMMLinearKernel, ScaledMMLinearKernel,
) )
from vllm.model_executor.kernels.linear.scaled_mm.triton import ( from vllm.model_executor.kernels.linear.scaled_mm.triton import (
TritonFP8ScaledMMLinearKernel,
TritonInt8ScaledMMLinearKernel, TritonInt8ScaledMMLinearKernel,
) )
from vllm.model_executor.kernels.linear.scaled_mm.xpu import ( from vllm.model_executor.kernels.linear.scaled_mm.xpu import (
@@ -65,6 +66,7 @@ __all__ = [
"PerTensorTorchFP8ScaledMMLinearKernel", "PerTensorTorchFP8ScaledMMLinearKernel",
"RowWiseTorchFP8ScaledMMLinearKernel", "RowWiseTorchFP8ScaledMMLinearKernel",
"ROCmFP8ScaledMMLinearKernel", "ROCmFP8ScaledMMLinearKernel",
"TritonFP8ScaledMMLinearKernel",
"TritonInt8ScaledMMLinearKernel", "TritonInt8ScaledMMLinearKernel",
"ZentorchInt8ScaledMMLinearKernel", "ZentorchInt8ScaledMMLinearKernel",
"Fp8BlockScaledMMLinearKernel", "Fp8BlockScaledMMLinearKernel",
@@ -20,6 +20,8 @@ from .BlockScaledMMLinearKernel import (
) )
from .cutlass import CutlassInt8ScaledMMLinearKernel from .cutlass import CutlassInt8ScaledMMLinearKernel
from .ScaledMMLinearKernel import ( from .ScaledMMLinearKernel import (
FP8ScaledMMLinearKernel,
FP8ScaledMMLinearLayerConfig,
Int8ScaledMMLinearLayerConfig, Int8ScaledMMLinearLayerConfig,
) )
@@ -156,6 +158,46 @@ class TritonInt8ScaledMMLinearKernel(CutlassInt8ScaledMMLinearKernel):
return out return out
class TritonFP8ScaledMMLinearKernel(FP8ScaledMMLinearKernel):
@classmethod
def is_supported(
cls, compute_capability: int | None = None
) -> tuple[bool, str | None]:
if not current_platform.is_cuda_alike():
return False, "requires ROCm or CUDA."
return True, None
@classmethod
def can_implement(cls, c: FP8ScaledMMLinearLayerConfig) -> tuple[bool, str | None]:
act_group = c.activation_quant_key.scale.group_shape
weight_group = c.weight_quant_key.scale.group_shape
if not (act_group.is_per_tensor() or act_group.is_per_token()):
return False, "requires per-tensor or per-token activation scales."
if not (
weight_group.is_per_tensor()
or weight_group.is_per_channel()
or weight_group.is_per_token()
):
return False, "requires per-tensor, per-channel or per-token weight scales."
return True, None
def apply_scaled_mm(
self,
*,
A: torch.Tensor,
B: torch.Tensor,
out_dtype: torch.dtype,
As: torch.Tensor,
Bs: torch.Tensor,
bias: torch.Tensor | None,
output_shape: list,
) -> torch.Tensor:
out = triton_scaled_mm(
A, B, scale_a=As, scale_b=Bs, out_dtype=out_dtype, bias=bias
)
return out.view(*output_shape)
class TritonFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel): class TritonFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
@classmethod @classmethod
def is_supported(cls, compute_capability=None): def is_supported(cls, compute_capability=None):
@@ -51,6 +51,10 @@ class AiterMxfp8Experts(Mxfp8TritonExpertsBase):
def _supports_parallel_config(moe_parallel_config) -> bool: def _supports_parallel_config(moe_parallel_config) -> bool:
return True return True
@staticmethod
def _supports_batch_invariance() -> bool:
return False
@staticmethod @staticmethod
def is_supported_config( def is_supported_config(
cls, moe_config, weight_key, activation_key, activation_format cls, moe_config, weight_key, activation_key, activation_format
@@ -218,7 +218,7 @@ class OCP_MXQuantizationEmulationTritonExperts(TritonExperts):
global_num_experts=global_num_experts, global_num_experts=global_num_experts,
expert_map=expert_map, expert_map=expert_map,
a1q_scale=None, a1q_scale=None,
a2_scale=None, a2_scale=a2_scale,
workspace13=workspace13, workspace13=workspace13,
workspace2=workspace2, workspace2=workspace2,
expert_tokens_meta=expert_tokens_meta, expert_tokens_meta=expert_tokens_meta,
@@ -150,16 +150,16 @@ def scaled_mm_kernel(
scale_b = scale_b.broadcast_to((BLOCK_SIZE_N, 1)) scale_b = scale_b.broadcast_to((BLOCK_SIZE_N, 1))
accumulator = scale_b.T * accumulator.to(tl.float32) accumulator = scale_b.T * accumulator.to(tl.float32)
# Convert to output format. # Add bias in accumulator dtype for less rounding error.
c = accumulator.to(c_ptr.type.element_ty)
# Add bias, it's already in output format, so add it after conversion.
if bias_ptr: if bias_ptr:
offsets_bias = offsets_bn offsets_bias = offsets_bn
bias_ptrs = bias_ptr + offsets_bias bias_ptrs = bias_ptr + offsets_bias
bias_mask = offsets_bias < N bias_mask = offsets_bias < N
bias = tl.load(bias_ptrs, bias_mask) bias = tl.load(bias_ptrs, bias_mask)
c += bias accumulator += bias
# Convert to output format.
c = accumulator.to(c_ptr.type.element_ty)
# Save output # Save output
offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64) offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
@@ -224,6 +224,8 @@ def triton_scaled_mm(
tile_shape = (64, 128, 128) tile_shape = (64, 128, 128)
else: else:
tile_shape = (128, 128, 128) tile_shape = (128, 128, 128)
else:
tile_shape = (block_size_m, block_size_n, block_size_k)
block_size_m, block_size_n, block_size_k = tile_shape block_size_m, block_size_n, block_size_k = tile_shape
@@ -9,10 +9,10 @@ import vllm.envs as envs
from vllm.config import get_current_vllm_config from vllm.config import get_current_vllm_config
from vllm.distributed import get_tensor_model_parallel_world_size from vllm.distributed import get_tensor_model_parallel_world_size
from vllm.model_executor.kernels.linear import ( from vllm.model_executor.kernels.linear import (
fp8_scaled_mm_is_batch_invariant,
init_fp8_linear_kernel, init_fp8_linear_kernel,
) )
from vllm.model_executor.kernels.linear.scaled_mm import ( from vllm.model_executor.kernels.linear.scaled_mm import (
CutlassFP8ScaledMMLinearKernel,
MarlinFP8ScaledMMLinearKernel, MarlinFP8ScaledMMLinearKernel,
) )
from vllm.model_executor.layers.attention import Attention from vllm.model_executor.layers.attention import Attention
@@ -432,7 +432,7 @@ class Fp8LinearMethod(LinearMethodBase):
bias, bias,
) )
else: else:
if isinstance(self.fp8_linear, CutlassFP8ScaledMMLinearKernel): if fp8_scaled_mm_is_batch_invariant(self.fp8_linear):
return self.fp8_linear.apply_weights(layer, x, bias) return self.fp8_linear.apply_weights(layer, x, bias)
# per-tensor/channel: dequant to BF16 and run GEMM # per-tensor/channel: dequant to BF16 and run GEMM
@@ -15,9 +15,11 @@ if TYPE_CHECKING:
import vllm.envs as envs import vllm.envs as envs
from vllm import _custom_ops as ops from vllm import _custom_ops as ops
from vllm.config import get_current_vllm_config from vllm.config import get_current_vllm_config
from vllm.model_executor.kernels.linear import init_fp8_linear_kernel from vllm.model_executor.kernels.linear import (
fp8_scaled_mm_is_batch_invariant,
init_fp8_linear_kernel,
)
from vllm.model_executor.kernels.linear.scaled_mm import ( from vllm.model_executor.kernels.linear.scaled_mm import (
CutlassFP8ScaledMMLinearKernel,
MarlinFP8ScaledMMLinearKernel, MarlinFP8ScaledMMLinearKernel,
) )
from vllm.model_executor.layers.fused_moe import RoutedExperts from vllm.model_executor.layers.fused_moe import RoutedExperts
@@ -278,7 +280,7 @@ class Fp8PerTensorOnlineLinearMethod(OnlineLinearBase):
) -> torch.Tensor: ) -> torch.Tensor:
# if batch invariant mode is enabled, use BF16 dequant # if batch invariant mode is enabled, use BF16 dequant
if envs.VLLM_BATCH_INVARIANT: if envs.VLLM_BATCH_INVARIANT:
if isinstance(self.fp8_linear, CutlassFP8ScaledMMLinearKernel): if fp8_scaled_mm_is_batch_invariant(self.fp8_linear):
return self.fp8_linear.apply_weights(layer, x, bias) return self.fp8_linear.apply_weights(layer, x, bias)
weight_fp8 = layer.weight.to(torch.bfloat16) weight_fp8 = layer.weight.to(torch.bfloat16)
@@ -466,7 +468,7 @@ class Fp8PtpcOnlineLinearMethod(OnlineLinearBase):
# if batch invariant mode is enabled dequant # if batch invariant mode is enabled dequant
if ( if (
envs.VLLM_BATCH_INVARIANT envs.VLLM_BATCH_INVARIANT
and not isinstance(self.fp8_linear, CutlassFP8ScaledMMLinearKernel) and not fp8_scaled_mm_is_batch_invariant(self.fp8_linear)
and not isinstance(x, QuantizedActivation) and not isinstance(x, QuantizedActivation)
): ):
weight_dequant = ( weight_dequant = (
@@ -1041,7 +1041,10 @@ def w8a8_triton_block_scaled_mm(
C_shape = A.shape[:-1] + (N,) C_shape = A.shape[:-1] + (N,)
C = A.new_empty(C_shape, dtype=output_dtype) C = A.new_empty(C_shape, dtype=output_dtype)
configs = get_w8a8_block_fp8_configs(N, K, block_size[0], block_size[1]) if envs.VLLM_BATCH_INVARIANT:
configs = None # use the default config
else:
configs = get_w8a8_block_fp8_configs(N, K, block_size[0], block_size[1])
if configs: if configs:
# Get the optimal config if there is one # Get the optimal config if there is one
config = configs[min(configs.keys(), key=lambda x: abs(x - M))] config = configs[min(configs.keys(), key=lambda x: abs(x - M))]
+7
View File
@@ -288,6 +288,13 @@ def wvsplitkrc_dispatch(n: int, k: int, m: int, cu_count: int) -> tuple[int, boo
def rocm_unquantized_gemm_impl( def rocm_unquantized_gemm_impl(
x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None = None x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None = None
) -> torch.Tensor: ) -> torch.Tensor:
if envs.VLLM_BATCH_INVARIANT:
from vllm.model_executor.determinism.batch_invariant import (
linear_batch_invariant,
)
return linear_batch_invariant(x, weight, bias)
from vllm.platforms.rocm import on_gfx1x, on_gfx9, on_gfx950, on_gfx1250 from vllm.platforms.rocm import on_gfx1x, on_gfx9, on_gfx950, on_gfx1250
n = x.numel() // x.size(-1) n = x.numel() // x.size(-1)
+9
View File
@@ -1068,6 +1068,15 @@ class RocmPlatform(Platform):
if use_aiter_fp8_linear and "-quant_fp8" not in compilation_config.custom_ops: if use_aiter_fp8_linear and "-quant_fp8" not in compilation_config.custom_ops:
compilation_config.custom_ops.append("+quant_fp8") compilation_config.custom_ops.append("+quant_fp8")
# Only RMSNorm's custom op reaches rms_norm_batch_invariant. Inductor
# lowers the native one to a reduction whose block size it picks by
# benchmarking, so the summation order varies with the batch and run.
if (
envs.VLLM_BATCH_INVARIANT
and "-rms_norm" not in compilation_config.custom_ops
):
compilation_config.custom_ops.append("+rms_norm")
if use_aiter_fused_se and "-grouped_topk" in compilation_config.custom_ops: if use_aiter_fused_se and "-grouped_topk" in compilation_config.custom_ops:
logger.warning_once( logger.warning_once(
"VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled, which " "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled, which "
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
import torch import torch
import vllm.envs as envs
from vllm.platforms import current_platform from vllm.platforms import current_platform
from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
@@ -47,6 +48,15 @@ class AiterFlashAttnPrefillBackend(MLAPrefillBackend):
return rocm_aiter_ops.is_enabled() return rocm_aiter_ops.is_enabled()
@classmethod
def validate_configuration(cls, device_capability, selector_config):
invalid_reasons = super().validate_configuration(
device_capability, selector_config
)
if envs.VLLM_BATCH_INVARIANT:
invalid_reasons.append("batch invariance not supported")
return invalid_reasons
def __init__( def __init__(
self, self,
num_heads: int, num_heads: int,
@@ -427,13 +427,13 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
kwargs["return_softmax_lse"] = return_softmax_lse kwargs["return_softmax_lse"] = return_softmax_lse
kwargs["out"] = out kwargs["out"] = out
kwargs["output_scale"] = output_scale kwargs["output_scale"] = output_scale
if envs.VLLM_BATCH_INVARIANT:
kwargs["num_splits"] = 1
else: else:
# ROCm leverages the upstream flash_attn, which takes a parameter # ROCm leverages the upstream flash_attn, which takes a parameter
# called "return_attn_probs" instead of return_softmax_lse # called "return_attn_probs" instead of return_softmax_lse
kwargs["return_attn_probs"] = return_softmax_lse kwargs["return_attn_probs"] = return_softmax_lse
assert out is None and output_scale is None assert out is None and output_scale is None
if envs.VLLM_BATCH_INVARIANT:
kwargs["num_splits"] = 1
attn_out = _FA4_MLA_PREFILL_KERNEL( attn_out = _FA4_MLA_PREFILL_KERNEL(
q=q, q=q,