Files
Liger-Kernel/benchmark/benchmarks_visualizer.py
7401db04e3 [cuTile] Add cutile backend for geglu, layer_norm, fused_linear_jsd, cross_entropy (#1250)
## Summary

Adds **cuTile backend implementations** for four Liger-Kernel operators,
mirroring the existing Triton kernels under
src/liger_kernel/ops/cutile/ops/:

- cross_entropy — fused forward + in-place gradient, online
numerically-stable logsumexp
- fused_linear_jsd — chunked fused linear + JSD (reuses the cuTile
jsd_kernel)
- geglu — GEGLU (tanh-approx GELU) forward/backward
- layer_norm — forward (single-pass mean/var via fold trick) + combined
DX/DW/DB backward

Also adds a shared element_mul_kernel to cutile/ops/utils.py and wires
all new symbols into cutile/ops/__init__.py.

## What's included

- cutile/ops/cross_entropy.py — LigerCrossEntropyFunction,
cross_entropy_forward/backward
  - Supports label_smoothing, lse_square_scale (z-loss), per-class
weight, softcap, ignore_index, reduction
  - Adds optional return_token_accuracy / return_predicted_tokens
(cross-chunk argmax tracking) — **new logic not present in upstream
Triton**
- cutile/ops/fused_linear_jsd.py — LigerFusedLinearJSDFunction,
fused_linear_jsd_forward/backward
- cutile/ops/geglu.py — LigerGELUMulFunction, geglu_forward/backward
(aligned/check_bounds=False fast path)
- cutile/ops/layer_norm.py — LigerLayerNormFunction,
layer_norm_forward/backward (fast path skips DW/DB when not needed)
- cutile/ops/utils.py — element_mul_kernel
- Benchmark data refresh in benchmark/data/all_benchmark_data.csv and
all_benchmark_data_cutile.csv

## Implementation notes

- All kernels are faithful ports of the upstream Liger Triton
implementations, including the math (online logsumexp, label-smoothing,
softcap chain rule sech² = 1 − tanh², GEGLU tanh-approx fwd/bwd,
layer-norm c1/c2 reduction) and dtype conventions (loss_1d/Mean/RSTD
follow upstream).
- Aligned-shape fast paths (check_bounds=False) are used when n_cols is
a power of 2 to enable the hardware TMA path.
- layer_norm backward uses a persistent grid (grid = sm_count) with
per-block DW/DB partials reduced on host.

## Benchmark results
<img width="718" height="322" alt="image"
src="https://github.com/user-attachments/assets/db40a765-5f81-49e8-aafa-f418782043f6"
/>
<img width="650" height="401" alt="image"
src="https://github.com/user-attachments/assets/e44190ce-3651-4830-a7d7-9538c906f782"
/>
<img width="649" height="409" alt="image"
src="https://github.com/user-attachments/assets/27f98c48-3cc8-4689-986b-1cb551ce63dd"
/>
<img width="654" height="418" alt="image"
src="https://github.com/user-attachments/assets/f39a557c-440b-4812-ac03-1a4cd3715a1e"
/>
<img width="654" height="402" alt="image"
src="https://github.com/user-attachments/assets/88afd7e0-d681-4676-b617-1cf66764114f"
/>
<img width="666" height="405" alt="image"
src="https://github.com/user-attachments/assets/3072d59c-cd1d-48c1-8e68-49b5f2679c7a"
/>



## Testing Done
<!--- This is a required section; please describe how this change was
tested. --->

<!-- 
<img width="2198" height="528" alt="image"
src="https://github.com/user-attachments/assets/bb6e1a05-8475-4a06-b5c3-f9ebf5be43ef"
/>

Complete the following tasks before sending your PR, and replace `[ ]`
with
`[x]` to indicate you have done them. 
-->

- Hardware Type: <BLANK>
- [x] run `make test` to ensure correctness
- [x] run `make checkstyle` to ensure code style
- [ ] run `make test-convergence` to ensure convergence

---------

Co-authored-by: jinmanx (generated by with_the_same_user script) <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: jinmanx (generated by with_the_same_user script) <[email protected]>
Co-authored-by: jinmanx (generated by with_the_same_user script) <[email protected]>
Co-authored-by: Vaibhav Jindal <[email protected]>
2026-06-25 23:33:58 +00:00

457 lines
18 KiB
Python

import json
import os
import sys
from argparse import ArgumentParser
from dataclasses import dataclass
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
DEFAULT_DATA_FILE = "data/all_benchmark_data.csv"
DATA_DIR = os.path.abspath(os.path.dirname(__file__))
VISUALIZATIONS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "visualizations/"))
def resolve_data_path(data_file: str | None) -> str:
"""Resolve --data-file to an absolute CSV path; falls back to the default."""
path = data_file or DEFAULT_DATA_FILE
return path if os.path.isabs(path) else os.path.abspath(os.path.join(DATA_DIR, path))
# Map --sweep-mode values to the x_name used in benchmark CSV data.
# "model_config" sweeps always write x_name="model_config"; token-length
# sweeps use kernel-specific names (e.g. "T"), so we match them by exclusion.
SWEEP_MODE_X_NAME = "model_config"
@dataclass
class VisualizationsConfig:
"""
Configuration for the visualizations script.
Args:
kernel_name (str): Kernel name to benchmark. (Will run `scripts/benchmark_{kernel_name}.py`)
metric_name (str): Metric name to visualize (speed/memory)
kernel_operation_mode (str): Kernel operation mode to visualize (forward/backward/full). Defaults to "full"
sweep_mode (str, optional): Sweep mode to filter data. "token_length" selects
token/sequence-length sweep data; "model_config" selects model-configuration
sweep data. When None, all data is considered (legacy behaviour).
extra_config_filter (str, optional): A string to filter extra_benchmark_config.
Can be a substring to match or a 'key=value' pair (e.g., "'H': 4096").
Defaults to None, which means the first available config will be used if multiple exist.
display (bool): Display the visualization. Defaults to False
overwrite (bool): Overwrite existing visualization, if none exist this flag has no effect as ones are always created and saved. Defaults to False
"""
kernel_name: str
metric_name: str
kernel_operation_mode: str = "full"
sweep_mode: str = "token_length"
extra_config_filter: str | None = None
gpu_filter: str | None = None
data_file: str | None = None
display: bool = False
overwrite: bool = False
def parse_args() -> VisualizationsConfig:
"""Parse command line arguments into a configuration object.
Returns:
VisualizationsConfig: Configuration object for the visualizations script.
"""
parser = ArgumentParser()
parser.add_argument("--kernel-name", type=str, required=True, help="Kernel name to benchmark")
parser.add_argument(
"--metric-name",
type=str,
required=True,
help="Metric name to visualize (speed/memory)",
)
parser.add_argument(
"--kernel-operation-mode",
type=str,
nargs="*",
default=None,
help="Kernel operation modes to visualize (forward/backward/full). If not provided, generate for all available modes.",
)
parser.add_argument(
"--sweep-mode",
type=str,
choices=["token_length", "model_config"],
default="token_length",
help="Sweep mode used when running the benchmark. "
"'token_length' selects token/sequence-length sweep data (default); "
"'model_config' selects model-configuration sweep data.",
)
parser.add_argument(
"--extra-config-filter",
type=str,
default=None,
help="A string to filter extra_benchmark_config. "
"Can be a substring to match or a JSON-like 'key=value' pair (e.g., \"'H': 4096\" or \"H=4096\" for simple cases). "
"Defaults to None (first available config if multiple exist).",
)
parser.add_argument(
"--gpu-filter",
type=str,
default=None,
help="Filter by GPU name. When multiple devices are present, selects "
"the matching GPU (uses most recent match if multiple found). "
"If omitted, the most recent device is used automatically.",
)
parser.add_argument(
"--data-file",
type=str,
default=None,
help="Benchmark CSV to read, relative to benchmark/ or absolute. "
"Defaults to data/all_benchmark_data.csv. Use "
"data/all_benchmark_data_cutile.csv for Triton vs CuTile comparisons.",
)
parser.add_argument("--display", action="store_true", help="Display the visualization")
parser.add_argument(
"--overwrite",
action="store_true",
help="Overwrite existing visualization, if none exist this flag has no effect as one are always created",
)
args = parser.parse_args()
return args
def gpu_name_filter(df: pd.DataFrame, gpu_filter: str | None = None) -> pd.DataFrame:
"""Filter benchmark data by GPU name when multiple devices are present.
Args:
df: Pre-filtered benchmark dataframe.
gpu_filter: Optional GPU name substring to match. If provided, selects
the matching GPU (uses most recent if multiple match). If None,
automatically picks the most recent device.
Returns:
pd.DataFrame: Dataframe filtered to a single GPU.
"""
if "gpu_name" not in df.columns or df.empty:
return df
unique_gpus = df["gpu_name"].unique()
if len(unique_gpus) <= 1:
return df
if gpu_filter:
matched = [g for g in unique_gpus if gpu_filter in g]
if matched:
if len(matched) > 1:
# Multiple matches — pick the most recent
matched_df = df[df["gpu_name"].isin(matched)]
selected = matched_df.sort_values("timestamp", ascending=False)["gpu_name"].iloc[0]
print(
f"Warning: Multiple GPUs match filter '{gpu_filter}': {matched}. "
f"Using the most recent: '{selected}'."
)
else:
selected = matched[0]
else:
# No match — fall back to most recent GPU
selected = df.sort_values("timestamp", ascending=False)["gpu_name"].iloc[0]
print(
f"Warning: No GPU matches filter '{gpu_filter}'. "
f"Available GPUs: {list(unique_gpus)}. "
f"Falling back to most recent device: '{selected}'."
)
else:
# No filter provided — pick the most recent device
selected = df.sort_values("timestamp", ascending=False)["gpu_name"].iloc[0]
print(
f"Warning: Data contains entries from multiple devices: {list(unique_gpus)}. "
f"Using data from the most recent device: '{selected}'. "
f"Use --gpu-filter to select a specific device."
)
return df[df["gpu_name"] == selected]
def extra_config_filter(df: pd.DataFrame, config: VisualizationsConfig) -> pd.DataFrame:
"""Filter benchmark data by extra_benchmark_config.
Args:
df: Pre-filtered benchmark dataframe (already filtered by kernel, metric, etc.).
config: Visualization configuration with optional extra_config_filter.
Returns:
pd.DataFrame: Dataframe filtered to a single extra_benchmark_config.
"""
unique_extra_configs_str = df["extra_benchmark_config_str"].unique()
selected_extra_config_str = None
if len(unique_extra_configs_str) == 0:
print(
"Warning: No extra_benchmark_config found for the initial filters. "
"Proceeding with all data from initial filter."
)
return df
if config.extra_config_filter:
matched_configs = []
try:
if "=" in config.extra_config_filter:
key_filter, value_filter = config.extra_config_filter.split("=", 1)
for cfg_str in unique_extra_configs_str:
cfg_json = json.loads(cfg_str)
if str(cfg_json.get(key_filter.strip("'\" "))) == value_filter.strip("'\" "):
matched_configs.append(cfg_str)
if not matched_configs:
matched_configs = [
cfg_str for cfg_str in unique_extra_configs_str if config.extra_config_filter in cfg_str
]
except Exception as e:
print(
f"Note: Could not parse extra_config_filter '{config.extra_config_filter}' as key=value ({e}), using substring match."
)
matched_configs = [cfg_str for cfg_str in unique_extra_configs_str if config.extra_config_filter in cfg_str]
if matched_configs:
if len(matched_configs) > 1:
print(
f"Warning: Multiple extra_benchmark_configs match filter '{config.extra_config_filter}': {matched_configs}. "
f"Using the first one: {matched_configs[0]}"
)
selected_extra_config_str = matched_configs[0]
else:
print(
f"Warning: No extra_benchmark_config matches filter '{config.extra_config_filter}'. "
f"Available configs for {config.kernel_name} ({config.metric_name}, {config.kernel_operation_mode}): {list(unique_extra_configs_str)}"
)
if len(unique_extra_configs_str) > 0:
selected_extra_config_str = unique_extra_configs_str[0]
print(f"Defaulting to the first available extra_benchmark_config: {selected_extra_config_str}")
else:
raise ValueError("No extra_benchmark_config available to select after failed filter attempt.")
elif len(unique_extra_configs_str) > 1:
selected_extra_config_str = unique_extra_configs_str[0]
print(
f"Warning: Multiple extra_benchmark_configs found for {config.kernel_name} ({config.metric_name}, {config.kernel_operation_mode})."
)
print(f"Defaulting to use: {selected_extra_config_str}")
print(f"Available configs: {list(unique_extra_configs_str)}")
print(
"Use the --extra-config-filter argument to select a specific one "
"(e.g., --extra-config-filter \"'H': 4096\" or a substring like \"'seq_len': 512\")."
)
elif len(unique_extra_configs_str) == 1:
selected_extra_config_str = unique_extra_configs_str[0]
print(f"Using unique extra_benchmark_config: {selected_extra_config_str}")
if selected_extra_config_str:
result_df = df[df["extra_benchmark_config_str"] == selected_extra_config_str]
else:
print("Warning: Could not select an extra_benchmark_config. Using data from initial filter if any.")
result_df = df
if result_df.empty:
raise ValueError(
f"No data found after attempting to filter by extra_benchmark_config. "
f"Selected/Defaulted extra_config_str: {selected_extra_config_str}"
if selected_extra_config_str
else "No specific extra_config was selected."
)
print(
f"Plotting data for extra_benchmark_config: {json.loads(selected_extra_config_str if selected_extra_config_str else '{}')}"
)
return result_df
def load_data(config: VisualizationsConfig) -> pd.DataFrame:
"""Loads the benchmark data from the CSV file and filters it based on the configuration.
Applies filters in order: kernel/metric/mode → sweep mode → GPU → extra config.
Args:
config (VisualizationsConfig): Configuration object for the visualizations script.
Raises:
ValueError: If no data is found for the given filters.
Returns:
pd.DataFrame: Filtered benchmark dataframe.
"""
df = pd.read_csv(resolve_data_path(config.data_file))
df["extra_benchmark_config"] = df["extra_benchmark_config_str"].apply(json.loads)
mask = (
(df["kernel_name"] == config.kernel_name)
& (df["metric_name"] == config.metric_name)
& (df["kernel_operation_mode"] == config.kernel_operation_mode)
)
# Filter by sweep mode early, before extra_benchmark_config resolution.
if config.sweep_mode == "model_config":
mask = mask & (df["x_name"] == SWEEP_MODE_X_NAME)
elif config.sweep_mode == "token_length":
mask = mask & (df["x_name"] != SWEEP_MODE_X_NAME)
base_filtered_df = df[mask]
if base_filtered_df.empty:
raise ValueError(
f"No data found for kernel_name='{config.kernel_name}', "
f"metric_name='{config.metric_name}', "
f"kernel_operation_mode='{config.kernel_operation_mode}'."
)
# Apply GPU filter, then extra config filter
base_filtered_df = gpu_name_filter(base_filtered_df, config.gpu_filter)
return extra_config_filter(base_filtered_df, config)
def plot_data(df: pd.DataFrame, config: VisualizationsConfig):
"""Plots the benchmark data, saving the result if needed.
Args:
df (pd.DataFrame): Filtered benchmark dataframe.
config (VisualizationsConfig): Configuration object for the visualizations script.
"""
for col in ["y_value_20", "y_value_50", "y_value_80"]:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
# Convert x_value to numeric where possible so matplotlib uses a real
# numeric axis (proper proportional spacing). String x_values (e.g.
# model names) stay as-is and will be treated as categorical (evenly spaced).
x_numeric = pd.to_numeric(df["x_value"], errors="coerce")
is_numeric_x = x_numeric.notna().all()
if is_numeric_x:
df["x_value"] = x_numeric
xlabel = df["x_label"].iloc[0]
ylabel = f"{config.metric_name} ({df['metric_unit'].iloc[0]})"
# Sort by "kernel_provider" so color assignment is stable across runs, with
# "liger" pinned to the END so seaborn draws it LAST (on top). Without this,
# any other provider with identical y-values would obscure Liger at the overlap
# — e.g., megatron-unfused has bit-identical memory to Liger in the
# megatron_cross_entropy benchmark and would hide it under alphabetical sort.
providers = df["kernel_provider"].unique().tolist()
non_liger = sorted(p for p in providers if p != "liger")
order = non_liger + (["liger"] if "liger" in providers else [])
df["kernel_provider"] = pd.Categorical(df["kernel_provider"], categories=order, ordered=True)
df = df.sort_values(by="kernel_provider")
plt.figure(figsize=(10, 6))
sns.set(style="whitegrid")
try:
ax = sns.lineplot(
data=df,
x="x_value",
y="y_value_50",
hue="kernel_provider",
marker="o",
palette="tab10",
errorbar=("ci", None),
)
except Exception:
ax = sns.lineplot(
data=df,
x="x_value",
y="y_value_50",
hue="kernel_provider",
marker="o",
palette="tab10",
errorbar=None,
)
# For numeric x axes, show tick labels only at actual data points
if is_numeric_x:
tick_values = sorted(df["x_value"].unique())
ax.set_xticks(tick_values)
ax.set_xticklabels([str(int(v)) if v == int(v) else str(v) for v in tick_values])
# Seaborn can't plot pre-computed error bars, so we need to do it manually
lines = ax.get_lines()
colors = [line.get_color() for line in lines]
for (_, group_data), color in zip(df.groupby("kernel_provider"), colors):
y_error_lower = group_data["y_value_50"] - group_data["y_value_20"]
y_error_upper = group_data["y_value_80"] - group_data["y_value_50"]
y_error = [y_error_lower, y_error_upper]
plt.errorbar(
group_data["x_value"],
group_data["y_value_50"],
yerr=y_error,
fmt="o",
color=color,
capsize=5,
)
# Title includes kernel name, metric, operation mode, and GPU so the
# PNG is self-describing without relying on the filename.
gpu = df["gpu_name"].iloc[0] if "gpu_name" in df.columns and not df["gpu_name"].empty else ""
title_parts = [
config.kernel_name,
f"{config.metric_name} ({df['metric_unit'].iloc[0]})",
f"{config.kernel_operation_mode} pass",
]
if gpu:
title_parts.append(gpu)
plt.title(" — ".join(title_parts))
plt.legend(title="Kernel Provider")
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.tight_layout()
sweep_suffix = f"_{config.sweep_mode}" if config.sweep_mode else ""
out_path = os.path.join(
VISUALIZATIONS_PATH,
f"{config.kernel_name}_{config.metric_name}_{config.kernel_operation_mode}{sweep_suffix}.png",
)
if config.display:
plt.show()
if config.overwrite or not os.path.exists(
out_path
): # Save the plot if it doesn't exist or if we want to overwrite it
os.makedirs(VISUALIZATIONS_PATH, exist_ok=True)
plt.savefig(out_path)
plt.close()
def main():
args = parse_args()
all_df = pd.read_csv(resolve_data_path(args.data_file))
all_df["extra_benchmark_config"] = all_df["extra_benchmark_config_str"].apply(json.loads)
if args.metric_name == "memory":
modes = ["full"]
elif args.kernel_operation_mode:
modes = args.kernel_operation_mode
else:
filtered = all_df[(all_df["kernel_name"] == args.kernel_name) & (all_df["metric_name"] == args.metric_name)]
modes = filtered["kernel_operation_mode"].unique().tolist()
if not modes:
print(f"No data found for kernel '{args.kernel_name}' and metric '{args.metric_name}'.", file=sys.stderr)
sys.exit(1)
for mode in modes:
config = VisualizationsConfig(
kernel_name=args.kernel_name,
metric_name=args.metric_name,
kernel_operation_mode=mode,
sweep_mode=args.sweep_mode,
extra_config_filter=args.extra_config_filter,
gpu_filter=args.gpu_filter,
data_file=args.data_file,
display=args.display,
overwrite=args.overwrite,
)
df = load_data(config)
plot_data(df, config)
if __name__ == "__main__":
main()