Files
Liger-Kernel/benchmark/scripts/benchmark_modulated_rms_norm.py
Yuki Uehara 30b8486a2b Add fused modulated RMSNorm (#1225)
## Summary

Adds a fused Modulated RMSNorm kernel for the common pattern:

```python
y = rms_norm(x, weight, eps) * (1 + scale) + shift
```

This is intended for FiLM/AdaRMSNorm-style modulation where RMSNorm
output is scaled and optionally shifted by per-hidden-dimension
modulation tensors.

Related issue: #1224

## Changes

- Add `LigerModulatedRMSNormFunction` with fused forward/backward Triton
kernels.
- Add `LigerModulatedRMSNorm` module wrapper and
`liger_modulated_rms_norm` functional API.
- Support optional RMSNorm weight, optional shift, and existing RMSNorm
casting modes: `llama`, `gemma`, `none`.
- Support modulation shapes that flatten to `(hidden,)`, `(batch,
hidden)`, or row-wise `(batch, seq, hidden)` when rows divide the
hidden-state rows.
- Add correctness tests across fp32/bf16, casting modes, modulation
broadcast modes, optional shift, and optional affine weight.
- Add benchmark script for speed and memory comparison.

## Benchmark

Environment: NVIDIA GeForce RTX 3090, bf16, hidden size 4096, row-wise
`scale` and `shift`.

### Speed, p50 ms

| BT | Mode | Fused | LigerRMSNorm + torch mod | Speedup |
|---:|---|---:|---:|---:|
| 1024 | forward | 0.0430 | 0.1091 | 2.5x |
| 1024 | backward | 0.0763 | 0.1403 | 1.8x |
| 1024 | full | 0.1403 | 0.2642 | 1.9x |
| 4096 | forward | 0.1546 | 0.3999 | 2.6x |
| 4096 | backward | 0.2540 | 0.4895 | 1.9x |
| 4096 | full | 0.4787 | 1.3793 | 2.9x |

### Memory, p50 MB

| BT | Mode | Fused | LigerRMSNorm + torch mod | Savings |
|---:|---|---:|---:|---:|
| 1024 | forward | 32.02 | 56.02 | 42.8% |
| 1024 | backward | 89.33 | 97.33 | 8.2% |
| 1024 | full | 89.33 | 97.33 | 8.2% |
| 4096 | forward | 128.03 | 224.03 | 42.9% |
| 4096 | backward | 353.34 | 385.34 | 8.3% |
| 4096 | full | 353.34 | 385.34 | 8.3% |
2026-05-18 14:07:43 +00:00

219 lines
7.7 KiB
Python

import math
import os
import sys
import torch
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
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 estimate_kernel_peak_memory
from benchmark_model_configs import get_benchmark_model_config
from test.transformers.test_modulated_rms_norm import ModulatedRMSNormReference
from utils import SingleBenchmarkRunInput
from utils import SingleBenchmarkRunOutput
from utils import parse_benchmark_script_args
from utils import run_benchmarks
from utils import run_memory_benchmark
from utils import run_speed_benchmark
from liger_kernel.transformers.modulated_rms_norm import LigerModulatedRMSNorm
from liger_kernel.utils import infer_device
device = infer_device()
def _setup_modulated_rms_norm(input: SingleBenchmarkRunInput):
"""Create input tensors and ModulatedRMSNorm layer from benchmark config."""
cfg = input.extra_benchmark_config
hidden_size = cfg["hidden_size"]
eps = cfg["eps"]
has_shift = cfg["has_shift"]
x = torch.randn(
input.x,
hidden_size,
device=device,
dtype=cfg["dtype"],
requires_grad=True,
)
scale = (torch.randn_like(x) * 0.1).requires_grad_(True)
shift = (torch.randn_like(x) * 0.1).requires_grad_(True) if has_shift else None
if input.kernel_provider == "liger":
layer = LigerModulatedRMSNorm(hidden_size=hidden_size, eps=eps, in_place=False).to(device)
elif input.kernel_provider == "huggingface":
layer = ModulatedRMSNormReference(hidden_size=hidden_size, eps=eps).to(device)
else:
raise ValueError(f"Invalid provider: {input.kernel_provider} for ModulatedRMSNorm")
return x, scale, shift, layer
def _input_tensors(x, scale, shift):
tensors = [x, scale]
if shift is not None:
tensors.append(shift)
return tensors
def bench_speed_modulated_rms_norm(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, scale, shift, layer = _setup_modulated_rms_norm(input)
return run_speed_benchmark(
lambda: layer(x, scale, shift), input.kernel_operation_mode, _input_tensors(x, scale, shift)
)
def bench_memory_modulated_rms_norm(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, scale, shift, layer = _setup_modulated_rms_norm(input)
return run_memory_benchmark(lambda: layer(x, scale, shift), input.kernel_operation_mode)
def _resolve_model_config_modulated_rms_norm(input: SingleBenchmarkRunInput):
cfg = input.extra_benchmark_config
model_info = cfg["model_configs"][input.x]
return _setup_modulated_rms_norm(
SingleBenchmarkRunInput(
x=cfg["BT"],
kernel_provider=input.kernel_provider,
extra_benchmark_config={
"hidden_size": model_info["hidden_size"],
"dtype": model_info["dtype"],
"eps": cfg["eps"],
"has_shift": cfg["has_shift"],
},
)
)
def bench_speed_modulated_rms_norm_model_config(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, scale, shift, layer = _resolve_model_config_modulated_rms_norm(input)
return run_speed_benchmark(
lambda: layer(x, scale, shift), input.kernel_operation_mode, _input_tensors(x, scale, shift)
)
def bench_memory_modulated_rms_norm_model_config(input: SingleBenchmarkRunInput) -> SingleBenchmarkRunOutput:
x, scale, shift, layer = _resolve_model_config_modulated_rms_norm(input)
return run_memory_benchmark(lambda: layer(x, scale, shift), input.kernel_operation_mode)
if __name__ == "__main__":
args = parse_benchmark_script_args()
if args.sweep_mode == "model_config":
all_model_configs = list(MODEL_REGISTRY.values())
def _probe_factory(model_cfg, probe_bt):
def _probe():
probe_input = SingleBenchmarkRunInput(
x=probe_bt,
kernel_provider="huggingface",
extra_benchmark_config={
"hidden_size": model_cfg.hidden_size,
"dtype": model_cfg.dtype,
"eps": 1e-6,
"has_shift": True,
},
)
x, scale, shift, layer = _setup_modulated_rms_norm(probe_input)
return layer(x, scale, shift)
return _probe
sweep = compute_model_config_sweep_config(all_model_configs, probe_fn_factory=_probe_factory, bt=args.bt)
model_configs_info = {
cfg.name: {
"hidden_size": cfg.hidden_size,
"dtype": cfg.dtype,
}
for cfg in sweep.model_configs
}
common_configs = {
"kernel_name": "modulated_rms_norm",
"x_name": "model_config",
"x_label": "model configuration",
"x_values": [cfg.name for cfg in sweep.model_configs],
"kernel_providers": ["liger", "huggingface"],
"extra_benchmark_configs": [
{
"model_configs": model_configs_info,
"BT": sweep.bt,
"eps": 1e-6,
"has_shift": True,
}
],
"overwrite": args.overwrite,
}
run_benchmarks(
bench_test_fn=bench_speed_modulated_rms_norm_model_config,
kernel_operation_modes=["full", "forward", "backward"],
metric_name="speed",
metric_unit="ms",
**common_configs,
)
run_benchmarks(
bench_test_fn=bench_memory_modulated_rms_norm_model_config,
kernel_operation_modes=["full"],
metric_name="memory",
metric_unit="MB",
**common_configs,
)
else:
model = get_benchmark_model_config(args.model)
probe_bt = 1024
def _probe():
probe_input = SingleBenchmarkRunInput(
x=probe_bt,
kernel_provider="huggingface",
extra_benchmark_config={
"hidden_size": model.hidden_size,
"dtype": model.dtype,
"eps": 1e-6,
"has_shift": True,
},
)
x, scale, shift, layer = _setup_modulated_rms_norm(probe_input)
return layer(x, scale, shift)
peak_bytes = estimate_kernel_peak_memory(probe_fn=_probe)
kernel_bpt = peak_bytes // probe_bt
config = compute_seq_len_sweep_config(model, kernel_bytes_per_token=kernel_bpt)
common_configs = {
"kernel_name": "modulated_rms_norm",
"x_name": "BT",
"x_label": "B * T",
"x_values": [2**i for i in range(10, int(math.log2(config.batch_size * config.seq_len)) + 1)],
"kernel_providers": ["liger", "huggingface"],
"extra_benchmark_configs": [
{
"hidden_size": model.hidden_size,
"dtype": model.dtype,
"eps": 1e-6,
"has_shift": True,
}
],
"overwrite": args.overwrite,
}
run_benchmarks(
bench_test_fn=bench_speed_modulated_rms_norm,
kernel_operation_modes=["full", "forward", "backward"],
metric_name="speed",
metric_unit="ms",
**common_configs,
)
run_benchmarks(
bench_test_fn=bench_memory_modulated_rms_norm,
kernel_operation_modes=["full"],
metric_name="memory",
metric_unit="MB",
**common_configs,
)