Kernels
Jangwoong Kim Claude Opus 4.6 (1M context) commited on
Commit
536f0b2
·
1 Parent(s): 0c42208

bench: MLA RoPE fused vs vanilla sweep

Browse files

Adds cases/mla_rope.py (DiffCase) and registers it in run_cases.py with
mla_rope-specific correctness configs and dummy `dim=[0]` (MLA head dims
are fixed by motif3: H_q=80, H_kv=16, D_nope=128, D_rope=64, D_v=128).

Also adds bench_mla_rope.yaml (single-GPU TrainJob) and a small mkdir
in run_cases.py so triton's perf_report can savefig into a fresh path.

B200, motif3 config (bs=8, sl=4096): fwd 6.90x, bwd 5.53x.

Run: `python run_cases.py --case mla_rope --dtype bf16 --save-path ...`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

benchmarks/bench_mla_rope.yaml ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: trainer.kubeflow.org/v1alpha1
2
+ kind: TrainJob
3
+ metadata:
4
+ name: jangwoong-mla-rope-bench
5
+ namespace: kbm-g-np-motif
6
+ spec:
7
+ managedBy: trainer.kubeflow.org/trainjob-controller
8
+ podTemplateOverrides:
9
+ - spec:
10
+ containers:
11
+ - name: node
12
+ volumeMounts:
13
+ - mountPath: /dev/shm
14
+ name: shm
15
+ - mountPath: /mair
16
+ name: mair
17
+ volumes:
18
+ - emptyDir:
19
+ medium: Memory
20
+ sizeLimit: 64Gi
21
+ name: shm
22
+ - name: mair
23
+ persistentVolumeClaim:
24
+ claimName: mair
25
+ targetJobs:
26
+ - name: node
27
+ runtimeRef:
28
+ apiGroup: trainer.kubeflow.org
29
+ kind: ClusterTrainingRuntime
30
+ name: torch-distributed
31
+ suspend: false
32
+ trainer:
33
+ args:
34
+ - /bin/bash
35
+ - '-c'
36
+ - |
37
+ set -e
38
+ ACTIVATIONPATH=/mair/team-sys/jangwoong/activation
39
+ DATESTAMP=$(date +'%y_%m_%d_%H_%M')
40
+ SAVE_PATH=$ACTIVATIONPATH/benchmarks/results/mla_rope/${DATESTAMP}
41
+ mkdir -p $SAVE_PATH
42
+
43
+ pip install triton pandas
44
+
45
+ # Build activation from local source (copy to /tmp to avoid NFS race)
46
+ mkdir -p /tmp/activation_src && rm -rf /tmp/activation_src/* && \
47
+ rsync -a --exclude=build $ACTIVATIONPATH/ /tmp/activation_src/ && \
48
+ pip install --no-build-isolation /tmp/activation_src 2>&1 | tail -50
49
+
50
+ python -c "import activation; print('fused_q_rope_inplace:', activation.fused_q_rope_inplace); print('fused_kv_split_rope_cat:', activation.fused_kv_split_rope_cat)"
51
+
52
+ nvidia-smi | tee $SAVE_PATH/nvidia_smi.txt
53
+
54
+ echo "=== MLA RoPE benchmark ==="
55
+ cd $ACTIVATIONPATH/benchmarks
56
+ CUDA_VISIBLE_DEVICES=0 python run_cases.py --case mla_rope --dtype bf16 \
57
+ --save-path $SAVE_PATH/bench 2>&1 | tee $SAVE_PATH/bench.log
58
+
59
+ echo "=== Done. Results at: $SAVE_PATH ==="
60
+ exit 0
61
+ env:
62
+ - name: PYTHONUNBUFFERED
63
+ value: '1'
64
+ - name: PYTORCH_ALLOC_CONF
65
+ value: expandable_segments:True
66
+ - name: CUDA_LAUNCH_BLOCKING
67
+ value: '0'
68
+ - name: OMP_NUM_THREADS
69
+ value: '1'
70
+ image: ghcr.io/motiftechnologies/llm-training:v0.1.8
71
+ numNodes: 1
72
+ numProcPerNode: 1
73
+ resourcesPerNode:
74
+ limits:
75
+ cpu: '96'
76
+ memory: 1024Gi
77
+ nvidia.com/gpu: '8'
78
+ requests:
79
+ cpu: '96'
80
+ memory: 1024Gi
81
+ nvidia.com/gpu: '8'
benchmarks/cases/mla_rope.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MLA RoPE case: fused (activation Triton) vs vanilla (PyTorch native).
2
+
3
+ MLA head dims are fixed by motif3 spec (H_q=80, H_kv=16, D_nope=128,
4
+ D_rope=64, D_v=128); the benchmark's only sweep axes are (bs, sl).
5
+ The framework's ``dim`` axis is a dummy here — pass 0 in configs.
6
+ """
7
+
8
+ import torch
9
+ from torch import nn
10
+
11
+ import activation
12
+
13
+ from common.diff_engine import DiffCase
14
+
15
+ # ---- MLA shapes (motif3_seq) -----------------------------------------------
16
+ H_Q, H_KV = 80, 16
17
+ D_NOPE, D_ROPE, D_V = 128, 64, 128
18
+ D_QK = D_NOPE + D_ROPE # 192
19
+
20
+
21
+ # ---- reference (PyTorch native) --------------------------------------------
22
+ def _precompute_freqs_cis(dim, end, theta=10000.0):
23
+ freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: dim // 2].float() / dim))
24
+ t = torch.arange(end, dtype=torch.float32)
25
+ freqs = torch.outer(t, freqs)
26
+ return torch.polar(torch.ones_like(freqs), freqs)
27
+
28
+
29
+ def _apply_rotary_emb_single(x, freqs_cis):
30
+ x_ = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
31
+ freqs_cis = freqs_cis[: x_.shape[1]].view(1, x_.shape[1], 1, x_.shape[3])
32
+ out = torch.view_as_real(x_ * freqs_cis).flatten(3)
33
+ return out.type_as(x)
34
+
35
+
36
+ def _reorder(qk, rope_dim):
37
+ B, S = qk.shape[0], qk.shape[1]
38
+ qk = qk.view(B, S, -1, rope_dim // 2, 2).transpose(3, 4)
39
+ return qk.reshape(B, S, -1, rope_dim)
40
+
41
+
42
+ def _vanilla(q, kv_latent, k_pe, freqs_cis):
43
+ q_nope, q_pe = torch.split(q, [D_NOPE, D_ROPE], dim=-1)
44
+ q_pe = _reorder(_apply_rotary_emb_single(q_pe, freqs_cis), D_ROPE)
45
+ q_total = torch.cat([q_nope, q_pe], dim=-1)
46
+ k_pe_roped = _reorder(_apply_rotary_emb_single(k_pe.unsqueeze(2), freqs_cis), D_ROPE)
47
+ k_nope, v = torch.split(kv_latent, [D_NOPE, D_V], dim=-1)
48
+ k_full = torch.cat([k_nope, k_pe_roped.expand(-1, -1, H_KV, -1)], dim=-1)
49
+ return q_total, k_full, v
50
+
51
+
52
+ def _fused(q, kv_latent, k_pe, freqs_cis):
53
+ q_total = activation.fused_q_rope_inplace(q, freqs_cis, D_NOPE, D_ROPE)
54
+ # k_pe RoPE stays PyTorch native (head-shared, too small for custom kernel)
55
+ k_pe_roped = _reorder(_apply_rotary_emb_single(k_pe.unsqueeze(2), freqs_cis), D_ROPE)
56
+ k_full, v = activation.fused_kv_split_rope_cat(kv_latent, k_pe_roped, D_NOPE, D_V, D_ROPE)
57
+ return q_total, k_full, v
58
+
59
+
60
+ class _VanillaModule(nn.Module):
61
+ def forward(self, q, kv_latent, k_pe, freqs_cis):
62
+ return _vanilla(q, kv_latent, k_pe, freqs_cis)
63
+
64
+
65
+ class _FusedModule(nn.Module):
66
+ def forward(self, q, kv_latent, k_pe, freqs_cis):
67
+ return _fused(q, kv_latent, k_pe, freqs_cis)
68
+
69
+
70
+ class MLARoPE(DiffCase):
71
+ # Framework calls build_inputs(bs, sl, dim, dtype, eps) — dim unused.
72
+ def build_inputs(self, bs, sl, hidden, dtype, eps):
73
+ return {
74
+ "q": (torch.randn(bs, sl, H_Q, D_QK, dtype=dtype) * 0.5).requires_grad_(True),
75
+ "kv_latent": (torch.randn(bs, sl, H_KV, D_NOPE + D_V, dtype=dtype) * 0.5).requires_grad_(True),
76
+ "k_pe": (torch.randn(bs, sl, D_ROPE, dtype=dtype) * 0.5).requires_grad_(True),
77
+ "freqs_cis": _precompute_freqs_cis(D_ROPE, sl),
78
+ }
79
+
80
+ def make_naive(self, I):
81
+ return _VanillaModule()
82
+
83
+ def make_cuda(self, I):
84
+ return _FusedModule()
85
+
86
+ def forward(self, obj, I):
87
+ # fused_q_rope_inplace needs non-leaf q; wrap both paths for fairness
88
+ q_in = I["q"] * 1.0
89
+ kv_in = I["kv_latent"] * 1.0
90
+ kpe_in = I["k_pe"] * 1.0
91
+ return obj(q_in, kv_in, kpe_in, I["freqs_cis"])
92
+
93
+ def grad_inputs(self, I):
94
+ return [I["q"], I["kv_latent"], I["k_pe"]]
95
+
96
+
97
+ CASE = MLARoPE()
benchmarks/run_cases.py CHANGED
@@ -62,7 +62,8 @@ def main():
62
  ap = argparse.ArgumentParser()
63
  ap.add_argument(
64
  "--case",
65
- choices=["rms", "add_rms", "poly", "mul_poly", "grouped_mul_poly"],
 
66
  required=True)
67
  ap.add_argument("--plot", action="store_true")
68
  ap.add_argument(
@@ -95,12 +96,26 @@ def main():
95
  case: DiffCase = mod.CASE
96
 
97
  # Correctness checks across multiple configs
98
- for bs, sl, hid in [(2, 128, 4096), (8, 4096, 1280), (1, 32768, 1280)]:
 
 
 
 
 
 
 
 
 
99
  print(
100
  f"Checking correctness: bs={bs}, sl={sl}, D={hid} "
101
  f"(N={bs*sl})...",
102
  end=" ")
103
- calculate_diff(case, batch_size=bs, seq_len=sl, hidden_size=hid)
 
 
 
 
 
104
  print("✅")
105
 
106
  for dtype_name, dtype in dtypes:
@@ -109,6 +124,7 @@ def main():
109
  print(f"{'=' * 60}\n")
110
 
111
  save_dir = os.path.join(args.save_path, args.case, dtype_name)
 
112
  is_grouped = args.case == "grouped_mul_poly"
113
 
114
  if args.plot:
@@ -118,6 +134,8 @@ def main():
118
  dim = [1280]
119
  elif "poly" in args.case:
120
  dim = [8192, 16384]
 
 
121
  else:
122
  dim = [2048, 4096]
123
  configs = list(
@@ -170,6 +188,8 @@ def main():
170
  dim = [1280]
171
  elif "poly" in args.case:
172
  dim = [8192, 16384]
 
 
173
  else:
174
  dim = [2048, 4096]
175
  configs = list(
 
62
  ap = argparse.ArgumentParser()
63
  ap.add_argument(
64
  "--case",
65
+ choices=["rms", "add_rms", "poly", "mul_poly", "grouped_mul_poly",
66
+ "mla_rope"],
67
  required=True)
68
  ap.add_argument("--plot", action="store_true")
69
  ap.add_argument(
 
96
  case: DiffCase = mod.CASE
97
 
98
  # Correctness checks across multiple configs
99
+ # NOTE: calculate_diff positionally calls build_inputs(hidden_size, bs, sl);
100
+ # bench framework positionally calls build_inputs(bs, sl, dim). These
101
+ # disagree — rms-style cases don't care (all 3 axes are flat dims), but
102
+ # mla_rope does. We match the bench convention in cases/mla_rope.py, so
103
+ # we swap arg names at the correctness call site below for that case.
104
+ if args.case == "mla_rope":
105
+ cfgs = [(1, 1024, 0), (4, 4096, 0), (8, 4096, 0)] # (bs, sl, dummy)
106
+ else:
107
+ cfgs = [(2, 128, 4096), (8, 4096, 1280), (1, 32768, 1280)]
108
+ for bs, sl, hid in cfgs:
109
  print(
110
  f"Checking correctness: bs={bs}, sl={sl}, D={hid} "
111
  f"(N={bs*sl})...",
112
  end=" ")
113
+ if args.case == "mla_rope":
114
+ # Swap so positional (hidden_size, batch_size, seq_len) maps to
115
+ # our build_inputs(bs, sl, dim) as (bs, sl, dummy).
116
+ calculate_diff(case, batch_size=sl, seq_len=hid, hidden_size=bs)
117
+ else:
118
+ calculate_diff(case, batch_size=bs, seq_len=sl, hidden_size=hid)
119
  print("✅")
120
 
121
  for dtype_name, dtype in dtypes:
 
124
  print(f"{'=' * 60}\n")
125
 
126
  save_dir = os.path.join(args.save_path, args.case, dtype_name)
127
+ os.makedirs(save_dir, exist_ok=True)
128
  is_grouped = args.case == "grouped_mul_poly"
129
 
130
  if args.plot:
 
134
  dim = [1280]
135
  elif "poly" in args.case:
136
  dim = [8192, 16384]
137
+ elif args.case == "mla_rope":
138
+ dim = [0] # MLA head dims are fixed; dim axis is a dummy
139
  else:
140
  dim = [2048, 4096]
141
  configs = list(
 
188
  dim = [1280]
189
  elif "poly" in args.case:
190
  dim = [8192, 16384]
191
+ elif args.case == "mla_rope":
192
+ dim = [0] # MLA head dims are fixed; dim axis is a dummy
193
  else:
194
  dim = [2048, 4096]
195
  configs = list(