Skip to content

Commit 40b8c30

Browse files
committed
add syrk
1 parent 0f4e1ee commit 40b8c30

5 files changed

Lines changed: 477 additions & 1 deletion

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
from .syrk import *
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# type: ignore
16+
import torch
17+
import triton
18+
import triton.language as tl
19+
20+
try:
21+
from triton.tools.tensor_descriptor import TensorDescriptor
22+
except ImportError:
23+
raise ImportError(
24+
f"Triton version ({triton.__version__}) doesn't support tensor descriptor API. Minimum required version is 3.4.0."
25+
)
26+
27+
28+
__all__ = ["ssyrk", "tsyrk_ex"]
29+
30+
31+
@triton.jit
32+
def cvt_tf32_rn(x: tl.tensor) -> tl.tensor:
33+
return tl.inline_asm_elementwise("cvt.rna.tf32.f32 $0, $1;", "=r, r", [x], dtype=tl.float32, is_pure=True, pack=1)
34+
35+
36+
@triton.autotune(
37+
configs=[
38+
triton.Config({"TILE_N": tn, "TILE_K": tk}, num_warps=nw, num_stages=ns)
39+
for tn in (64, 128)
40+
for tk in (16, 32, 64)
41+
for nw in (4, 8)
42+
for ns in (3, 4)
43+
],
44+
key=["N", "K", "ALLOW_TF32"],
45+
)
46+
@triton.jit
47+
def syrk_op_n_simple_kernel(
48+
c_ptr,
49+
a_ptr,
50+
N: tl.constexpr,
51+
K: tl.constexpr,
52+
STRIDE_N: tl.constexpr,
53+
STRIDE_K: tl.constexpr,
54+
ALLOW_TF32: tl.constexpr,
55+
TILE_N: tl.constexpr,
56+
TILE_K: tl.constexpr,
57+
):
58+
# receives tensor of shape (N, K)
59+
# computes A * A^T (-> produces NxN)
60+
61+
pid_row = tl.program_id(0)
62+
pid_col = tl.program_id(1)
63+
64+
IS_BELOW_DIAG = pid_row < pid_col
65+
IS_ABOVE_DIAG = pid_row > pid_col
66+
67+
if IS_ABOVE_DIAG:
68+
return
69+
70+
offs_row = pid_row * TILE_N + tl.arange(0, TILE_N)
71+
offs_col = pid_col * TILE_N + tl.arange(0, TILE_N)
72+
offs_k = tl.arange(0, TILE_K)
73+
74+
mask_row = offs_row < N
75+
mask_col = offs_col < N
76+
77+
a_ptrs_x = a_ptr + offs_row[:, None] * STRIDE_N + offs_k[None, :] * STRIDE_K
78+
a_ptrs_y = a_ptr + offs_col[None, :] * STRIDE_N + offs_k[:, None] * STRIDE_K
79+
80+
acc = tl.zeros((TILE_N, TILE_N), dtype=tl.float32)
81+
82+
num_tiles_k = tl.cdiv(K, TILE_K)
83+
for k in range(0, num_tiles_k):
84+
mask_k = offs_k < K - k * TILE_K
85+
mask_x = mask_row[:, None] & mask_k[None, :]
86+
mask_y = mask_col[None, :] & mask_k[:, None]
87+
x = tl.load(a_ptrs_x, mask=mask_x, other=0.0)
88+
y = tl.load(a_ptrs_y, mask=mask_y, other=0.0)
89+
90+
if ALLOW_TF32 == 0:
91+
acc = tl.dot(x, y, acc=acc, input_precision="ieee")
92+
elif ALLOW_TF32 == 1:
93+
x = cvt_tf32_rn(x)
94+
y = cvt_tf32_rn(y)
95+
acc = tl.dot(x, y, acc=acc, input_precision="tf32")
96+
else:
97+
tl.static_assert(False, "Unsupported precision.")
98+
99+
a_ptrs_x += TILE_K * STRIDE_K
100+
a_ptrs_y += TILE_K * STRIDE_K
101+
102+
# store diagonal or below diagonal values
103+
c_ptrs = c_ptr + offs_row[:, None] * N + offs_col[None, :]
104+
mask_c = mask_row[:, None] & mask_col[None, :]
105+
tl.store(c_ptrs, acc, mask=mask_c)
106+
107+
# store replicated values above diagonal
108+
if IS_BELOW_DIAG:
109+
c_ptrs_diag = c_ptr + offs_col[None, :] * N + offs_row[:, None]
110+
tl.store(c_ptrs_diag, acc, mask=mask_c)
111+
112+
113+
def ssyrk(a: torch.Tensor, trans: bool = False) -> torch.Tensor:
114+
"""Triton implementation of BLAS ssyrk operation.
115+
116+
Note:
117+
This function assumes row major layout of the input tensor.
118+
119+
TODO(mstadler): Add support for alpha, beta and c.
120+
121+
Args:
122+
a: Input tensor of shape (N, K) or (K, N)
123+
trans: Whether to compute A * A^T (trans=False) or A^T * A (trans=True)
124+
125+
Returns:
126+
Output tensor of shape (N, N)
127+
"""
128+
assert a.dim() == 2, "Input tensor must be 2D"
129+
N, K = a.shape
130+
if trans:
131+
raise NotImplementedError("Transpose is not supported yet.")
132+
133+
STRIDE_N = a.stride(0)
134+
STRIDE_K = a.stride(1)
135+
136+
if (fp32_matmul_prec := torch.get_float32_matmul_precision()) == "highest":
137+
ALLOW_TF32 = 0
138+
elif fp32_matmul_prec == "high":
139+
ALLOW_TF32 = 1
140+
else:
141+
raise ValueError(f"Unsupported precision {fp32_matmul_prec}, only 'highest' and 'high' are supported.")
142+
143+
c = torch.empty((N, N), dtype=a.dtype, device=a.device)
144+
145+
def grid(META):
146+
return (triton.cdiv(N, META["TILE_N"]), triton.cdiv(N, META["TILE_N"]))
147+
148+
if not trans:
149+
syrk_op_n_simple_kernel[grid](c, a, N, K, STRIDE_N, STRIDE_K, ALLOW_TF32)
150+
151+
return c
152+
153+
154+
def prune_invalid_configs(configs: list[triton.Config], named_args: dict, **kwargs) -> list[triton.Config]:
155+
"""Prune invalid Triton kernel configs based on input size and tile parameters.
156+
157+
Args:
158+
configs: List of Triton kernel configs.
159+
named_args: Named arguments for the kernel.
160+
**kwargs: Additional keyword arguments.
161+
162+
Returns:
163+
List of valid Triton kernel configs.
164+
"""
165+
N = named_args["N"]
166+
167+
conf = []
168+
for c in configs:
169+
TILE_M = c.kwargs.get("TILE_M", 0)
170+
TILE_N = c.kwargs.get("TILE_N", 0)
171+
TILE_K = c.kwargs.get("TILE_K", 0)
172+
num_warps = c.num_warps
173+
174+
# 5000 is an empirically determined threshold from size shmoo to select the best config
175+
if N >= 5000:
176+
if TILE_M == 128 and TILE_N == 256 and TILE_K == 64:
177+
conf.append(c)
178+
else:
179+
if TILE_M <= 128 and TILE_N >= TILE_M and TILE_K <= 128:
180+
conf.append(c)
181+
return conf
182+
183+
184+
def matmul_tma_set_block_size_hook(nargs: dict) -> None:
185+
"""Sets the block shapes for tensor descriptors based on tile sizes.
186+
187+
Args:
188+
nargs: Named arguments for the kernel.
189+
"""
190+
TILE_M = nargs["TILE_M"]
191+
TILE_N = nargs["TILE_N"]
192+
TILE_K = nargs["TILE_K"]
193+
TRANS = nargs["TRANS"]
194+
GROUP_SIZE_M = nargs["GROUP_SIZE_M"]
195+
nargs["a_desc"].block_shape = [TILE_K, TILE_M] if TRANS else [TILE_M, TILE_K]
196+
nargs["a_t_desc"].block_shape = [TILE_K, TILE_N] if TRANS else [TILE_N, TILE_K]
197+
if nargs["c_desc"] is not None:
198+
nargs["c_desc"].block_shape = [TILE_M, TILE_N]
199+
nargs["d_desc"].block_shape = [TILE_M, TILE_N]
200+
nargs["d_t_desc"].block_shape = [TILE_N, TILE_M]
201+
202+
203+
@triton.autotune(
204+
configs=[
205+
triton.Config(
206+
{"TILE_M": tm, "TILE_N": tn, "TILE_K": tk, "GROUP_SIZE_M": gm},
207+
num_warps=nw,
208+
num_stages=ns,
209+
num_ctas=nc,
210+
pre_hook=matmul_tma_set_block_size_hook,
211+
)
212+
for tm in (64, 128, 256)
213+
for tn in (64, 128, 256)
214+
for tk in (64, 128, 256)
215+
for gm in (2, 4, 8)
216+
for nw in (4, 8)
217+
for ns in (2, 3, 4)
218+
for nc in (1,)
219+
],
220+
key=["N", "K", "TRANS", "WARP_SPECIALIZE"],
221+
prune_configs_by={"early_config_prune": prune_invalid_configs},
222+
)
223+
@triton.jit
224+
def syrk_kernel_bf16(
225+
d_desc,
226+
d_t_desc,
227+
a_desc,
228+
a_t_desc,
229+
c_desc,
230+
alpha: tl.constexpr,
231+
beta: tl.constexpr,
232+
SKIP_UPPER_TRIANGLE: tl.constexpr,
233+
TRANS: tl.constexpr,
234+
N: tl.constexpr,
235+
K: tl.constexpr,
236+
TILE_M: tl.constexpr,
237+
TILE_N: tl.constexpr,
238+
TILE_K: tl.constexpr,
239+
GROUP_SIZE_M: tl.constexpr,
240+
WARP_SPECIALIZE: tl.constexpr,
241+
):
242+
# input A tensor of shape (N, K)
243+
# computes D = alpha * A * A^T + beta * C (-> produces NxN)
244+
# NOTE: If beta != 0, then C must be a symmetric matrix (i.e., C == C^T)
245+
246+
pid = tl.program_id(axis=0)
247+
num_pid_m = tl.cdiv(N, TILE_M)
248+
num_pid_n = tl.cdiv(N, TILE_N)
249+
num_pid_in_group = GROUP_SIZE_M * num_pid_n
250+
group_id = pid // num_pid_in_group
251+
first_pid_m = group_id * GROUP_SIZE_M
252+
group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
253+
pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
254+
pid_n = (pid % num_pid_in_group) // group_size_m
255+
256+
IS_BELOW_DIAG = pid_m * TILE_M >= pid_n * TILE_N + TILE_N
257+
IS_ABOVE_DIAG = pid_m * TILE_M + TILE_M <= pid_n * TILE_N
258+
IS_SQUARE_TILE = TILE_M == TILE_N
259+
260+
if IS_ABOVE_DIAG:
261+
return
262+
263+
# hints for the compiler
264+
tl.assume(pid_m >= 0)
265+
tl.assume(pid_n >= 0)
266+
267+
offs_row = pid_m * TILE_M
268+
offs_col = pid_n * TILE_N
269+
270+
acc = tl.zeros((TILE_M, TILE_N), dtype=tl.float32)
271+
272+
num_tiles_k = tl.cdiv(K, TILE_K)
273+
for k in tl.range(num_tiles_k, warp_specialize=WARP_SPECIALIZE):
274+
offs_k = k * TILE_K
275+
if TRANS:
276+
x = a_desc.load([offs_k, offs_row])
277+
y = a_t_desc.load([offs_k, offs_col])
278+
acc = tl.dot(x.T, y, acc=acc)
279+
else:
280+
x = a_desc.load([offs_row, offs_k])
281+
y = a_t_desc.load([offs_col, offs_k])
282+
acc = tl.dot(x, y.T, acc=acc)
283+
284+
if alpha != 1.0:
285+
acc = alpha * acc
286+
if beta != 0.0:
287+
z = c_desc.load([offs_row, offs_col]).to(tl.float32)
288+
acc = beta * z + acc
289+
290+
d = acc.to(tl.bfloat16)
291+
292+
offs_row = pid_m * TILE_M
293+
offs_col = pid_n * TILE_N
294+
d_desc.store([offs_row, offs_col], d)
295+
296+
# store replicated values above diagonal. if skip_upper_triangle is True, we only store the values below the diagonal.
297+
if (IS_SQUARE_TILE and IS_BELOW_DIAG) or (not IS_SQUARE_TILE and not IS_ABOVE_DIAG):
298+
if not SKIP_UPPER_TRIANGLE:
299+
d_t_desc.store([offs_col, offs_row], d.T)
300+
301+
302+
def tsyrk_ex(
303+
a: torch.Tensor, c: torch.Tensor = None, alpha: float = 1.0, beta: float = 0.0, skip_upper_triangle: bool = False
304+
) -> torch.Tensor:
305+
"""Triton implementation of bf16 syrk operation, following cuBLAS naming conventions with 't' denoting bf16.
306+
307+
Note:
308+
If beta != 0, then a must be a symmetric matrix (i.e., a == a.T)
309+
310+
Args:
311+
a: Input tensor of shape (N, K)
312+
c: None or symmetric input tensor of shape (N, N)
313+
alpha: Scaling factor for the matrix multiplication
314+
beta: Scaling factor for the matrix addition
315+
skip_upper_triangle: Whether to skip the upper triangle part of the output
316+
317+
Returns:
318+
Output tensor of shape (N, N)
319+
"""
320+
321+
assert a.dim() == 2, "Input tensor must be 2D"
322+
assert a.is_contiguous() or a.T.is_contiguous(), "invalid input tensor layout. a or a.T must be contiguous."
323+
324+
N, K = a.shape
325+
assert (c is None and beta == 0.0) or (
326+
c is not None and c.shape == (N, N)
327+
), "if c is provided, c must be of shape (N, N)"
328+
assert c is None or c.is_contiguous() or c.T.is_contiguous(), "if c is provided, c or c.T must be contiguous"
329+
330+
d = torch.empty((N, N), device=a.device, dtype=a.dtype)
331+
332+
dummy_block = [1, 1]
333+
334+
is_trans = a.T.is_contiguous()
335+
336+
if is_trans:
337+
# the descriptor relys on contiguous tensor to load the data
338+
a = a.T
339+
# descriptor to load [TILE_M, TILE_K] from a
340+
a_desc = TensorDescriptor(a, a.shape, a.stride(), dummy_block)
341+
# descriptor to load [TILE_K, TILE_N] from a.T
342+
a_t_desc = TensorDescriptor(a, a.shape, a.stride(), dummy_block)
343+
# descriptor to store [TILE_M, TILE_N] to d
344+
d_desc = TensorDescriptor(d, d.shape, d.stride(), dummy_block)
345+
# descriptor to store [TILE_M, TILE_N] to d.T
346+
d_t_desc = TensorDescriptor(d, d.shape, d.stride(), dummy_block)
347+
348+
if beta != 0.0:
349+
c = c.T if c.T.is_contiguous() else c
350+
# descriptor to load [TILE_M, TILE_N] from a
351+
c_desc = TensorDescriptor(c, c.shape, c.stride(), dummy_block)
352+
else:
353+
c_desc = None
354+
355+
def grid(META):
356+
return (triton.cdiv(N, META["TILE_M"]) * triton.cdiv(N, META["TILE_N"]),)
357+
358+
syrk_kernel_bf16[grid](
359+
d_desc,
360+
d_t_desc,
361+
a_desc,
362+
a_t_desc,
363+
c_desc,
364+
alpha,
365+
beta,
366+
skip_upper_triangle,
367+
is_trans,
368+
N,
369+
K,
370+
WARP_SPECIALIZE=False,
371+
)
372+
return d

tests/ci/L0_Tests_GPU.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ coverage run -p --source=emerging_optimizers tests/test_soap_utils.py
2121
coverage run -p --source=emerging_optimizers tests/soap_smoke_test.py
2222
coverage run -p --source=emerging_optimizers tests/soap_mnist_test.py
2323
coverage run -p --source=emerging_optimizers tests/test_scalar_optimizers.py --device=cuda
24-
coverage run -p --source=emerging_optimizers tests/test_spectral_clipping_utils.py
24+
coverage run -p --source=emerging_optimizers tests/test_spectral_clipping_utils.py
25+
coverage run -p --source=emerging_optimizers tests/test_triton_kernels.py TritonKernelsIntegerInputTest

0 commit comments

Comments
 (0)