chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
@@ -0,0 +1,25 @@
# isort: skip_file
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=unused-import
"""Intrinsics for tensorization."""
from tvm.runtime import enabled
from . import cuda
if enabled("llvm"):
from . import arm_cpu, x86, rocm, hexagon, riscv_cpu
+777
View File
@@ -0,0 +1,777 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name,missing-function-docstring,unused-import
# ruff: noqa: E501, F401
"""Intrinsics for ARM tensorization."""
from tvm import tirx
from tvm.script import tirx as T
from tvm.script.ir_builder import IRBuilder
from tvm.script.ir_builder.tirx import prim_func as build_prim_func
from tvm.target.codegen import llvm_version_major
from .. import TensorIntrin
from .dot_product_common import (
DP4A_S8S8S32_INTRIN,
DP4A_S8U8S32_INTRIN,
DP4A_U8S8S32_INTRIN,
DP4A_U8U8U32_INTRIN,
)
# TODO(masahi): Parametrize the TVMScript description of dot product by
# shape and dtype, and share the common description with x86.
@T.prim_func(s_tir=True)
def neon_4x4_i8i8i32_desc(
A: T.Buffer((4,), "int8", offset_factor=1),
B: T.Buffer((4, 4), "int8", offset_factor=1),
C: T.Buffer((4,), "int32", offset_factor=1),
) -> None:
with T.sblock("root"):
T.reads(C[0:4], A[0:4], B[0:4, 0:4])
T.writes(C[0:4])
for i in T.serial(0, 4):
for k in T.serial(0, 4):
with T.sblock("update"):
vi, vk = T.axis.remap("SR", [i, k])
C[vi] = C[vi] + T.cast(A[vk], "int32") * T.cast(B[vi, vk], "int32")
@T.prim_func(s_tir=True)
def neon_4x4_i8i8i32_impl(
A: T.Buffer((4,), "int8", offset_factor=1),
B: T.Buffer((4, 4), "int8", offset_factor=1),
C: T.Buffer((4,), "int32", offset_factor=1),
) -> None:
with T.sblock("root"):
T.reads(C[0:4], A[0:4], B[0:4, 0:4])
T.writes(C[0:4])
A_int8 = A.vload([0], "int8x4")
re_int32 = T.reinterpret(A_int8, dtype="int32")
vec_ai32 = T.broadcast(re_int32, 2)
vec_a = T.reinterpret(vec_ai32, dtype="int8x8")
vec_b = B.vload([0, 0], dtype="int8x16")
# TODO(masahi): Remove duplication when inlined function call is supported
vec_b_low = T.vectorlow(vec_b, dtype="int8x8")
multiply_low = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.aarch64.neon.smull.v8i16"),
vec_a,
vec_b_low,
dtype="int16x8",
)
pairwise_reduction_low = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.aarch64.neon.saddlp.v4i32.v8i16"),
multiply_low,
dtype="int32x4",
)
vec_b_high = T.vectorhigh(vec_b, dtype="int8x8")
multiply_high = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.aarch64.neon.smull.v8i16"),
vec_a,
vec_b_high,
dtype="int16x8",
)
pairwise_reduction_high = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.aarch64.neon.saddlp.v4i32.v8i16"),
multiply_high,
dtype="int32x4",
)
C[T.ramp(T.int32(0), 1, 4)] += T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.aarch64.neon.addp.v4i32"),
pairwise_reduction_low,
pairwise_reduction_high,
dtype="int32x4",
)
def get_dotprod_intrin(in_dtype, out_dtype):
if in_dtype == "uint8":
instr = "udot.v4u32.v16u8"
else: # if in_dtype == "int8"
instr = "sdot.v4i32.v16i8"
in_dtype_x4 = f"{in_dtype}x4"
out_dtype_x4 = f"{out_dtype}x4"
in_dtype_x16 = f"{in_dtype}x16"
@T.prim_func(s_tir=True)
def dot_prod_desc(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (4,), dtype=in_dtype, offset_factor=1)
B = T.match_buffer(b, (4, 4), dtype=in_dtype, offset_factor=1)
C = T.match_buffer(c, (4,), dtype=out_dtype, offset_factor=1)
with T.sblock("root"):
T.reads(C[0:4], A[0:4], B[0:4, 0:4])
T.writes(C[0:4])
for i in T.serial(0, 4):
for k in T.serial(0, 4):
with T.sblock("update"):
vi, vk = T.axis.remap("SR", [i, k])
C[vi] = C[vi] + T.cast(A[vk], dtype=out_dtype) * T.cast(
B[vi, vk], dtype=out_dtype
)
@T.prim_func(s_tir=True)
def dot_prod_impl(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (4,), dtype=in_dtype, offset_factor=1)
B = T.match_buffer(b, (4, 4), dtype=in_dtype, offset_factor=1)
C = T.match_buffer(c, (4,), dtype=out_dtype, offset_factor=1)
with T.sblock("root"):
T.reads(C[0:4], A[0:4], B[0:4, 0:4])
T.writes(C[0:4])
A_i8x4 = A.vload([0], in_dtype_x4)
A_i32 = T.reinterpret(A_i8x4, dtype=out_dtype)
vec_ai32 = T.broadcast(A_i32, 4)
vec_a = T.reinterpret(vec_ai32, dtype=in_dtype_x16)
vec_b = B.vload([0, 0], dtype=in_dtype_x16)
vec_c = C.vload([0], dtype=out_dtype_x4)
C[T.ramp(T.int32(0), 1, 4)] = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id(f"llvm.aarch64.neon.{instr}"),
vec_c,
vec_a,
vec_b,
dtype=out_dtype_x4,
)
return dot_prod_desc, dot_prod_impl
def _create_ptrue_mask(dtype):
"""
Creates a mask that enables all lanes of a scalable vector.
"""
return T.broadcast(T.bool(True), tirx.get_vscale_expr(dtype))
def _create_active_lane_mask(tensor, relative_offsets, vertical_limit):
"""
Get the active lane mask intrinsic call for predicated accesses.
Parameters
----------
tensor : tvm.tirx.Buffer
The tensor the buffer access will be performed on.
relative_offsets : Tuple[Expr, Expr]
The vertical and horizontal offsets into the accumulator tile.
vertical_limit : Expr
An absolute offset specifying the limit at which rows should be stored.
Returns
-------
Expr
The active lane mask intrinsic.
"""
vertical_offset, horizontal_offset = relative_offsets
stride = tensor.strides[0]
# The base is the offset of the first value we wish to store
base = T.int32(tensor.offset_of([vertical_offset, horizontal_offset])[0])
# The limit is the maximum offset in the current row of 'base' that we wish to allow values
# to be stored. Calculating this limit is a bit tricky since we can only request offsets of
# elements in the tensorized tile of the output tensor. One way to calculate this is to find
# the offset of the first value in the row of the output tensor that 'base' is in and add
# 'stride' to it.
limit = (
base
- T.int32(horizontal_offset)
- T.int32(tensor.offset_of([0, 0])[0] % stride)
+ T.int32(stride)
)
limit = T.Min(limit, T.Cast("int32", vertical_limit) * stride)
return T.get_active_lane_mask(
"uint1xvscalex4",
T.Cast("int32", base),
T.Cast("int32", limit),
)
def get_sme_transpose_interleave_2svlx2svl_fp32_intrin(cols, rows):
"""
Transpose a matrix of size 2SVL x 2SVL (where 'SVL' is the Scalable Vector Length) using
the Scalable Matrix Extension (SME).
This is completed by loading rows of the input matrix into the accumulator tile,
then storing the columns. The SME accumulator tile is divided into a series of sub-tiles
which must be loaded to / stored from independently.
Example
-------
An example case for float32. In this instance the accumulator tile is divided into 4
sub-tiles of size SVLxSVL numbered 0-3. We start by loading rows of A, each SVL in length,
into each of the sub-tiles. In the diagram below, each load for a sub-tile is sequenced by
a, b, ... till the tile is full.
The columns of each sub-tile are then stored into A_t. Note that to perform a transpose,
the contents of sub-tile 1 and 2 are stored in opposite locations - see the diagram
below.
::
A: Accumulator tile: A_t:
2SVL 2SVL 2SVL
+----------------+ +-----------------+ +-------------------+
| --0a-- --1a-- | | | | | | | | |
| --0b-- --1b-- | | 0 1 | | 0a 0b .. 2a 2b .. |
| ... ... | ld1w.horiz | | st1w.vert | | | | | |
2SVL | --2a-- --3a-- | ====> 2SVL | | ====> 2SVL | | | | | |
| --2a-- --3b-- | | 2 3 | | 1a 1b .. 3a 3b .. |
| ... ... | | | | | | | | |
+----------------+ +-----------------+ +-------------------+
Returns
-------
intrin : TensorIntrin
The SME TensorIntrin that can be used in tensorizing a schedule.
"""
SVF = tirx.get_vscale_expr("float32")
SVF2 = 2 * SVF
@T.prim_func(s_tir=True)
def desc(a: T.handle, a_t: T.handle) -> None:
A = T.match_buffer(a, (SVF2, SVF2), dtype="float32", offset_factor=1)
A_t = T.match_buffer(a_t, (SVF2, SVF2), dtype="float32", offset_factor=1)
with T.sblock("root"):
T.reads(A[0:SVF2, 0:SVF2])
T.writes(A_t[0:SVF2, 0:SVF2])
for k, m in T.grid(SVF2, SVF2):
with T.sblock("transpose"):
v_m, v_k = T.axis.remap("SS", [m, k])
A_t[v_k, v_m] = A[v_m, v_k]
def impl():
sub_tile_count = 4
with IRBuilder() as ib:
with build_prim_func():
a = T.arg("a", T.handle())
a_t = T.arg("a_t", T.handle())
A = T.match_buffer(
a, (SVF2, SVF2), "float32", offset_factor=1, strides=[T.int32(), 1]
)
A_t = T.match_buffer(
a_t,
(SVF2, SVF2),
"float32",
offset_factor=1,
strides=[T.int32(), 1],
)
with T.sblock("root"):
T.reads(A[0:SVF2, 0:SVF2])
T.writes(A_t[0:SVF2, 0:SVF2])
# Load rows of the input matrix
with T.serial(0, SVF) as slice_idx:
for sub_tile_idx in range(0, sub_tile_count):
row_offset = SVF if sub_tile_idx >= (sub_tile_count // 2) else 0
col_offset = SVF if sub_tile_idx % 2 else 0
offset = (slice_idx + row_offset) * A.strides[0] + col_offset
input_ptr = A.access_ptr("r", offset=offset)
sub_tile = T.int32(sub_tile_idx)
predicate = _create_active_lane_mask(
A, (row_offset + slice_idx, col_offset), cols
)
T.evaluate(
T.call_llvm_intrin(
"void",
"llvm.aarch64.sme.ld1w.horiz",
predicate,
input_ptr,
sub_tile,
slice_idx,
)
)
# Store columns to the output matrix
with T.serial(0, SVF) as slice_idx:
for sub_tile_idx in range(0, sub_tile_count):
col_offset = SVF if sub_tile_idx >= (sub_tile_count // 2) else 0
row_offset = SVF if sub_tile_idx % 2 else 0
offset = (slice_idx + row_offset) * A_t.strides[0] + col_offset
output_ptr = A_t.access_ptr("w", offset=offset)
sub_tile = T.int32(sub_tile_idx)
predicate = _create_active_lane_mask(
A_t, (row_offset + slice_idx, col_offset), rows
)
T.evaluate(
T.call_llvm_intrin(
"void",
"llvm.aarch64.sme.st1w.vert",
predicate,
output_ptr,
sub_tile,
slice_idx,
)
)
return ib.get()
return desc, impl()
def get_sme_transpose_interleave_block2_2svl_fp16_intrin():
# pylint: disable=line-too-long
"""
Transpose and block pack a matrix of size 2SVL x 1SVL (where 'SVL' is the Scalable Vector
Length for the fp16 datatype) using the Scalable Matrix Extension (SME).
Rows of the fp16 input matrix are loaded into the accumulator tile and columns are stored
as fp32 SVL length vectors to the output matrix. When loading, the accumulator tile is
interpreted to be of shape 2 * 8 * vscale x 8 * vscale. When storing, we interpret the
accumulator tile to be of shape 2 * 4 * vscale x 2 * 4 * vscale.
Example
-------
In the fp16 instance, the accumulator tile consists of two sub-tiles numbered 0-1. Rows
of A are loaded onto the accumulator tile by interleaving rows in the first half (0, SVL//2]
of the tile and rows in the second half (SVL//2, SVL]. Columns of fp32 values are stored
into the output buffer. The fp32 store is used to group pairs of consecutive values together,
resulting in the arrangement displayed below::
A: Accumulator tile:
+----------------+ +----------------+
|-------0a-------| |-------0a-------|
|-------0b-------| |-------0x-------|
| ... | |-------0b-------| A_t:
|-------0x-------| |-------0y-------| +------------------------------------------------+
|-------0y-------| | ... | |0a.0 0a.1 0b.0 0b.1 | 1a.0 1a.1 1b.0 1b.1 |
| ... | ld1h.horiz | | st1w.vert |0x.0 0x.1 0y.0 0y.1 | 1x.0 1x.1 1y.0 1y.1 |
|================| ====> |================| ====> |0a.2 0a.3 0b.2 0b.3 ...| 1a.2 1a.3 1b.2 1b.3 ...|
|-------1a-------| |-------1a-------| |0x.2 0x.3 0y.2 0y.3 | 1x.2 1x.3 1y.2 1y.3 |
|-------1b-------| |-------1x-------| |... ... ... ... | ... ... ... ... |
| ... | |-------1b-------| +------------------------------------------------+
|-------1x-------| |-------1y-------|
|-------1y-------| | ... |
| ... | | |
+----------------+ +----------------+
In the A_t output matrix in the diagram above, .x is used to denote the offset into the
labelled row.
Returns
-------
intrin : TensorIntrin
The SME TensorIntrin that can be used in tensorizing a schedule.
"""
# pylint: enable=line-too-long
SVF = tirx.get_vscale_expr("float16")
SVF2 = 2 * SVF
@T.prim_func(s_tir=True)
def desc(a: T.handle, a_t: T.handle) -> None:
A = T.match_buffer(a, (SVF2, SVF), dtype="float16", offset_factor=1)
A_t = T.match_buffer(a_t, (SVF, SVF2), dtype="float16", offset_factor=1)
with T.sblock("root"):
T.reads(A[0:SVF2, 0:SVF])
T.writes(A_t[0:SVF, 0:SVF2])
for k, m in T.grid(SVF, SVF2):
with T.sblock("transpose"):
v_m, v_k = T.axis.remap("SS", [m, k])
A_t[v_k, v_m] = A[v_m, v_k]
def impl():
with IRBuilder() as ib:
with build_prim_func():
a = T.arg("a", T.handle())
a_t = T.arg("a_t", T.handle())
A = T.match_buffer(
a, (SVF2, SVF), "float16", offset_factor=1, strides=[T.int32(), 1]
)
A_t = T.match_buffer(
a_t, (SVF, SVF2), "float16", offset_factor=1, strides=[T.int32(), 1]
)
ptrue_fp16 = _create_ptrue_mask("float16")
ptrue_fp32 = _create_ptrue_mask("float32")
with T.sblock("root"):
T.reads(A[0:SVF2, 0:SVF])
T.writes(A_t[0:SVF, 0:SVF2])
# Load rows of the input matrix
with T.serial(SVF // 2) as slice_idx:
for sub_tile_idx in range(2):
offset = slice_idx * A.strides[0] + (SVF * A.strides[0] * sub_tile_idx)
input_ptr = A.access_ptr("r", offset=offset)
T.evaluate(
T.call_llvm_intrin(
"void",
"llvm.aarch64.sme.ld1h.horiz",
ptrue_fp16,
input_ptr,
sub_tile_idx,
slice_idx * 2,
)
)
input_ptr = A.access_ptr("r", offset=offset + (SVF // 2) * A.strides[0])
T.evaluate(
T.call_llvm_intrin(
"void",
"llvm.aarch64.sme.ld1h.horiz",
ptrue_fp16,
input_ptr,
sub_tile_idx,
slice_idx * 2 + 1,
)
)
# Store columns to the output matrix
with T.serial(SVF // 2) as slice_idx:
for sub_tile_idx in range(2):
offset = slice_idx * 2 * A_t.strides[0] + (SVF * sub_tile_idx)
output_ptr = A_t.access_ptr("w", offset=offset)
T.evaluate(
T.call_llvm_intrin(
"void",
"llvm.aarch64.sme.st1w.vert",
ptrue_fp32,
output_ptr,
sub_tile_idx,
slice_idx,
)
)
output_ptr = A_t.access_ptr("w", offset=offset + A_t.strides[0])
T.evaluate(
T.call_llvm_intrin(
"void",
"llvm.aarch64.sme.st1w.vert",
ptrue_fp32,
output_ptr,
sub_tile_idx + 2,
slice_idx,
)
)
return ib.get()
return desc, impl()
def get_transpose_interleave_intrin_name(in_dtype, out_dtype, extent_cols, extent_rows):
if in_dtype == "float32" and out_dtype == "float32":
sme_transpose_interleave_intrin_name = (
ARM_SME_2SVLx2SVL_FP32_TRANSPOSE_INTERLEAVE + f"_{extent_cols}_{extent_rows}"
)
tirx.TensorIntrin.register(
sme_transpose_interleave_intrin_name,
*get_sme_transpose_interleave_2svlx2svl_fp32_intrin(extent_cols, extent_rows),
override=True,
)
return sme_transpose_interleave_intrin_name
elif in_dtype == "float16" and out_dtype == "float32":
return ARM_SME_BLOCK2_2SVLx1SVL_FP16_TRANSPOSE_INTERLEAVE
else:
raise ValueError("Input/output data type combination not supported.")
def get_sme_gemm_interleaved_mopa_2svlx2svl_intrin(M, K, in_dtype):
"""
Compute a GEMM of size 2SVL x 2SVL (where 'SVL' is the Scalable Vector Length using
outer product operations from the Scalable Matrix Extension (SME).
The inputs A and B are expected to be of size K x 2SVL and produce a result C of
size 2SVL x 2SVL.
The SME accumulator tile is divided into sub-tiles, each of which is utilized to
calculate the outer-product using columns / rows of A and B respectively. For each
sub-tile, elements in the first column of input matrix A (accessed sequentially due
to being transpose-interleaved) and first row of input matrix B are used to calculate
an outer-product. This is then accumulated with the result of performing an
outer-product on the second column and row of A and B respectively. This process is
repeated K times. Finally, the results of the accumulation are stored.
Note: The input tensor 'A' must be transpose-interleaved.
Example
-------
Diagram showing outer-product performed on each of the accumulator sub-tiles
for the fp32 datatype:
::
SVL SVL
+----------------------------+
| l | h | K
K +----------------------------+
+---+ +----------------------------+
| | | 0: 1: |-+
| | | mopa(l, l) mopa(l, h) | |-+
l | | | | | |
| | | | | |
|---| | | | |
| | | 2: 3: | | |
h | | | mopa(h, l) mopa(h, h) | | |
| | | | | |
| | | | | |
+---+ +----------------------------+ | |
+----------------------------+ |
+---------------------------+
(accumulate K times)
Pseudo code computing 2SVL x 2SVL GEMM for fp32 inputs:
.. code-block:: c
// Number of fp32 elements in a scalable vector
int SVF = SVL / 32;
// Reset the accumulator tile
sme.zero();
// Calculate outer products and accumulate
for (k = 0; k < K; k++) {
float32xSVF A_row_0 = A[k][0];
float32xSVF A_row_1 = A[k][SVF];
float32xSVF B_row_0 = B[k][0];
float32xSVF B_row_1 = B[k][SVF];
float32xSVFxSVF sub_tile_0 += sme.mopa(A_row_0, B_row_0);
float32xSVFxSVF sub_tile_1 += sme.mopa(A_row_0, B_row_1);
float32xSVFxSVF sub_tile_2 += sme.mopa(A_row_1, B_row_0);
float32xSVFxSVF sub_tile_3 += sme.mopa(A_row_1, B_row_1);
}
// Store the results of accumulation
for (i = 0; i < SVF; i++) {
C[i][0] = sme.horiz(sub_tile_0[i]);
C[i][0] = sme.horiz(sub_tile_0[i + SVF]);
C[i + SVF][0] = sme.horiz(sub_tile_0[i]);
C[i + SVF][0] = sme.horiz(sub_tile_0[i + SVF]);
}
Notes:
- Recall that A has been transposed beforehand such that each column is now accessed
by row.
- 'sme.zero' resets the accumulator tile to contain all zero's.
- 'sme.mopa' is the outer product and accumulate intrinsic.
- 'sme.horiz' stores rows of an accumulator sub-tile to memory.
Returns
-------
intrin : TensorIntrin
The SME TensorIntrin that can be used in tensorizing a schedule.
"""
SVF = tirx.get_vscale_expr("float32")
SVF2 = 2 * SVF
fmopa_intrin = (
"llvm.aarch64.sme.mopa" if in_dtype == "float32" else "llvm.aarch64.sme.mopa.wide"
)
@T.prim_func(s_tir=True)
def desc(a: T.handle, b: T.handle, c: T.handle):
A = T.match_buffer(a, (K, SVF2), dtype=in_dtype, offset_factor=1)
B = T.match_buffer(b, (K, SVF2), dtype=in_dtype, offset_factor=1)
C = T.match_buffer(c, (SVF2, SVF2), dtype="float32", offset_factor=1)
with T.sblock("root"):
T.reads(C[0:SVF2, 0:SVF2], A[0:K, 0:SVF2], B[0:K, 0:SVF2])
T.writes(C[0:SVF2, 0:SVF2])
for m, n, k in T.grid(SVF2, SVF2, K):
with T.sblock("gemm"):
v_m, v_n, v_k = T.axis.remap("SSR", [m, n, k])
C[v_m, v_n] += T.Cast("float32", A[v_k, v_m]) * T.Cast("float32", B[v_k, v_n])
def impl():
sub_tile_count = 4
with IRBuilder() as ib:
with build_prim_func():
a = T.arg("a", T.handle())
b = T.arg("b", T.handle())
c = T.arg("c", T.handle())
A = T.match_buffer(a, (K, SVF2), in_dtype, offset_factor=1, strides=[T.int32(), 1])
B = T.match_buffer(b, (K, SVF2), in_dtype, offset_factor=1, strides=[T.int32(), 1])
C = T.match_buffer(
c, (SVF2, SVF2), "float32", offset_factor=1, strides=[T.int32(), 1]
)
ptrue = _create_ptrue_mask(in_dtype)
with T.sblock("root"):
T.reads(C[0:SVF2, 0:SVF2], A[0:K, 0:SVF2], B[0:K, 0:SVF2])
T.writes(C[0:SVF2, 0:SVF2])
# Iterate over the reduction axis applying outer product and accumulate
rows_per_iter = 1 if in_dtype == "float32" else 2
with T.serial(T.ceildiv(K, rows_per_iter)) as k:
k_row = k * rows_per_iter
in_dtype_svf = tirx.get_vscale_expr(in_dtype)
# Ideally we'd rely on predicating the loads and use the same predicate
# for the outer product operation. However, support for predicated
# buffers is not currently supported by multiple lowering passes such as
# "LowerMatchBuffer", therefore the predicate is passed directly to the
# outer product operation for now.
if in_dtype == "float32":
a_low = (
T.BufferLoad(A, [k_row, T.Ramp(0, 1, in_dtype_svf)]),
_create_active_lane_mask(A, (k_row, 0), K),
)
b_low = (
T.BufferLoad(B, [k_row, T.Ramp(0, 1, in_dtype_svf)]),
_create_active_lane_mask(B, (k_row, 0), K),
)
a_high = (
T.BufferLoad(A, [k_row, T.Ramp(in_dtype_svf, 1, in_dtype_svf)]),
_create_active_lane_mask(A, (k_row, in_dtype_svf), K),
)
b_high = (
T.BufferLoad(B, [k_row, T.Ramp(in_dtype_svf, 1, in_dtype_svf)]),
_create_active_lane_mask(B, (k_row, in_dtype_svf), K),
)
else:
a_low = (T.BufferLoad(A, [k_row, T.Ramp(0, 1, in_dtype_svf)]), ptrue)
b_low = (T.BufferLoad(B, [k_row, T.Ramp(0, 1, in_dtype_svf)]), ptrue)
a_high = (
T.BufferLoad(A, [k_row + 1, T.Ramp(0, 1, in_dtype_svf)]),
ptrue,
)
b_high = (
T.BufferLoad(B, [k_row + 1, T.Ramp(0, 1, in_dtype_svf)]),
ptrue,
)
input_combinations = [
(a_low, b_low),
(a_low, b_high),
(a_high, b_low),
(a_high, b_high),
]
for sub_tile_idx in range(0, sub_tile_count):
sub_tile = T.int32(sub_tile_idx)
input_1 = input_combinations[sub_tile_idx][0]
input_2 = input_combinations[sub_tile_idx][1]
T.evaluate(
T.call_llvm_intrin(
"void",
fmopa_intrin,
sub_tile,
input_1[1],
input_2[1],
input_1[0],
input_2[0],
)
)
# Store the accumulated tile results
with T.serial(SVF) as slice_idx:
for sub_tile_idx in range(sub_tile_count):
vert_offset = SVF if sub_tile_idx >= (sub_tile_count // 2) else 0
horiz_offset = SVF if sub_tile_idx % 2 else 0
local_offset = (slice_idx + vert_offset) * C.strides[0] + horiz_offset
output_ptr = C.access_ptr("w", offset=local_offset, extent=SVF)
T.evaluate(
T.call_llvm_intrin(
"void",
"llvm.aarch64.sme.st1w.horiz",
_create_active_lane_mask(
C, (vert_offset + slice_idx, horiz_offset), M
),
output_ptr,
T.int32(sub_tile_idx),
T.int32(slice_idx),
)
)
return ib.get()
return desc, impl()
def get_sme_init_intrin():
"""
Reset the entire matrix tile storage to 0.
"""
SVF2 = 2 * 4 * T.vscale()
@T.prim_func(s_tir=True)
def desc(c: T.handle) -> None:
C = T.match_buffer(c, (SVF2, SVF2), "float32", offset_factor=1)
with T.sblock("root"):
T.reads()
T.writes(C[0:SVF2, 0:SVF2])
for m, n in T.grid(SVF2, SVF2):
with T.sblock("init"):
v_m, v_n = T.axis.remap("SS", [m, n])
C[v_m, v_n] = T.float32(0)
@T.prim_func(s_tir=True)
def impl(c: T.handle) -> None:
C = T.match_buffer(c, (SVF2, SVF2), "float32", offset_factor=1)
with T.sblock("root"):
T.reads()
T.writes(C[0:SVF2, 0:SVF2])
clear_all_tiles = T.int32(255)
T.evaluate(T.call_llvm_intrin("void", "llvm.aarch64.sme.zero", clear_all_tiles))
return desc, impl
ARM_DOT_4x4_i8_NEON_INTRIN = "dot_4x4_i8i8s32_neon"
ARM_DOT_4x4_i8_SDOT_INTRIN = "dot_4x4_i8i8s32_sdot"
ARM_DOT_4x4_u8_UDOT_INTRIN = "dot_4x4_u8u8u32_udot"
ARM_DOT_4x4_u8_HDOT_INTRIN = "dot_4x4_u8u8i32_hdot"
TensorIntrin.register(ARM_DOT_4x4_i8_NEON_INTRIN, neon_4x4_i8i8i32_desc, neon_4x4_i8i8i32_impl)
TensorIntrin.register(ARM_DOT_4x4_i8_SDOT_INTRIN, *get_dotprod_intrin("int8", "int32"))
TensorIntrin.register(ARM_DOT_4x4_u8_UDOT_INTRIN, *get_dotprod_intrin("uint8", "uint32"))
TensorIntrin.register(ARM_DOT_4x4_u8_HDOT_INTRIN, *get_dotprod_intrin("uint8", "int32"))
ARM_SME_INIT = "sme_init"
ARM_SME_2SVLx2SVL_FP32_TRANSPOSE_INTERLEAVE = "sme_2svlx2svl_fp32_transpose_interleave"
ARM_SME_BLOCK2_2SVLx1SVL_FP16_TRANSPOSE_INTERLEAVE = (
"sme_block2_2svlx1svl_fp16_transpose_interleave"
)
ARM_SME_2SVLx2SVL_GEMM_INTERLEAVED_MOPA = "sme_2svlx2svl_gemm_interleaved_mopa"
# The following tensor intrinsics use LLVM intrinsics that are only available
# in versions of LLVM >= 15. Installations with older versions of LLVM will
# not be able to use them.
if llvm_version_major() >= 15:
TensorIntrin.register(
ARM_SME_BLOCK2_2SVLx1SVL_FP16_TRANSPOSE_INTERLEAVE,
*get_sme_transpose_interleave_block2_2svl_fp16_intrin(),
)
TensorIntrin.register(ARM_SME_INIT, *get_sme_init_intrin())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name,missing-function-docstring
"""Dot product related intrinsics."""
from tvm.script import tirx as T
from .. import TensorIntrin
def get_dp4a_intrin(dtype_a, dtype_b, dtype_c):
if dtype_c == "uint32":
assert dtype_a == dtype_b == "uint8"
vec_type_a = "int8x4" if dtype_a == "int8" else "uint8x4"
vec_type_b = "int8x4" if dtype_b == "int8" else "uint8x4"
@T.prim_func(s_tir=True)
def dp4a_desc(
A: T.Buffer((4,), dtype_a, offset_factor=1, align=4, scope="shared"),
B: T.Buffer((4,), dtype_b, offset_factor=1, align=4, scope="shared"),
C: T.Buffer((1,), dtype_c, offset_factor=1, align=4, scope="local"),
) -> None:
with T.sblock("root"):
T.reads(C[0], A[0:4], B[0:4])
T.writes(C[0])
for i in range(0, 4):
with T.sblock("update"):
vi = T.axis.remap("R", [i])
C[0] = C[0] + T.cast(A[vi], dtype_c) * T.cast(B[vi], dtype_c)
@T.prim_func(s_tir=True)
def dp4a_impl(
A: T.Buffer((4,), dtype_a, offset_factor=1, align=4, scope="shared"),
B: T.Buffer((4,), dtype_b, offset_factor=1, align=4, scope="shared"),
C: T.Buffer((1,), dtype_c, offset_factor=1, align=4, scope="local"),
) -> None:
with T.sblock("root"):
T.reads(C[0], A[0:4], B[0:4])
T.writes(C[0])
C[0] += T.call_pure_extern(
"__dp4a",
A.vload([0], vec_type_a),
B.vload([0], vec_type_b),
T.uint32(0) if dtype_c == "uint32" else T.int32(0),
dtype=dtype_c,
)
return dp4a_desc, dp4a_impl
DP4A_S8S8S32_INTRIN = "dp4a_s8s8s32"
TensorIntrin.register(DP4A_S8S8S32_INTRIN, *get_dp4a_intrin("int8", "int8", "int32"))
DP4A_U8S8S32_INTRIN = "dp4a_u8s8s32"
TensorIntrin.register(DP4A_U8S8S32_INTRIN, *get_dp4a_intrin("uint8", "int8", "int32"))
DP4A_S8U8S32_INTRIN = "dp4a_s8u8s32"
TensorIntrin.register(DP4A_S8U8S32_INTRIN, *get_dp4a_intrin("int8", "uint8", "int32"))
DP4A_U8U8U32_INTRIN = "dp4a_u8u8u32"
TensorIntrin.register(DP4A_U8U8U32_INTRIN, *get_dp4a_intrin("uint8", "uint8", "uint32"))
+225
View File
@@ -0,0 +1,225 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name,missing-function-docstring
"""Intrinsics for Hexagon tensorization."""
from tvm.script import tirx as T
from .. import TensorIntrin
def generate_dma_load_intrin(
size: int,
dtype: str,
):
"""Generator of dma_load intrins"""
@T.prim_func(s_tir=True)
def sync_dma_load_desc(a: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (size), dtype, offset_factor=1, scope="global")
C = T.match_buffer(c, (size), dtype, offset_factor=1, scope="global.vtcm")
with T.sblock("root"):
T.reads(A[0:size])
T.writes(C[0:size])
for i in T.serial(size):
with T.sblock("load"):
vii = T.axis.remap("S", [i])
C[vii] = A[vii]
@T.prim_func(s_tir=True)
def sync_dma_load_impl(a: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (size), dtype, offset_factor=1, scope="global")
C = T.match_buffer(c, (size), dtype, offset_factor=1, scope="global.vtcm")
with T.sblock("root"):
T.reads(A[0:size])
T.writes(C[0:size])
T.evaluate(
T.tvm_call_packed(
"device_api.hexagon.dma_copy_dltensor",
T.tvm_stack_make_array(
T.address_of(C[0], dtype="handle"),
T.tvm_stack_make_shape(size, dtype="handle"),
0,
1,
C.dtype,
0,
dtype="handle",
),
T.tvm_stack_make_array(
T.address_of(A[0], dtype="handle"),
T.tvm_stack_make_shape(size, dtype="handle"),
0,
1,
A.dtype,
0,
dtype="handle",
),
T.cast(size, dtype="int"),
False, # Do not use experimental bypass mode.
dtype="int32",
)
)
return sync_dma_load_desc, sync_dma_load_impl
def generate_dot_product_32x4_u8u8i32(mem_scope="global"):
@T.prim_func(s_tir=True)
def dot_product_32x4_u8u8i32_desc(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (4,), "uint8", offset_factor=1, scope=mem_scope)
B = T.match_buffer(b, (32, 4), "uint8", offset_factor=1, scope=mem_scope)
C = T.match_buffer(c, (32,), "int32", offset_factor=1, scope=mem_scope)
with T.sblock("root"):
T.reads(C[0:32], A[0:4], B[0:32, 0:4])
T.writes(C[0:32])
for i in T.serial(0, 32):
for k in T.serial(0, 4):
with T.sblock("update"):
vi, vk = T.axis.remap("SR", [i, k])
C[vi] = C[vi] + T.cast(A[vk], "int32") * T.cast(B[vi, vk], "int32")
@T.prim_func(s_tir=True)
def dot_product_32x4_u8u8i32_vrmpy(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (4,), "uint8", offset_factor=1, scope=mem_scope)
B = T.match_buffer(b, (32, 4), "uint8", offset_factor=1, scope=mem_scope)
C = T.match_buffer(c, (32,), "int32", offset_factor=1, scope=mem_scope)
with T.sblock("root"):
T.reads(C[0:32], A[0:4], B[0:32, 0:4])
T.writes(C[0:32])
A_u8x4 = A.vload([0], "uint8x4")
A_i32 = T.reinterpret(A_u8x4, dtype="int32")
B_i8x128 = B.vload([0, 0], dtype="uint8x128")
B_i32x32 = T.reinterpret(B_i8x128, dtype="int32x32")
C[T.ramp(T.int32(0), 1, 32)] = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.hexagon.V6.vrmpyub.acc.128B"),
C[T.ramp(T.int32(0), 1, 32)],
B_i32x32,
A_i32,
dtype="int32x32",
)
return dot_product_32x4_u8u8i32_desc, dot_product_32x4_u8u8i32_vrmpy
def generate_dot_product_32x4_u8i8i32(mem_scope="global"):
@T.prim_func(s_tir=True)
def dot_product_32x4_u8i8i32_desc(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (4,), "uint8", offset_factor=1, scope=mem_scope)
B = T.match_buffer(b, (32, 4), "int8", offset_factor=1, scope=mem_scope)
C = T.match_buffer(c, (32,), "int32", offset_factor=1, scope=mem_scope)
with T.sblock("root"):
T.reads(C[0:32], A[0:4], B[0:32, 0:4])
T.writes(C[0:32])
for i in T.serial(0, 32):
for k in T.serial(0, 4):
with T.sblock("update"):
vi, vk = T.axis.remap("SR", [i, k])
C[vi] = C[vi] + T.cast(A[vk], "int32") * T.cast(B[vi, vk], "int32")
@T.prim_func(s_tir=True)
def dot_product_32x4_u8i8i32_vrmpy(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (4,), "uint8", offset_factor=1, scope=mem_scope)
B = T.match_buffer(b, (32, 4), "int8", offset_factor=1, scope=mem_scope)
C = T.match_buffer(c, (32,), "int32", offset_factor=1, scope=mem_scope)
with T.sblock("root"):
T.reads(C[0:32], A[0:4], B[0:32, 0:4])
T.writes(C[0:32])
A_u8x4 = A.vload([0], "uint8x4")
A_i32 = T.reinterpret(A_u8x4, dtype="int32")
B_i8x128 = B.vload([0, 0], dtype="int8x128")
B_i32x32 = T.reinterpret(B_i8x128, dtype="int32x32")
C[T.ramp(T.int32(0), 1, 32)] = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.hexagon.V6.vrmpybusv.acc.128B"),
C[T.ramp(T.int32(0), 1, 32)],
T.broadcast(A_i32, 32),
B_i32x32,
dtype="int32x32",
)
return dot_product_32x4_u8i8i32_desc, dot_product_32x4_u8i8i32_vrmpy
def generate_dot_product_32x2_i16i16i32(mem_scope="global"):
@T.prim_func(s_tir=True)
def dot_product_32x2_i16i16i32_desc(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (2,), "int16", offset_factor=1, scope=mem_scope)
B = T.match_buffer(b, (32, 2), "int16", offset_factor=1, scope=mem_scope)
C = T.match_buffer(c, (32,), "int32", offset_factor=1, scope=mem_scope)
with T.sblock("root"):
T.reads(C[0:32], A[0:2], B[0:32, 0:2])
T.writes(C[0:32])
for i in T.serial(0, 32):
for k in T.serial(0, 2):
with T.sblock("update"):
vi, vk = T.axis.remap("SR", [i, k])
C[vi] = C[vi] + T.cast(A[vk], "int32") * T.cast(B[vi, vk], "int32")
@T.prim_func(s_tir=True)
def dot_product_32x2_i16i16i32_vdmpy(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (2,), "int16", offset_factor=1, scope=mem_scope)
B = T.match_buffer(b, (32, 2), "int16", offset_factor=1, scope=mem_scope)
C = T.match_buffer(c, (32,), "int32", offset_factor=1, scope=mem_scope)
with T.sblock("root"):
T.reads(C[0:32], A[0:2], B[0:32, 0:2])
T.writes(C[0:32])
A_i16x2 = A.vload([0], "int16x2")
A_i32 = T.reinterpret(A_i16x2, dtype="int32")
B_i16x64 = B.vload([0, 0], dtype="int16x64")
B_i32x32 = T.reinterpret(B_i16x64, dtype="int32x32")
C[T.ramp(T.int32(0), 1, 32)] = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.hexagon.V6.vdmpyhvsat.acc.128B"),
C[T.ramp(T.int32(0), 1, 32)],
T.Broadcast(A_i32, 32),
B_i32x32,
dtype="int32x32",
)
return dot_product_32x2_i16i16i32_desc, dot_product_32x2_i16i16i32_vdmpy
VRMPY_u8u8i32_INTRIN = "dot_32x4_u8u8i32_vrmpy"
TensorIntrin.register(VRMPY_u8u8i32_INTRIN, *generate_dot_product_32x4_u8u8i32())
VRMPY_u8i8i32_INTRIN = "dot_32x4_u8i8i32_vrmpy"
TensorIntrin.register(VRMPY_u8i8i32_INTRIN, *generate_dot_product_32x4_u8i8i32())
VDMPY_i16i16i32_INTRIN = "dot_product_32x2_i16i16i32_vdmpy"
TensorIntrin.register(VDMPY_i16i16i32_INTRIN, *generate_dot_product_32x2_i16i16i32())
VRMPY_u8u8i32_VTCM_INTRIN = "dot_32x4_u8u8i32_vtcm_vrmpy"
TensorIntrin.register(VRMPY_u8u8i32_VTCM_INTRIN, *generate_dot_product_32x4_u8u8i32("global.vtcm"))
VRMPY_u8i8i32_VTCM_INTRIN = "dot_32x4_u8i8i32_vtcm_vrmpy"
TensorIntrin.register(VRMPY_u8i8i32_VTCM_INTRIN, *generate_dot_product_32x4_u8i8i32("global.vtcm"))
DMA_READ_128_u8 = "dma_read_128_u8"
TensorIntrin.register(DMA_READ_128_u8, *generate_dma_load_intrin(128, "uint8"))
DMA_READ_128_i8 = "dma_read_128_i8"
TensorIntrin.register(DMA_READ_128_i8, *generate_dma_load_intrin(128, "int8"))
+351
View File
@@ -0,0 +1,351 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name,missing-function-docstring,unused-variable
"""Intrinsics for tensorization on Apple GPU."""
from typing import Literal
from tvm.script import tirx as T
from tvm.tirx import Buffer, Expr, PrimFunc, TensorIntrin
######## simdgroup matrix intrinsics ########
def get_simdgroup_index(buffer: Buffer, stride: Expr, col: int, row: int):
"""Compute simdgroup index using elem_offset of the buffer"""
# NOTE: Need further check the usage between `col`` and `row`
# Currently, Metal only supports 8x8, which means the values of `col` and `row` are the same
frag_index_m = buffer.elem_offset // stride // col
frag_index_n = buffer.elem_offset % stride // row
num_fragments_per_row = stride // row
return frag_index_m * num_fragments_per_row + frag_index_n
def get_make_filled_simdgroup_matrix_intrin(
dtype: str, col: int = 8, row: int = 8
) -> tuple[PrimFunc, PrimFunc]:
@T.prim_func(s_tir=True)
def desc(a: T.handle) -> None:
A = T.match_buffer(a, (col, row), dtype, scope="metal.simdgroup", offset_factor=1)
with T.sblock("root"):
T.reads()
T.writes(A[0:col, 0:row])
for i, j in T.grid(col, row):
with T.sblock("init"):
vi, vj = T.axis.remap("SS", [i, j])
A[vi, vj] = T.float32(0)
@T.prim_func(s_tir=True)
def impl(a: T.handle) -> None:
d0, d1 = T.int32(), T.int32()
A = T.match_buffer(
a, (col, row), dtype, scope="metal.simdgroup", strides=[d1, d0], offset_factor=1
)
with T.sblock("root"):
T.reads()
T.writes(A[0:col, 0:row])
T.metal.make_filled_simdgroup_matrix(
A.data,
index=get_simdgroup_index(A, d1, col, row),
value=T.float32(0),
col=col,
row=row,
)
return desc, impl
def get_simdgroup_load_intrin(
dtype: str,
scope: Literal["global", "shared"],
col: int = 8,
row: int = 8,
transpose_matrix: bool = False,
) -> tuple[PrimFunc, PrimFunc]:
align = col * row
@T.prim_func(s_tir=True)
def desc(a: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (col, row), dtype, align=align, scope=scope, offset_factor=1)
C = T.match_buffer(
c, (col, row), dtype, align=align, scope="metal.simdgroup", offset_factor=1
)
with T.sblock("root"):
T.reads(A[0:col, 0:row])
T.writes(C[0:col, 0:row])
for i, j in T.grid(col, row):
with T.sblock("load"):
vii, vjj = T.axis.remap("SS", [i, j])
if transpose_matrix:
# C[vii, vjj] = A[vjj, vii]
C[vjj, vii] = A[vii, vjj]
else:
C[vii, vjj] = A[vii, vjj]
@T.prim_func(s_tir=True)
def impl(a: T.handle, c: T.handle) -> None:
s0, s1, d0, d1 = T.int32(), T.int32(), T.int32(), T.int32()
A = T.match_buffer(
a,
(col, row),
dtype,
align=align,
scope=scope,
strides=[s1, s0],
offset_factor=1,
)
C = T.match_buffer(
c,
(col, row),
dtype,
align=align,
scope="metal.simdgroup",
strides=[d1, d0],
offset_factor=1,
)
with T.sblock("root"):
T.reads(A[0:col, 0:row])
T.writes(C[0:col, 0:row])
T.metal.simdgroup_load(
C.data,
index=get_simdgroup_index(C, d1, col, row),
ptr=A.access_ptr("r", ptr_type=dtype),
stride=s1,
col=col,
row=row,
transpose_matrix=transpose_matrix,
)
return desc, impl
def get_simdgroup_store_intrin(
dtype: str,
scope: Literal["global", "shared"],
col: int = 8,
row: int = 8,
transpose_matrix: bool = False,
) -> tuple[PrimFunc, PrimFunc]:
align = col * row
@T.prim_func(s_tir=True)
def desc(a: T.handle, c: T.handle) -> None:
A = T.match_buffer(
a, (col, row), dtype, align=align, scope="metal.simdgroup", offset_factor=1
)
C = T.match_buffer(c, (col, row), dtype, align=align, scope=scope, offset_factor=1)
with T.sblock("root"):
T.reads(A[0:col, 0:row])
T.writes(C[0:col, 0:row])
for i, j in T.grid(col, row):
with T.sblock("store"):
vii, vjj = T.axis.remap("SS", [i, j])
if transpose_matrix:
C[vjj, vii] = A[vii, vjj]
else:
C[vii, vjj] = A[vii, vjj]
@T.prim_func(s_tir=True)
def impl(a: T.handle, c: T.handle) -> None:
s0, s1, d0, d1 = T.int32(), T.int32(), T.int32(), T.int32()
A = T.match_buffer(
a,
(col, row),
dtype,
align=align,
scope="metal.simdgroup",
strides=[s1, s0],
offset_factor=1,
)
C = T.match_buffer(
c, (col, row), dtype, align=align, scope=scope, strides=[d1, d0], offset_factor=1
)
with T.sblock("root"):
T.reads(A[0:col, 0:row])
T.writes(C[0:col, 0:row])
T.metal.simdgroup_store(
A.data,
index=get_simdgroup_index(A, s1, col, row),
ptr=C.access_ptr("w", ptr_type=dtype),
stride=d1,
col=col,
row=row,
transpose_matrix=transpose_matrix,
)
return desc, impl
def get_simdgroup_multiply_accumulate_intrin(
m_dim: int, n_dim: int, k_dim: int, dtype: str
) -> tuple[PrimFunc, PrimFunc]:
@T.prim_func(s_tir=True)
def desc(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (m_dim, k_dim), dtype, scope="metal.simdgroup", offset_factor=1)
B = T.match_buffer(b, (k_dim, n_dim), dtype, scope="metal.simdgroup", offset_factor=1)
C = T.match_buffer(c, (m_dim, n_dim), dtype, scope="metal.simdgroup", offset_factor=1)
with T.sblock("root"):
T.reads(C[0:m_dim, 0:n_dim], A[0:m_dim, 0:k_dim], B[0:k_dim, 0:n_dim])
T.writes(C[0:m_dim, 0:n_dim])
for i, j, k in T.grid(m_dim, n_dim, k_dim):
with T.sblock(""):
vii, vjj, vkk = T.axis.remap("SSR", [i, j, k])
C[vii, vjj] += A[vii, vkk] * B[vkk, vjj]
@T.prim_func(s_tir=True)
def impl(a: T.handle, b: T.handle, c: T.handle) -> None:
a0, a1, b0, b1, c0, c1 = T.int32(), T.int32(), T.int32(), T.int32(), T.int32(), T.int32()
A = T.match_buffer(
a, (m_dim, k_dim), dtype, scope="metal.simdgroup", strides=[a1, a0], offset_factor=1
)
B = T.match_buffer(
b, (k_dim, n_dim), dtype, scope="metal.simdgroup", strides=[b1, b0], offset_factor=1
)
C = T.match_buffer(
c, (m_dim, n_dim), dtype, scope="metal.simdgroup", strides=[c1, c0], offset_factor=1
)
with T.sblock("root"):
T.reads(C[0:m_dim, 0:n_dim], A[0:m_dim, 0:k_dim], B[0:k_dim, 0:n_dim])
T.writes(C[0:m_dim, 0:n_dim])
T.metal.simdgroup_multiply_accumulate(
C.data,
get_simdgroup_index(C, c1, m_dim, n_dim),
A.data,
get_simdgroup_index(A, a1, m_dim, k_dim),
B.data,
get_simdgroup_index(B, b1, k_dim, n_dim),
C.data,
get_simdgroup_index(C, c1, m_dim, n_dim),
)
return desc, impl
# Make filled simdgroup matrix intrinsics
SIMDGROUP_MAKE_FILLED_8x8x8_f16_INTRIN = "simdgroup_make_filled_8x8x8_f16"
TensorIntrin.register(
SIMDGROUP_MAKE_FILLED_8x8x8_f16_INTRIN,
*get_make_filled_simdgroup_matrix_intrin("float16", 8, 8),
)
SIMDGROUP_FILLED_8x8x8_f32_INTRIN = "simdgroup_fill_8x8x8_f32"
TensorIntrin.register(
SIMDGROUP_FILLED_8x8x8_f32_INTRIN, *get_make_filled_simdgroup_matrix_intrin("float32", 8, 8)
)
SIMDGROUP_FILLED_8x8x8_bf16_INTRIN = "simdgroup_fill_8x8x8_bf16"
TensorIntrin.register(
SIMDGROUP_FILLED_8x8x8_bf16_INTRIN, *get_make_filled_simdgroup_matrix_intrin("bfloat16", 8, 8)
)
# Load intrinsics
SIMDGROUP_LOAD_8x8x8_f16_SHARED_INTRIN = "simdgroup_load_8x8x8_f16_shared"
TensorIntrin.register(
SIMDGROUP_LOAD_8x8x8_f16_SHARED_INTRIN,
*get_simdgroup_load_intrin("float16", "shared", 8, 8, False),
)
SIMDGROUP_LOAD_8x8x8_f16_SHARED_TRANS_INTRIN = "simdgroup_load_8x8x8_f16_shared_trans"
TensorIntrin.register(
SIMDGROUP_LOAD_8x8x8_f16_SHARED_TRANS_INTRIN,
*get_simdgroup_load_intrin("float16", "shared", 8, 8, True),
)
# Store intrinsics
SIMDGROUP_STORE_8x8x8_f16_GLOBAL_INTRIN = "simdgroup_store_8x8x8_f16_global"
TensorIntrin.register(
SIMDGROUP_STORE_8x8x8_f16_GLOBAL_INTRIN,
*get_simdgroup_store_intrin("float16", "global", 8, 8, False),
)
SIMDGROUP_STORE_8x8x8_f16_SHARED_INTRIN = "simdgroup_store_8x8x8_f16_shared"
TensorIntrin.register(
SIMDGROUP_STORE_8x8x8_f16_SHARED_INTRIN,
*get_simdgroup_store_intrin("float16", "shared", 8, 8, False),
)
# Multiply accumulate intrinsics
SIMDGROUP_MULTI_ACC_8x8x8_f16_INTRIN = "simdgroup_multiply_accumulate_8x8x8_f16"
TensorIntrin.register(
SIMDGROUP_MULTI_ACC_8x8x8_f16_INTRIN,
*get_simdgroup_multiply_accumulate_intrin(8, 8, 8, "float16"),
)
def get_simdgroup_intrin_group(
load_scope: Literal["shared"],
store_scope: Literal["global", "shared"],
dtype: str,
trans_a: bool = False,
trans_b: bool = False,
) -> dict[str, str]:
"""Get a group of intrinsics for tensorization on Apple GPU.
Parameters
----------
load_scope : Literal["shared"]
The memory scope of the input buffer.
store_scope : Literal["global", "shared"]
The memory scope of the result buffer.
dtype : str
The data type of the input and output buffers.
trans_a : bool
Whether the input matrix A is transposed.
trans_b : bool
Whether the input matrix B is transposed.
Returns
-------
ret : Dict[str, str]
A group of tensor intrinsics.
"""
assert load_scope in ["shared"]
assert store_scope in ["global", "shared"]
assert dtype in ["float16", "bfloat16", "float32"]
shape = "8x8x8"
dtype = "f16" if dtype == "float16" else "bf16" if dtype == "bfloat16" else "f32"
trans_a = "_trans" if trans_a else ""
trans_b = "_trans" if trans_b else ""
# e.g. simdgroup_load_8x8x8_f16_shared
load_a_intrin = f"simdgroup_load_{shape}_{dtype}_{load_scope}{trans_a}"
# e.g. simdgroup_load_8x8x8_f16_shared_trans
load_b_intrin = f"simdgroup_load_{shape}_{dtype}_{load_scope}{trans_b}"
# e.g. simdgroup_multiply_accumulate_8x8x8_f16
compute_intrin = f"simdgroup_multiply_accumulate_{shape}_{dtype}"
# e.g. simdgroup_make_filled_8x8x8_f16
init_intrin = f"simdgroup_make_filled_{shape}_{dtype}"
# e.g. simdgroup_store_8x8x8_f16_global
store_intrin = f"simdgroup_store_{shape}_{dtype}_{store_scope}"
return {
"init": init_intrin,
"load_a": load_a_intrin,
"load_b": load_b_intrin,
"compute": compute_intrin,
"store": store_intrin,
}
+241
View File
@@ -0,0 +1,241 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name,line-too-long
# ruff: noqa: E501
"""Intrinsics for RISCV tensorization"""
import logging
import tvm_ffi
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.target.codegen import Target, llvm_get_vector_width, target_has_features
from .. import TensorIntrin
logger = logging.getLogger(__name__)
def get_max_elems(vlen: int, lmul: int, sew: int) -> int:
"""Returns number of elements of a given data type (SEW)
that fits multiple (LMUL) of the vector registers (VLEN).
Args:
vlen (int): VLEN vector length in bits
lmul (int): LMUL vector lenght multiplier
sew (int): SEW standard (single) element width
Returns:
int: Number of elements
"""
return (vlen // sew) * lmul
def rvv_vec_dot_product_kernels(
n_elems: int,
n_lanes: int,
data_dtype: str,
weight_dtype: str,
out_dtype: str,
lmul: int,
):
"""Dot product of vector and matrix rows using RISC-V vector instructions.
These kernels takes two arrays A[ELEMS] and B[ELEMS][MACS] and computes
dot product of A[ELEMS] with each row of B[LANES], accumulating results
with C[LANES].
The pseudo code is as follows:
.. code-block:: c
void vec_dot_prod(A[ELEMS], B[LANES][ELEMS], C[LANES]){
for (j = 0; j < LANES; j++) {
for (k = 0; k < ELEMS; k++) {
C[j] += A[k] * B[j][k]
}
}
}
"""
@T.prim_func(s_tir=True)
def rvv_vec_dot_prod_desc(
A: T.Buffer((n_elems,), data_dtype, offset_factor=1),
B: T.Buffer((n_lanes, n_elems), weight_dtype, offset_factor=1),
C: T.Buffer((n_lanes,), out_dtype, offset_factor=1),
) -> None:
with T.sblock("root"):
T.reads(C[0:n_lanes], A[0:n_elems], B[0:n_lanes, 0:n_elems])
T.writes(C[0:n_lanes])
for j in T.serial(0, n_lanes):
for k in T.serial(0, n_elems):
with T.sblock("update"):
vj, vk = T.axis.remap("SR", [j, k])
C[vj] = C[vj] + T.cast(A[vk], out_dtype) * T.cast(B[vj, vk], out_dtype)
# LLVM only supports ELEN=32 or ELEN=64
# https://llvm.org/docs//RISCV/RISCVVectorExtension.html
d_dtype_lanes = (64 // DataType(data_dtype).bits) * lmul
w_dtype_lanes = (64 // DataType(weight_dtype).bits) * lmul
# reduction lanes narrows
o_dtype_lanes = (64 // DataType(out_dtype).bits) * lmul // n_lanes
# data type widening case
o_dtype_lanes = max(o_dtype_lanes, 2)
mask_args = () if data_dtype[0] in ("i", "u") else (T.uint64(7),)
wide_dtype = out_dtype
if DataType(out_dtype).bits > DataType(data_dtype).bits:
wide_dtype = "".join(c for c in data_dtype if not c.isdigit())
wide_dtype += str(DataType(data_dtype).bits * 2)
# fmt: off
@T.prim_func(s_tir=True)
def rvv_vec_dot_prod_impl(
A: T.Buffer((n_elems,), data_dtype, offset_factor=1),
B: T.Buffer((n_lanes, n_elems), weight_dtype, offset_factor=1),
C: T.Buffer((n_lanes,), out_dtype, offset_factor=1),
) -> None:
with T.sblock("root"):
T.reads(C[0:n_lanes], A[0:n_elems], B[0:n_lanes, 0:n_elems])
T.writes(C[0:n_lanes])
vec_A = T.call_llvm_intrin(
f"{data_dtype}xvscalex{d_dtype_lanes}",
"llvm.riscv.vle",
T.broadcast(T.Cast(data_dtype, 0), T.vscale() * d_dtype_lanes),
T.tvm_access_ptr(T.type_annotation(data_dtype), A.data, 0, n_elems, 1),
T.int64(n_elems))
for i in range(n_lanes):
with T.sblock("update"):
T.reads(B[i, 0:n_elems])
T.writes(C[i])
vec_B_row = T.call_llvm_intrin(
f"{weight_dtype}xvscalex{w_dtype_lanes}",
"llvm.riscv.vle",
T.broadcast(T.Cast(data_dtype, 0), T.vscale() * w_dtype_lanes),
T.tvm_access_ptr(T.type_annotation(weight_dtype), B.data, i * n_elems, n_elems, 1),
T.int64(n_elems))
product = T.call_llvm_intrin(
f"{wide_dtype}xvscalex{w_dtype_lanes}",
"llvm.riscv.vfmul" if out_dtype[0] == "f" else \
"llvm.riscv.vwmulsu" if (data_dtype[0] != weight_dtype[0]) else \
"llvm.riscv.vwmul",
T.broadcast(T.Cast(wide_dtype, 0), T.vscale() * w_dtype_lanes),
vec_B_row,
vec_A,
*mask_args,
T.uint64(n_elems))
ini_acc = T.call_llvm_intrin(
f"{out_dtype}xvscalex{o_dtype_lanes}",
"llvm.riscv.vle",
T.broadcast(T.Cast(out_dtype, 0), T.vscale() * o_dtype_lanes),
T.tvm_access_ptr(T.type_annotation(out_dtype), C.data, i, 1, 1),
T.int64(1))
red_sum = T.call_llvm_intrin(
f"{out_dtype}xvscalex{o_dtype_lanes}",
"llvm.riscv.vfredusum" if out_dtype[0] == "f" else \
"llvm.riscv.vwredsum",
T.broadcast(T.Cast(out_dtype, 0), T.vscale() * o_dtype_lanes),
product,
ini_acc,
*mask_args,
T.uint64(n_elems))
C[i] = T.call_llvm_intrin(
out_dtype,
"llvm.riscv.vfmv.f.s" if out_dtype[0] == "f" else \
"llvm.riscv.vmv.x.s",
red_sum)
# fmt: on
return rvv_vec_dot_prod_desc, rvv_vec_dot_prod_impl
@tvm_ffi.register_global_func("tirx.tensor_intrin.register_rvv_isa_intrinsics")
def register_rvv_isa_intrinsics(target: Target, inventory_only=False) -> dict():
"""Register RISCV V (vector) intrinsics
[x] Implementation follows version 1.0 vector specifications:
https://github.com/riscvarchive/riscv-v-spec/releases/tag/v1.0
Args:
target (Target): TVM target
inventory_only (bool): No registration inventory only
Returns:
dict(): A catalog with registered kernel names and properties
"""
if not target_has_features("v", target):
raise RuntimeError("Current target does not support `v` extension.")
vlen = llvm_get_vector_width(target)
# get maximum reduction lanes (without grouping)
n_lanes = get_max_elems(vlen, lmul=1, sew=32)
kernels_inventory = {}
data_dtype = ["uint8", "int8", "float16", "float32"]
weight_dtype = ["int8", "int8", "float16", "float32"]
output_dtype = ["int32", "int32", "float16", "float32"]
for d_dtype, w_dtype, o_dtype in zip(data_dtype, weight_dtype, output_dtype):
# max elements to grouped registers
max_elems = get_max_elems(vlen, lmul=8, sew=DataType(d_dtype).bits)
# data widening halves available vector registers
if DataType(o_dtype).bits > DataType(d_dtype).bits:
max_elems //= 2
# compute optimal LMUL for full load
lmul = max_elems // (vlen // DataType(d_dtype).bits)
n_elems = max_elems
while n_elems >= 4:
dt = DataType(d_dtype)
wt = DataType(w_dtype)
ot = DataType(o_dtype)
kernel_name = "rvv_dot"
kernel_name += f"_{n_elems}{dt[0]}{dt.bits}"
kernel_name += f"_{n_lanes}x{n_elems}{wt[0]}{wt.bits}"
kernel_name += f"_{n_lanes}{ot[0]}{ot.bits}"
kernels_inventory[kernel_name] = n_elems
if not inventory_only:
logger.debug(f"Registering kernel {kernel_name}")
desc, impl = rvv_vec_dot_product_kernels(
n_elems, n_lanes, d_dtype, w_dtype, o_dtype, lmul
)
TensorIntrin.register(kernel_name, desc, impl, override=True)
n_elems //= 2
return kernels_inventory
def register_riscv_intrinsics(target: Target):
"""Register RISCV intrinsics
Args:
target (Target): TVM target
"""
# RISCV `v` 1.0 extension templates
_ = register_rvv_isa_intrinsics(target)
logger.debug("Finished registering riscv intrinsics.")
+477
View File
@@ -0,0 +1,477 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name,missing-function-docstring
"""Intrinsics for AMDGPU tensorization."""
from tvm.runtime import convert
from tvm.script import tirx as T
from tvm.tirx.expr import Cast, IntImm
from .. import TensorIntrin
from .dot_product_common import get_dp4a_intrin
lift = convert
@T.prim_func(s_tir=True)
def sdot4(
A: T.Buffer((4,), "int8", offset_factor=1, align=4, scope="shared"),
B: T.Buffer((4,), "int8", offset_factor=1, align=4, scope="shared"),
C: T.Buffer((1,), "int32", offset_factor=1, align=4, scope="local"),
) -> None:
with T.sblock("root"):
T.reads(C[0], A[0:4], B[0:4])
T.writes(C[0])
C[0] += T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.amdgcn.sdot4"),
T.reinterpret(A.vload([0], "int8x4"), dtype="int32"),
T.reinterpret(B.vload([0], "int8x4"), dtype="int32"),
T.int32(0),
T.bool(1),
dtype="int32",
)
AMDGPU_SDOT4_INTRIN = "sdot4"
dp4a_desc, _ = get_dp4a_intrin("int8", "int8", "int32")
TensorIntrin.register(AMDGPU_SDOT4_INTRIN, dp4a_desc, sdot4)
WARP_SIZE = 64
M_DIM = 16
N_DIM = 16
def shared_16x4_to_local_64x1_layout_A(i, j):
thread_id = j * 16 + i
return thread_id, convert(0)
def thread_id_shared_access_64x1_to_16x4_layout_A(thread_id, local_id):
i = thread_id % 16
j = thread_id // 16 + local_id
return i, j
def shared_4x16_to_local_64x1_layout_B(i, j):
thread_id = i * 16 + j
return thread_id, convert(0)
def thread_id_shared_access_64x1_to_4x16_layout_B(thread_id, local_id):
i = thread_id // 16
j = thread_id % 16 + local_id
return i, j
def shared_16x16_to_local_64x4_layout_C(i, j):
thread_id = j + (i // 4) * 16
local = i % 4
return thread_id, local
def thread_id_shared_access_64x4_to_16x16_layout_A(thread_id, local_id):
i = thread_id % 16
j = (thread_id // 16) * 4 + local_id
return i, j
def shared_16x16_to_local_64x4_layout_A(i, j):
thread_id = i + 16 * (j // 4)
local = j % 4
return thread_id, local
def thread_id_shared_access_64x4_to_16x16_layout_B(thread_id, local_id):
i = local_id + (thread_id // 16) * 4
j = thread_id % 16
return i, j
def shared_16x16_to_local_64x4_layout_B(i, j):
thread_id = j + (i // 4) * 16
local = i % 4
return thread_id, local
def thread_id_shared_access_64x4_to_16x16_layout_C(thread_id, local_id):
i = local_id + (thread_id // 16) * 4
j = thread_id % 16
return i, j
def get_mma_fill_intrin(dtype, local_size):
zero = IntImm("int32", 0).astype(dtype)
# Assume M = N = 16
index_map = shared_16x16_to_local_64x4_layout_C
@T.prim_func(s_tir=True)
def mma_fill_desc(a: T.handle) -> None:
C_warp = T.match_buffer(a, [WARP_SIZE, local_size], dtype=dtype, scope="warp")
with T.sblock("root"):
T.reads()
T.writes(C_warp[0:WARP_SIZE, 0:local_size])
for i0, i1 in T.grid(M_DIM, N_DIM):
with T.sblock("C_warp"):
i, j = T.axis.remap("SS", [i0, i1])
thread_id, local_id = T.meta_var(index_map(i, j))
T.reads()
T.writes(C_warp[thread_id, local_id])
C_warp[thread_id, local_id] = zero
@T.prim_func(s_tir=True)
def mma_fill_impl(a: T.handle) -> None:
C_warp = T.match_buffer(
a, [WARP_SIZE, local_size], dtype=dtype, scope="warp", offset_factor=1
)
with T.sblock("root"):
T.reads()
T.writes(C_warp[0:WARP_SIZE, 0:local_size])
tx = T.env_thread("threadIdx.x")
T.launch_thread(tx, WARP_SIZE)
for local_id in T.serial(0, local_size):
C_warp[tx, local_id] = zero
return mma_fill_desc, mma_fill_impl
def get_mfma_load_intrin(
k_dim=4,
dtype="float32",
scope="shared",
is_b=False,
transposed=False,
):
local_size = (M_DIM * k_dim) // WARP_SIZE if not is_b else (N_DIM * k_dim) // WARP_SIZE
memory_shape = (M_DIM, k_dim)
if is_b:
memory_shape = (N_DIM, k_dim) if transposed else (k_dim, N_DIM)
row_dim, col_dim = memory_shape
if k_dim == 4:
index_map = shared_16x4_to_local_64x1_layout_A
reverse_index_map = thread_id_shared_access_64x1_to_16x4_layout_A
if is_b:
index_map = (
shared_16x4_to_local_64x1_layout_A
if transposed
else shared_4x16_to_local_64x1_layout_B
)
reverse_index_map = (
thread_id_shared_access_64x1_to_16x4_layout_A
if transposed
else thread_id_shared_access_64x1_to_4x16_layout_B
)
elif k_dim == 16:
index_map = shared_16x16_to_local_64x4_layout_A
reverse_index_map = thread_id_shared_access_64x4_to_16x16_layout_A
if is_b:
index_map = (
shared_16x16_to_local_64x4_layout_A
if transposed
else shared_16x16_to_local_64x4_layout_B
)
reverse_index_map = (
thread_id_shared_access_64x4_to_16x16_layout_A
if transposed
else thread_id_shared_access_64x4_to_16x16_layout_B
)
else:
raise ValueError("k_dim must be 4 or 16 currently")
@T.prim_func(s_tir=True)
def mfma_load_desc(reg_handle: T.handle, memory_handle: T.handle) -> None:
memory = T.match_buffer(
memory_handle,
memory_shape,
dtype,
offset_factor=1,
scope=scope,
)
reg = T.match_buffer(
reg_handle, (WARP_SIZE, local_size), dtype, offset_factor=1, scope="warp"
)
with T.sblock("root"):
T.reads(memory[0:row_dim, 0:col_dim])
T.writes(reg[0:WARP_SIZE, 0:local_size])
for ax0, ax1 in T.grid(row_dim, col_dim):
with T.sblock("memory_reg"):
v0, v1 = T.axis.remap("SS", [ax0, ax1])
T.reads(memory[v0, v1])
thread_id, local_id = T.meta_var(index_map(v0, v1))
T.writes(reg[thread_id, local_id])
reg[thread_id, local_id] = memory[v0, v1]
@T.prim_func(s_tir=True)
def mfma_load_impl(reg_handle: T.handle, memory_handle: T.handle) -> None:
s0 = T.int32()
s1 = T.int32()
memory = T.match_buffer(
memory_handle,
memory_shape,
dtype,
align=64,
offset_factor=1,
scope=scope,
strides=[s0, s1],
)
reg = T.match_buffer(
reg_handle, (WARP_SIZE, local_size), dtype, align=64, offset_factor=1, scope="warp"
)
with T.sblock("root"):
T.reads(memory[0:row_dim, 0:col_dim])
T.writes(reg[0:WARP_SIZE, 0:local_size])
tx = T.env_thread("threadIdx.x")
for local_id in T.serial(0, local_size):
row, col = T.meta_var(reverse_index_map(tx, local_id))
T.launch_thread(tx, WARP_SIZE)
reg[tx, local_id] = memory[row, col]
return mfma_load_desc, mfma_load_impl
def get_mfma_intrin(k_dim, in_dtype="float32", out_dtype="float32", b_transposed=False):
local_size = (M_DIM * k_dim) // WARP_SIZE
local_size_out = (M_DIM * N_DIM) // WARP_SIZE
if k_dim == 4:
index_map_A = shared_16x4_to_local_64x1_layout_A
index_map_B = shared_4x16_to_local_64x1_layout_B
index_map_C = shared_16x16_to_local_64x4_layout_C
elif k_dim == 16:
index_map_A = shared_16x16_to_local_64x4_layout_A
index_map_B = shared_16x16_to_local_64x4_layout_B
index_map_C = shared_16x16_to_local_64x4_layout_C
else:
raise ValueError("k_dim must be 4 or 16 currently")
out_dtype_abbrv = {"float16": "f16", "float32": "f32", "int8": "i8", "int32": "i32"}[out_dtype]
in_dtype_abbrv = {"float16": "f16", "float32": "f32", "int8": "i8", "int32": "i32"}[in_dtype]
mfma_intrin = f"llvm.amdgcn.mfma.{out_dtype_abbrv}.{M_DIM}x{N_DIM}x{k_dim}{in_dtype_abbrv}"
def maybe_cast(v):
if out_dtype != in_dtype:
return Cast(out_dtype, v)
return v
def maybe_swap(i, j):
if b_transposed:
return j, i
return i, j
@T.prim_func(s_tir=True)
def mfma_sync_desc(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (WARP_SIZE, local_size), in_dtype, offset_factor=1, scope="warp")
B = T.match_buffer(b, (WARP_SIZE, local_size), in_dtype, offset_factor=1, scope="warp")
C = T.match_buffer(c, (WARP_SIZE, local_size_out), out_dtype, offset_factor=1, scope="warp")
with T.sblock("root"):
T.reads(
C[0:WARP_SIZE, 0:local_size_out],
A[0:WARP_SIZE, 0:local_size],
B[0:WARP_SIZE, 0:local_size],
)
T.writes(C[0:WARP_SIZE, 0:local_size_out])
for i, j, k in T.grid(M_DIM, N_DIM, k_dim):
with T.sblock("C"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
b_row_ind, b_col_ind = T.meta_var(maybe_swap(vk, vj))
thread_id_C, local_id_C = T.meta_var(index_map_C(vi, vj))
thread_id_A, local_id_A = T.meta_var(index_map_A(vi, vk))
thread_id_B, local_id_B = T.meta_var(index_map_B(b_row_ind, b_col_ind))
T.reads(
C[thread_id_C, local_id_C],
A[thread_id_A, local_id_A],
B[thread_id_B, local_id_B],
)
T.writes(C[thread_id_C, local_id_C])
C[thread_id_C, local_id_C] += maybe_cast(
A[thread_id_A, local_id_A]
) * maybe_cast(B[thread_id_B, local_id_B])
@T.prim_func(s_tir=True)
def mfma_sync_impl_float(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (WARP_SIZE, local_size), in_dtype, offset_factor=1, scope="warp")
B = T.match_buffer(b, (WARP_SIZE, local_size), in_dtype, offset_factor=1, scope="warp")
C = T.match_buffer(c, (WARP_SIZE, local_size_out), out_dtype, offset_factor=1, scope="warp")
with T.sblock("root"):
T.reads(
A[0:WARP_SIZE, 0:local_size],
B[0:WARP_SIZE, 0:local_size],
C[0:WARP_SIZE, 0:local_size_out],
)
T.writes(C[0:WARP_SIZE, 0:local_size_out])
tx = T.env_thread("threadIdx.x")
T.launch_thread(tx, WARP_SIZE)
C[tx, 0:local_size_out] = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id(mfma_intrin),
A[tx, 0:local_size],
B[tx, 0:local_size],
C[tx, 0:local_size_out],
T.int32(0),
T.int32(0),
T.int32(0),
dtype=f"{out_dtype}x4",
)
@T.prim_func(s_tir=True)
def mfma_sync_impl_integer(a: T.handle, b: T.handle, c: T.handle) -> None:
A = T.match_buffer(a, (WARP_SIZE, local_size), in_dtype, offset_factor=1, scope="warp")
B = T.match_buffer(b, (WARP_SIZE, local_size), in_dtype, offset_factor=1, scope="warp")
C = T.match_buffer(c, (WARP_SIZE, local_size_out), out_dtype, offset_factor=1, scope="warp")
with T.sblock("root"):
T.reads(
A[0:WARP_SIZE, 0:local_size],
B[0:WARP_SIZE, 0:local_size],
C[0:WARP_SIZE, 0:local_size_out],
)
T.writes(C[0:WARP_SIZE, 0:local_size_out])
tx = T.env_thread("threadIdx.x")
T.launch_thread(tx, WARP_SIZE)
C[tx, 0:local_size_out] = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id(mfma_intrin),
T.call_intrin("int32", "tirx.reinterpret", A[tx, 0:local_size]),
T.call_intrin("int32", "tirx.reinterpret", A[tx, 0:local_size]),
C[tx, 0:local_size_out],
T.int32(0),
T.int32(0),
T.int32(0),
dtype=f"{out_dtype}x4",
)
return (
(mfma_sync_desc, mfma_sync_impl_integer)
if in_dtype == "int8"
else (mfma_sync_desc, mfma_sync_impl_float)
)
def get_mfma_store_intrin(local_size=4, dtype="float32", scope="global"):
index_map = shared_16x16_to_local_64x4_layout_C
@T.prim_func(s_tir=True)
def mfma_store_desc(a: T.handle, c: T.handle) -> None:
C_warp = T.match_buffer(a, [WARP_SIZE, local_size], dtype=dtype, scope="warp")
C = T.match_buffer(c, [M_DIM, N_DIM], dtype=dtype, scope=scope)
with T.sblock("root"):
T.reads(C_warp[0:WARP_SIZE, 0:local_size])
T.writes(C[0:M_DIM, 0:N_DIM])
for i0, i1 in T.grid(M_DIM, N_DIM):
with T.sblock("C_warp"):
v0, v1 = T.axis.remap("SS", [i0, i1])
thread_id, local_id = T.meta_var(index_map(v0, v1))
T.reads(C_warp[thread_id, local_id])
T.writes(C[v0, v1])
C[v0, v1] = C_warp[thread_id, local_id]
@T.prim_func(s_tir=True)
def mfma_store_impl(a: T.handle, c: T.handle) -> None:
s0 = T.int32()
s1 = T.int32()
C_warp = T.match_buffer(
a, [WARP_SIZE, local_size], dtype=dtype, scope="warp", offset_factor=1
)
C = T.match_buffer(
c, [M_DIM, N_DIM], dtype=dtype, scope=scope, offset_factor=1, strides=[s0, s1]
)
with T.sblock("root"):
T.reads(C_warp[0:WARP_SIZE, 0:local_size])
T.writes(C[0:M_DIM, 0:N_DIM])
tx = T.env_thread("threadIdx.x")
T.launch_thread(tx, WARP_SIZE)
for i in range(local_size):
C[((tx // 16) * 4) + i, (tx % 16)] = C_warp[tx, i]
return mfma_store_desc, mfma_store_impl
ROCM_MFMA_fill_16x16_f32_INTRIN = "ROCM_mfma_fill_16x16_f32"
TensorIntrin.register(ROCM_MFMA_fill_16x16_f32_INTRIN, *get_mma_fill_intrin("float32", 4))
ROCM_MFMA_fill_16x16_i32_INTRIN = "ROCM_mfma_fill_16x16_i32"
TensorIntrin.register(ROCM_MFMA_fill_16x16_i32_INTRIN, *get_mma_fill_intrin("int", 4))
ROCM_MFMA_LOAD_16x16_A_SHARED_s8_INTRIN = "rocm_mfma_load_16x16_a_shared_s8"
TensorIntrin.register(
ROCM_MFMA_LOAD_16x16_A_SHARED_s8_INTRIN, *get_mfma_load_intrin(16, "int8", "shared")
)
ROCM_MFMA_LOAD_16x16_B_SHARED_s8_INTRIN = "rocm_mfma_load_b_16x16_shared_s8"
TensorIntrin.register(
ROCM_MFMA_LOAD_16x16_B_SHARED_s8_INTRIN, *get_mfma_load_intrin(16, "int8", "shared", is_b=True)
)
ROCM_MFMA_LOAD_16x16_A_SHARED_f16_INTRIN = "rocm_mfma_load_16x16_a_shared_f16"
TensorIntrin.register(
ROCM_MFMA_LOAD_16x16_A_SHARED_f16_INTRIN, *get_mfma_load_intrin(16, "float16", "shared")
)
ROCM_MFMA_LOAD_16x16_B_SHARED_f16_INTRIN = "rocm_mfma_load_b_16x16_shared_f16"
TensorIntrin.register(
ROCM_MFMA_LOAD_16x16_B_SHARED_f16_INTRIN,
*get_mfma_load_intrin(16, "float16", "shared", is_b=True),
)
ROCM_MFMA_LOAD_16x4_A_SHARED_f32_INTRIN = "rocm_mfma_load_16x4_a_shared_f32"
TensorIntrin.register(
ROCM_MFMA_LOAD_16x4_A_SHARED_f32_INTRIN, *get_mfma_load_intrin(4, "float32", "shared")
)
ROCM_MFMA_LOAD_16x4_B_SHARED_f32_INTRIN = "rocm_mfma_load_b_16x4_shared_f32"
TensorIntrin.register(
ROCM_MFMA_LOAD_16x4_B_SHARED_f32_INTRIN,
*get_mfma_load_intrin(4, "float32", "shared", is_b=True),
)
ROCM_MFMA_f32f32f32_INTRIN = "rocm_mfma_f32f32f32"
TensorIntrin.register(ROCM_MFMA_f32f32f32_INTRIN, *get_mfma_intrin(4, "float32", "float32"))
ROCM_MFMA_f16f16f32_INTRIN = "rocm_mfma_f16f16f32"
TensorIntrin.register(ROCM_MFMA_f16f16f32_INTRIN, *get_mfma_intrin(16, "float16", "float32"))
ROCM_MFMA_s8s8s32_INTRIN = "rocm_mfma_s8s8s32"
TensorIntrin.register(ROCM_MFMA_s8s8s32_INTRIN, *get_mfma_intrin(16, "int8", "int32"))
ROCM_MFMA_STORE_16x16_s32_INTRIN = "rocm_mfma_store_16x16_s32"
TensorIntrin.register(
ROCM_MFMA_STORE_16x16_s32_INTRIN, *get_mfma_store_intrin(4, "int32", "global")
)
ROCM_MFMA_STORE_16x16_f32_INTRIN = "rocm_mfma_store_16x16_f32"
TensorIntrin.register(
ROCM_MFMA_STORE_16x16_f32_INTRIN, *get_mfma_store_intrin(4, "float32", "global")
)
+112
View File
@@ -0,0 +1,112 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name,missing-function-docstring
"""Intrinsics for x86 tensorization."""
from tvm.script import tirx as T
from .. import TensorIntrin
# Tensorized intrinsic description and VNNI-specific implementation.
# Equivalent to the ones in topi/x86/tensor_intrin.py
@T.prim_func(s_tir=True)
def dot_product_16x4_u8i8i32_desc(
A: T.Buffer((4,), "uint8", offset_factor=1),
B: T.Buffer((16, 4), "int8", offset_factor=1),
C: T.Buffer((16,), "int32", offset_factor=1),
) -> None:
with T.sblock("root"):
T.reads(C[0:16], A[0:4], B[0:16, 0:4])
T.writes(C[0:16])
for i in T.serial(0, 16):
for k in T.serial(0, 4):
with T.sblock("update"):
vi, vk = T.axis.remap("SR", [i, k])
C[vi] = C[vi] + T.cast(A[vk], "int32") * T.cast(B[vi, vk], "int32")
@T.prim_func(s_tir=True)
def dot_product_16x4_u8i8i32_vnni(
A: T.Buffer((4,), "uint8", offset_factor=1),
B: T.Buffer((16, 4), "int8", offset_factor=1),
C: T.Buffer((16,), "int32", offset_factor=1),
) -> None:
with T.sblock("root"):
T.reads(C[0:16], A[0:4], B[0:16, 0:4])
T.writes(C[0:16])
A_u8x4 = A.vload([0], "uint8x4")
A_i32 = T.reinterpret(A_u8x4, dtype="int32")
B_i8x64 = B.vload([0, 0], dtype="int8x64")
B_i32x16 = T.reinterpret(B_i8x64, dtype="int32x16")
C_i32x16 = C.vload([0], dtype="int32x16")
C[T.ramp(T.int32(0), 1, 16)] = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.x86.avx512.vpdpbusd.512"),
C_i32x16,
T.broadcast(A_i32, 16),
B_i32x16,
dtype="int32x16",
)
@T.prim_func(s_tir=True)
def dot_product_16x4_u8i8i32_avx512(
A: T.Buffer((4,), "uint8", offset_factor=1),
B: T.Buffer((16, 4), "int8", offset_factor=1),
C: T.Buffer((16,), "int32", offset_factor=1),
) -> None:
with T.sblock("root"):
T.reads(C[0:16], A[0:4], B[0:16, 0:4])
T.writes(C[0:16])
A_u8x4 = A.vload([0], "uint8x4")
A_i32 = T.reinterpret(A_u8x4, dtype="int32")
A_brdcst = T.broadcast(A_i32, 16)
A_u8x64 = T.reinterpret(A_brdcst, dtype="uint8x64")
B_i8x64 = B.vload([0, 0], dtype="int8x64")
Red = T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.x86.avx512.pmaddubs.w.512"),
A_u8x64,
B_i8x64,
dtype="int16x32",
)
C[T.ramp(T.int32(0), 1, 16)] += T.call_llvm_pure_intrin(
T.llvm_lookup_intrinsic_id("llvm.x86.avx512.pmaddw.d.512"),
Red,
T.int16x32(1),
dtype="int32x16",
)
VNNI_DOT_16x4_INTRIN = "dot_16x4_vnni"
TensorIntrin.register(
VNNI_DOT_16x4_INTRIN, dot_product_16x4_u8i8i32_desc, dot_product_16x4_u8i8i32_vnni
)
AVX512_DOT_16x4_INTRIN = "dot_16x4_avx512"
TensorIntrin.register(
AVX512_DOT_16x4_INTRIN, dot_product_16x4_u8i8i32_desc, dot_product_16x4_u8i8i32_avx512
)