diff --git a/analysis/activation_analysis/plot_learned_splines.py b/analysis/activation_analysis/plot_learned_splines.py
new file mode 100644
index 0000000000..db26eb53f9
--- /dev/null
+++ b/analysis/activation_analysis/plot_learned_splines.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+"""Plot learned spline activations from one or more checkpoints.
+
+This script finds learned-spline knot parameters (`x_vals`, `y_vals`) in checkpoint
+state dicts, reconstructs the spline curves, and writes a single interactive Plotly
+HTML figure with one trace per layer/path.
+"""
+
+import argparse
+import re
+from pathlib import Path
+
+import plotly.graph_objects as go
+import torch
+
+
+def _load_state_dict(checkpoint_path: Path):
+ ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
+
+ # Common checkpoint layouts in this repo:
+ # 1) {'model': state_dict, ...}
+ # 2) raw state_dict
+ if isinstance(ckpt, dict) and "model" in ckpt and isinstance(ckpt["model"], dict):
+ return ckpt["model"]
+ if isinstance(ckpt, dict):
+ return ckpt
+
+ raise ValueError(f"Unsupported checkpoint format for: {checkpoint_path}")
+
+
+def _natural_cubic_spline_eval(x_vals: torch.Tensor, y_vals: torch.Tensor, x_query: torch.Tensor):
+ """Mirror LearnedSplineActivation's natural cubic spline evaluation."""
+ x_vals = x_vals.to(dtype=torch.float64)
+ y_vals = y_vals.to(dtype=torch.float64)
+ x_query = x_query.to(dtype=torch.float64)
+
+ n = x_vals.numel()
+ if n < 2:
+ raise ValueError("Need at least 2 knots to evaluate spline.")
+
+ h = x_vals[1:] - x_vals[:-1]
+ delta = (y_vals[1:] - y_vals[:-1]) / h
+
+ a_mat = torch.zeros((n, n), dtype=torch.float64)
+ rhs = torch.zeros((n,), dtype=torch.float64)
+
+ a_mat[0, 0] = 1.0
+ a_mat[-1, -1] = 1.0
+
+ for i in range(1, n - 1):
+ a_mat[i, i - 1] = h[i - 1]
+ a_mat[i, i] = 2 * (h[i - 1] + h[i])
+ a_mat[i, i + 1] = h[i]
+ rhs[i] = 3 * (delta[i] - delta[i - 1])
+
+ m_vals = torch.linalg.solve(a_mat, rhs)
+
+ # Interval coefficients
+ a_coef = y_vals[:-1]
+ b_coef = delta - h * (2 * m_vals[:-1] + m_vals[1:]) / 3
+ c_coef = m_vals[:-1] / 2
+ d_coef = (m_vals[1:] - m_vals[:-1]) / (6 * h)
+
+ indices = torch.searchsorted(x_vals, x_query, right=False) - 1
+ indices = torch.clamp(indices, 0, n - 2)
+
+ x_k = x_vals[indices]
+ dx = x_query - x_k
+
+ return (
+ a_coef[indices]
+ + b_coef[indices] * dx
+ + c_coef[indices] * dx**2
+ + d_coef[indices] * dx**3
+ )
+
+
+def _extract_layer_index(param_name: str):
+ match = re.search(r"\.h\.(\d+)\.", param_name)
+ if match:
+ return int(match.group(1))
+ return None
+
+
+def _collect_learned_splines(state_dict: dict, checkpoint_path: Path):
+ traces = []
+
+ # LearnedSplineActivation in this codebase stores parameters as:
+ # ...activation_variant.x_vals and ...activation_variant.y_vals
+ x_keys = [k for k in state_dict if k.endswith(".x_vals") and "activation_variant" in k]
+
+ for x_key in x_keys:
+ y_key = x_key[:-len(".x_vals")] + ".y_vals"
+ if y_key not in state_dict:
+ continue
+
+ x_vals = state_dict[x_key]
+ y_vals = state_dict[y_key]
+ if not isinstance(x_vals, torch.Tensor) or not isinstance(y_vals, torch.Tensor):
+ continue
+ if x_vals.ndim != 1 or y_vals.ndim != 1:
+ continue
+ if x_vals.numel() != y_vals.numel() or x_vals.numel() < 4:
+ continue
+
+ layer_idx = _extract_layer_index(x_key)
+ traces.append(
+ {
+ "checkpoint": str(checkpoint_path),
+ "layer_idx": layer_idx,
+ "x_key": x_key,
+ "x_vals": x_vals.detach().cpu(),
+ "y_vals": y_vals.detach().cpu(),
+ }
+ )
+
+ traces.sort(key=lambda t: (t["checkpoint"], -1 if t["layer_idx"] is None else t["layer_idx"], t["x_key"]))
+ return traces
+
+
+def _build_html_with_range_controls(fig, html_path: Path, default_xmin: float, default_xmax: float):
+ div = fig.to_html(full_html=False, include_plotlyjs="cdn", div_id="spline_plot")
+
+ html = f"""
+
+
+
+ Learned spline curves
+
+
+
+ Learned spline curves
+
+
+
+
+
+
+
+ Legend labels include checkpoint path + layer + parameter path.
+
+ {div}
+
+
+
+
+"""
+
+ html_path.write_text(html, encoding="utf-8")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Plot learned spline activations from checkpoints.")
+ parser.add_argument(
+ "checkpoints",
+ nargs="+",
+ type=Path,
+ help="Checkpoint path(s), e.g. out/run1/ckpt.pt out/run2/ckpt.pt",
+ )
+ parser.add_argument(
+ "--output_html",
+ type=Path,
+ default=Path("analysis/activation_analysis/learned_splines.html"),
+ help="Output HTML path.",
+ )
+ parser.add_argument("--x_min", type=float, default=-10.0, help="Default plot x-axis minimum.")
+ parser.add_argument("--x_max", type=float, default=10.0, help="Default plot x-axis maximum.")
+ parser.add_argument("--num_points", type=int, default=1200, help="Points per spline trace.")
+ args = parser.parse_args()
+
+ if args.x_min >= args.x_max:
+ raise ValueError("x_min must be smaller than x_max")
+ if args.num_points < 8:
+ raise ValueError("num_points should be >= 8")
+
+ x_query = torch.linspace(args.x_min, args.x_max, args.num_points, dtype=torch.float64)
+
+ figure = go.Figure()
+ total_splines = 0
+
+ for checkpoint_path in args.checkpoints:
+ state_dict = _load_state_dict(checkpoint_path)
+ splines = _collect_learned_splines(state_dict, checkpoint_path)
+
+ for spline in splines:
+ y_query = _natural_cubic_spline_eval(spline["x_vals"], spline["y_vals"], x_query)
+ layer_label = f"layer {spline['layer_idx']}" if spline["layer_idx"] is not None else "layer ?"
+ label = f"{spline['checkpoint']} | {layer_label} | {spline['x_key']}"
+
+ figure.add_trace(
+ go.Scatter(
+ x=x_query.tolist(),
+ y=y_query.tolist(),
+ mode="lines",
+ name=label,
+ hovertemplate="x=%{x:.3f}
y=%{y:.3f}%{fullData.name}",
+ )
+ )
+ total_splines += 1
+
+ if total_splines == 0:
+ raise RuntimeError(
+ "No learned spline parameters were found. "
+ "Expected keys like '...activation_variant.x_vals' + '...activation_variant.y_vals'."
+ )
+
+ figure.update_layout(
+ title=f"Learned spline curves ({total_splines} traces)",
+ xaxis_title="x",
+ yaxis_title="activation(x)",
+ template="plotly_white",
+ legend=dict(orientation="v"),
+ )
+ figure.update_xaxes(range=[args.x_min, args.x_max])
+
+ args.output_html.parent.mkdir(parents=True, exist_ok=True)
+ _build_html_with_range_controls(figure, args.output_html, args.x_min, args.x_max)
+ print(f"Saved: {args.output_html} ({total_splines} traces)")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/explorations/default_inf_lsa_init_mlp_swiglu.yaml b/explorations/default_inf_lsa_init_mlp_swiglu.yaml
new file mode 100644
index 0000000000..00a04cdfc7
--- /dev/null
+++ b/explorations/default_inf_lsa_init_mlp_swiglu.yaml
@@ -0,0 +1,57 @@
+# default_inf_lsa_init_mlp_swiglu.yaml
+---
+
+named_static_groups:
+ # QK Norm
+ - named_group: "qk_norm"
+ use_qk_norm: [true]
+ use_qk_norm_scale: [true]
+
+ # Norm Type
+ - named_group: "peri_ln"
+ use_pre_ln: [true]
+ use_peri_ln: [true]
+ use_post_ln: [false]
+
+ # Position Embeddings
+ - named_group: "rotary"
+ use_rotary_embeddings: [true]
+ use_abs_pos_embeddings: [false]
+
+ # Relu2Max
+ - named_group: "relu2max"
+ softmax_variant_attn: ["relu2max"]
+
+ # Infinite Attention
+ - named_group: "infinite"
+ attention_variant: ["infinite"]
+ use_concat_heads: [true]
+
+ # MQA
+ - named_group: "mqa"
+ n_kv_group: [1]
+
+ # Head Dimension
+ - named_group: "hd_100"
+ n_qk_head_dim: [100]
+ n_v_head_dim: [100]
+
+common_group:
+ dataset: ["minipile"]
+ eval_interval: [2500]
+ max_iters: [10000]
+ never_save_checkpoint: [false]
+ compile: [true]
+ log_rankme: [true]
+ log_areq: [true]
+
+parameter_groups:
+ - named_group_static:
+ - "qk_norm"
+ - "peri_ln"
+ - "rotary"
+ - "infinite"
+ - "hd_100"
+ mlp_variant: ["mlp", "swiglu"]
+ activation_variant: ["learned_spline"]
+ lsa_init_activation: ["mish", "silu", "gelu", "squared_relu", "identity", "relu"]
diff --git a/gpt_conf.py b/gpt_conf.py
index 5b1323e3fe..fed13416aa 100644
--- a/gpt_conf.py
+++ b/gpt_conf.py
@@ -442,6 +442,7 @@ class GPTConfig:
## LearnedSplineActivation - lsa
lsa_num_knots: int = 30
+ lsa_init_activation: str = "gelu"
# Linear Alternatives
diff --git a/plot_view.py b/plot_view.py
index 0845e2a97c..b04b9baccf 100644
--- a/plot_view.py
+++ b/plot_view.py
@@ -430,6 +430,23 @@ def plot_multi_bars_trim( # NEW
fig.write_image(f"{safe}.png", scale=2)
fig.show()
+def plot_bars_trim(
+ rows: List[Dict[str, Any]],
+ *,
+ y: str,
+ label_cols: List[str],
+) -> None:
+ """
+ Backward-compatible single-metric Δ-bar helper used by monitor ``z`` hotkey.
+
+ This intentionally mirrors ``plot_bars`` args while delegating to the
+ multi-metric trimmed implementation.
+ """
+ if not label_cols:
+ raise ValueError("Need ≥1 label column for bar chart")
+ plot_multi_bars_trim(rows, y_cols=[y], label_cols=label_cols)
+
+
# ───────────────────────────── CLI wrapper ──────────────────────────
diff --git a/train_args.py b/train_args.py
index 9553b7d92b..6c4096e641 100644
--- a/train_args.py
+++ b/train_args.py
@@ -835,6 +835,10 @@ def parse_args():
"tanh",
"identity",
]
+ lsa_init_activation_variations = [
+ variation for variation in activation_variations
+ if variation != "learned_spline"
+ ]
## DynamicActivations
model_group.add_argument("--dact_activation", type=str, default="tanh", choices=activation_variations)
@@ -873,6 +877,13 @@ def parse_args():
## LearnedSplineActivation - lsa
model_group.add_argument("--lsa_num_knots", type=int, default=30)
+ model_group.add_argument(
+ "--lsa_init_activation",
+ type=str,
+ default="gelu",
+ choices=lsa_init_activation_variations,
+ help="Activation used to initialize learned_spline knot y-values.",
+ )
# Attention Variations
diff --git a/variations/activation_variations.py b/variations/activation_variations.py
index e253c2ce48..64267d785b 100644
--- a/variations/activation_variations.py
+++ b/variations/activation_variations.py
@@ -173,17 +173,40 @@ class LearnedSplineActivation(nn.Module):
def __init__(self, config, num_knots=10, init_x_range=(-5, 5)):
super().__init__()
- self.num_knots = config.lsa_num_knots
+ self.num_knots = getattr(config, "lsa_num_knots", num_knots)
+ self.init_activation_name = getattr(config, "lsa_init_activation", "gelu")
- # Initialize learnable x_vals and y_vals
- x_init = torch.linspace(init_x_range[0], init_x_range[1], num_knots)
+ if self.init_activation_name == "learned_spline":
+ raise ValueError(
+ "lsa_init_activation cannot be 'learned_spline' because it would recurse during initialization."
+ )
- # Create an instance of GELU
- gelu = nn.GELU()
- y_init = gelu(x_init) # Use the GELU instance here
+ init_activation_cls = activation_dictionary.get(self.init_activation_name)
+ if init_activation_cls is None:
+ raise ValueError(
+ f"Unknown lsa_init_activation '{self.init_activation_name}'. "
+ f"Expected one of: {sorted(activation_dictionary.keys())}"
+ )
+
+ # Initialize learnable x_vals and y_vals
+ x_init = torch.linspace(init_x_range[0], init_x_range[1], self.num_knots)
+ init_activation = init_activation_cls(config)
+
+ with torch.no_grad():
+ if self.init_activation_name == "glu":
+ # GLU halves the last dimension, so feed a 2-channel input and squeeze back.
+ y_init = init_activation(torch.stack((x_init, x_init), dim=-1)).squeeze(-1)
+ else:
+ y_init = init_activation(x_init)
+
+ if y_init.shape != x_init.shape:
+ raise ValueError(
+ f"lsa_init_activation='{self.init_activation_name}' must preserve shape. "
+ f"Got output shape {tuple(y_init.shape)} for input shape {tuple(x_init.shape)}."
+ )
self.x_vals = nn.Parameter(x_init)
- self.y_vals = nn.Parameter(y_init)
+ self.y_vals = nn.Parameter(y_init.detach().clone())
def forward(self, x):
# Compute spline coefficients