blob: e775ca1265145154b9a1fc656afd57b9545868f3 [file]
// Copyright 2022 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "iree/builtins/ukernel/mmt4d.h"
#include "iree/builtins/ukernel/mmt4d_internal.h"
static void iree_uk_mmt4d_validate(const iree_uk_mmt4d_params_t* params) {
#ifdef IREE_UK_ENABLE_ASSERTS
const iree_uk_uint32_t allflags =
IREE_UK_FLAG_MMT4D_TYPE_MASK | IREE_UK_FLAG_MMT4D_ACCUMULATE |
IREE_UK_FLAG_MMT4D_SKIP_INTERMEDIATE_ROUNDINGS;
IREE_UK_ASSERT(!(params->flags & ~allflags));
iree_uk_uint32_t flags_type = params->flags & IREE_UK_FLAG_MMT4D_TYPE_MASK;
IREE_UK_ASSERT(flags_type < IREE_UK_FLAG_MMT4D_TYPE_END);
// Some implementations may wish to avoid supporting absurdly wide types. For
// instance, K is the innermost (i.e. hottest) loop bound, so some 32bit
// targets may benefit from K being int32, not int64. We still let K be of
// type int64 to be future-proof, as types are hard to change later. But we
// enforce a narrower range here, as we can always relax that later as needed.
IREE_UK_ASSERT(IREE_UK_VALUE_IN_UNSIGNED_INT_RANGE(params->M, 31));
IREE_UK_ASSERT(IREE_UK_VALUE_IN_UNSIGNED_INT_RANGE(params->N, 31));
IREE_UK_ASSERT(IREE_UK_VALUE_IN_UNSIGNED_INT_RANGE(params->K, 31));
// int32 is overkill for the inner tile sizes. Enforce int16 range for now.
IREE_UK_ASSERT(IREE_UK_VALUE_IN_UNSIGNED_INT_RANGE(params->M0, 15));
IREE_UK_ASSERT(IREE_UK_VALUE_IN_UNSIGNED_INT_RANGE(params->N0, 15));
IREE_UK_ASSERT(IREE_UK_VALUE_IN_UNSIGNED_INT_RANGE(params->K0, 15));
// Requirements on sub-byte element type cases
// - Ensure that the output type is not sub-byte.
iree_uk_mmt4d_type_t mmt4d_type = iree_uk_mmt4d_type(params->flags);
IREE_UK_ASSERT(iree_uk_type_bit_count(iree_uk_mmt4d_out_type(mmt4d_type)) >=
8);
// - Ensure that (K0 * {LHS,RHS} element bits) is a multiple of 8 bits.
int lhs_bits = iree_uk_type_bit_count(iree_uk_mmt4d_lhs_type(mmt4d_type));
int rhs_bits = iree_uk_type_bit_count(iree_uk_mmt4d_lhs_type(mmt4d_type));
IREE_UK_ASSERT(!((params->K0 * lhs_bits) % 8));
IREE_UK_ASSERT(!((params->K0 * rhs_bits) % 8));
// - Ensure that {LHS,RHS} strides are multiples of 8 bits.
IREE_UK_ASSERT(!((params->lhs_stride0 * lhs_bits) % 8));
IREE_UK_ASSERT(!((params->rhs_stride0 * rhs_bits) % 8));
// Ensure iree_uk_mmt4d_tile_generic_max_bytes large enough for this tile.
IREE_UK_ASSERT(params->M0 * params->N0 *
iree_uk_type_size(iree_uk_mmt4d_out_type(mmt4d_type)) <=
iree_uk_mmt4d_tile_generic_max_bytes);
#endif // IREE_UK_ENABLE_ASSERTS
}
// General mmt4d implementation, shared among all cases. The idea is that the
// only really performance-critical part is the inner-most loop, and that's
// handled by the tile_func passed as argument here. Sharing the outer loops
// across all cases is a roughly 2x code shrink compared to if we were
// emitting the whole loop nest for each case.
static void iree_uk_mmt4d_using_tile_func(const iree_uk_mmt4d_params_t* params,
iree_uk_mmt4d_tile_func_t tile_func) {
const iree_uk_int32_t M = params->M;
const iree_uk_int32_t N = params->N;
const iree_uk_int16_t M0 = params->M0;
const iree_uk_int16_t N0 = params->N0;
iree_uk_mmt4d_type_t mmt4d_type = iree_uk_mmt4d_type(params->flags);
const iree_uk_type_t lhs_type = iree_uk_mmt4d_lhs_type(mmt4d_type);
const iree_uk_type_t rhs_type = iree_uk_mmt4d_rhs_type(mmt4d_type);
const iree_uk_type_t out_type = iree_uk_mmt4d_out_type(mmt4d_type);
const iree_uk_int16_t lhs_elem_bits_log2 =
iree_uk_type_bit_count_log2(lhs_type);
const iree_uk_int16_t rhs_elem_bits_log2 =
iree_uk_type_bit_count_log2(rhs_type);
const iree_uk_int16_t out_elem_size_log2 = iree_uk_type_size_log2(out_type);
char* out_tile_row =
(char*)params->out_buffer + (params->out_offset << out_elem_size_log2);
const char* lhs_panel =
(const char*)params->lhs_buffer +
iree_uk_bits_to_bytes_exact(params->lhs_offset << lhs_elem_bits_log2);
const char* rhs_panel_start =
(const char*)params->rhs_buffer +
iree_uk_bits_to_bytes_exact(params->rhs_offset << rhs_elem_bits_log2);
iree_uk_int32_t out_tile_size = (M0 * N0) << out_elem_size_log2;
iree_uk_index_t lhs_panel_stride =
iree_uk_bits_to_bytes_exact(params->lhs_stride0 << lhs_elem_bits_log2);
iree_uk_index_t rhs_panel_stride =
iree_uk_bits_to_bytes_exact(params->rhs_stride0 << rhs_elem_bits_log2);
iree_uk_index_t out_stride = params->out_stride0 << out_elem_size_log2;
for (iree_uk_int32_t i = 0; i < M; ++i) {
char* out_tile = out_tile_row;
const char* rhs_panel = rhs_panel_start;
// Prefetches needed on ARM Cortex-X2, Issue #13332.
IREE_UK_PREFETCH_RW(out_tile_row, IREE_UK_PREFETCH_LOCALITY_L3);
IREE_UK_PREFETCH_RO(lhs_panel, IREE_UK_PREFETCH_LOCALITY_L1);
IREE_UK_PREFETCH_RO(rhs_panel, IREE_UK_PREFETCH_LOCALITY_L1);
for (iree_uk_int32_t j = 0; j < N; ++j) {
tile_func(out_tile, lhs_panel, rhs_panel, params);
out_tile += out_tile_size;
rhs_panel += rhs_panel_stride;
}
out_tile_row += out_stride;
lhs_panel += lhs_panel_stride;
}
}
// Helper for early-return path when K==0 and we just need to clear the output.
static void iree_uk_mmt4d_zero_out(const iree_uk_mmt4d_params_t* params) {
iree_uk_mmt4d_type_t mmt4d_type = iree_uk_mmt4d_type(params->flags);
iree_uk_type_t out_type = iree_uk_mmt4d_out_type(mmt4d_type);
int out_elem_size_log2 = iree_uk_type_size_log2(out_type);
iree_uk_index_t contiguous_size = params->N * params->M0 * params->N0
<< out_elem_size_log2;
iree_uk_index_t stride = params->out_stride0 << out_elem_size_log2;
char* out_ptr =
(char*)params->out_buffer + (params->out_offset << out_elem_size_log2);
for (iree_uk_index_t i = 0; i < params->M; ++i) {
iree_uk_memset(out_ptr, 0, contiguous_size);
out_ptr += stride;
}
}
// Early-return code paths, including trivial or near-trivial cases (when one
// of the dimensions is 0) and in the future, hardware ports that specialize
// the entire loop nest.
// Returns true if already done.
static bool iree_uk_mmt4d_early(const iree_uk_mmt4d_params_t* params) {
// Trivial cases
if (params->M == 0 || params->N == 0) {
return true;
}
if (params->K == 0) {
if (params->flags & IREE_UK_FLAG_MMT4D_ACCUMULATE) {
// Nothing to do!
} else {
iree_uk_mmt4d_zero_out(params);
}
return true;
}
// Targets that want to specialize the entire loop nest can do so here.
return false;
}
IREE_UK_EXPORT int iree_uk_mmt4d(const iree_uk_mmt4d_params_t* params) {
iree_uk_mmt4d_validate(params);
// Maybe handle this mmt4d "early", without needing to select a tile_func.
// Typical cases include trivial cases (e.g. when params->K == 0) and hardware
// targets that want to handle the entire loop nest in target-specific code.
if (iree_uk_mmt4d_early(params)) return 0;
// Select a target-specific tile_func (inner loop on K, computing one M0xN0
// tile) and use that with generic outer loops.
iree_uk_mmt4d_tile_func_t tile_func = iree_uk_mmt4d_select_tile_func(params);
iree_uk_mmt4d_using_tile_func(params, tile_func);
return 0;
}