Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
fbd9b57
Initial implementation
MatthewBonanni Jan 30, 2026
dee2092
Add implementation
MatthewBonanni Jan 30, 2026
aaca43b
Update test
MatthewBonanni Jan 30, 2026
6f3bbc9
Remove unnecessary skip
MatthewBonanni Jan 30, 2026
77d5461
Cleanup
MatthewBonanni Jan 30, 2026
a6072d8
Merge branch 'main' into fi_sparse
MatthewBonanni Feb 2, 2026
38e6fc9
Address refactor
MatthewBonanni Feb 2, 2026
ead72ab
Move kernel to sparse_utils
MatthewBonanni Feb 2, 2026
49cd209
Fix from refactor
MatthewBonanni Feb 2, 2026
1404478
Super init call not necessary
MatthewBonanni Feb 2, 2026
349da87
More fixes
MatthewBonanni Feb 2, 2026
d4536e2
Fix uniform query lengths
MatthewBonanni Feb 3, 2026
98530a2
Update check_and_update_config
MatthewBonanni Feb 3, 2026
bb4e94e
Update block size support
MatthewBonanni Feb 3, 2026
ad8d06e
Parameterize block sizes
MatthewBonanni Feb 3, 2026
f604adb
Update benchmark
MatthewBonanni Feb 3, 2026
f281b28
Clean up
MatthewBonanni Feb 3, 2026
9a6fb87
Fix
MatthewBonanni Feb 3, 2026
5dcf058
Use single batch layout
MatthewBonanni Feb 3, 2026
9bdfa66
Remove unnecessary abstraction
MatthewBonanni Feb 3, 2026
9043242
Fix indexing
MatthewBonanni Feb 3, 2026
1fb6fb0
Improve test
MatthewBonanni Feb 3, 2026
c35a9c6
fix up attention benchmarks
LucasWilkinson Jan 29, 2026
24a5400
clean
MatthewBonanni Feb 4, 2026
e8e0f4e
add smoke
MatthewBonanni Feb 4, 2026
1daa3d0
Fix and update test
MatthewBonanni Feb 4, 2026
23780f1
Merge branch 'main' into fi_sparse
MatthewBonanni Feb 4, 2026
f245674
Merge branch 'lwilkinson/fix-up-attention-benchmarks' into fi_sparse
MatthewBonanni Feb 4, 2026
f326062
Fix benchmarks
MatthewBonanni Feb 5, 2026
553b140
More benchmark ux improvements
MatthewBonanni Feb 5, 2026
1e29943
Update mla_decode
MatthewBonanni Feb 5, 2026
c19e113
Sort benchmark output
MatthewBonanni Feb 5, 2026
5cb64d6
Add mla prefill case
MatthewBonanni Feb 6, 2026
9a31d73
Prefer FlashInfer at low head counts
MatthewBonanni Feb 9, 2026
ab3fb88
Merge branch 'main' into fi_sparse
MatthewBonanni Feb 9, 2026
6d7b2c3
Update other platforms
MatthewBonanni Feb 9, 2026
fa0655d
Merge branch 'main' into fi_sparse
MatthewBonanni Feb 9, 2026
db66e7b
Merge branch 'main' into fi_sparse
MatthewBonanni Feb 11, 2026
c69830b
Merge branch 'main' into fi_sparse
LucasWilkinson Feb 11, 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
47 changes: 28 additions & 19 deletions benchmarks/attention_benchmarks/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
ModelParameterSweep,
ParameterSweep,
ResultsFormatter,
batch_spec_sort_key,
is_mla_backend,
)

