Skip to content

Commit 3ad235e

Browse files
authored
[KernelGen][MThreads] Add permute_copy Moore Threads specialized operator (#117)
1 parent d0b81cf commit 3ad235e

2 files changed

Lines changed: 298 additions & 0 deletions

File tree

src/flag_gems/runtime/backend/_mthreads/ops/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from .one_hot import one_hot
4444
from .ones import ones
4545
from .ones_like import ones_like
46+
from .permute_copy import permute_copy
4647
from .prod import prod, prod_dim
4748
from .rand import rand
4849
from .rand_like import rand_like
@@ -107,6 +108,7 @@
107108
"one_hot",
108109
"ones",
109110
"ones_like",
111+
"permute_copy",
110112
"prod",
111113
"prod_dim",
112114
"rand",
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
from typing import Tuple
17+
18+
import torch
19+
import triton
20+
import triton.language as tl
21+
22+
from flag_gems.ops.permute_copy import permute_copy as default_permute_copy
23+
from flag_gems.runtime import torch_device_fn
24+
from flag_gems.utils import libentry
25+
26+
logger = logging.getLogger(
27+
f'flag_gems.runtime.backend._mthreads.ops.{__name__.split(".")[-1]}'
28+
)
29+
30+
# Moore Threads hardware does not support fp64; int64 is supported for
31+
# indexing, but we keep the supported dtype set aligned with the other
32+
# mthreads specialized operators and fall back to the generic impl
33+
# otherwise.
34+
_SUPPORTED_DTYPES = {
35+
torch.float16,
36+
torch.bfloat16,
37+
torch.float32,
38+
torch.int32,
39+
torch.int64,
40+
torch.bool,
41+
}
42+
43+
# Highest tensor rank handled directly by the specialized kernel. Tensors
44+
# with a larger rank fall back to the generic implementation.
45+
_MAX_KERNEL_RANK = 5
46+
47+
48+
@libentry()
49+
@triton.autotune(
50+
configs=[
51+
# BLOCK_M = 1: one output row per program, large inner block. Best when
52+
# the last output dimension (N) is large, giving a long coalesced run.
53+
triton.Config({"BLOCK_M": 1, "BLOCK_N": 512}, num_warps=4, num_stages=2),
54+
triton.Config({"BLOCK_M": 1, "BLOCK_N": 1024}, num_warps=4, num_stages=2),
55+
triton.Config({"BLOCK_M": 1, "BLOCK_N": 1024}, num_warps=8, num_stages=2),
56+
triton.Config({"BLOCK_M": 1, "BLOCK_N": 2048}, num_warps=8, num_stages=2),
57+
# BLOCK_M = 1, small inner block, 2 warps: best for rank-3 permutes
58+
# where the last output dim is small (N ~= 64) — a short coalesced run
59+
# with minimal thread overhead.
60+
triton.Config({"BLOCK_M": 1, "BLOCK_N": 128}, num_warps=2, num_stages=2),
61+
triton.Config({"BLOCK_M": 1, "BLOCK_N": 256}, num_warps=2, num_stages=2),
62+
# BLOCK_M = 2 / 4: a few rows per program, mid-sized inner block.
63+
triton.Config({"BLOCK_M": 2, "BLOCK_N": 512}, num_warps=4, num_stages=2),
64+
triton.Config({"BLOCK_M": 2, "BLOCK_N": 1024}, num_warps=8, num_stages=2),
65+
triton.Config({"BLOCK_M": 4, "BLOCK_N": 256}, num_warps=4, num_stages=2),
66+
triton.Config({"BLOCK_M": 4, "BLOCK_N": 512}, num_warps=4, num_stages=2),
67+
# BLOCK_M = 8 / 16: many rows per program, small inner block. Best when
68+
# the last output dimension is small (e.g. rank-3 permutes with N=64),
69+
# so each program still has enough work without wasting threads on
70+
# masked-out elements.
71+
triton.Config({"BLOCK_M": 8, "BLOCK_N": 128}, num_warps=4, num_stages=2),
72+
triton.Config({"BLOCK_M": 8, "BLOCK_N": 256}, num_warps=4, num_stages=2),
73+
triton.Config({"BLOCK_M": 16, "BLOCK_N": 128}, num_warps=4, num_stages=2),
74+
triton.Config({"BLOCK_M": 16, "BLOCK_N": 256}, num_warps=8, num_stages=2),
75+
],
76+
key=["n_elements", "N", "dtype_size"],
77+
)
78+
@triton.jit
79+
def _permute_copy_kernel(
80+
src_ptr,
81+
dst_ptr,
82+
n_elements,
83+
M,
84+
N,
85+
dtype_size,
86+
# Outer (all-but-last) output shapes, padded to 4 with leading 1s.
87+
out_shape0,
88+
out_shape1,
89+
out_shape2,
90+
out_shape3,
91+
# Reordered source strides (``src_stride_j = input.stride(perm[j])``),
92+
# padded to 5 with leading 0s; index 4 is the inner (last) dim.
93+
src_stride0,
94+
src_stride1,
95+
src_stride2,
96+
src_stride3,
97+
src_stride4,
98+
BLOCK_M: tl.constexpr,
99+
BLOCK_N: tl.constexpr,
100+
):
101+
pid_m = tl.program_id(0)
102+
pid_n = tl.program_id(1)
103+
104+
rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) # outer flat index
105+
rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) # last-dim index
106+
mask_m = rm < M
107+
mask_n = rn < N
108+
mask = mask_m[:, None] & mask_n[None, :]
109+
110+
# Decompose the outer flat index into per-dimension output indices. This is
111+
# done once per program (scalar work over BLOCK_M rows) rather than per
112+
# element, which is the key difference from a flat 1-D kernel and keeps the
113+
# inner-dim load/store loop free of integer divisions. Leading size-1
114+
# dimensions (padding for ranks < 5) simply yield index 0.
115+
rem = rm
116+
o3 = rem % out_shape3
117+
rem = rem // out_shape3
118+
o2 = rem % out_shape2
119+
rem = rem // out_shape2
120+
o1 = rem % out_shape1
121+
rem = rem // out_shape1
122+
o0 = rem % out_shape0
123+
124+
# Source offset: dot product of the (reordered) source strides with the
125+
# output indices. The outer part is constant across the inner dim, so it
126+
# forms a per-row base; the inner part is the strided last-dim access.
127+
src_base = o0 * src_stride0 + o1 * src_stride1 + o2 * src_stride2 + o3 * src_stride3
128+
src_offset = src_base[:, None] + rn[None, :] * src_stride4
129+
130+
# Destination offset: the output is row-major contiguous, so the flat index
131+
# is simply ``outer_index * N + inner_index`` (inner stride == 1).
132+
dst_offset = rm[:, None] * N + rn[None, :]
133+
134+
vals = tl.load(src_ptr + src_offset, mask=mask)
135+
tl.store(dst_ptr + dst_offset, vals, mask=mask)
136+
137+
138+
@libentry()
139+
@triton.autotune(
140+
configs=[
141+
# For rank-3 permutes the output is small and launch-bound, so a tight
142+
# 3-D grid (one program per (d0, d1) cell, BLOCK_N covering the last
143+
# dim) with few warps minimizes per-program overhead and avoids the
144+
# integer divisions of the flat-index decomposition above.
145+
# num_warps=2 gives the best warm latency on Moore Threads for the
146+
# small per-program workloads (one row of the last dim).
147+
triton.Config({"BLOCK_N": 64}, num_warps=2, num_stages=2),
148+
triton.Config({"BLOCK_N": 128}, num_warps=2, num_stages=2),
149+
triton.Config({"BLOCK_N": 256}, num_warps=2, num_stages=2),
150+
],
151+
key=["n_elements", "d2", "dtype_size"],
152+
)
153+
@triton.jit
154+
def _permute_copy_kernel_3d(
155+
src_ptr,
156+
dst_ptr,
157+
n_elements,
158+
d0,
159+
d1,
160+
d2,
161+
dtype_size,
162+
# Reordered source strides (``src_stride_j = input.stride(perm[j])``).
163+
src_stride0,
164+
src_stride1,
165+
src_stride2,
166+
# Output strides (row-major contiguous).
167+
out_stride0,
168+
out_stride1,
169+
out_stride2,
170+
BLOCK_N: tl.constexpr,
171+
):
172+
p0 = tl.program_id(0) # output dim 0
173+
p1 = tl.program_id(1) # output dim 1
174+
p2 = tl.program_id(2) # output dim 2 (blocked)
175+
176+
rn = p2 * BLOCK_N + tl.arange(0, BLOCK_N)
177+
mask = rn < d2
178+
179+
# Source offset: dot product of the reordered source strides with the
180+
# output indices. p0/p1 are scalar per program; rn is the inner run.
181+
src_offset = p0 * src_stride0 + p1 * src_stride1 + rn * src_stride2
182+
# Destination offset: the output is row-major contiguous.
183+
dst_offset = p0 * out_stride0 + p1 * out_stride1 + rn * out_stride2
184+
185+
vals = tl.load(src_ptr + src_offset, mask=mask)
186+
tl.store(dst_ptr + dst_offset, vals, mask=mask)
187+
188+
189+
def _use_triton_kernel(x: torch.Tensor, dims) -> Tuple[bool, int]:
190+
if not isinstance(x, torch.Tensor):
191+
return False, 0
192+
if x.device.type != "musa" or x.dtype not in _SUPPORTED_DTYPES:
193+
return False, 0
194+
if x.ndim == 0 or x.ndim > _MAX_KERNEL_RANK:
195+
return False, 0
196+
if x.numel() == 0 or not x.is_contiguous():
197+
return False, 0
198+
# ``dims`` must be a genuine permutation of range(ndim); otherwise the
199+
# copy semantics differ from ``aten::permute_copy``.
200+
ndim = x.ndim
201+
try:
202+
normalized = [d if d >= 0 else d + ndim for d in dims]
203+
except TypeError:
204+
return False, 0
205+
if sorted(normalized) != list(range(ndim)) or len(normalized) != ndim:
206+
return False, 0
207+
return True, x.element_size()
208+
209+
210+
def _launch_permute_copy(
211+
x: torch.Tensor, out: torch.Tensor, dims, dtype_size: int
212+
) -> torch.Tensor:
213+
ndim = x.ndim
214+
215+
# Reorder the input strides by the permutation so the kernel can compute
216+
# the source offset as a plain dot product of the output indices.
217+
in_strides = list(x.stride())
218+
src_strides = [in_strides[dims[j]] for j in range(ndim)]
219+
out_strides = list(out.stride())
220+
out_shapes = list(out.shape)
221+
n_elements = out.numel()
222+
223+
with torch_device_fn.device(out.device):
224+
if ndim == 3:
225+
# Rank-3 fast path: a 3-D grid with no flat-index decomposition.
226+
grid = lambda meta: (
227+
out_shapes[0],
228+
out_shapes[1],
229+
triton.cdiv(out_shapes[2], meta["BLOCK_N"]),
230+
)
231+
_permute_copy_kernel_3d[grid](
232+
x,
233+
out,
234+
n_elements,
235+
out_shapes[0],
236+
out_shapes[1],
237+
out_shapes[2],
238+
dtype_size,
239+
src_strides[0],
240+
src_strides[1],
241+
src_strides[2],
242+
out_strides[0],
243+
out_strides[1],
244+
out_strides[2],
245+
)
246+
return out
247+
248+
# General path for ranks 1, 2, 4, 5: pad shapes/strides up to the fixed
249+
# 5-D kernel signature. The inner (last) dimension is kept real; leading
250+
# dimensions are padded with shape 1 (index always 0) and stride 0 (no
251+
# offset contribution).
252+
pad_n = _MAX_KERNEL_RANK - ndim
253+
out_shapes_pad = [1] * pad_n + out_shapes
254+
src_strides_pad = [0] * pad_n + src_strides
255+
256+
# Outer = all but the last dim; inner = last dim.
257+
M = 1
258+
for s in out_shapes_pad[:-1]:
259+
M = M * s
260+
N = out_shapes_pad[-1]
261+
262+
grid = lambda meta: (
263+
triton.cdiv(M, meta["BLOCK_M"]),
264+
triton.cdiv(N, meta["BLOCK_N"]),
265+
)
266+
_permute_copy_kernel[grid](
267+
x,
268+
out,
269+
n_elements,
270+
M,
271+
N,
272+
dtype_size,
273+
out_shapes_pad[0],
274+
out_shapes_pad[1],
275+
out_shapes_pad[2],
276+
out_shapes_pad[3],
277+
src_strides_pad[0],
278+
src_strides_pad[1],
279+
src_strides_pad[2],
280+
src_strides_pad[3],
281+
src_strides_pad[4],
282+
)
283+
return out
284+
285+
286+
def permute_copy(x: torch.Tensor, dims):
287+
logger.debug("GEMS_MTHREADS PERMUTE_COPY")
288+
use_triton, dtype_size = _use_triton_kernel(x, dims)
289+
if not use_triton:
290+
return default_permute_copy(x, dims)
291+
292+
ndim = x.ndim
293+
normalized = [d if d >= 0 else d + ndim for d in dims]
294+
out_shape = [x.shape[d] for d in normalized]
295+
out = torch.empty(out_shape, dtype=x.dtype, device=x.device)
296+
return _launch_permute_copy(x, out, normalized, dtype_size)

0 commit comments

Comments
 (0)