Files
Liger-Kernel/benchmark/scripts/benchmark_multi_token_attention.py
Shivam Sharma 97b6fe2bb6 [Benchmark] Add compute_seq_len_sweep_config_with_probe with linear/quadratic scaling support (#1218)
## Summary

Refs #1200. Addresses non-linear memory scaling in benchmark sweep
config inference.

The existing `compute_seq_len_sweep_config` inverts memory via
`max_tokens = usable_bytes / kernel_bytes_per_token`, which only holds
for linear-scaling kernels. For O(L²) kernels (e.g.
`benchmark_sparse_multi_token_attention.py`), this overestimates
capacity by orders of magnitude — the existing workaround there divides
by `probe_L * probe_L`, but the downstream sweep math still treats the
result as linear bytes-per-token.

Per discussion on the issue
(https://github.com/linkedin/Liger-Kernel/issues/1200#issuecomment-4396956347),
this PR adds a new helper rather than threading `scaling_method` through
the existing function — 16+ benchmark scripts call
`estimate_kernel_peak_memory` today, and a wider signature change would
conflict with in-flight benchmark refactors (#1199, #1180).
Linear-scaling callers are unchanged; only quadratic-scaling benchmarks
opt in.

### What changed

- **`benchmark/scripts/benchmark_model_configs.py`** — adds
`compute_seq_len_sweep_config_with_probe(model_cfg, probe_fn,
probe_seq_len, probe_batch_size=1, scaling_method="linear" |
"quadratic", ...)`. Internalizes the probe call + inversion; reuses
`estimate_kernel_peak_memory` for the measurement.
- **`benchmark/scripts/benchmark_sparse_multi_token_attention.py`** —
switches the `token_length` sweep mode to the new helper with
`scaling_method="quadratic"`, dropping the manual `peak_bytes //
(probe_L * probe_L)` workaround.

`estimate_kernel_peak_memory` and `compute_seq_len_sweep_config` are
untouched.

## Validation

Hardware: A10G 24GB (g5.xlarge).

Synthetic O(L²) probe (B=2, L=2048, allocates `B * L * L` floats) using
`LLAMA_3_8B` config and `max_seq_len=2**20` to bypass the model cap so
the raw inversion is visible:

```
quadratic: SeqLenSweepConfig(batch_size=2, seq_len=8192)
linear:    SeqLenSweepConfig(batch_size=2, seq_len=65536)
```

The 8× gap (≈17× before snap-to-power-of-2) demonstrates the inversion
difference: `linear` claims a sweep at L=65536 fits, when in reality L²
at that size would require multiple TBs. `quadratic` lands at a
realistic L=8192. This matches the issue's premise — for
non-linear-scaling kernels, the existing inversion overestimates
capacity and would OOM at the predicted boundary.

## Testing Done

- [x] Synthetic O(L²) sanity check on A10G — confirms `quadratic`
predicts L=8192 vs `linear` predicts L=65536 for the same probe (8×
separation, scales as expected).
- [x] `benchmark_sparse_multi_token_attention.py` imports + helper
resolution verified locally.
- [ ] Full sparse-attention end-to-end sweep on A10G (deferred —
synthetic test already isolates the inversion math from kernel-specific
noise).

cc @Tcc0403
2026-05-09 20:08:52 +00:00

342 lines
11 KiB
Python

import math
import os
import sys
import torch
import triton
from benchmark_model_configs import MODEL_REGISTRY
from benchmark_model_configs import compute_model_config_sweep_config
from benchmark_model_configs import compute_seq_len_sweep_config
from benchmark_model_configs import get_benchmark_model_config
from utils import QUANTILES
from utils import SingleBenchmarkRunInput
from utils import SingleBenchmarkRunOutput
from utils import _test_memory
from utils import parse_benchmark_script_args
from utils import run_benchmarks
from liger_kernel.transformers.multi_token_attention import LigerMultiTokenAttention
from liger_kernel.utils import infer_device
device = infer_device()
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
class TorchMultiTokenAttention(torch.nn.Module):
def __init__(self, C_in, C_out, K, groups, bias, dtype, device):
super().__init__()
self.weight = torch.nn.Parameter(torch.empty(C_out, C_in // groups, K, K, dtype=dtype, device=device))
self.bias = torch.nn.Parameter(torch.empty(C_out, dtype=dtype, device=device)) if bias else None
self.K = K
self.groups = groups
def forward(self, scores):
B, C_in, L, _ = scores.shape
mask = torch.tril(torch.ones(L, L, dtype=torch.bool, device=scores.device)).view(1, 1, L, L)
inf = torch.tensor(-1e9, device=scores.device, dtype=scores.dtype)
zero = torch.tensor(0.0, device=scores.device, dtype=scores.dtype)
s_inf = scores.masked_fill(~mask, inf)
probs = torch.nn.functional.softmax(s_inf, dim=-1)
out_c = torch.nn.functional.conv2d(
probs, self.weight, self.bias, stride=1, padding=self.K // 2, groups=self.groups
)
return out_c.masked_fill(~mask, zero)
def _setup_multi_token_attention(input: SingleBenchmarkRunInput):
"""Create input tensors and multi-token attention from benchmark config."""
cfg = input.extra_benchmark_config
C_in = cfg["C_in"]
C_out = cfg["C_out"]
K = cfg["K"]
groups = cfg["groups"]
bias = cfg["bias"]
dtype = cfg["dtype"]
B = cfg.get("B", 2)
L = cfg.get("L", input.x)
liger_attn = (
LigerMultiTokenAttention(
in_channels=C_in,
out_channels=C_out,
kernel_size=K,
stride=1,
padding=K // 2,
dilation=1,
groups=groups,
bias=bias,
)
.to(device)
.to(dtype)
)
torch_attn = TorchMultiTokenAttention(
C_in=C_in, C_out=C_out, K=K, groups=groups, bias=bias, dtype=dtype, device=device
)
with torch.no_grad():
torch_attn.weight.copy_(liger_attn.weight)
if bias:
torch_attn.bias.copy_(liger_attn.bias)
x = torch.randn(B, C_in, L, L, dtype=dtype, device=device, requires_grad=True)
dy = torch.randn_like(x)
if input.kernel_provider == "liger":
fwd_fn = lambda: liger_attn(x)
elif input.kernel_provider == "torch":
fwd_fn = lambda: torch_attn(x)
else:
raise ValueError(f"Invalid provider: {input.kernel_provider} for multi-token attention")
# Warmup
_ = fwd_fn()
_.backward(dy, retain_graph=True)
return x, dy, fwd_fn
def bench_speed_multi_token_attention(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, dy, fwd = _setup_multi_token_attention(input)
mode = input.kernel_operation_mode
if mode == "forward":
ms_50, ms_20, ms_80 = triton.testing.do_bench(fwd, grad_to_none=[x], rep=100, quantiles=QUANTILES)
elif mode == "backward":
y = fwd()
ms_50, ms_20, ms_80 = triton.testing.do_bench(
lambda: y.backward(dy, retain_graph=True),
grad_to_none=[x],
rep=100,
quantiles=QUANTILES,
)
elif mode == "full":
def full():
y = fwd()
y.backward(dy, retain_graph=True)
ms_50, ms_20, ms_80 = triton.testing.do_bench(full, grad_to_none=[x], rep=100, quantiles=QUANTILES)
else:
raise ValueError(f"Unsupported mode: {mode}")
return SingleBenchmarkRunOutput(
y_20=ms_20,
y_50=ms_50,
y_80=ms_80,
)
def bench_memory_multi_token_attention(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, dy, fwd_fn = _setup_multi_token_attention(input)
def full():
y = fwd_fn()
y.backward(dy, retain_graph=True)
mem_50, mem_20, mem_80 = _test_memory(full, quantiles=QUANTILES)
return SingleBenchmarkRunOutput(
y_20=mem_20,
y_50=mem_50,
y_80=mem_80,
)
def _resolve_model_config_multi_token_attention(input: SingleBenchmarkRunInput):
cfg = input.extra_benchmark_config
model_info = cfg["model_configs"][input.x]
return _setup_multi_token_attention(
SingleBenchmarkRunInput(
x=input.x,
kernel_provider=input.kernel_provider,
extra_benchmark_config={
"C_in": cfg["C_in"],
"C_out": cfg["C_out"],
"K": cfg["K"],
"groups": cfg["groups"],
"bias": cfg["bias"],
"dtype": model_info["dtype"],
"B": cfg["B"],
"L": cfg["L"],
},
)
)
def bench_speed_multi_token_attention_model_config(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, dy, fwd_fn = _resolve_model_config_multi_token_attention(input)
mode = input.kernel_operation_mode
if mode == "forward":
ms_50, ms_20, ms_80 = triton.testing.do_bench(fwd_fn, grad_to_none=[x], rep=100, quantiles=QUANTILES)
elif mode == "backward":
y = fwd_fn()
ms_50, ms_20, ms_80 = triton.testing.do_bench(
lambda: y.backward(dy, retain_graph=True),
grad_to_none=[x],
rep=100,
quantiles=QUANTILES,
)
elif mode == "full":
def full():
y = fwd_fn()
y.backward(dy, retain_graph=True)
ms_50, ms_20, ms_80 = triton.testing.do_bench(full, grad_to_none=[x], rep=100, quantiles=QUANTILES)
else:
raise ValueError(f"Unsupported mode: {mode}")
return SingleBenchmarkRunOutput(
y_20=ms_20,
y_50=ms_50,
y_80=ms_80,
)
def bench_memory_multi_token_attention_model_config(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, dy, fwd_fn = _resolve_model_config_multi_token_attention(input)
def full():
y = fwd_fn()
y.backward(dy, retain_graph=True)
mem_50, mem_20, mem_80 = _test_memory(full, quantiles=QUANTILES)
return SingleBenchmarkRunOutput(
y_20=mem_20,
y_50=mem_50,
y_80=mem_80,
)
if __name__ == "__main__":
args = parse_benchmark_script_args()
if args.sweep_mode == "model_config":
all_model_configs = list(MODEL_REGISTRY.values())
B = 2
def _probe_factory(model_cfg, probe_bt):
def _probe():
# Memory scales as O(L^2) due to (B, C_in, L, L) shape
L = int((probe_bt // B) ** 0.5)
probe_input = SingleBenchmarkRunInput(
x=0,
kernel_provider="torch",
extra_benchmark_config={
"C_in": 4,
"C_out": 4,
"K": 3,
"groups": 1,
"bias": True,
"dtype": model_cfg.dtype,
"B": B,
"L": L,
},
)
_, _, fwd_fn = _setup_multi_token_attention(probe_input)
return fwd_fn()
return _probe
sweep = compute_model_config_sweep_config(all_model_configs, probe_fn_factory=_probe_factory, bt=args.bt)
# sweep.seq_len assumes linear scaling, but memory scales as O(L^2)
# So we need to take sqrt to get the actual safe L value
safe_L = int((sweep.seq_len) ** 0.5)
model_configs_info = {cfg.name: {"dtype": cfg.dtype} for cfg in sweep.model_configs}
common_configs = {
"kernel_name": "multi_token_attention",
"x_name": "model_config",
"x_label": "model configuration",
"x_values": [cfg.name for cfg in sweep.model_configs],
"kernel_providers": ["liger", "torch"],
"extra_benchmark_configs": [
{
"model_configs": model_configs_info,
"C_in": 4,
"C_out": 4,
"K": 3,
"groups": 1,
"bias": True,
"B": sweep.batch_size,
"L": safe_L,
}
],
"overwrite": args.overwrite,
}
run_benchmarks(
bench_test_fn=bench_speed_multi_token_attention_model_config,
kernel_operation_modes=["forward", "backward", "full"],
metric_name="speed",
metric_unit="ms",
**common_configs,
)
run_benchmarks(
bench_test_fn=bench_memory_multi_token_attention_model_config,
kernel_operation_modes=["full"],
metric_name="memory",
metric_unit="MB",
**common_configs,
)
else:
model = get_benchmark_model_config(args.model)
B = 2
probe_L = 256
def _probe():
probe_input = SingleBenchmarkRunInput(
x=0,
kernel_provider="torch",
extra_benchmark_config={
"C_in": 4,
"C_out": 4,
"K": 3,
"groups": 1,
"bias": True,
"dtype": model.dtype,
"B": B,
"L": probe_L,
},
)
_, _, fwd_fn = _setup_multi_token_attention(probe_input)
return fwd_fn()
# Memory scales as O(L^2)
config = compute_seq_len_sweep_config(
model,
probe_fn=_probe,
probe_seq_len=probe_L,
probe_batch_size=B,
scaling_method="quadratic",
)
common_configs = {
"kernel_name": "multi_token_attention",
"x_name": "L",
"x_label": "sequence length",
"x_values": [2**i for i in range(5, int(math.log2(max(32, config.seq_len))) + 1)],
"kernel_providers": ["liger", "torch"],
"extra_benchmark_configs": [
{"C_in": 4, "C_out": 4, "K": 3, "groups": 1, "bias": True, "dtype": model.dtype, "B": B}
],
"overwrite": args.overwrite,
}
run_benchmarks(
bench_test_fn=bench_speed_multi_token_attention,
kernel_operation_modes=["forward", "backward", "full"],
metric_name="speed",
metric_unit="ms",
**common_configs,
)
run_benchmarks(
bench_test_fn=bench_memory_multi_token_attention,
kernel_operation_modes=["full"],
metric_name="memory",
metric_unit="MB",
**common_configs,
)