Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0188c01
Added batch invariant matmul and linear kernels
tzielinski-habana Jul 15, 2026
dd2943f
Added faster matmul kernel
tzielinski-habana May 14, 2026
16e12ac
Fixed matmul_descriptor_persistent kernel
tzielinski-habana Jun 10, 2026
d97066d
Removed xfail from test
tzielinski-habana Jul 17, 2026
356ff06
Merge branch 'vllm-project:main' into matmul_linear_bi
tzielinski-habana Jul 20, 2026
1f15744
Added documentation
tzielinski-habana Jul 20, 2026
4650800
Merge branch 'main' into matmul_linear_bi
tzielinski-habana Jul 21, 2026
864fdb4
Changed handling of _NUM_SMS
tzielinski-habana Jul 27, 2026
9236cc5
Set TRITON_ATTN as default attn backend for XPU with batch invariance
tzielinski-habana Jul 27, 2026
5caa52d
Moved triton allocator under XPU branch
tzielinski-habana Jul 27, 2026
daea0b6
Better config for matmul_kernel_descriptor_persistent
tzielinski-habana Aug 3, 2026
c43b879
Removed backward kernels
tzielinski-habana Aug 5, 2026
f8148e0
Applied review suggestions
tzielinski-habana Aug 13, 2026
825979d
Merge branch 'main' into matmul_linear_bi
tzielinski-habana Aug 17, 2026
7c15fe0
Merge branch 'main' into matmul_linear_bi
mergify[bot] Aug 18, 2026
b086894
Merge branch 'main' into matmul_linear_bi
tzielinski-habana Aug 19, 2026
3cd8aee
Merge branch 'main' into matmul_linear_bi
tzielinski-habana Aug 21, 2026
210b6f9
Merge branch 'main' into matmul_linear_bi
mergify[bot] Aug 22, 2026
62948f2
Merge remote-tracking branch 'upstream/main' into matmul_linear_bi
tzielinski-habana Aug 25, 2026
53f2a77
refactor: move XPU descriptor matmul config to configs module
tzielinski-habana Aug 26, 2026
83e2b22
Merge branch 'main' into matmul_linear_bi
tzielinski-habana Aug 31, 2026
b2137aa
Update docs/features/batch_invariance.md
tzielinski-habana Sep 1, 2026
efbdfdc
Merge branch 'main' into matmul_linear_bi
tzielinski-habana Sep 1, 2026
5535256
Merge branch 'main' into matmul_linear_bi
tzielinski-habana Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion docs/features/batch_invariance.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,29 @@ Batch invariance is crucial for several use cases:

## Hardware Requirements

Batch invariance requires NVIDIA GPUs with compute capability 8.0 or higher.
Batch invariance is supported on the following platforms:

- NVIDIA GPUs with compute capability 8.0 or higher.
- Intel XPUs with Triton support.

### Attention Backend Selection for XPU

On XPU, Triton Attention backend is required for batch invariance.
Select this backend using Qwen/Qwen3-1.7B model as an example:

```python
llm = LLM(
model="Qwen/Qwen3-1.7B",
attention_config={"backend": "TRITON_ATTN"},
)
```

Or via the CLI:

```bash
VLLM_BATCH_INVARIANT=1 vllm serve Qwen/Qwen3-1.7B \
--attention-config '{"backend": "TRITON_ATTN"}'
Comment thread
tzielinski-habana marked this conversation as resolved.
Outdated
```

## Enabling Batch Invariance

Expand Down
11 changes: 0 additions & 11 deletions tests/v1/determinism/test_batch_invariance.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import vllm.envs as envs
from vllm import LLM, SamplingParams
from vllm.platforms import current_platform


