liangsu9988 commited on
Commit
a1d71b9
·
verified ·
1 Parent(s): 4b4283f

Promote latest kernel artifacts to main

Browse files
README.md CHANGED
@@ -1,9 +1,69 @@
1
- # flashrt/transformer-layout-primitives
2
 
3
- This repository is a compatibility mirror for older `kernels` clients
4
- that resolve repositories through the default Hugging Face model repo API.
5
 
6
- Canonical Kernel Hub repo: https://huggingface.co/kernels/flashrt/transformer-layout-primitives
7
 
8
- Do not edit this mirror by hand. It is generated from the Kernel Hub
9
- `vN` branches and contains the same `build/**` artifacts.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # transformer-layout-primitives
2
 
3
+ Generic BF16 layout, RoPE, and text-state primitives for transformer pipelines.
 
4
 
5
+ Hub repo: `flashrt/transformer-layout-primitives`
6
 
7
+ ## Public API
8
+
9
+ - `fill_neginf_bf16(dst) -> dst`
10
+ - `add_bias_bf16_(data, bias) -> data`
11
+ - `repeat_interleave_heads_bf16(src, repeat) -> bf16`
12
+ - `text_gather_bf16(src, batch, seq) -> bf16`
13
+ - `text_scatter_bf16(dst, src, batch, seq) -> dst`
14
+ - `rope_rotate_half_bf16_(x, cos, sin) -> x`
15
+ - `qk_rmsnorm_rope_bf16_(qk, weight, cos, sin, eps=1e-6) -> qk`
16
+ - `qk_pair_rmsnorm_rope_bf16(q, k, q_weight, k_weight, cos, sin, eps=1e-6) -> (q, k)`
17
+ - `gather_rows_bf16(src, row_indices, out=None) -> bf16`
18
+ - `scatter_rows_bf16(src, row_indices, rows, out=None) -> bf16`
19
+
20
+ The package is intentionally model-neutral. It exposes Tensor APIs for common
21
+ transformer integration gaps: head repeat for GQA/MQA, first/last token gather
22
+ and scatter, bias add, RoPE rotate-half, and fused Q/K RMSNorm+RoPE. The pair
23
+ API handles different Q and KV head counts in one launch.
24
+
25
+ ## Example
26
+
27
+ ```python
28
+ from kernels import get_kernel
29
+ import torch
30
+
31
+ ops = get_kernel("flashrt/transformer-layout-primitives", version=1)
32
+
33
+ q = torch.randn((128, 32, 128), device="cuda", dtype=torch.bfloat16)
34
+ weight = torch.ones((128,), device="cuda", dtype=torch.bfloat16)
35
+ cos = torch.randn((128, 128), device="cuda", dtype=torch.bfloat16)
36
+ sin = torch.randn((128, 128), device="cuda", dtype=torch.bfloat16)
37
+ ops.qk_rmsnorm_rope_bf16_(q, weight, cos, sin)
38
+
39
+ k = torch.randn((128, 8, 128), device="cuda", dtype=torch.bfloat16)
40
+ q, k = ops.qk_pair_rmsnorm_rope_bf16(
41
+ q, k, weight, weight, cos, sin
42
+ )
43
+ ```
44
+
45
+ ## Shape contract
46
+
47
+ - All tensors are contiguous CUDA BF16 tensors.
48
+ - `repeat_interleave_heads_bf16`: `src` is `(seq, heads, head_dim)`.
49
+ - `text_gather_bf16`: `src` is flattened `(batch * seq, dim)` and returns
50
+ first and last token rows as `(2 * batch, dim)`.
51
+ - `text_scatter_bf16`: writes `(2 * batch, dim)` rows back to first and last
52
+ positions in `(batch * seq, dim)`.
53
+ - RoPE functions use rotate-half layout with `cos/sin` shaped `(seq, head_dim)`
54
+ or `(rows, head_dim)`.
55
+ - `qk_pair_rmsnorm_rope_bf16` accepts Q `(rows, q_heads, head_dim)` and K
56
+ `(rows, kv_heads, head_dim)`. `head_dim` must be even and in `[8, 256]`.
57
+ Q and K may have different head counts.
58
+ - `gather_rows_bf16` and `scatter_rows_bf16` use contiguous CUDA int64 row
59
+ indices. Scatter indices must be unique.
60
+ - Indexed-row validation includes the Cosmos3-Edge production layout
61
+ `128 -> 60` rows with hidden size `2048`, including exact CUDA Graph replay.
62
+
63
+ ## Validation
64
+
65
+ Correctness is tested against PyTorch BF16/FP32 reference formulas with exact
66
+ checks for pure layout operations and strict BF16 tolerances for math ops. The
67
+ pair API must additionally match the two staged single-tensor kernels exactly.
68
+
69
+ See `benchmarks/RESULTS.md` for current local RTX 5090 source benchmark data.
tests/test_transformer_layout_primitives.py ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Correctness tests for transformer-layout-primitives."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import torch
13
+
14
+ ROOT = Path(__file__).resolve().parents[2]
15
+ PACKAGE = ROOT / "transformer-layout-primitives"
16
+ REGISTRATION_INCLUDE = ROOT.parent / "kernels" / "kernel-builder" / "src" / "pyproject" / "templates" / "torch"
17
+
18
+
19
+ class SourceOps:
20
+ def __init__(self, namespace: str) -> None:
21
+ self.ops = getattr(torch.ops, namespace)
22
+
23
+ def fill_neginf_bf16(self, dst):
24
+ self.ops.fill_neginf_bf16(dst)
25
+ return dst
26
+
27
+ def add_bias_bf16_(self, data, bias):
28
+ self.ops.add_bias_bf16_(data, bias)
29
+ return data
30
+
31
+ def repeat_interleave_heads_bf16(self, src, repeat, out=None):
32
+ if out is None:
33
+ out = torch.empty((src.shape[0], src.shape[1] * repeat, src.shape[2]), device=src.device, dtype=src.dtype)
34
+ self.ops.repeat_interleave_heads_bf16(src, int(repeat), out)
35
+ return out
36
+
37
+ def text_gather_bf16(self, src, batch, seq, out=None):
38
+ if out is None:
39
+ out = torch.empty((2 * batch, src.shape[1]), device=src.device, dtype=src.dtype)
40
+ self.ops.text_gather_bf16(src, int(batch), int(seq), out)
41
+ return out
42
+
43
+ def text_scatter_bf16(self, dst, src, batch, seq):
44
+ self.ops.text_scatter_bf16(dst, src, int(batch), int(seq))
45
+ return dst
46
+
47
+ def rope_rotate_half_bf16_(self, x, cos, sin):
48
+ self.ops.rope_rotate_half_bf16_(x, cos, sin)
49
+ return x
50
+
51
+ def qk_rmsnorm_rope_bf16_(self, qk, weight, cos, sin, eps=1e-6):
52
+ self.ops.qk_rmsnorm_rope_bf16_(qk, weight, cos, sin, float(eps))
53
+ return qk
54
+
55
+ def qk_pair_rmsnorm_rope_bf16(
56
+ self, q, k, q_weight, k_weight, cos, sin, eps=1e-6, q_out=None, k_out=None
57
+ ):
58
+ if q_out is None:
59
+ q_out = torch.empty_like(q)
60
+ if k_out is None:
61
+ k_out = torch.empty_like(k)
62
+ self.ops.qk_pair_rmsnorm_rope_bf16(
63
+ q, k, q_weight, k_weight, cos, sin, float(eps), q_out, k_out
64
+ )
65
+ return q_out, k_out
66
+
67
+ def gather_rows_bf16(self, src, row_indices, out=None):
68
+ if out is None:
69
+ out = torch.empty(
70
+ (row_indices.numel(), src.shape[1]), device=src.device, dtype=src.dtype
71
+ )
72
+ self.ops.gather_rows_bf16(src, row_indices, out)
73
+ return out
74
+
75
+ def scatter_rows_bf16(self, src, row_indices, destination_rows, out=None):
76
+ if out is None:
77
+ out = torch.zeros(
78
+ (destination_rows, src.shape[1]), device=src.device, dtype=src.dtype
79
+ )
80
+ self.ops.scatter_rows_bf16(src, row_indices, out)
81
+ return out
82
+
83
+
84
+ def _arch_list() -> str:
85
+ major, minor = torch.cuda.get_device_capability(0)
86
+ return "12.0a" if major >= 12 else f"{major}.{minor}"
87
+
88
+
89
+ def load_source_ops() -> SourceOps:
90
+ from torch.utils.cpp_extension import load
91
+
92
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _arch_list())
93
+ namespace = "transformer_layout_primitives_source_test"
94
+ load(
95
+ name=namespace,
96
+ sources=[
97
+ str(PACKAGE / "torch-ext" / "torch_binding.cpp"),
98
+ str(PACKAGE / "csrc" / "transformer_layout_primitives.cu"),
99
+ ],
100
+ extra_include_paths=[str(PACKAGE / "csrc"), str(REGISTRATION_INCLUDE)],
101
+ extra_cflags=["-O3", "-DCUDA_KERNEL"],
102
+ extra_cuda_cflags=[
103
+ "-O3",
104
+ "--expt-relaxed-constexpr",
105
+ "-DCUDA_KERNEL",
106
+ "-U__CUDA_NO_BFLOAT16_CONVERSIONS__",
107
+ "-U__CUDA_NO_BFLOAT16_OPERATORS__",
108
+ "-U__CUDA_NO_BFLOAT162_OPERATORS__",
109
+ ],
110
+ is_python_module=False,
111
+ verbose=False,
112
+ )
113
+ return SourceOps(namespace)
114
+
115
+
116
+ def load_installed_ops(artifact: str | None):
117
+ if artifact:
118
+ sys.path.insert(0, artifact)
119
+ try:
120
+ return importlib.import_module("transformer_layout_primitives")
121
+ finally:
122
+ if artifact:
123
+ sys.path.remove(artifact)
124
+
125
+
126
+ def rotate_half_ref(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
127
+ half = x.shape[-1] // 2
128
+ lo = x[..., :half].float()
129
+ hi = x[..., half:].float()
130
+ c = cos[:, None, :half].float()
131
+ s = sin[:, None, :half].float()
132
+ return torch.cat([lo * c - hi * s, hi * c + lo * s], dim=-1).to(torch.bfloat16)
133
+
134
+
135
+ def qk_rmsnorm_rope_ref(
136
+ qk: torch.Tensor,
137
+ weight: torch.Tensor,
138
+ cos: torch.Tensor,
139
+ sin: torch.Tensor,
140
+ eps: float = 1e-6,
141
+ ) -> torch.Tensor:
142
+ rms = torch.rsqrt((qk.float() * qk.float()).mean(dim=-1, keepdim=True) + eps)
143
+ normed = (qk.float() * rms * weight.float()).to(torch.bfloat16)
144
+ return rotate_half_ref(normed, cos, sin)
145
+
146
+
147
+ def qk_pair_rmsnorm_rope_ref(
148
+ q: torch.Tensor,
149
+ k: torch.Tensor,
150
+ q_weight: torch.Tensor,
151
+ k_weight: torch.Tensor,
152
+ cos: torch.Tensor,
153
+ sin: torch.Tensor,
154
+ eps: float = 1e-6,
155
+ ) -> tuple[torch.Tensor, torch.Tensor]:
156
+ return (
157
+ qk_rmsnorm_rope_ref(q, q_weight, cos, sin, eps),
158
+ qk_rmsnorm_rope_ref(k, k_weight, cos, sin, eps),
159
+ )
160
+
161
+
162
+ def metrics(got: torch.Tensor, ref: torch.Tensor) -> tuple[float, float, float]:
163
+ diff = (got.float() - ref.float()).abs()
164
+ cos = torch.nn.functional.cosine_similarity(got.float().flatten(), ref.float().flatten(), dim=0).item()
165
+ return float(diff.max().item()), float(diff.mean().item()), float(cos)
166
+
167
+
168
+ def bf16_max_ulp(got: torch.Tensor, ref: torch.Tensor) -> int:
169
+ def ordered_bits(value: torch.Tensor) -> torch.Tensor:
170
+ bits = value.contiguous().view(torch.int16).to(torch.int32) & 0xFFFF
171
+ magnitude = bits & 0x7FFF
172
+ return torch.where(
173
+ (bits & 0x8000) != 0,
174
+ 0x8000 - magnitude,
175
+ 0x8000 + magnitude,
176
+ )
177
+
178
+ return int(
179
+ (ordered_bits(got) - ordered_bits(ref)).abs().max().item()
180
+ )
181
+
182
+
183
+ def assert_close(
184
+ name: str,
185
+ got: torch.Tensor,
186
+ ref: torch.Tensor,
187
+ atol: float,
188
+ cos_min: float,
189
+ rtol: float = 0.0,
190
+ ) -> None:
191
+ max_abs, mean_abs, cos = metrics(got, ref)
192
+ diff = (got.float() - ref.float()).abs()
193
+ p99_abs = float(torch.quantile(diff, 0.99).item())
194
+ tolerance = atol + rtol * ref.float().abs()
195
+ violations = int((diff > tolerance).sum().item())
196
+ max_ulp = bf16_max_ulp(got, ref)
197
+ print(
198
+ f"{name}: max_abs={max_abs:.6f} p99_abs={p99_abs:.6f} "
199
+ f"mean_abs={mean_abs:.6e} max_ulp={max_ulp} cosine={cos:.8f} "
200
+ f"violations={violations} rtol={rtol} atol={atol}"
201
+ )
202
+ if violations or cos < cos_min:
203
+ raise AssertionError(
204
+ f"{name} failed: max_abs={max_abs} p99_abs={p99_abs} "
205
+ f"max_ulp={max_ulp} cosine={cos} violations={violations}"
206
+ )
207
+
208
+
209
+ def expect_runtime_error(name: str, fn) -> None:
210
+ try:
211
+ fn()
212
+ except RuntimeError:
213
+ print(f"{name}: rejected")
214
+ return
215
+ raise AssertionError(f"{name}: expected RuntimeError")
216
+
217
+
218
+ def run(ops, mode: str) -> int:
219
+ torch.manual_seed(31)
220
+ count = 0
221
+ layout_shapes = [(2, 5, 128), (3, 49, 256)] if mode == "smoke" else [
222
+ (1, 1, 128),
223
+ (2, 5, 128),
224
+ (3, 49, 256),
225
+ (4, 256, 1024),
226
+ (2, 2520, 2048),
227
+ ]
228
+ for batch, seq, dim in layout_shapes:
229
+ x = torch.randn((batch * seq, dim), device="cuda", dtype=torch.bfloat16)
230
+
231
+ dst = torch.empty_like(x)
232
+ ops.fill_neginf_bf16(dst)
233
+ ref_neginf = torch.full_like(x, -1e30)
234
+ torch.testing.assert_close(dst.float().cpu(), ref_neginf.float().cpu(), rtol=0, atol=0)
235
+ count += 1
236
+
237
+ bias = torch.randn((dim,), device="cuda", dtype=torch.bfloat16)
238
+ got = x.clone()
239
+ ops.add_bias_bf16_(got, bias)
240
+ ref = (x.float() + bias.float()).to(torch.bfloat16)
241
+ torch.testing.assert_close(got.cpu(), ref.cpu(), rtol=0, atol=0)
242
+ count += 1
243
+
244
+ gathered = ops.text_gather_bf16(x, batch, seq)
245
+ ref_gather = torch.stack([x[b * seq + offset] for b in range(batch) for offset in (0, seq - 1)], dim=0)
246
+ torch.testing.assert_close(gathered.cpu(), ref_gather.cpu(), rtol=0, atol=0)
247
+ count += 1
248
+
249
+ scattered = torch.zeros_like(x)
250
+ ops.text_scatter_bf16(scattered, gathered, batch, seq)
251
+ ref_scatter = torch.zeros_like(x)
252
+ for b in range(batch):
253
+ ref_scatter[b * seq] = gathered[2 * b]
254
+ ref_scatter[b * seq + seq - 1] = gathered[2 * b + 1]
255
+ torch.testing.assert_close(scattered.cpu(), ref_scatter.cpu(), rtol=0, atol=0)
256
+ count += 1
257
+
258
+ cpu_rng_state = torch.random.get_rng_state()
259
+ cuda_rng_state = torch.cuda.get_rng_state()
260
+ indexed_shapes = [(17, 5, 64), (277, 51, 128)] if mode == "smoke" else [
261
+ (1, 1, 8),
262
+ (17, 5, 64),
263
+ (277, 51, 128),
264
+ (128, 60, 2048),
265
+ (2520, 105, 1152),
266
+ (5070, 257, 4096),
267
+ ]
268
+ for source_rows, selected_rows, hidden in indexed_shapes:
269
+ src = torch.randn(
270
+ (source_rows, hidden), device="cuda", dtype=torch.bfloat16
271
+ )
272
+ indices = torch.randperm(source_rows, device="cuda", dtype=torch.int64)[
273
+ :selected_rows
274
+ ].contiguous()
275
+ got = ops.gather_rows_bf16(src, indices)
276
+ ref = src.index_select(0, indices)
277
+ torch.testing.assert_close(got, ref, rtol=0, atol=0)
278
+ count += 1
279
+
280
+ destination_rows = source_rows + 3
281
+ got = ops.scatter_rows_bf16(ref, indices, destination_rows)
282
+ ref_scatter = torch.zeros(
283
+ (destination_rows, hidden), device="cuda", dtype=torch.bfloat16
284
+ )
285
+ ref_scatter.index_copy_(0, indices, ref)
286
+ torch.testing.assert_close(got, ref_scatter, rtol=0, atol=0)
287
+ count += 1
288
+ torch.random.set_rng_state(cpu_rng_state)
289
+ torch.cuda.set_rng_state(cuda_rng_state)
290
+
291
+ repeat_shapes = [(17, 4, 64, 2), (128, 8, 128, 4)] if mode == "smoke" else [
292
+ (1, 1, 64, 8),
293
+ (17, 4, 64, 2),
294
+ (128, 8, 128, 4),
295
+ (2520, 8, 128, 4),
296
+ ]
297
+ for seq, heads, dim, repeat in repeat_shapes:
298
+ src = torch.randn((seq, heads, dim), device="cuda", dtype=torch.bfloat16)
299
+ got = ops.repeat_interleave_heads_bf16(src, repeat)
300
+ ref = src.repeat_interleave(repeat, dim=1)
301
+ torch.testing.assert_close(got.cpu(), ref.cpu(), rtol=0, atol=0)
302
+ count += 1
303
+
304
+ rope_shapes = [(17, 4, 64), (128, 8, 128)] if mode == "smoke" else [
305
+ (1, 1, 64),
306
+ (17, 4, 64),
307
+ (128, 8, 128),
308
+ (2520, 32, 128),
309
+ ]
310
+ for seq, heads, dim in rope_shapes:
311
+ x = torch.randn((seq, heads, dim), device="cuda", dtype=torch.bfloat16)
312
+ cos = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16)
313
+ sin = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16)
314
+ got = x.clone()
315
+ ops.rope_rotate_half_bf16_(got, cos, sin)
316
+ ref = rotate_half_ref(x, cos, sin)
317
+ assert_close(f"rope seq={seq} heads={heads} dim={dim}", got, ref, atol=0, cos_min=0.999999)
318
+ count += 1
319
+
320
+ weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16)
321
+ got = x.clone()
322
+ ops.qk_rmsnorm_rope_bf16_(got, weight, cos, sin)
323
+ ref = qk_rmsnorm_rope_ref(x, weight, cos, sin)
324
+ # The CUDA kernel is bitwise-gated against the established staged
325
+ # native path below. PyTorch eager may use a different FP32 reduction
326
+ # order, so this semantic comparison uses BF16 elementwise tolerances.
327
+ assert_close(
328
+ f"qk_rmsnorm_rope seq={seq} heads={heads} dim={dim}",
329
+ got,
330
+ ref,
331
+ atol=0.015625,
332
+ rtol=0.02,
333
+ cos_min=0.999999,
334
+ )
335
+ count += 1
336
+
337
+ pair_shapes = [(17, 16, 8, 128), (49, 16, 16, 72)] if mode == "smoke" else [
338
+ (1, 16, 8, 128),
339
+ (17, 16, 8, 128),
340
+ (49, 16, 16, 72),
341
+ (51, 16, 16, 80),
342
+ (65, 32, 8, 128),
343
+ (277, 16, 8, 128),
344
+ (512, 24, 24, 128),
345
+ (2520, 24, 24, 128),
346
+ (5070, 24, 24, 128),
347
+ ]
348
+ for rows, q_heads, k_heads, dim in pair_shapes:
349
+ q = torch.randn((rows, q_heads, dim), device="cuda", dtype=torch.bfloat16)
350
+ k = torch.randn((rows, k_heads, dim), device="cuda", dtype=torch.bfloat16)
351
+ q_weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16)
352
+ k_weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16)
353
+ angles = torch.randn((rows, dim // 2), device="cuda", dtype=torch.float32)
354
+ cos_half = angles.cos().to(torch.bfloat16)
355
+ sin_half = angles.sin().to(torch.bfloat16)
356
+ cos = torch.cat((cos_half, cos_half), dim=-1)
357
+ sin = torch.cat((sin_half, sin_half), dim=-1)
358
+ got_q, got_k = ops.qk_pair_rmsnorm_rope_bf16(
359
+ q, k, q_weight, k_weight, cos, sin
360
+ )
361
+ staged_q = q.clone()
362
+ staged_k = k.clone()
363
+ ops.qk_rmsnorm_rope_bf16_(staged_q, q_weight, cos, sin)
364
+ ops.qk_rmsnorm_rope_bf16_(staged_k, k_weight, cos, sin)
365
+ torch.testing.assert_close(got_q, staged_q, rtol=0, atol=0)
366
+ torch.testing.assert_close(got_k, staged_k, rtol=0, atol=0)
367
+ ref_q, ref_k = qk_pair_rmsnorm_rope_ref(
368
+ q, k, q_weight, k_weight, cos, sin
369
+ )
370
+ label = f"qk_pair rows={rows} qh={q_heads} kh={k_heads} dim={dim}"
371
+ # The fused path is already required to be bitwise equal to the
372
+ # established staged native kernels above. Torch 2.11's eager
373
+ # reduction order can differ by up to 0.03125 in BF16 at large rows.
374
+ assert_close(
375
+ f"{label}/q", got_q, ref_q, atol=0.015625, rtol=0.02, cos_min=0.999999
376
+ )
377
+ assert_close(
378
+ f"{label}/k", got_k, ref_k, atol=0.015625, rtol=0.02, cos_min=0.999999
379
+ )
380
+ count += 4
381
+
382
+ q = torch.randn((17, 4, 64), device="cuda", dtype=torch.bfloat16)
383
+ k = torch.randn((17, 2, 64), device="cuda", dtype=torch.bfloat16)
384
+ weight = torch.ones((64,), device="cuda", dtype=torch.bfloat16)
385
+ cos = torch.ones((17, 64), device="cuda", dtype=torch.bfloat16)
386
+ sin = torch.zeros_like(cos)
387
+ expect_runtime_error(
388
+ "qk_pair mismatched rows",
389
+ lambda: ops.qk_pair_rmsnorm_rope_bf16(
390
+ q, k[:-1].contiguous(), weight, weight, cos, sin
391
+ ),
392
+ )
393
+ expect_runtime_error(
394
+ "qk_pair invalid head_dim",
395
+ lambda: ops.qk_pair_rmsnorm_rope_bf16(
396
+ q[:, :, :-1].contiguous(),
397
+ k[:, :, :-1].contiguous(),
398
+ weight[:-1].contiguous(),
399
+ weight[:-1].contiguous(),
400
+ cos[:, :-1].contiguous(),
401
+ sin[:, :-1].contiguous(),
402
+ ),
403
+ )
404
+ expect_runtime_error(
405
+ "qk_pair noncontiguous",
406
+ lambda: ops.qk_pair_rmsnorm_rope_bf16(
407
+ q.transpose(0, 1), k, weight, weight, cos, sin
408
+ ),
409
+ )
410
+ count += 3
411
+
412
+ return count
413
+
414
+
415
+ def run_compile_default_eps(ops) -> int:
416
+ seq, heads, dim = 17, 4, 64
417
+ x = torch.randn((seq, heads, dim), device="cuda", dtype=torch.bfloat16)
418
+ weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16)
419
+ cos = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16)
420
+ sin = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16)
421
+ ref = qk_rmsnorm_rope_ref(x, weight, cos, sin)
422
+
423
+ def invoke(qk, rms_weight, rope_cos, rope_sin):
424
+ return ops.qk_rmsnorm_rope_bf16_(qk, rms_weight, rope_cos, rope_sin)
425
+
426
+ compiled = torch.compile(invoke, fullgraph=True)
427
+ got = x.clone()
428
+ compiled(got, weight, cos, sin)
429
+ assert_close("qk_rmsnorm_rope compile default eps", got, ref, atol=0.015625, cos_min=0.999999)
430
+
431
+ k = torch.randn((seq, 2, dim), device="cuda", dtype=torch.bfloat16)
432
+ k_weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16)
433
+ pair_ref = qk_pair_rmsnorm_rope_ref(x, k, weight, k_weight, cos, sin)
434
+
435
+ def invoke_pair(q, key, qw, kw, rope_cos, rope_sin):
436
+ return ops.qk_pair_rmsnorm_rope_bf16(q, key, qw, kw, rope_cos, rope_sin)
437
+
438
+ compiled_pair = torch.compile(invoke_pair, fullgraph=True)
439
+ got_q, got_k = compiled_pair(x, k, weight, k_weight, cos, sin)
440
+ assert_close("qk_pair compile default eps/q", got_q, pair_ref[0], atol=0.015625, cos_min=0.999999)
441
+ assert_close("qk_pair compile default eps/k", got_k, pair_ref[1], atol=0.015625, cos_min=0.999999)
442
+ return 3
443
+
444
+
445
+ def run_indexed_compile(ops) -> int:
446
+ src = torch.randn((277, 128), device="cuda", dtype=torch.bfloat16)
447
+ indices = torch.randperm(277, device="cuda", dtype=torch.int64)[:51].contiguous()
448
+
449
+ def invoke(src, indices):
450
+ gathered = ops.gather_rows_bf16(src, indices)
451
+ return ops.scatter_rows_bf16(gathered, indices, src.shape[0])
452
+
453
+ eager = invoke(src, indices)
454
+ compiled = torch.compile(invoke, fullgraph=True)(src, indices)
455
+ torch.testing.assert_close(compiled, eager, rtol=0, atol=0)
456
+ print("PASS gather/scatter torch.compile fullgraph")
457
+ return 1
458
+
459
+
460
+ def run_cosmos_edge_indexed_graph(ops) -> int:
461
+ source_rows, selected_rows, hidden = 128, 60, 2048
462
+ src = torch.randn((source_rows, hidden), device="cuda", dtype=torch.bfloat16)
463
+ indices = torch.randperm(
464
+ source_rows, device="cuda", dtype=torch.int64
465
+ )[:selected_rows].contiguous()
466
+ gathered = torch.empty(
467
+ (selected_rows, hidden), device="cuda", dtype=torch.bfloat16
468
+ )
469
+ scattered = torch.zeros(
470
+ (source_rows, hidden), device="cuda", dtype=torch.bfloat16
471
+ )
472
+
473
+ graph = torch.cuda.CUDAGraph()
474
+ torch.cuda.synchronize()
475
+ with torch.cuda.graph(graph):
476
+ ops.gather_rows_bf16(src, indices, out=gathered)
477
+ ops.scatter_rows_bf16(gathered, indices, source_rows, out=scattered)
478
+ graph.replay()
479
+ torch.cuda.synchronize()
480
+ expected_gathered = src.index_select(0, indices)
481
+ expected_scattered = torch.zeros_like(scattered)
482
+ expected_scattered.index_copy_(0, indices, expected_gathered)
483
+ torch.testing.assert_close(gathered, expected_gathered, rtol=0.0, atol=0.0)
484
+ torch.testing.assert_close(scattered, expected_scattered, rtol=0.0, atol=0.0)
485
+ first = (gathered.clone(), scattered.clone())
486
+ graph.replay()
487
+ torch.cuda.synchronize()
488
+ torch.testing.assert_close(gathered, first[0], rtol=0.0, atol=0.0)
489
+ torch.testing.assert_close(scattered, first[1], rtol=0.0, atol=0.0)
490
+ print("PASS Cosmos3-Edge gather/scatter CUDA Graph replay")
491
+ return 2
492
+
493
+
494
+ def main() -> int:
495
+ parser = argparse.ArgumentParser()
496
+ parser.add_argument("--backend", choices=["source", "installed"], default="source")
497
+ parser.add_argument("--artifact", default=None)
498
+ parser.add_argument("--mode", choices=["smoke", "full"], default="smoke")
499
+ args = parser.parse_args()
500
+ ops = load_source_ops() if args.backend == "source" else load_installed_ops(args.artifact)
501
+ count = run(ops, args.mode)
502
+ count += run_indexed_compile(ops)
503
+ if args.mode == "full":
504
+ count += run_cosmos_edge_indexed_graph(ops)
505
+ if args.backend == "installed":
506
+ count += run_compile_default_eps(ops)
507
+ print(f"transformer-layout-primitives {args.backend} {args.mode}: passed {count}/{count}")
508
+ return 0
509
+
510
+
511
+ if __name__ == "__main__":
512
+ raise SystemExit(main())