|
| 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