@skip_unsupported
Expand Down Expand Up @@ -53,11 +52,6 @@ def test_v1_generation_is_deterministic_across_batch_sizes_with_needle(
seed.
- Keep max_tokens and max_model_len bounded for speed and memory use.
"""
# Not all batch-invariant kernels are registered on XPU yet
# (e.g. attention, custom ops), so e2e determinism is not guaranteed.
if current_platform.is_xpu():
pytest.xfail("Not all batch-invariant kernels registered on XPU yet")

seed = int(os.getenv("VLLM_TEST_SEED", "12345"))
random.seed(seed)

Expand Down Expand Up @@ -177,11 +171,6 @@ def test_logprobs_bitwise_batch_invariance_bs1_vs_bsN(
block_m,
block_n,
):
# Not all batch-invariant kernels are registered on XPU yet
# (e.g. attention, custom ops), so e2e determinism is not guaranteed.
if current_platform.is_xpu():
pytest.xfail("Not all batch-invariant kernels registered on XPU yet")

seed = int(os.getenv("VLLM_TEST_SEED", "12345"))
random.seed(seed)
tp_size = int(os.getenv("VLLM_TEST_TP_SIZE", "1"))
Expand Down
13 changes: 13 additions & 0 deletions vllm/engine/arg_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2390,6 +2390,19 @@ def create_engine_config(
self.attention_backend
)

# Batch-invariant mode requires deterministic attention behavior.
# If no backend is explicitly requested, prefer Triton Attention.
if (
envs.VLLM_BATCH_INVARIANT
and attention_config.backend is None
and current_platform.is_xpu()
):
attention_config.backend = AttentionBackendEnum.TRITON_ATTN
logger.info(
"VLLM_BATCH_INVARIANT is enabled and no attention backend was "
"specified; defaulting to TRITON_ATTN."
)

# TurboQuant requires FlashAttention 2 — FA3 boundary layers assert
# FlashAttentionImpl which fails with TurboQuantAttentionImpl.
if resolved_cache_dtype.startswith("turboquant_") and (
Expand Down
246 changes: 226 additions & 20 deletions vllm/model_executor/layers/batch_invariant.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,196 @@ def matmul_kernel_persistent(
tl.store(c_ptrs, c, mask=c_mask)


@triton.jit(launch_metadata=_matmul_launch_metadata)
def matmul_kernel_descriptor_persistent(
a_ptr,
b_ptr,
c_ptr, #
bias_ptr,
M,
N,
K, #
BLOCK_SIZE_M: tl.constexpr, #
BLOCK_SIZE_N: tl.constexpr, #
BLOCK_SIZE_K: tl.constexpr, #
GROUP_SIZE_M: tl.constexpr, #
NUM_SMS: tl.constexpr, #
HAS_BIAS: tl.constexpr,
):
"""Persistent matmul using tensor descriptors for 2D block I/O.

Expects b_ptr to point to a transposed B matrix of shape [N, K] with
row-major (K-contiguous) layout. The dot product transposes each loaded
B-tile back: dot(A_tile, B_tile.T).

~3x faster than the pointer-based persistent kernel on Intel XPU because
tensor descriptors leverage hardware 2D block load/store with automatic
bounds checking (no explicit masks needed).
"""
dtype = c_ptr.dtype.element_ty
start_pid = tl.program_id(axis=0)
num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
k_tiles = tl.cdiv(K, BLOCK_SIZE_K)
num_tiles = num_pid_m * num_pid_n

a_desc = tl.make_tensor_descriptor(
a_ptr,
shape=[M, K],
strides=[K, 1],
block_shape=[BLOCK_SIZE_M, BLOCK_SIZE_K],
)
b_desc = tl.make_tensor_descriptor(
b_ptr,
shape=[N, K],
strides=[K, 1],
block_shape=[BLOCK_SIZE_N, BLOCK_SIZE_K],
)
c_desc = tl.make_tensor_descriptor(
c_ptr,
shape=[M, N],
strides=[N, 1],
block_shape=[BLOCK_SIZE_M, BLOCK_SIZE_N],
)

tile_id_c = start_pid - NUM_SMS
num_pid_in_group = GROUP_SIZE_M * num_pid_n

for tile_id in tl.range(start_pid, num_tiles, NUM_SMS, flatten=True):
pid_m, pid_n = _compute_pid(tile_id, num_pid_in_group, num_pid_m, GROUP_SIZE_M)
offs_am = pid_m * BLOCK_SIZE_M
offs_bn = pid_n * BLOCK_SIZE_N

accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
for ki in range(k_tiles):
offs_k = ki * BLOCK_SIZE_K
a = a_desc.load([offs_am, offs_k])
b = b_desc.load([offs_bn, offs_k])
accumulator = tl.dot(a, b.T, accumulator)

tile_id_c += NUM_SMS
pid_m, pid_n = _compute_pid(
tile_id_c, num_pid_in_group, num_pid_m, GROUP_SIZE_M
)
offs_cm = pid_m * BLOCK_SIZE_M
offs_cn = pid_n * BLOCK_SIZE_N

if HAS_BIAS:
bias_offs = offs_cn + tl.arange(0, BLOCK_SIZE_N)
bias_vals = tl.load(bias_ptr + bias_offs, mask=bias_offs < N, other=0.0).to(
tl.float32
)
accumulator += bias_vals[None, :]

c = accumulator.to(dtype)
c_desc.store([offs_cm, offs_cn], c)


def _get_descriptor_matmul_config(
M: int, N: int, K: int, dtype: torch.dtype
) -> dict[str, int]:
"""Select Triton kernel config based on problem shape and dtype.

The default 128×128×64 config is optimized for large square-ish GEMMs but
wastes resources on skinny shapes (small M during decode, or very tall/thin
matrices during prefill). Shape-dependent tuning closes the gap with oneDNN.
"""
# fp32 uses smaller BLOCK_SIZE_K due to register pressure
block_k = 32 if dtype == torch.float32 else 64

if M <= 16:
# Decode: M=1-16. Tiny M means most of a 128-row tile is wasted.
# Use small M-block, wide N-block to maximize useful work per tile.
return {
"BLOCK_SIZE_M": 16,
"BLOCK_SIZE_N": 256,
"BLOCK_SIZE_K": block_k,
"GROUP_SIZE_M": 1,
"num_stages": 4,
"num_warps": 8,
}
elif M <= 64:
# Small batch decode or very short prefill.
return {
"BLOCK_SIZE_M": 32,
"BLOCK_SIZE_N": 128,
"BLOCK_SIZE_K": block_k,
"GROUP_SIZE_M": 4,
"num_stages": 4,
"num_warps": 8,
}
else:
# Medium and large prefill (M > 64). 64×128 tiles provide the best
# balance of register pressure vs parallelism on Intel XPU.
# M=2048, N=4096 → 32×32 = 1024 tiles, well above ~160 compute units.
return {
"BLOCK_SIZE_M": 64,
"BLOCK_SIZE_N": 128,
"BLOCK_SIZE_K": block_k,
"GROUP_SIZE_M": 8,
"num_stages": 3,
"num_warps": 8,
}


def matmul_descriptor_persistent(
a: torch.Tensor, b: torch.Tensor, bias: torch.Tensor | None = None
):
"""Persistent matmul using tensor descriptors (Intel XPU fast path).

Args:
a: Input matrix [M, K], must be contiguous.
b: Weight matrix [K, N] (standard layout — transposed internally).
bias: Optional 1D bias vector [N].

Returns:
Output matrix [M, N] with dtype matching the inputs.
"""
assert a.shape[1] == b.shape[0], "Incompatible dimensions"
assert a.dtype == b.dtype, "Incompatible dtypes"
assert bias is None or bias.dim() == 1, "bias must be 1D"

M, K = a.shape
_, N = b.shape
dtype = a.dtype

# Tensor descriptors require contiguous row-major layout.
a = a.contiguous()
# Descriptor kernel expects B in [N, K] layout (K-contiguous).
b_t = b.t().contiguous()

c = torch.empty((M, N), device=a.device, dtype=dtype)
# This path is XPU-only. _NUM_SMS is set by enable_batch_invariant_mode()
# before torch.compile traces this function, so Dynamo can lift it
# as a compile-time constant.
NUM_SMS = _NUM_SMS if _NUM_SMS > 0 else num_compute_units(a.device.index)

def grid(META):
return (
min(
NUM_SMS,
triton.cdiv(M, META["BLOCK_SIZE_M"])
* triton.cdiv(N, META["BLOCK_SIZE_N"]),
),
)

cfg = _get_descriptor_matmul_config(M, N, K, dtype)

matmul_kernel_descriptor_persistent[grid](
a,
b_t,
c,
bias,
M,
N,
K,
NUM_SMS=NUM_SMS,
HAS_BIAS=bias is not None,
**cfg,
)
return c


def matmul_persistent(
a: torch.Tensor, b: torch.Tensor, bias: torch.Tensor | None = None
):
Expand Down Expand Up @@ -585,15 +775,17 @@ def mean_dim(
return output


def mm_batch_invariant(a, b):
return matmul_persistent(a, b)
def mm_batch_invariant(a, b, bias=None):
if a.device.type == "xpu":
return matmul_descriptor_persistent(a, b, bias=bias)
return matmul_persistent(a, b, bias=bias)


def matmul_batch_invariant(a, b, *, out=None):
# torch.matmul can handle various dimensions
# For 2D x 2D, it's the same as mm
if a.ndim == 2 and b.ndim == 2:
result = matmul_persistent(a, b)
result = mm_batch_invariant(a, b)
if out is not None:
out.copy_(result)
return out
Expand All @@ -605,7 +797,7 @@ def matmul_batch_invariant(a, b, *, out=None):
hidden = a.shape[-1]
out_dim = b.shape[-1]
a_2d = a.reshape(-1, hidden)
result_2d = matmul_persistent(a_2d, b)
result_2d = mm_batch_invariant(a_2d, b)
result = result_2d.reshape(batch_dims + (out_dim,))
if out is not None:
out.copy_(result)
Expand Down Expand Up @@ -728,7 +920,7 @@ def bmm_batch_invariant(a, b, *, out=None):


def addmm_batch_invariant(bias, a, b):
return matmul_persistent(a, b, bias=bias)
return mm_batch_invariant(a, b, bias=bias)


def _log_softmax_batch_invariant(input, dim, _half_to_float):
Expand Down Expand Up @@ -897,11 +1089,12 @@ def linear_batch_invariant(input, weight, bias=None):
_batch_invariant_MODE = False
_batch_invariant_LIB = None
_fp16_block_size_n = 256
_NUM_SMS: int = 0


def enable_batch_invariant_mode():
global _batch_invariant_MODE, _batch_invariant_LIB
global _fp16_block_size_n
global _fp16_block_size_n, _NUM_SMS

if _batch_invariant_MODE:
return
Expand All @@ -910,7 +1103,6 @@ def enable_batch_invariant_mode():
_batch_invariant_LIB = torch.library.Library("aten", "IMPL")

key = current_platform.dispatch_key

if current_platform.is_cuda():
if current_platform.is_device_capability_family(80):
# SM80 (Ampere) cannot rely on cuBLASLt-only determinism; install the
Expand All @@ -928,24 +1120,38 @@ def enable_batch_invariant_mode():

_fp16_block_size_n = 256 if get_max_shared_memory_bytes() > 106496 else 128
elif current_platform.is_xpu():
# Tensor descriptors need a global-memory allocator; must be set outside
# torch.compile regions (triton.set_allocator modifies global state).
def _triton_alloc_fn(size: int, alignment: int, stream: int | None):
return torch.empty(size, device="xpu", dtype=torch.int8)

triton.set_allocator(_triton_alloc_fn)

_NUM_SMS = num_compute_units(0)
_batch_invariant_LIB.impl("aten::mm", mm_batch_invariant, key)
_batch_invariant_LIB.impl("aten::addmm", addmm_batch_invariant, key)
# TODO: register matmul and linear for XPU
# once suitable Triton kernels are implemented
_batch_invariant_LIB.impl("aten::matmul", matmul_batch_invariant, key)
_batch_invariant_LIB.impl("aten::linear", linear_batch_invariant, key)

_fp16_block_size_n = 128

_batch_invariant_LIB.impl("aten::_log_softmax", _log_softmax_batch_invariant, key)
_batch_invariant_LIB.impl("aten::softmax", softmax_batch_invariant, key)
_batch_invariant_LIB.impl("aten::_softmax", softmax_batch_invariant, key)
_batch_invariant_LIB.impl("aten::mean.dim", mean_batch_invariant, key)
# torch 2.12+ registers a built-in Triton bmm kernel for CUDA
# (torch._native.ops.bmm_outer_product), so we need allow_override
# to replace it at the dispatcher level.
_batch_invariant_LIB.impl(
"aten::bmm", bmm_batch_invariant, key, allow_override=True
)
torch.bmm = bmm_batch_invariant
# Softmax, log_softmax, mean, and bmm are already batch-invariant on XPU
# (oneDNN backend produces bitwise-identical results regardless of batch
# context). Only register these overrides on CUDA where they are needed.
if not current_platform.is_xpu():
_batch_invariant_LIB.impl(
"aten::_log_softmax", _log_softmax_batch_invariant, key
)
_batch_invariant_LIB.impl("aten::softmax", softmax_batch_invariant, key)
_batch_invariant_LIB.impl("aten::_softmax", softmax_batch_invariant, key)
_batch_invariant_LIB.impl("aten::mean.dim", mean_batch_invariant, key)
# torch 2.12+ registers a built-in Triton bmm kernel for CUDA
# (torch._native.ops.bmm_outer_product), so we need allow_override
# to replace it at the dispatcher level.
_batch_invariant_LIB.impl(
"aten::bmm", bmm_batch_invariant, key, allow_override=True
)
torch.bmm = bmm_batch_invariant

reduced_precision_val = (
(False, False) if is_torch_equal_or_newer("2.10.0") else False
Expand Down
Loading
Loading