Expand Down Expand Up @@ -218,10 +219,13 @@ def run_model_parameter_sweep(
by_param_and_spec[key].append(r)
break

# Sort by param value then spec
# Sort by param value then spec (batch_size, q_len, kv_len)
sorted_keys = sorted(
by_param_and_spec.keys(),
key=lambda x: (int(x[0]) if x[0].isdigit() else x[0], x[1]),
key=lambda x: (
int(x[0]) if x[0].isdigit() else x[0],
batch_spec_sort_key(x[1]),
),
)

current_param_value = None
Expand Down Expand Up @@ -330,7 +334,7 @@ def run_parameter_sweep(
by_spec[spec] = []
by_spec[spec].append(r)

for spec in sorted(by_spec.keys()):
for spec in sorted(by_spec.keys(), key=batch_spec_sort_key):
results = by_spec[spec]
best = min(results, key=lambda r: r.mean_time)
console.print(
Expand Down Expand Up @@ -496,15 +500,18 @@ def main():
if "description" in yaml_config:
console.print(f"[dim]{yaml_config['description']}[/]")

# Override args with YAML values
# (YAML takes precedence unless CLI arg was explicitly set)
# Backend(s)
if "backend" in yaml_config:
args.backend = yaml_config["backend"]
args.backends = None
elif "backends" in yaml_config:
args.backends = yaml_config["backends"]
args.backend = None
# Override args with YAML values, but CLI args take precedence
# Check if CLI provided backends (they would be non-None and not default)
cli_backends_provided = args.backends is not None or args.backend is not None

# Backend(s) - only use YAML if CLI didn't specify
if not cli_backends_provided:
if "backend" in yaml_config:
args.backend = yaml_config["backend"]
args.backends = None
elif "backends" in yaml_config:
args.backends = yaml_config["backends"]
args.backend = None

# Check for special modes
if "mode" in yaml_config:
Expand Down Expand Up @@ -544,13 +551,15 @@ def main():
args.num_kv_heads = model.get("num_kv_heads", args.num_kv_heads)
args.block_size = model.get("block_size", args.block_size)

# Benchmark settings
if "benchmark" in yaml_config:
bench = yaml_config["benchmark"]
args.device = bench.get("device", args.device)
args.repeats = bench.get("repeats", args.repeats)
args.warmup_iters = bench.get("warmup_iters", args.warmup_iters)
args.profile_memory = bench.get("profile_memory", args.profile_memory)
# Benchmark settings (top-level keys)
if "device" in yaml_config:
args.device = yaml_config["device"]
if "repeats" in yaml_config:
args.repeats = yaml_config["repeats"]
if "warmup_iters" in yaml_config:
args.warmup_iters = yaml_config["warmup_iters"]
if "profile_memory" in yaml_config:
args.profile_memory = yaml_config["profile_memory"]

# Parameter sweep configuration
if "parameter_sweep" in yaml_config:
Expand Down
68 changes: 63 additions & 5 deletions benchmarks/attention_benchmarks/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,32 @@
from rich.console import Console
from rich.table import Table


def batch_spec_sort_key(spec: str) -> tuple[int, int, int]:
"""
Extract sorting key from batch spec: (batch_size, max_q_len, max_kv_len).

This ensures results are sorted by batch size first, then query length,
then sequence length, rather than alphabetically.
"""
try:
requests = parse_batch_spec(spec)
batch_size = len(requests)
max_q_len = max(r.q_len for r in requests) if requests else 0
max_kv_len = max(r.kv_len for r in requests) if requests else 0
return (batch_size, max_q_len, max_kv_len)
except Exception:
# Fallback for unparseable specs
return (0, 0, 0)


# Mock classes for vLLM attention infrastructure


class MockHfConfig:
"""Mock HuggingFace config that satisfies vLLM's requirements."""

def __init__(self, mla_dims: dict):
def __init__(self, mla_dims: dict, index_topk: int | None = None):
self.num_attention_heads = mla_dims["num_q_heads"]
self.num_key_value_heads = mla_dims["num_kv_heads"]
self.hidden_size = mla_dims["head_dim"] * mla_dims["num_q_heads"]
Expand All @@ -33,6 +52,8 @@ def __init__(self, mla_dims: dict):
self.qk_rope_head_dim = mla_dims["qk_rope_head_dim"]
self.v_head_dim = mla_dims["v_head_dim"]
self.qk_head_dim = mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"]
if index_topk is not None:
self.index_topk = index_topk

def get_text_config(self):
return self
Expand Down Expand Up @@ -83,6 +104,38 @@ def __call__(self, x: torch.Tensor) -> tuple[torch.Tensor]:
return (result,) # Return as tuple to match ColumnParallelLinear API


class MockIndexer:
"""Mock Indexer for sparse MLA backends.

Provides topk_indices_buffer that sparse MLA backends use to determine
which KV cache slots to attend to for each token.
"""

def __init__(
self,
max_num_tokens: int,
topk_tokens: int,
device: torch.device,
):
self.topk_tokens = topk_tokens
self.topk_indices_buffer = torch.zeros(
(max_num_tokens, topk_tokens),
dtype=torch.int32,
device=device,
)

def fill_random_indices(self, num_tokens: int, max_kv_len: int):
"""Fill topk_indices_buffer with random valid indices for benchmarking."""
indices = torch.randint(
0,
max_kv_len,
(num_tokens, self.topk_tokens),
dtype=torch.int32,
device=self.topk_indices_buffer.device,
)
self.topk_indices_buffer[:num_tokens] = indices


class MockLayer(AttentionLayerBase):
"""Mock attention layer with scale parameters and impl.

Expand Down Expand Up @@ -327,6 +380,9 @@ def print_table(
specs_order.append(spec)
by_spec[spec][r.config.backend] = r

# Sort specs by (batch_size, q_len, kv_len) instead of alphabetically
specs_order = sorted(by_spec.keys(), key=batch_spec_sort_key)

# Create shortened backend names for display
def shorten_backend_name(name: str) -> str:
"""Shorten long backend names for table display."""
Expand Down Expand Up @@ -493,18 +549,20 @@ def get_attention_scale(head_dim: int) -> float:

def is_mla_backend(backend: str) -> bool:
"""
Check if backend is an MLA backend using the backend's is_mla() property.
Check if backend is an MLA backend using the AttentionBackendEnum.

Args:
backend: Backend name (e.g., "CUTLASS_MLA", "FLASHINFER_MLA")
backend: Backend name matching AttentionBackendEnum exactly
(e.g., "FLASHMLA_SPARSE")

Returns:
True if the backend is an MLA backend, False otherwise
"""
from vllm.v1.attention.backends.registry import AttentionBackendEnum

try:
backend_class = AttentionBackendEnum[backend.upper()].get_class()
backend_enum = AttentionBackendEnum[backend]
backend_class = backend_enum.get_class()
return backend_class.is_mla()
except (KeyError, ValueError, ImportError):
except (KeyError, ValueError, ImportError, AttributeError):
return False
29 changes: 19 additions & 10 deletions benchmarks/attention_benchmarks/configs/mla_decode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
model:
name: "deepseek-v3"
num_layers: 60
num_q_heads: 128
num_q_heads: 128 # Base value, can be swept for TP simulation
num_kv_heads: 1 # MLA uses single latent KV
head_dim: 576
kv_lora_rank: 512
Expand All @@ -12,6 +12,13 @@ model:
v_head_dim: 128
block_size: 128 # CUTLASS MLA and FlashAttn MLA use 128

# Model parameter sweep: simulate tensor parallelism by varying num_q_heads
# TP=1: 128 heads, TP=2: 64 heads, TP=4: 32 heads, TP=8: 16 heads
model_parameter_sweep:
param_name: "num_q_heads"
values: [128, 64, 32, 16]
label_format: "{backend}_{value}h"

batch_specs:
# Small batches, varying sequence lengths
- "16q1s512" # 16 requests, 512 KV cache
Expand All @@ -34,28 +41,30 @@ batch_specs:
# Very large batches
- "128q1s1k" # 128 requests, 1k KV cache
- "128q1s2k" # 128 requests, 2k KV cache
- "128q1s4k" # 128 requests, 4k KV cache
- "128q1s8k" # 128 requests, 8k KV cache

# Long context
- "32q1s16k" # 32 requests, 16k KV cache
- "32q1s32k" # 32 requests, 32k KV cache

backends:
- cutlass_mla
- flashinfer_mla
- flashattn_mla # Hopper only
- flashmla # Hopper only
- CUTLASS_MLA
- FLASHINFER_MLA
- FLASH_ATTN_MLA # Hopper only
- FLASHMLA # Hopper only

device: "cuda:0"
repeats: 5
warmup_iters: 3
repeats: 100
warmup_iters: 10
profile_memory: true

# Backend-specific tuning
cutlass_mla:
CUTLASS_MLA:
num_kv_splits: auto # or specific value like 4, 8, 16

flashattn_mla:
FLASH_ATTN_MLA:
reorder_batch_threshold: 512

flashmla:
FLASHMLA:
reorder_batch_threshold: 1
8 changes: 4 additions & 4 deletions benchmarks/attention_benchmarks/configs/mla_mixed_batch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ batch_specs:
- "4q4k_60q1s4k" # 4 prefill + 60 decode

backends:
- cutlass_mla
- flashinfer_mla
- flashattn_mla # Hopper only
- flashmla # Hopper only
- CUTLASS_MLA
- FLASHINFER_MLA
- FLASH_ATTN_MLA # Hopper only
- FLASHMLA # Hopper only

device: "cuda:0"
repeats: 5
Expand Down
62 changes: 62 additions & 0 deletions benchmarks/attention_benchmarks/configs/mla_prefill.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# MLA prefill-only benchmark configuration for sparse backends

model:
name: "deepseek-v3"
num_layers: 60
num_q_heads: 128
num_kv_heads: 1
head_dim: 576
kv_lora_rank: 512
qk_nope_head_dim: 128
qk_rope_head_dim: 64
v_head_dim: 128
block_size: 128

# Model parameter sweep: simulate tensor parallelism by varying num_q_heads
# TP=1: 128 heads, TP=2: 64 heads, TP=4: 32 heads, TP=8: 16 heads
model_parameter_sweep:
param_name: "num_q_heads"
values: [128, 64, 32, 16]
label_format: "{backend}_{value}h"

batch_specs:
# Pure prefill
- "1q512"
- "1q1k"
- "1q2k"
- "1q4k"
- "1q8k"

# Batched pure prefill
- "2q512"
- "2q1k"
- "2q2k"
- "2q4k"
- "2q8k"
- "4q512"
- "4q1k"
- "4q2k"
- "4q4k"
- "4q8k"
- "8q512"
- "8q1k"
- "8q2k"
- "8q4k"
- "8q8k"

# Extend
- "1q512s4k"
- "1q512s8k"
- "1q1ks8k"
- "1q2ks8k"
- "1q2ks16k"
- "1q4ks16k"

backends:
- FLASHMLA_SPARSE
- FLASHINFER_MLA_SPARSE

device: "cuda:0"
repeats: 10
warmup_iters: 3
profile_memory: true
11 changes: 5 additions & 6 deletions benchmarks/attention_benchmarks/configs/reorder_threshold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
description: "Decode vs Prefill pipeline crossover analysis"

# Test FlashAttn MLA
backend: flashattn_mla
backend: FLASH_ATTN_MLA

# Mode: decode_vs_prefill comparison (special sweep mode)
# For each batch spec, we'll test both decode and prefill pipelines
Expand Down Expand Up @@ -62,11 +62,10 @@ model:
block_size: 128

# Benchmark settings
benchmark:
device: "cuda:0"
repeats: 15 # More repeats for spec decode variance
warmup_iters: 5
profile_memory: false
device: "cuda:0"
repeats: 15 # More repeats for spec decode variance
warmup_iters: 5
profile_memory: false

# Output
output:
Expand Down
15 changes: 7 additions & 8 deletions benchmarks/attention_benchmarks/configs/speculative_decode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,17 @@ batch_specs:

# Backends that support query length > 1
backends:
- flashattn_mla # reorder_batch_threshold = 512
- flashmla # reorder_batch_threshold = 1 (tunable)
- FLASH_ATTN_MLA # reorder_batch_threshold = 512
- FLASHMLA # reorder_batch_threshold = 1 (tunable)

# FlashInfer-MLA also supports uniform spec-as-decode but with different mechanism
# - flashinfer_mla
# - FLASHINFER_MLA

# Benchmark settings
benchmark:
device: "cuda:0"
repeats: 10 # More repeats for statistical significance
warmup_iters: 5
profile_memory: false
device: "cuda:0"
repeats: 10 # More repeats for statistical significance
warmup_iters: 5
profile_memory: false

# Test these threshold values for optimization
parameter_sweep:
Expand Down
Loading
Loading