blob: 48787088482069b8d7f329f617b16df48e0fe33a [file] [log] [blame]
# Copyright (c) 2021-2024, ARM Limited.
# SPDX-License-Identifier: Apache-2.0
import itertools
import logging
import math
import generator.tosa_utils as gtu
import numpy as np
from generator.tosa_error_if import ErrorIf
from generator.tosa_error_if import TosaErrorIfArgGen
from serializer.tosa_serializer import DTypeNames
from tosa.DType import DType
from tosa.Op import Op
from tosa.ResizeMode import ResizeMode
# DTypeNames, DType, Op and ResizeMode are convenience variables to the
# flatc-generated types that should be enums, but aren't
logging.basicConfig()
logger = logging.getLogger("tosa_verif_build_tests")
class TosaQuantGen:
"""QuantizedInfo random generator helper functions.
Specify with 'qgen': in the operator defintion.
"""
def __init__(self):
pass
@staticmethod
def getZeroPoint(rng, zeropoint, dtype, error_name=None):
if dtype == DType.INT8:
if zeropoint is not None:
return min(127, max(-128, zeropoint))
return rng.randInt(-128, 128)
elif dtype == DType.UINT8:
if zeropoint is not None:
return min(255, max(0, zeropoint))
return rng.randInt(0, 256)
elif error_name in [
ErrorIf.InputZeroPointNotZero,
ErrorIf.WeightZeroPointNotZero,
ErrorIf.OutputZeroPointNotZero,
]:
zero_point = rng.randInt(-128, 128)
if zero_point == 0:
zero_point = 1
return zero_point
return 0
@staticmethod
def qgUnary(rng, zeropoint, op, dtype, error_name=None):
if error_name == ErrorIf.InputZeroPointNotZero:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype, error_name),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype),
]
elif error_name == ErrorIf.OutputZeroPointNotZero:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype, error_name),
]
else:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype),
]
return qinfo
@staticmethod
def qgConv(rng, zeropoint, op, dtype_or_dtypeList, error_name=None):
if isinstance(dtype_or_dtypeList, list):
# a list of [input, weights, accumulator] dtypes
dtypeList = dtype_or_dtypeList
else:
# an int, [input, weights, accumulator] dtypes are the same
dtypeList = [dtype_or_dtypeList] * 3
if error_name == ErrorIf.InputZeroPointNotZero:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtypeList[0], error_name),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtypeList[1]),
]
elif error_name == ErrorIf.WeightZeroPointNotZero:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtypeList[0]),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtypeList[1], error_name),
]
else:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtypeList[0]),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtypeList[1]),
]
return qinfo
@staticmethod
def qgMatmul(rng, zeropoint, op, dtype, error_name=None):
if error_name == ErrorIf.InputZeroPointNotZero:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype, error_name),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype, error_name),
]
else:
qinfo = [
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype),
TosaQuantGen.getZeroPoint(rng, zeropoint, dtype),
]
return qinfo
@staticmethod
def computeMultiplierAndShift(scaleFp, scale32):
# Derived from computeMultiplierAndShiftTosaScale32
# Provide a floating-point scaling factor and the scale32 parameter
# to compute the multiplier and shift
if scale32:
scaleBits = 31
else:
scaleBits = 15
m, shift = math.frexp(scaleFp)
if scaleFp < 0.0:
m = -m
multiplier = round(m * (1 << scaleBits))
assert multiplier <= (1 << scaleBits)
if multiplier == (1 << scaleBits):
multiplier = multiplier // 2
shift = shift + 1
shift = (-shift) + scaleBits
logger.debug(
f"computeMultiplierAndShift: scalefp={scaleFp} scaleBits={scaleBits} m={m} mult={multiplier} shift={shift}"
)
# Adjust multiplier such that shift is in allowed value range.
if shift == 0:
multiplier = multiplier // 4
shift = shift + 2
elif shift == 1:
multiplier = multiplier // 2
shift = shift + 1
elif shift == 63:
multiplier = multiplier * 2
shift = shift - 1
assert multiplier <= (1 << scaleBits)
assert shift >= 2 and shift <= 62
return multiplier, shift
class TosaTensorGen:
"""Tensor generators create a shape list for the placeholder and const tensor
data operands for the operator.
The actual random data is generated separately for each test.
"""
def __init__(self):
pass
@staticmethod
def tgBasic(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
shape = testGen.makeShape(rng, rank)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
shape = TosaErrorIfArgGen.eiRestrictDimensions(shape)
shape_list = []
for i in range(pl + const):
shape_list.append(shape.copy())
# Generates an input rank mismatch for operators with more than one input
if error_name == ErrorIf.RankMismatch:
if rank == 1 and i != 1:
shape = testGen.makeShape(rng, rank + rng.choice([1, 2, 3]))
elif i != 1:
shape = testGen.makeShape(rng, rank + rng.choice([-1, 1]))
return shape_list
@staticmethod
def tgNHWC(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 4
shape = testGen.makeShape(rng, rank)
shape = testGen.constrictBatchSize(shape)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name and error_name != ErrorIf.MaxDimExceeded:
shape = TosaErrorIfArgGen.eiRestrictDimensions(shape)
shape_list = []
for i in range(pl + const):
shape_list.append(shape.copy())
return shape_list
@staticmethod
def tgGather(testGen, rng, opName, rank, error_name=None):
pl, const = opName["operands"]
assert pl == 2
assert const == 0
if error_name != ErrorIf.WrongRank:
assert rank == 3
values_shape = testGen.makeShape(rng, rank)
values_shape = testGen.constrictBatchSize(values_shape)
N = values_shape[0]
W = testGen.makeDimension(rng)
indices_shape = [N, W]
shape_list = [values_shape, indices_shape]
return shape_list
@staticmethod
def tgScatter(testGen, rng, opName, rank, error_name=None):
pl, const = opName["operands"]
assert pl == 3
assert const == 0
if error_name != ErrorIf.WrongRank:
assert rank == 3
values_in_shape = testGen.makeShape(rng, rank)
values_in_shape = testGen.constrictBatchSize(values_in_shape)
N = values_in_shape[0]
K = values_in_shape[1]
C = values_in_shape[2]
# Make sure W is not greater than K, as we can only write each output index
# once (having a W greater than K means that you have to repeat a K index)
W_min = min(testGen.args.tensor_shape_range[0], K)
W_max = min(testGen.args.tensor_shape_range[1], K)
W = rng.randInt(W_min, W_max) if W_min < W_max else W_min
input_shape = [N, W, C]
shape_list = []
shape_list.append(values_in_shape)
shape_list.append([N, W]) # indices
shape_list.append(input_shape)
return shape_list
@staticmethod
def _get_broadcast_shapes(testGen, rng, num_shapes, rank, error_name=None):
shape = testGen.makeShape(rng, rank)
shape_list = []
# Choose one of the inputs to broadcast
# Note: Simplifies OutputShaper code if we don't change first shape for errors
bcast_idx = rng.randInt(0 if error_name is None else 1, num_shapes)
fuzz_idx = rng.randInt(0, rank)
for i in range(num_shapes):
shape_bcast = shape.copy()
# To test broadcasting, the chosen fuzz index dimension should not be 1
if shape_bcast[fuzz_idx] == 1:
shape_bcast[fuzz_idx] += 1
# If the chosen input, pick a random index to broadcast
if i == bcast_idx:
if error_name == ErrorIf.RankMismatch:
# Add one rank to the shape (or more for rank of 1)
extra_ranks = rng.choice([1, 2, 3]) if rank == 1 else 1
shape_bcast = np.concatenate(
(shape_bcast, testGen.makeShape(rng, extra_ranks))
)
if rank != 1:
# Either keep the extra rank, or remove it
new_len = rng.choice([-2, len(shape_bcast)])
shape_bcast = shape_bcast[:new_len]
elif error_name == ErrorIf.BroadcastShapesMismatch:
shape_bcast[fuzz_idx] += 2
else:
shape_bcast[fuzz_idx] = 1
shape_list.append(shape_bcast)
return shape_list
@staticmethod
def tgBroadcastFuzz(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
num_shapes = pl + const
return TosaTensorGen._get_broadcast_shapes(
testGen, rng, num_shapes, rank, error_name
)
@staticmethod
def tgMul(testGen, rng, op, rank, error_name=None):
# Get broadcast shapes for the first 2 inputs as the 3rd is shift
shape_list = TosaTensorGen._get_broadcast_shapes(
testGen, rng, 2, rank, error_name
)
# Add a single dimension tensor for shift
shape_list.append([1])
return shape_list
@staticmethod
def tgConv2D(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 4
# IFM dimensions are NHWC
ifm_shape = testGen.makeShape(rng, rank)
ifm_shape = testGen.constrictBatchSize(ifm_shape)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
ifm_shape = TosaErrorIfArgGen.eiRestrictDimensions(
ifm_shape, max_dim=24, max_items=10000
)
# Get the filter height/width from the operator parameters
filter_hw = op["filter"]
# Generate a random OFM depth
ofm_depth = testGen.makeDimension(rng)
# The filter dimensions are OHWI
filter_shape = np.asarray([ofm_depth, filter_hw[0], filter_hw[1], ifm_shape[3]])
# The bias is OC
bias_shape = np.asarray([ofm_depth])
return [ifm_shape, filter_shape, bias_shape]
@staticmethod
def tgConv3D(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 5
# IFM dimensions are NDHWC
ifm_shape = testGen.makeShape(rng, rank)
ifm_shape = testGen.constrictBatchSize(ifm_shape)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
ifm_shape = TosaErrorIfArgGen.eiRestrictDimensions(
ifm_shape, max_dim=24, max_items=10000
)
# Get the filter depth/height/width from the operator parameters
filter_dhw = op["filter"]
# Generate a random OFM channel
ofm_channel = testGen.makeDimension(rng)
# The filter dimensions are ODHWI
filter_shape = np.asarray(
[ofm_channel, filter_dhw[0], filter_dhw[1], filter_dhw[2], ifm_shape[4]]
)
# The bias is OC
bias_shape = np.asarray([ofm_channel])
return [ifm_shape, filter_shape, bias_shape]
@staticmethod
def tgTransposeConv2D(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 4
# IFM dimensions are NHWC
ifm_shape = testGen.makeShape(rng, rank)
ifm_shape = testGen.constrictBatchSize(ifm_shape)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
ifm_shape = TosaErrorIfArgGen.eiRestrictDimensions(
ifm_shape, max_dim=24, max_items=10000
)
# Get the filter height/width from the operator parameters
filter_hw = op["filter"]
# Generate a random OFM depth
ofm_depth = testGen.makeDimension(rng)
# The filter dimensions are OHWI
filter_shape = np.asarray([ofm_depth, filter_hw[0], filter_hw[1], ifm_shape[3]])
# The bias is OC
bias_shape = np.asarray([ofm_depth])
return [ifm_shape, filter_shape, bias_shape]
@staticmethod
def tgDepthwiseConv2D(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 4
assert pl == 1 and const == 2
# IFM dimensions are NHWC
ifm_shape = testGen.makeShape(rng, rank)
ifm_shape = testGen.constrictBatchSize(ifm_shape)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
ifm_shape = TosaErrorIfArgGen.eiRestrictDimensions(
ifm_shape, max_dim=24, max_items=10000
)
# Get the filter height/width from the operator parameters
# Filter is KH, HW, C, M
filter_hw = op["filter"]
# Generate a random OFM depth, but don't let it get too big because
# the output depth is M * C
filter_m = (
testGen.makeDimension(rng) % (testGen.args.tensor_shape_range[1] // 4)
) + 1
# The filter dimensions are HWCM
filter_shape = np.asarray([filter_hw[0], filter_hw[1], ifm_shape[3], filter_m])
# The bias is M * C
bias_shape = np.asarray([ifm_shape[3] * filter_m])
return [ifm_shape, filter_shape, bias_shape]
@staticmethod
def tgFFT2d(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 3
assert pl == 2 and const == 0
# IFM dimensions are NHW
ifm_shape = testGen.makeShape(rng, rank)
# Select nearest lower power of two from input height and width
ifm_shape[1] = 2 ** int(math.log(ifm_shape[1], 2))
ifm_shape[2] = 2 ** int(math.log(ifm_shape[2], 2))
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
ifm_shape = TosaErrorIfArgGen.eiRestrictDimensions(ifm_shape)
# Generate an invalid kernel that is not a power of two
if error_name == ErrorIf.KernelNotPowerOfTwo:
inc_h = 2 if ifm_shape[1] == 1 else 1
inc_w = 2 if ifm_shape[2] == 1 else 1
inc_choices = [(inc_h, 0), (0, inc_w), (inc_h, inc_w)]
selected_inc = rng.choice(inc_choices)
ifm_shape[1] += selected_inc[0]
ifm_shape[2] += selected_inc[1]
ifm_shape = testGen.constrictBatchSize(ifm_shape)
ifm_shapes = [ifm_shape.copy(), ifm_shape.copy()]
if error_name == ErrorIf.FFTInputShapeMismatch:
modify_shape = rng.choice([0, 1])
# Only modify kernel (H, W)
modify_dim = rng.choice([1, 2])
ifm_shapes[modify_shape][modify_dim] *= 2
return [ifm_shapes[0], ifm_shapes[1]]
@staticmethod
def tgRFFT2d(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 3
assert pl == 1 and const == 0
# IFM dimensions are NHW
ifm_shape = testGen.makeShape(rng, rank)
# Select nearest lower power of two from input height and width
ifm_shape[1] = 2 ** int(math.log(ifm_shape[1], 2))
ifm_shape[2] = 2 ** int(math.log(ifm_shape[2], 2))
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
ifm_shape = TosaErrorIfArgGen.eiRestrictDimensions(ifm_shape)
# Generate an invalid kernel that is not a power of two
if error_name == ErrorIf.KernelNotPowerOfTwo:
# We must increment by 2 if current size is 1
inc_h = 2 if ifm_shape[1] == 1 else 1
inc_w = 2 if ifm_shape[2] == 1 else 1
inc_choices = [(inc_h, 0), (0, inc_w), (inc_h, inc_w)]
selected_inc = rng.choice(inc_choices)
ifm_shape[1] += selected_inc[0]
ifm_shape[2] += selected_inc[1]
ifm_shape = testGen.constrictBatchSize(ifm_shape)
return [ifm_shape]
@staticmethod
def tgFullyConnected(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 2
input_shape = testGen.makeShape(rng, rank)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
input_shape = TosaErrorIfArgGen.eiRestrictDimensions(input_shape)
filter_oc = rng.integers(
low=testGen.args.tensor_shape_range[0],
high=testGen.args.tensor_shape_range[1],
size=1,
)[0]
filter_shape = np.asarray([filter_oc, input_shape[1]])
bias_shape = np.asarray([filter_oc])
return [input_shape, filter_shape, bias_shape]
@staticmethod
def tgMatmul(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
if error_name != ErrorIf.WrongRank:
assert rank == 3
assert pl == 2 and const == 0
a_shape = testGen.makeShape(rng, rank)
# Constrict the overall size of the shape when creating ERROR_IF tests
if error_name:
a_shape = TosaErrorIfArgGen.eiRestrictDimensions(a_shape)
# Get a random number for b_oc even if target shape is defined
b_oc = np.int32(
rng.integers(
low=testGen.args.tensor_shape_range[0],
high=testGen.args.tensor_shape_range[1],
size=1,
)
)[0]
# If N or H is large let b_oc be 1 to reduce output tensor size
if max(a_shape) > 1000:
b_oc = 1
b_shape = np.asarray([a_shape[0], a_shape[2], b_oc])
return [a_shape, b_shape]
@staticmethod
def tgConcat(testGen, rng, op, rank, error_name=None):
pl, const = op["operands"]
shape = testGen.makeShape(rng, rank)
# Create extra tensors to concat.
# Take into account value of pl when getting maximum number of concats
num_tensors = rng.randInt(0, 4)
shape_list = []
for i in range(pl + const + num_tensors):
if error_name == ErrorIf.ConcatInputRankMismatch and i != 0:
remove = rng.choice([True, False])
wrongShape = shape.copy()
if remove and len(shape) > 1:
wrongShape = wrongShape[1:]
else:
wrongShape = list(wrongShape)
wrongShape.append(rng.integers(1, 10))
shape_list.append(wrongShape)
else:
shape_list.append(shape.copy())
return shape_list
@staticmethod
def tgConcatConstInput(rng, shapeList, axis, error_name=None):
if error_name in [
ErrorIf.AxisSmallerZero,
ErrorIf.AxisLargerRank,
ErrorIf.ConcatInputRankMismatch,
]:
return shapeList
# Split concat shape along axis to allow for multiple const inputs
# without making too many large tensors
if len(shapeList) == 2 or shapeList[0][axis] < len(shapeList):
# If axis can't be split we still need to invalidate other dimensions
if error_name == ErrorIf.ConcatInputDimMismatch:
for shape in shapeList[1:]:
# Negative test shapeLists are created individually for each test,
# so no need to copy the shape before altering it.
shape[(axis + 1) % len(shape)] += rng.integers(5, 10)
return shapeList
# Create copy of shape we are going to split (so we don't alter shapeList)
shape = shapeList[0].copy()
# Add original shape as first input
new_shapeList = [shape.copy()]
length_on_axis = shape[axis]
remaining_length = length_on_axis
for i in range(len(shapeList) - 2):
# Calculate split on axis and remaining value
split_shape_val = int(shape[axis] / 2)
remaining_length = remaining_length - split_shape_val
# Append new shape, and set remaining shape
shape[axis] = split_shape_val
new_shapeList.append(shape.copy())
# invalidate dimensions
if error_name == ErrorIf.ConcatInputDimMismatch:
shape[(axis + 1) % len(shape)] += rng.integers(5, 10)
else:
shape[axis] = remaining_length
if i == len(shapeList) - 3:
new_shapeList.append(shape.copy())
return new_shapeList
class TosaTensorValuesGen:
"""Tensor Value generators create the random data for each tensor in each test."""
def __init__(self):
pass
class TVGInfo:
"""Enhanced tensor values information including data gen dict."""
def __init__(self, tensorList, dataGenDict):
self.tensorList = tensorList
self.dataGenDict = dataGenDict
# Default high value for random numbers
TVG_FLOAT_HIGH_VALUE = {
DType.FP32: (1 << 128) - (1 << (127 - 23)),
DType.FP16: (1 << 16) - (1 << (15 - 10)),
DType.BF16: (1 << 128) - (1 << (127 - 7)),
DType.FP8E4M3: 448,
DType.FP8E5M2: 57344,
}
# Default lowest normal values for random numbers
TVG_FLOAT_LOW_VALUE = {
DType.FP32: np.exp2(-126),
DType.FP16: np.exp2(-14),
DType.BF16: np.exp2(-126),
DType.FP8E4M3: np.exp2(-9),
DType.FP8E5M2: np.exp2(-16),
}
@staticmethod
def _get_data_range(rng, dtype, highValueLookup, lowValueLookup=None):
# Return a tuple of (low,high) data range values for the given data
# type using a combination of per operator table limits, data limits
# and user supplied ranges for FP numbers
if dtype in highValueLookup:
type_range = rng.dTypeRange(dtype, high_inclusive=True)
high_val = highValueLookup[dtype]
if lowValueLookup is not None and dtype in lowValueLookup:
low_val = lowValueLookup[dtype]
else:
low_val = -high_val
# Set the values to something that won't produce infinity whilst
# respecting the default ranges if more/less than the low/high
# values
data_range = (
max(low_val, type_range[0]),
min(high_val, type_range[1]),
)
if data_range[0] > data_range[1]:
# Invalid data range from low to high created due to user
# constraints revert to using internal ranges as they are
# known to work
logger.info(
f"Using safe data range ({low_val} to {high_val}) instead of supplied ({type_range[0]} to {type_range[1]})"
)
data_range = (low_val, high_val)
return data_range
return None
@staticmethod
def tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
# Variable inputs versus constants
pCount, cCount = testGen.TOSA_OP_LIST[opName]["operands"]
if "p_count" in argsDict:
# Override for operators like CONCAT
pCount = argsDict["p_count"]
cCount = argsDict["c_count"]
assert pCount + cCount == len(
shapeList
), "Placeholders & Constant tensors must match shapes list"
tens_ser_list = []
if (
error_name is not None
or not gtu.dtypeIsSupportedByCompliance(dtypeList[0])
or "data_gen" not in testGen.TOSA_OP_LIST[opName]
):
# Fall back to internal data gen when dealing with unsupported types or ops
data_range = argsDict["data_range"] if "data_range" in argsDict else None
for idx, info in enumerate(zip(shapeList, dtypeList)):
roundMode = False
shape, dtype = info
if "data_range_list" in argsDict:
data_range = argsDict["data_range_list"][idx]["range"]
roundMode = (
"round" in argsDict["data_range_list"][idx]
and argsDict["data_range_list"][idx]["round"] is True
)
if data_range is not None and dtype not in (
DType.FP16,
DType.FP32,
DType.BF16,
DType.FP8E4M3,
DType.FP8E5M2,
):
# Change from inclusive to exclusive range
data_range = (data_range[0], data_range[1] + 1)
# Ignore lazy data gen option and create data array using any range limits
if "fixed_data" in argsDict and argsDict["fixed_data"][idx] is not None:
if dtype == DType.SHAPE:
arr = np.int64(argsDict["fixed_data"][idx])
elif dtype == DType.INT8:
arr = np.int8(argsDict["fixed_data"][idx])
elif dtype == DType.INT16:
arr = np.int16(argsDict["fixed_data"][idx])
elif dtype == DType.INT32:
arr = np.int32(argsDict["fixed_data"][idx])
else:
assert False, "Unsupported fixed_data type"
else:
arr = rng.randTensor(shape, dtype, data_range)
if roundMode:
arr = np.round(arr)
if idx < pCount:
tens_ser_list.append(testGen.ser.addPlaceholder(shape, dtype, arr))
else:
tens_ser_list.append(testGen.ser.addConst(shape, dtype, arr))
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
# Create data generator meta-data
dg_type = argsDict["dg_type"]
tens_data = {
"version": "0.1",
"tensors": {},
}
dg_tens_meta = tens_data["tensors"]
for idx, shape in enumerate(shapeList):
tens_meta = {}
if "fixed_data" in argsDict and argsDict["fixed_data"][idx] is not None:
tens_meta["generator"] = gtu.DataGenType(
gtu.DataGenType.FIXED_DATA
).name
else:
tens_meta["generator"] = gtu.DataGenType(dg_type).name
tens_meta["data_type"] = gtu.DTYPE_ATTRIBUTES[dtypeList[idx]]["json"]
tens_meta["shape"] = [int(i) for i in shape]
tens_meta["input_pos"] = idx
tens_meta["op"] = gtu.getOpNameFromOpListName(opName).upper()
if idx < pCount:
tens_meta["input_type"] = "VARIABLE"
else:
tens_meta["input_type"] = "CONSTANT"
if dg_type == gtu.DataGenType.PSEUDO_RANDOM:
info = {}
if (
tens_meta["generator"]
== gtu.DataGenType(gtu.DataGenType.FIXED_DATA).name
):
info["data"] = [int(i) for i in argsDict["fixed_data"][idx]]
tens_meta["fixed_data_info"] = info
else:
info["rng_seed"] = rng.seed
data_range = None
if "data_range_list" in argsDict:
data_range = argsDict["data_range_list"][idx]["range"]
if "round" in argsDict["data_range_list"][idx]:
info["round"] = argsDict["data_range_list"][idx]["round"]
elif "data_range" in argsDict:
data_range = argsDict["data_range"]
if data_range is None:
data_range = rng.dTypeRange(dtypeList[idx], high_inclusive=True)
info["range"] = [str(v) for v in data_range]
tens_meta["pseudo_random_info"] = info
elif dg_type == gtu.DataGenType.DOT_PRODUCT:
info = {}
info["s"] = argsDict["s"]
info["ks"] = int(argsDict["ks"])
if "acc_type" in argsDict:
# Convert type number into JSON name
info["acc_type"] = gtu.DTYPE_ATTRIBUTES[argsDict["acc_type"]][
"json"
]
if "kernel" in argsDict:
info["kernel"] = [int(k) for k in argsDict["kernel"]]
if "axis" in argsDict:
info["axis"] = int(argsDict["axis"])
tens_meta["dot_product_info"] = info
elif dg_type == gtu.DataGenType.FULL_RANGE:
info = {}
info["start_val"] = int(
rng.randInt(0, gtu.DTYPE_ATTRIBUTES[dtypeList[idx]]["fullset"])
)
tens_meta["full_range_info"] = info
else:
# TODO - other data gen type
assert False, "TODO: support other data gen types"
# Using the finished generate config meta data - generate the data if
# needed and assign a tensor name from the serializer
# Need to generate data when not lazy or for the bias tensor as we need
# to work out if the bias data is non-zero for compliance
if not testGen.args.lazy_data_gen or (
idx == 2 and dg_type == gtu.DataGenType.DOT_PRODUCT
):
# Give this tensor a temporary name until we get one from the serializer
temp_name = f"placeholder_{idx}"
dg_tens_meta[temp_name] = tens_meta
# Create data now using the temporary name to access meta details
data = testGen.dgl.get_tensor_data(temp_name, tens_data)
if tens_meta["data_type"] == "SHAPE":
# Tensor type SHAPE and Numpy file type must be the same
data = np.int64(data)
# Remove the item as we will give it the correct name later
del dg_tens_meta[temp_name]
if idx == 2 and dg_type == gtu.DataGenType.DOT_PRODUCT:
# The KS value used by compliance verification is altered when the
# bias data is non-zero
if max(abs(data)) > 0.0:
argsDict["ksb"] = argsDict["ks"] + 1
if testGen.args.lazy_data_gen:
data = None
if tens_meta["input_type"] == "VARIABLE":
tens = testGen.ser.addPlaceholder(shape, dtypeList[idx], data)
else:
tens = testGen.ser.addConst(shape, dtypeList[idx], data)
tens_ser_list.append(tens)
# Add the meta data to the list using the serializer tensor name
dg_tens_meta[tens.name] = tens_meta
return TosaTensorValuesGen.TVGInfo(tens_ser_list, tens_data)
@staticmethod
def tvgNegate(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
if dtypeList[0] == DType.INT32 and error_name is None:
# Integer test
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 1 and cCount == 0
), "Op.NEGATE must have 1 placeholders, 0 consts"
# Must create tensors with values within accumulator (int32) negatable
# range
max_val = (1 << 31) - 1
min_val = -max_val
arr = np.int32(
rng.integers(low=min_val, high=(max_val + 1), size=shapeList[0])
)
tens_ser_list = []
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtypeList[0], arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
# ERROR_IF or floating point test
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
# Set the ADD/SUB data range to half the largest value to avoid infinities
TVG_FLOAT_HIGH_VALUE_ADDSUB = {
DType.FP32: (TVG_FLOAT_HIGH_VALUE[DType.FP32] / 2),
DType.FP16: (TVG_FLOAT_HIGH_VALUE[DType.FP16] / 2),
DType.BF16: (TVG_FLOAT_HIGH_VALUE[DType.BF16] / 2),
}
@staticmethod
def tvgAddSub(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
if dtypeList[0] in (DType.INT32, DType.SHAPE) and error_name is None:
# Make sure the integer operation does not cause value saturation - where
# the number wraps due to limited number of bits to store the answer
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 2 and cCount == 0
), "Op.ADD / Op.SUB must have 2 placeholders, 0 consts"
tens_ser_list = []
add = op["op"] in (Op.ADD, Op.ADD_SHAPE)
data_range = testGen.args.tensor_shape_range
a_arr = rng.randTensor(shapeList[0], dtypeList[0], data_range)
b_arr = rng.randTensor(shapeList[1], dtypeList[1], data_range)
if add:
res_arr = np.add(a_arr, b_arr, dtype=np.int64)
else:
res_arr = np.subtract(a_arr, b_arr, dtype=np.int64)
# Work out the saturation limits
max_i32 = (1 << 31) - 1
min_i32 = -(1 << 31)
max_arr = np.full(shapeList[1], max_i32)
min_arr = np.full(shapeList[1], min_i32)
# Find how much values exceed the maximum/minimums
sat_max_arr = np.maximum(res_arr - max_arr, 0)
sat_min_arr = np.minimum(res_arr - min_arr, 0)
if not add:
# Swap saturation values and negate values as we need to perform opposite operations
sat_max_arr, sat_min_arr = -sat_min_arr, -sat_max_arr
# Create new array of unsaturated values by clipping values as needed
b_unsat_arr = b_arr
if (sat_max_arr != 0).any():
# Clip values that cause saturation
b_unsat_arr = np.subtract(b_unsat_arr, sat_max_arr, dtype=np.int32)
# Reduce axes in unsaturated tensor to match original tensor
for axis, dim in enumerate(b_arr.shape):
if dim != b_unsat_arr.shape[axis]:
assert (
dim == 1
), "Op.ADD / SUB dimension must be 1 or matching to be broadcastable"
b_unsat_arr = np.amin(b_unsat_arr, axis=axis, keepdims=True)
if (sat_min_arr != 0).any():
# Clip values that cause saturation
b_unsat_arr = np.subtract(b_unsat_arr, sat_min_arr, dtype=np.int32)
# Reduce axes in unsaturated tensor to match original tensor
for axis, dim in enumerate(b_arr.shape):
if dim != b_unsat_arr.shape[axis]:
assert (
dim == 1
), "Op.ADD / SUB dimension must be 1 or matching to be broadcastable"
b_unsat_arr = np.amax(b_unsat_arr, axis=axis, keepdims=True)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtypeList[0], a_arr)
)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[1], dtypeList[1], b_unsat_arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
# ERROR_IF or floating point test
data_range = TosaTensorValuesGen._get_data_range(
rng, dtypeList[0], TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_ADDSUB
)
if data_range:
argsDict["data_range"] = data_range
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgCondIfWhileLoop(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
if dtypeList[0] in (
DType.INT32,
DType.INT16,
DType.INT8,
):
# Limit input tensors with cond_if_binary or while_loop to stop
# saturation of add/sub ops with int32 and keep all logical shift
# values between 0 to 31 for int16 or int8
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
pRemain = pCount
tens_ser_list = []
for idx, shape in enumerate(shapeList[:]):
if dtypeList[0] == DType.INT32:
arr = rng.randTensor(shapeList[idx], DType.INT16)
else:
arr = np.int32(rng.integers(low=0, high=32, size=shapeList[idx]))
if pRemain > 0:
tens_ser_list.append(
testGen.ser.addPlaceholder(shape, dtypeList[idx], arr)
)
pRemain -= 1
else:
tens_ser_list.append(
testGen.ser.addConst(shape, dtypeList[idx], arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgArithmeticRightShift(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
# Force value of operand[1] to be within [0, num_bits]
assert (
pCount == 2 and cCount == 0
), "Op.ArithmeticRightShift must have 2 placeholders, 0 consts"
tens_ser_list = []
for idx, shape in enumerate(shapeList[:]):
if idx == 1:
if dtypeList[idx] == DType.INT8:
arr = np.int32(rng.integers(low=0, high=8, size=shape))
elif dtypeList[idx] == DType.INT16:
arr = np.int32(rng.integers(low=0, high=16, size=shape))
elif dtypeList[idx] == DType.INT32:
arr = np.int32(rng.integers(low=0, high=32, size=shape))
elif error_name == ErrorIf.WrongInputType:
arr = np.int32(rng.integers(low=0, high=8, size=shape))
else:
raise Exception("OpArithmeticRightShift: invalid input dtype")
else:
arr = rng.randTensor(shape, dtypeList[idx])
tens_ser_list.append(testGen.ser.addPlaceholder(shape, dtypeList[idx], arr))
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
@staticmethod
def tvgReshape(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
dtypeList[1] = DType.SHAPE
shapeList[1] = [len(argsDict["new_shape"])]
# Create a new list for the pre-generated data in argsDict["fixed_data"]
argsDict["fixed_data"] = [None, argsDict["new_shape"]]
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgRescale(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
scale32 = argsDict["scale"]
multiplier_arr = argsDict["multiplier"]
shift_arr = argsDict["shift"]
if scale32:
dtypeList[1] = DType.INT32
else:
dtypeList[1] = DType.INT16
shapeList[1] = [len(multiplier_arr)]
dtypeList[2] = DType.INT8
shapeList[2] = [len(shift_arr)]
# Create a new list for the pre-generated data in argsDict["fixed_data"]
argsDict["fixed_data"] = [None, multiplier_arr, shift_arr]
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgPad(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
# argsDict["pad"] is 2D array, need to flatten it to get list of values
pad_values = argsDict["pad"].flatten()
dtypeList[1] = DType.SHAPE
shapeList[1] = [len(pad_values)]
# Create a new list for the pre-generated data in argsDict["fixed_data"]
argsDict["fixed_data"] = [None, pad_values]
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgSlice(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
dtypeList[1] = DType.SHAPE
shapeList[1] = [len(argsDict["start"])]
dtypeList[2] = DType.SHAPE
shapeList[2] = [len(argsDict["size"])]
# Create a new list for the pre-generated data in argsDict["fixed_data"]
argsDict["fixed_data"] = [None, argsDict["start"], argsDict["size"]]
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgTile(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
dtypeList[1] = DType.SHAPE
shapeList[1] = [len(argsDict["multiples"])]
argsDict["fixed_data"] = [None, argsDict["multiples"]]
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgSelect(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
# Set datatype of condition tensor to boolean
dtypeList[0] = DType.BOOL
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgIntDiv(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
if error_name is None:
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 2 and cCount == 0
), "Op.INTDIV must have 2 placeholders, 0 consts"
tens_ser_list = []
# Two invalid cases for Op.INTDIV:
# 1. divisor == 0
# 2. dividend == -(1<<31) and divisor == -1
while True:
dividend_arr = rng.randTensor(shapeList[0], dtypeList[0])
divisor_arr = rng.randTensor(shapeList[1], dtypeList[1])
if (divisor_arr == 0).any():
continue
if (dividend_arr == -(2**31)).any() and (divisor_arr == -1).any():
continue
break
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtypeList[0], dividend_arr)
)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[1], dtypeList[1], divisor_arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
# Set the MUL data range to the square root of the largest value
# to avoid infinities
TVG_FLOAT_HIGH_VALUE_MUL = {
DType.FP32: math.sqrt(TVG_FLOAT_HIGH_VALUE[DType.FP32]),
DType.FP16: math.sqrt(TVG_FLOAT_HIGH_VALUE[DType.FP16]),
DType.BF16: math.sqrt(TVG_FLOAT_HIGH_VALUE[DType.BF16]),
}
@staticmethod
def tvgMul(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
if error_name is not None or dtypeList[0] in (
DType.FP16,
DType.BF16,
DType.FP32,
):
# ERROR_IF or floating point test
data_range = TosaTensorValuesGen._get_data_range(
rng, dtypeList[0], TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_MUL
)
if data_range:
argsDict["data_range"] = data_range
if dtypeList[0] != DType.SHAPE:
# Need to supply shift tensor for MUL (not needed for MUL_SHAPE)
dtypeList[2] = DType.INT8
shapeList[2] = [1]
# Create a new list for the pre-generated data in argsDict["fixed_data"]
argsDict["fixed_data"] = [None, None, [argsDict["shift"]]]
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
else:
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
tens_ser_list = []
# Make sure multiply result in int32 range
if dtypeList[0] == DType.SHAPE:
shift = 0
else:
shift = argsDict["shift"]
if dtypeList[0] == DType.INT8:
num_bits = 8
elif dtypeList[0] == DType.INT16:
num_bits = 16
elif dtypeList[0] in (DType.INT32, DType.SHAPE):
num_bits = 32
elif error_name == ErrorIf.WrongInputType:
num_bits = 8
else:
raise Exception(
f"OpMul: invalid input dtype {gtu.DTYPE_ATTRIBUTES[dtypeList[0]]['str']}"
)
for idx, shape in enumerate(shapeList[:]):
if dtypeList[idx] == DType.SHAPE:
low = testGen.args.tensor_shape_range[0]
high = testGen.args.tensor_shape_range[1]
else:
low = -(2 ** (num_bits - 1))
high = (2 ** (num_bits - 1)) - 1
a_arr = np.int32(rng.integers(low=low, high=high, size=shapeList[0]))
b_arr = np.int32(rng.integers(low=low, high=high, size=shapeList[1]))
i = 0
while True:
a_arr_64 = a_arr.astype(np.int64)
b_arr_64 = b_arr.astype(np.int64)
if shift > 0:
rounding = 1 << (shift - 1)
result_arr = ((a_arr_64 * b_arr_64) + rounding) >> shift
else:
result_arr = a_arr_64 * b_arr_64
if (result_arr > -(2**31)).all() and (
result_arr <= ((2**31) - 1)
).all():
break
i = i + 1
a_arr = a_arr // 2
b_arr = b_arr // 2
if dtypeList[0] == DType.SHAPE:
# MUL_SHAPE with 2 inputs
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtypeList[0], a_arr_64)
)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[1], dtypeList[1], b_arr_64)
)
else:
# MUL with 3 inputs (3rd is shift)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtypeList[0], a_arr)
)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[1], dtypeList[1], b_arr)
)
tens_ser_list.append(
testGen.ser.addPlaceholder([1], DType.INT8, np.int8([shift]))
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
@staticmethod
def tvgConcat(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
count = len(shapeList) - testGen.args.num_const_inputs_concat
if count < 1:
count = 1
if testGen.args.num_const_inputs_concat == 0:
count = len(shapeList)
op = testGen.TOSA_OP_LIST[opName]
if op["op"] == Op.CONCAT_SHAPE:
# Set the axis to 0
shapeList = TosaTensorGen.tgConcatConstInput(rng, shapeList, 0, error_name)
else:
shapeList = TosaTensorGen.tgConcatConstInput(
rng, shapeList, argsDict["axis"], error_name
)
# Override default pCount/cCount for operator
argsDict["p_count"] = count
argsDict["c_count"] = len(shapeList) - count
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgLogicalShift(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 2 and cCount == 0
), "Op.LOGICAL_LEFT_SHIFT or Op.LOGICAL_RIGHT_SHIFT must have 2 placeholders, 0 consts"
values_arr = rng.randTensor(shapeList[0], dtypeList[0])
shift_arr = np.int32(rng.integers(low=0, high=32, size=shapeList[1]))
tens_ser_list = []
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtypeList[0], values_arr)
)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[1], dtypeList[1], shift_arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
@staticmethod
def tvgEqual(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
if error_name is None and not gtu.dtypeIsSupportedByCompliance(dtypeList[0]):
# Integer
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 2 and cCount == 0
), "Op.EQUAL must have 2 placeholders, 0 consts"
a_arr = rng.randTensor(shapeList[0], dtypeList[0])
b_arr = rng.randTensor(shapeList[1], dtypeList[1])
# Using random numbers means that it will be very unlikely that
# there are any matching (equal) values, therefore force that
# there are twice the number of matching values as the tensor rank
for num in range(0, len(shapeList[0]) * 2):
a_index = []
b_index = []
# Choose an index in each axis for the whole shape
for axis in range(0, len(shapeList[0])):
# Index can be up to the largest dimension in both shapes
index = np.int32(
rng.integers(0, max(shapeList[0][axis], shapeList[1][axis]))
)
# Reduce the index down to a shape's dim for broadcasting
a_index.append(min(shapeList[0][axis] - 1, index))
b_index.append(min(shapeList[1][axis] - 1, index))
a_arr[tuple(a_index)] = b_arr[tuple(b_index)]
tens_ser_list = []
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtypeList[0], a_arr)
)
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[1], dtypeList[1], b_arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
# ERROR_IF or floating point test
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgReduceSum(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
dtype = dtypeList[0]
if dtype == DType.INT32:
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 1 and cCount == 0
), "Op.REDUCE_SUM must have 1 placeholders, 0 consts"
# Limit values so that the sum cannot exceed the range of an int32 during
# summation of any axis
range_val = int((1 << 31) / max(shapeList[0]))
values_arr = np.int32(
rng.integers(low=-range_val, high=range_val, size=shapeList[0])
)
tens_ser_list = []
tens_ser_list.append(
testGen.ser.addPlaceholder(shapeList[0], dtype, values_arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
# ERROR_IF or dot product floating point test
if (
error_name is None
and argsDict["dg_type"] != gtu.ComplianceMode.DOT_PRODUCT
):
# Limit ranges for (non error & non compliance) tests by using
# values that can be summed on any axis to not hit infinity
highval_lookup = {
dtype: TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE[dtype]
/ max(shapeList[0])
}
data_range = TosaTensorValuesGen._get_data_range(
rng, dtype, highval_lookup
)
assert data_range is not None
argsDict["data_range"] = data_range
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgReduceProduct(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
dtype = dtypeList[0]
if error_name is None:
# Limit ranges for (non error) tests by using
# values that can be multiplied on any axis to not hit infinity
highval_lookup = {
dtype: math.pow(
TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE[dtype],
1 / max(shapeList[0]),
)
}
data_range = TosaTensorValuesGen._get_data_range(rng, dtype, highval_lookup)
assert data_range is not None
argsDict["data_range"] = data_range
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgResize(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
data_range = TosaTensorValuesGen._get_data_range(
rng,
dtypeList[0],
TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE,
)
if data_range:
argsDict["data_range"] = data_range
# Needed for compliance
argsDict["max_abs_value"] = data_range[1]
scale_values = argsDict["scale"]
offset_values = argsDict["offset"]
border_values = argsDict["border"]
dtypeList[1] = DType.SHAPE
dtypeList[2] = DType.SHAPE
dtypeList[3] = DType.SHAPE
shapeList[1] = [len(scale_values)]
shapeList[2] = [len(offset_values)]
shapeList[3] = [len(border_values)]
argsDict["fixed_data"] = [None, scale_values, offset_values, border_values]
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
# Set the POW exponent high data range
TVG_FLOAT_HIGH_VALUE_POW_EXP = {
DType.FP32: 10.0,
DType.FP16: 10.0,
DType.BF16: 10.0,
}
# POW highest base value (within a safe margin of error) that can be raised
# to +ve exponent that doesn't become Infinity
TVG_FLOAT_HIGH_VALUE_POW_BASE = {
DType.FP32: math.floor(
math.pow(
TVG_FLOAT_HIGH_VALUE[DType.FP32],
1.0 / TVG_FLOAT_HIGH_VALUE_POW_EXP[DType.FP32],
)
),
DType.FP16: math.floor(
math.pow(
TVG_FLOAT_HIGH_VALUE[DType.FP16],
1.0 / TVG_FLOAT_HIGH_VALUE_POW_EXP[DType.FP16],
)
),
DType.BF16: math.floor(
math.pow(
TVG_FLOAT_HIGH_VALUE[DType.BF16],
1.0 / TVG_FLOAT_HIGH_VALUE_POW_EXP[DType.BF16],
)
),
}
# POW lowest base value (within a safe margin of error) that can be raised
# to -ve exponent that doesn't become Infinity
TVG_FLOAT_LOW_VALUE_POW_BASE = {
DType.FP32: math.ceil(
math.pow(
1.0 / TVG_FLOAT_HIGH_VALUE[DType.FP32],
1.0 / TVG_FLOAT_HIGH_VALUE_POW_EXP[DType.FP32],
)
* 1000
)
/ 1000,
DType.FP16: math.ceil(
math.pow(
1.0 / TVG_FLOAT_HIGH_VALUE[DType.FP16],
1.0 / TVG_FLOAT_HIGH_VALUE_POW_EXP[DType.FP16],
)
* 1000
)
/ 1000,
DType.BF16: math.ceil(
math.pow(
1.0 / TVG_FLOAT_HIGH_VALUE[DType.BF16],
1.0 / TVG_FLOAT_HIGH_VALUE_POW_EXP[DType.BF16],
)
* 1000
)
/ 1000,
}
@staticmethod
def tvgPow(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
if error_name is not None:
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
dtype = dtypeList[0]
# Different ranges for POW
test_set = argsDict["s"]
if test_set == 0:
# Positive base with fractional exponent
base_range = TosaTensorValuesGen._get_data_range(
rng,
dtype,
TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_POW_BASE,
TosaTensorValuesGen.TVG_FLOAT_LOW_VALUE_POW_BASE,
)
exp_range = TosaTensorValuesGen._get_data_range(
rng, dtype, TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_POW_EXP
)
exp_round = False
else:
# Integer exponent
exp_range = TosaTensorValuesGen._get_data_range(
rng, dtype, TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_POW_EXP
)
exp_round = True
if test_set == 1:
# Positive base
base_range = TosaTensorValuesGen._get_data_range(
rng,
dtype,
TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_POW_BASE,
TosaTensorValuesGen.TVG_FLOAT_LOW_VALUE_POW_BASE,
)
else:
assert test_set == 2
# Negative base
# Supply new look up tables with negative values
base_range = TosaTensorValuesGen._get_data_range(
rng,
dtype,
{dtype: -TosaTensorValuesGen.TVG_FLOAT_LOW_VALUE_POW_BASE[dtype]},
{dtype: -TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_POW_BASE[dtype]},
)
data_range_list = (
{
"range": base_range,
},
{
"range": exp_range,
"round": exp_round,
},
)
argsDict["data_range_list"] = data_range_list
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgLogRsqrt(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
# LOG & RSQRT data range from lowest expressible positive number to
# largest to avoid NaNs
data_range = TosaTensorValuesGen._get_data_range(
rng,
dtypeList[0],
TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE,
TosaTensorValuesGen.TVG_FLOAT_LOW_VALUE,
)
if data_range:
argsDict["data_range"] = data_range
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
# Set the EXP data range to the log of the largest to smallest values
# to avoid infinities or making the result zero
TVG_FLOAT_HIGH_VALUE_EXP = {
DType.FP32: math.log(TVG_FLOAT_HIGH_VALUE[DType.FP32]),
DType.FP16: math.log(TVG_FLOAT_HIGH_VALUE[DType.FP16]),
DType.BF16: math.log(TVG_FLOAT_HIGH_VALUE[DType.BF16]),
}
TVG_FLOAT_LOW_VALUE_EXP = {
DType.FP32: math.log(TVG_FLOAT_LOW_VALUE[DType.FP32]),
DType.FP16: math.log(TVG_FLOAT_LOW_VALUE[DType.FP16]),
DType.BF16: math.log(TVG_FLOAT_LOW_VALUE[DType.BF16]),
}
@staticmethod
def tvgExp(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
data_range = TosaTensorValuesGen._get_data_range(
rng,
dtypeList[0],
TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE_EXP,
TosaTensorValuesGen.TVG_FLOAT_LOW_VALUE_EXP,
)
if data_range:
argsDict["data_range"] = data_range
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgFullyConnected(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
dtype = dtypeList[0]
if (
error_name is None
and argsDict["dg_type"] != gtu.ComplianceMode.DOT_PRODUCT
and dtype in (DType.BF16,)
):
# TODO - Remove once BF16 enabled for DOT_PRODUCT compliance
# Limit ranges for (non error & non compliance) FP tests by using
# values that can be multiplied on any axis to not hit infinity/NaN
IC = shapeList[0][1]
highval_lookup = {
dtype: math.pow(TosaTensorValuesGen.TVG_FLOAT_HIGH_VALUE[dtype], 1 / IC)
}
data_range = TosaTensorValuesGen._get_data_range(rng, dtype, highval_lookup)
assert data_range is not None
argsDict["data_range"] = data_range
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgCast(testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None):
in_dtype = dtypeList[0]
out_dtype = argsDict["out_type"]
# Create look up to limit input tensor to output type maximums to avoid
# FP infinities and saturation of integers
out_range = rng.dTypeRange(out_dtype, high_inclusive=True)
highval_lookup = {in_dtype: out_range[1]}
data_range = TosaTensorValuesGen._get_data_range(
rng,
in_dtype,
highval_lookup,
)
assert data_range is not None
argsDict["data_range"] = data_range
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgGather(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
K = shapeList[0][1]
# Fix the type of the indices tensor
dtypeList[1] = DType.INT32
dtype = dtypeList[0]
if not gtu.dtypeIsSupportedByCompliance(dtype):
# Test unsupported by data generator
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 2 and cCount == 0
), "Op.GATHER must have 2 placeholders, 0 consts"
tens_ser_list = []
for idx, shape in enumerate(shapeList):
dtype = dtypeList[idx]
if idx != 1:
arr = rng.randTensor(shape, dtype)
tens_ser_list.append(testGen.ser.addPlaceholder(shape, dtype, arr))
else:
# Limit data range of indices tensor upto K (exclusive)
arr = rng.randTensor(shape, dtype, (0, K))
# To match old functionality - create indices as CONST
tens_ser_list.append(testGen.ser.addConst(shape, dtype, arr))
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
# ERROR_IF or floating point test
# Use inclusive values upto index K for indices tensor
data_range_list = (
{"range": None},
{"range": (0, K - 1)},
)
argsDict["data_range_list"] = data_range_list
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
@staticmethod
def tvgScatter(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name=None
):
K = shapeList[0][1]
W = shapeList[2][1]
# Work out an indices tensor here with data that doesn't exceed the
# dimension K of the values_in tensor and does NOT repeat the same K
# location as needed by the spec:
# "It is not permitted to repeat the same output index within a single
# SCATTER operation and so each output index occurs at most once."
assert K >= W, "Op.SCATTER W must be smaller or equal to K"
# Fix the type of the indices tensor
dtypeList[1] = DType.INT32
dtype = dtypeList[0]
if not gtu.dtypeIsSupportedByCompliance(dtype):
# Test unsupported by data generator
op = testGen.TOSA_OP_LIST[opName]
pCount, cCount = op["operands"]
assert (
pCount == 3 and cCount == 0
), "Op.SCATTER must have 3 placeholders, 0 consts"
tens_ser_list = []
for idx, shape in enumerate(shapeList):
dtype = dtypeList[idx]
if idx != 1:
arr = rng.randTensor(shape, dtype)
tens_ser_list.append(testGen.ser.addPlaceholder(shape, dtype, arr))
else:
# Create the indices array
assert dtype == DType.INT32, "Op.SCATTER unexpected indices type"
arr = []
for n in range(shape[0]):
# Get a shuffled list of output indices (0 to K-1) and
# limit length to W
arr.append(rng.permutation(K)[:W])
indices_arr = np.array(arr, dtype=np.int32) # (N, W)
# To match old functionality - create indices as CONST
tens_ser_list.append(
testGen.ser.addConst(shape, dtype, indices_arr)
)
return TosaTensorValuesGen.TVGInfo(tens_ser_list, None)
else:
# ERROR_IF or floating point test
# Use inclusive values upto index K for indices tensor
data_range_list = (
{"range": None},
{"range": (0, K - 1)},
{"range": None},
)
argsDict["data_range_list"] = data_range_list
return TosaTensorValuesGen.tvgLazyGenDefault(
testGen, rng, opName, dtypeList, shapeList, argsDict, error_name
)
class TosaArgGen:
"""Argument generators create exhaustive or random lists of attributes for
operators that take attributes or other parameters.
The return value is a list of (descriptive_name, [arglist]) tuples where
the descriptive_name is appended to the test name and the arglist is expanded
as arguments to the operator build function.
"""
def __init__(self):
pass
@staticmethod
def _add_data_generators(testGen, opName, shapeList, dtype, arg_list, error_name):
"""Add extra tests for each type of data generator for this op."""
if (
error_name is None
and "data_gen" in testGen.TOSA_OP_LIST[opName]
and gtu.dtypeIsSupportedByCompliance(dtype)
):
if gtu.dtypeIsFloat(dtype):
dataGenTypesList = testGen.TOSA_OP_LIST[opName]["data_gen"]["fp"]
else:
dataGenTypesList = testGen.TOSA_OP_LIST[opName]["data_gen"]["int"]
else:
# Error test or No data generator types listed - assume random
dataGenTypesList = (gtu.DataGenType.PSEUDO_RANDOM,)
# Expand arg list with other data generator types
new_arg_list = []
for dg_type in dataGenTypesList:
for arg_str, args_dict in arg_list:
if dg_type == gtu.DataGenType.FULL_RANGE:
tensor_size = gtu.product(shapeList[0])
if tensor_size >= gtu.DTYPE_ATTRIBUTES[dtype]["fullset"]:
# Large enough tensor data size for full range, add a single test
num_test_sets = 0
else:
# Not enough data size for full range of values, revert to random numbers
dg_type = gtu.DataGenType.PSEUDO_RANDOM
if dg_type == gtu.DataGenType.PSEUDO_RANDOM:
if error_name is None:
num_test_sets = (
args_dict["num_test_sets"]
if "num_test_sets" in args_dict
else 0
)
else:
# Add single test for pseudo random
num_test_sets = 0
elif dg_type == gtu.DataGenType.DOT_PRODUCT:
# Extra tests for each dot product test set
dot_products = args_dict["dot_products"]
if dot_products < testGen.TOSA_MI_DOT_PRODUCT_MIN:
shape_info = (
" ({})".format(testGen.shapeStr(args_dict["shape"]))
if "shape" in args_dict
else ""
)
logger.info(
f"Skipping {opName}{shape_info} dot product test as too few calculations {dot_products} < {testGen.TOSA_MI_DOT_PRODUCT_MIN}"
)
continue
# KS and acc_type is required by all dot product generators
assert "ks" in args_dict
assert "acc_type" in args_dict
num_test_sets = testGen.TOSA_MI_DOT_PRODUCT_TEST_SETS
if num_test_sets > 0:
for s in range(0, num_test_sets):
set_arg_str = f"{arg_str}_s{s}" if arg_str else f"s{s}"
set_args_dict = args_dict.copy()
set_args_dict["s"] = s
set_args_dict["dg_type"] = dg_type
new_arg_list.append((set_arg_str, set_args_dict))
else:
# Default is a single test
new_args_dict = args_dict.copy()
new_args_dict["dg_type"] = dg_type
new_arg_list.append((arg_str, new_args_dict))
return new_arg_list
@staticmethod
def agNone(testGen, rng, opName, shapeList, dtype, error_name=None):
"""A trivial argument generator for operators that don't take any
non-tensor arguments"""
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
[("", {})],
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agPow(testGen, rng, opName, shapeList, dtype, error_name=None):
"""Pow operator needs different test sets to cover random numbers
without creating NaNs or Infs"""
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
[("", {"num_test_sets": 3})],
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agAxis(testGen, rng, opName, shapeList, dtype, error_name=None):
"""Build the axis argument for operators that take a single axis"""
arg_list = []
shape = shapeList[0]
if error_name == ErrorIf.AxisSmallerZero:
# Set too small axis
axes = [rng.integers(-5, 0)]
elif error_name == ErrorIf.AxisLargerRank:
# Set too large axis
axes = [rng.integers(len(shape) + 1, len(shape) + 10)]
else:
# Create tests for each dimension
axes = range(0, len(shape))
opid = testGen.TOSA_OP_LIST[opName]["op"]
for a in axes:
args_dict = {"axis": int(a)}
if opid == Op.REDUCE_SUM:
output_shape = shape.copy()
if error_name is None:
# It only matters that we calculate the dot_products correctly
# for non error_if tests as they should never be run
output_shape[a] = 1
args_dict["dot_products"] = gtu.product(output_shape)
args_dict["shape"] = shape
args_dict["ks"] = int(shape[a]) if a >= 0 and a < len(shape) else 1
args_dict["acc_type"] = dtype if dtype != DType.BF16 else DType.FP32
arg_list.append(("axis{}".format(a), args_dict))
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def _calculate_sparsity(num_tests, sparsity_factor):
sparsity = num_tests // sparsity_factor + 1
# If there are only a small number of tests, just select them all
if sparsity < 13:
sparsity = 1
# To get a variety of parameter combinations sparsity should not be a
# multiple of 2, 3 or 5
while sparsity % 2 == 0 or sparsity % 3 == 0 or sparsity % 5 == 0:
sparsity += 1
return sparsity
# Maximum number of error_if variants to produce
MAX_CONV_ERROR_IFS = 3
@staticmethod
def agConv(testGen, rng, opName, shapeList, dtypes, error_name=None):
# Used by CONV2D, CONV3D and DEPTHWISE_CONV2D
arg_list = []
if testGen.args.level8k and error_name is not None:
# Don't produce negative large tests
return arg_list
# Shape: Batches, (Depth), Height, Width, Channels
ifm_shape = shapeList[0]
# Shape: (OFM channels), (KD), KH, KW, IFM channels
filter_shape = shapeList[1]
accum_dtypes = gtu.get_accum_dtypes_from_tgTypes(dtypes)
if error_name == ErrorIf.WrongAccumulatorType:
accum_dtypes = (
[DType.BF16] if gtu.dtypeIsFloat(dtypes[0]) else [DType.INT16]
)
# Op type checks
conv3d = opName.startswith("conv3d")
depthwise = opName.startswith("depthwise")
# Check the rank
rank = 5 if conv3d else 4
if error_name != ErrorIf.WrongRank:
assert len(ifm_shape) == rank
assert len(filter_shape) == rank
# kernel rank omits channels
k_rank = rank - 2
k_pos = 0 if depthwise else 1
k_shape = tuple(filter_shape[k_pos : (k_pos + k_rank)])
# compliance size - KS
k_size = gtu.product(k_shape)
if not depthwise:
k_size *= ifm_shape[-1]
if not testGen.args.level8k:
if error_name in (
ErrorIf.PadSmallerZero,
ErrorIf.StrideSmallerOne,
ErrorIf.DilationSmallerOne,
):
# Use specific invalid value(s)
if error_name == ErrorIf.PadSmallerZero:
# Create negative paddings but with positive opposite paddings
neg_pad = rng.choice(range(-5, 0))
p_vals = [neg_pad, abs(neg_pad)]
else:
p_vals = [0, 0]
if error_name == ErrorIf.StrideSmallerOne:
# Can't use stride=0, as it is used to derive output shape, as a divisor
s_vals = [rng.choice(range(-5, 0))]
else:
s_vals = [1]
if error_name == ErrorIf.DilationSmallerOne:
d_vals = [rng.choice(range(-5, 1))]
else:
d_vals = [1]
p = p_vals * k_rank
s = s_vals * k_rank
d = d_vals * k_rank
# Fix values to produce valid error_if
for index in range(k_rank):
pad_offset = index * 2
fixed = False
while not fixed:
partial = (
ifm_shape[index + 1]
- 1
+ p[pad_offset]
+ p[pad_offset + 1]
- (k_shape[index] - 1) * d[index]
)
remainder = partial % s[index]
if partial <= 0:
p[pad_offset + 1] += abs(partial) + 1
elif remainder:
# Stride will be negative for StrideSmallerOne
assert remainder < 0
p[pad_offset + 1] += abs(remainder)
else:
fixed = True
paddings = {tuple(p)}
strides = {tuple(s)}
dilations = {tuple(d)}
logger.debug(f"agConv: error pad={p} stride={s} dilation={d}")
else:
# Generate comprehensive argument lists
p_vals = [x for x in range(0, testGen.args.max_conv_padding + 1)]
paddings = {x for x in itertools.product(*([p_vals] * k_rank * 2))}
# Stride must be greater than 1 to force non-integer error
startStride = (
1 if error_name != ErrorIf.ConvOutputShapeNonInteger else 2
)
s_vals = [
x for x in range(startStride, testGen.args.max_conv_stride + 1)
]
d_vals = [x for x in range(1, testGen.args.max_conv_dilation + 1)]
strides = {x for x in itertools.product(*([s_vals] * k_rank))}
dilations = {x for x in itertools.product(*([d_vals] * k_rank))}
if not error_name and testGen.args.oversize:
# add some oversize argument values
if max(ifm_shape) < 64:
bigPadding = 9
paddings.update(
{
x
for x in itertools.product(
*([[0, bigPadding]] * (k_rank * 2))
)
}
)
bigStride = 8
strides.update(
{x for x in itertools.product(*([[1, bigStride]] * k_rank))}
)
bigDilation = 7
dilations.update(
{x for x in itertools.product(*([[1, bigDilation]] * k_rank))}
)
max_dim_size = None
if error_name:
# Cycle through all error_if tests but we only keep the first few
sparsity = 1
else:
# There are too many parameter combinations, so generate them sparsely,
sparsity_factor = 120
sparsity = TosaArgGen._calculate_sparsity(
len(paddings) * len(strides) * len(dilations), sparsity_factor
)
else:
# Only test 8k levels boundaries
bigStride = testGen.TOSA_8K_LEVEL_MAX_STRIDE
bigKernel = testGen.TOSA_8K_LEVEL_MAX_KERNEL
bigPadding = bigKernel
dilation_shape = [1] * k_rank
pad_shape = [0] * k_rank * 2
if conv3d:
# Small stride apart from for big kernel (see below) to keep
# tensor size/calculation small
stride_shape = [1] * k_rank
for idx in range(k_rank):
pad_offset = idx * 2
if k_shape[idx] == bigKernel:
# Padding shape needs to account for tensor shape
pad_shape[pad_offset] = bigPadding - ifm_shape[idx + 1]
pad_shape[pad_offset + 1] = bigPadding - dilation_shape[idx] + 1
# Big stride to reduce output size
stride_shape[idx] = bigKernel
else:
# Account for kernel size
pad_shape[pad_offset] = k_shape[idx] - 1
else:
# Always have a large stride with extra padding and dilation to keep
# tensor calculation reasonable
stride_shape = [bigKernel] * k_rank
for idx in range(k_rank):
# Dilation shape must account for kernel size
dilation_shape[idx] = bigKernel // k_shape[idx]
# Padding shape needs to accommodate tensor/kernel & dilation
pad_offset = idx * 2
pad_shape[pad_offset] = bigPadding - ifm_shape[idx + 1]
pad_shape[pad_offset + 1] = bigPadding - dilation_shape[idx] + 1
strides = {tuple(stride_shape)}
dilations = {tuple(dilation_shape)}
paddings = {tuple(pad_shape)}
# Create a limit for the output dimensions size
max_dim_size = testGen.TOSA_8K_LEVEL_MAX_KERNEL
# Currently allow all combinations that are reasonable size
sparsity = 1
more_tests = True
n = 0
for a in accum_dtypes:
for s in sorted(list(strides)):
for p in sorted(list(paddings)):
for d in sorted(list(dilations)):
if (
more_tests
and n % sparsity == 0
# the padded shape must exceed the dilation * kernel to get a positive
# sized output shape
and (ifm_shape[1] - 1 + p[0] + p[1])
> d[0] * (k_shape[0] - 1)
and (ifm_shape[2] - 1 + p[2] + p[3])
> d[1] * (k_shape[1] - 1)
and (
k_rank < 3
or (
(ifm_shape[3] - 1 + p[4] + p[5])
> d[2] * (k_shape[2] - 1)
)
)
):
remainders = []
outputs = []
for index in range(k_rank):
pad_offset = index * 2
partial = (
ifm_shape[index + 1]
- 1
+ p[pad_offset]
+ p[pad_offset + 1]
- (k_shape[index] - 1) * d[index]
)
remainders.append(partial % s[index])
outputs.append((partial // s[index]) + 1)
if (
# the parameters must produce integer exact output
error_name != ErrorIf.ConvOutputShapeNonInteger
and max(remainders) == 0
) or (
error_name == ErrorIf.ConvOutputShapeNonInteger
and max(remainders) > 0
):
if (
max_dim_size is not None
and max(outputs) >= max_dim_size
):
# Test will consume too much memory - skip it
continue
# Compliance - number of dot product calculations
if depthwise:
# N*OH*OW*C*M
dots = gtu.product(
(ifm_shape[0], *outputs, *filter_shape[2:])
)
else:
# N*OH*OW*OC or N*OD*OH*OW*OC
dots = gtu.product(
(ifm_shape[0], *outputs, filter_shape[0])
)
args_dict = {
"acc_type": a,
"stride": s,
"pad": p,
"dilation": d,
"kernel": k_shape,
"ks": k_size,
"dot_products": dots,
"shape": ifm_shape,
}
# Support for larger values than 9 needs different delimiter
delim = "" if max(s + p + d) <= 9 else "x"
arg_list.append(
(
"acc{}_st{}_pad{}_dilat{}".format(
testGen.typeStr(a),
delim.join([str(x) for x in s]),
delim.join([str(x) for x in p]),
delim.join([str(x) for x in d]),
),
args_dict,
)
)
if (
error_name
and len(arg_list) >= TosaArgGen.MAX_CONV_ERROR_IFS
):
# Found enough errors
logger.debug(
f"Skipping creating more conv error tests for {error_name}"
)
more_tests = False
n += 1
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtypes[0],
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agFullyConnected(testGen, rng, opName, shapeList, dtypes, error_name=None):
assert isinstance(dtypes, (list, tuple)), f"{dtypes} unexpected"
input_dtype = dtypes[0]
if error_name == ErrorIf.WrongOutputType:
accum_dtype = gtu.get_wrong_output_type(opName, rng, input_dtype)
elif error_name == ErrorIf.WrongInputType:
# Pick some potentially correct output dtype if input type is incorrect
accum_dtype = DType.INT32
else:
accum_dtype = dtypes[-1] # use output dtype as accum_dtype
# Set up compliance info
args_dict = {
"acc_type": accum_dtype,
"ks": int(shapeList[0][1]), # Set KS = IC, from input A (N,IC)
"dot_products": gtu.product((shapeList[0][0], shapeList[1][0])),
"shape": shapeList[0],
}
arg_list = [(f"acc{testGen.typeStr(accum_dtype)}", args_dict)]
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
input_dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agMatMul(testGen, rng, opName, shapeList, dtype, error_name=None):
# Get valid accumulate type(s)
if dtype == DType.INT8:
accum_dtypes = [DType.INT32]
elif dtype == DType.INT16:
accum_dtypes = [DType.INT48]
elif dtype == DType.FP16:
accum_dtypes = [DType.FP16, DType.FP32]
elif dtype == DType.BF16:
accum_dtypes = [DType.FP32]
elif dtype == DType.FP32:
accum_dtypes = [DType.FP32]
elif dtype == DType.FP8E4M3 or dtype == DType.FP8E5M2:
accum_dtypes = [DType.FP16]
elif error_name is None:
assert False, f"Invalid I/O DType for MatMul: {DTypeNames[dtype]}"
if error_name == ErrorIf.WrongOutputType:
# Get incorrect output dtype for ErrorIf case
accum_dtypes = [gtu.get_wrong_output_type(opName, rng, dtype)]
elif error_name == ErrorIf.WrongInputType:
# Pick some potentially correct output dtype if input type is incorrect
accum_dtypes = [DType.INT32]
# Set up compliance info
args_dict = {
"ks": int(shapeList[0][2]), # Set KS = C, from input A (N,H,C)
# Set dot_products = N*H*W
"dot_products": gtu.product(
(shapeList[0][0], shapeList[0][1], shapeList[1][2])
),
"shape": shapeList[0],
}
# Create arg tuple of string and dict
arg_list = []
for a in accum_dtypes:
d = args_dict.copy()
d["acc_type"] = a
arg_list.append((f"acc{testGen.typeStr(a)}", d))
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agTransposeConv2D(testGen, rng, opName, shapeList, dtypes, error_name=None):
arg_list = []
if testGen.args.level8k and error_name is not None:
# Don't produce negative large tests
return arg_list
ifm_shape = shapeList[0]
filter_shape = shapeList[1]
accum_dtypes = gtu.get_accum_dtypes_from_tgTypes(dtypes)
if error_name == ErrorIf.WrongAccumulatorType:
accum_dtypes = (
[DType.BF16] if gtu.dtypeIsFloat(dtypes[0]) else [DType.INT16]
)
# Must be rank 4
if error_name != ErrorIf.WrongRank:
assert len(ifm_shape) == 4
assert len(filter_shape) == 4
k_shape = tuple(filter_shape[1:3])
# compliance size - KS
k_size = gtu.product((*k_shape, ifm_shape[3]))
if not testGen.args.level8k:
# Generate comprehensive argument lists
# - except for named errors, which use specific invalid value(s)
smallest_padding_size = -min(k_shape[0], k_shape[1]) + 1
if error_name == ErrorIf.PadLargerEqualKernel:
max_filter_size = -max(k_shape[0], k_shape[1])
p_vals = [rng.choice(range(max_filter_size - 10, max_filter_size))]
else:
p_vals = [
x
for x in range(
smallest_padding_size, testGen.args.max_conv_padding + 1
)
]
paddings = {x for x in itertools.product(*([p_vals] * 4))}
if error_name == ErrorIf.StrideSmallerOne:
# Can't use stride=0, as it is used to derive output shape, as a divisor
s_vals = [rng.choice(range(-5, 0))]
else:
s_vals = [x for x in range(1, testGen.args.max_conv_stride + 1)]
strides = {x for x in itertools.product(*([s_vals] * 2))}
if not error_name and testGen.args.oversize:
# add some oversize argument values
if max(ifm_shape) < 64:
bigPadding = 9
paddings.update(
{
x
for x in itertools.product(
*([[smallest_padding_size, bigPadding]] * 4)
)
}
)
bigStride = 8
strides.update({x for x in itertools.product(*([[1, bigStride]] * 2))})
# There are too many parameter combinations, so generate them sparsely,
# very sparse for negative tests
sparsity_factor = 2 if error_name else 10
sparsity = len(paddings) * len(strides) // sparsity_factor + 1
# If there are only a small number of tests, just select them all
if sparsity < 13:
sparsity = 1
# To get a variety of parameter combinations sparsity should not be a
# multiple of 2, 3 or 5
while sparsity % 2 == 0 or sparsity % 3 == 0 or sparsity % 5 == 0:
sparsity += 1
else:
# Only test 8k levels boundaries
bigStride = testGen.TOSA_8K_LEVEL_MAX_STRIDE
bigKernel = testGen.TOSA_8K_LEVEL_MAX_KERNEL
bigPadding = bigKernel
pad_shape = [0] * (len(k_shape) * 2)
stride_shape = [1] * len(k_shape)
# The point at which input dimension combined with the stride will
# create large output sizes!
LARGE_SIZE = 2
for idx in range(len(k_shape)):
pad_offset = idx * 2
if k_shape[idx] == bigKernel:
# Set large stride
stride_shape[idx] = bigKernel
# Use negative output padding to reduce shape size
pad_shape[pad_offset] = -(bigPadding - 1)
if ifm_shape[idx + 1] > LARGE_SIZE:
pad_shape[pad_offset + 1] = -(bigPadding - 1)
else:
# The other dimension should be the bigKernel
alt_idx = 1 - idx
if (
k_shape[alt_idx] == bigKernel
and ifm_shape[alt_idx + 1] < LARGE_SIZE
):
# As the input is small, the large stride won't
# affect the output so we can add some padding
pad_shape[pad_offset + 1] = bigPadding
strides = {tuple(stride_shape)}
paddings = {tuple(pad_shape)}
# Currently allow all combinations that are reasonable size
sparsity = 1
n = 0
for a in accum_dtypes:
for s in sorted(list(strides)):
for p in sorted(list(paddings)):
if n % sparsity == 0:
# Determine the output shape
oh = (ifm_shape[1] - 1) * s[0] + p[0] + p[1] + k_shape[0]
ow = (ifm_shape[2] - 1) * s[1] + p[2] + p[3] + k_shape[1]
os = [ifm_shape[0], oh, ow, filter_shape[0]]
# N*OH*OW*OC
dots = gtu.product((ifm_shape[0], oh, ow, filter_shape[0]))
args_dict = {
"acc_type": a,
"stride": s,
"pad": p,
"kernel": k_shape,
"ks": k_size,
"dot_products": dots,
"shape": ifm_shape,
"out_shape": os,
}
# Support for larger values than 9 needs different delimiter
delim = "" if max(s + p) <= 9 else "x"
arg_list.append(
(
"acc{}_st{}_pad{}_os{}".format(
testGen.typeStr(a),
delim.join([str(x) for x in s]),
delim.join([str(x) for x in p]),
"x".join([str(x) for x in os]),
),
args_dict,
)
)
n += 1
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtypes[0],
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agPad(testGen, rng, opName, shapeList, dtype, error_name=None):
rank = len(shapeList[0])
# Exhaustively test combinations of padding on each side of each dimension
# - the range of padding values is defined by pad_min and pad_max
# - for padding >9, the name format needs to be more distinctive
pad_min, pad_max = 0, 1
pad_values = [x for x in range(pad_min, pad_max + 1)]
if error_name == ErrorIf.PadSmallerZero:
pad_values = [x for x in range(-2, 0)]
axis_pad_values = [x for x in itertools.product(pad_values, pad_values)]
shape_pad_values = itertools.product(*([axis_pad_values] * rank))
if dtype in [DType.BOOL, DType.INT8, DType.INT16, DType.INT32]:
pad_const_int = rng.randNumberDType(dtype)
pad_const_fp = 0
elif gtu.dtypeIsFloat(dtype):
pad_const_int = 0
pad_const_fp = rng.randNumberDType(dtype)
else:
assert error_name == ErrorIf.WrongInputType
pad_const_int = 0
pad_const_fp = 0
list_shape_pad_values = list(shape_pad_values)
# If we are producing tests for rank 6 or greater use sparsity
if len(list_shape_pad_values) > 1024:
sparsity_factor = 2 if error_name else 120
sparsity = TosaArgGen._calculate_sparsity(
len(list_shape_pad_values), sparsity_factor
)
else:
sparsity = 1
# Build arg list
arg_list = []
for n, paddings in enumerate(list_shape_pad_values):
paddings = list(paddings)
args_valid = True
if error_name == ErrorIf.PadSmallerZero:
# Prevent negative output shapes while ensuring still testing for negative padding
for i in range(rank):
dim_after_padding = (
paddings[i][0] + paddings[i][1] + shapeList[0][i]
)
if dim_after_padding < 1:
paddings[i] = (0, 0)
if all([p > -1 for p in paddings[i]]):
args_valid = False
if args_valid and n % sparsity == 0:
name = "pad"
for r in range(rank):
before, after = paddings[r]
name = f"{name}{before}{after}"
args_dict = {
"pad": np.array(paddings),
"pad_const_int": pad_const_int,
"pad_const_fp": pad_const_fp,
}
arg_list.append((name, args_dict))
if error_name == ErrorIf.PadSmallerZero and len(arg_list) == 0:
logger.debug(
f"agPad: No PadSmallerZero ErrorIf test created for input shape: {shapeList[0]}"
)
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agPooling(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
shape = shapeList[0]
if error_name != ErrorIf.WrongRank:
assert len(shape) == 4
test_level8k = testGen.args.level8k and error_name is None
startStride = 1 if error_name != ErrorIf.PoolingOutputShapeNonInteger else 2
startKernel = 2
startPad = 0
if not test_level8k:
# Generate comprehensive argument lists
p_vals = [x for x in range(startPad, testGen.args.max_pooling_padding + 1)]
paddings = {x for x in itertools.product(*([p_vals] * 4))}
# Stride must be greater than 1 to force non-integer error
s_vals = [
x for x in range(startStride, testGen.args.max_pooling_stride + 1)
]
strides = {x for x in itertools.product(*([s_vals] * 2))}
k_vals = [
x for x in range(startKernel, testGen.args.max_pooling_kernel + 1)
]
kernels = {x for x in itertools.product(*([k_vals] * 2))}
max_dim_size = None
else:
# Only test 8k levels
bigStride = testGen.TOSA_8K_LEVEL_MAX_STRIDE
bigKernel = testGen.TOSA_8K_LEVEL_MAX_KERNEL
strides = {(1, bigStride), (bigStride, 4)}
kernels = {(1, bigKernel), (bigKernel, 3)}
paddings = set()
for s in sorted(list(strides)):
for k in sorted(list(kernels)):
padding = []
for idx in range(len(k)):
total_padding = s[idx] - shape[idx + 1] + k[idx]
while total_padding < 0:
# Must meet: shape + padding > kernel
total_padding += s[idx]
if total_padding < k[idx]:
padding.extend([0, total_padding])
else:
# Note this may produce padding >= k[idx] which is not
# allowed - but will be ignored in the creation loop below
padding.extend([k[idx] - 1, total_padding - (k[idx] - 1)])
paddings.add(tuple(padding))
# Create a limit for the output dimensions size
max_dim_size = testGen.TOSA_8K_LEVEL_MAX_KERNEL
if opName == "max_pool2d":
accum_dtypes = [None] # max_pool has no accumulate dtype
elif dtype == DType.INT8 or dtype == DType.INT16:
accum_dtypes = [DType.INT32]
elif dtype == DType.FP16:
accum_dtypes = [DType.FP16, DType.FP32]
elif dtype == DType.BF16 or dtype == DType.FP32:
accum_dtypes = [DType.FP32]
elif dtype == DType.FP8E4M3 or dtype == DType.FP8E5M2:
accum_dtypes = [DType.FP16]
elif error_name is None:
assert False, f"Invalid I/O DType for pooling: {DTypeNames[dtype]}"
else:
# Set to something for the ErrorIf case which has
# incorrect input data-type
accum_dtypes = [DType.INT32]
if error_name == ErrorIf.WrongAccumulatorType:
accum_dtypes = list(gtu.usableDTypes(excludes=accum_dtypes))
if not test_level8k:
if testGen.args.oversize:
# add some oversize argument values
bigStride = 7
bigKernel = 9
strides.update(
{x for x in itertools.product(*([[startStride, bigStride]] * 2))}
)
kernels.update(
{x for x in itertools.product(*([[startKernel, bigKernel]] * 2))}
)
if max(shape) < 64:
# padding must be less than the kernel size
bigPadding = bigKernel - 1
paddings.update(
{x for x in itertools.product(*([[startPad, bigPadding]] * 4))}
)
# There are too many parameter combinations, so generate them sparsely,
# very sparse for negative tests
sparsity_factor = 2 if error_name else 500
sparsity = (
len(paddings) * len(strides) * len(kernels) // sparsity_factor + 1
)
else:
# We have already limited test output combinations for 8k tests
sparsity = 1
arg_str = (
"acc{}_st{}_kern{}_pad{}"
if accum_dtypes[0] is not None
else "st{}_kern{}_pad{}"
)
def get_arg_list_element(accum, stride, pad, kern, dot_products=0, shape=[]):
# Return tuple containing the formatted argument string and
# the corresponding argument values in a dictionary
# Support for larger values than 9 needs different delimiter
delim = "" if max(stride + kern + pad) <= 9 else "x"
arg_str_elems = [
delim.join([str(x) for x in stride]),
delim.join([str(x) for x in kern]),
delim.join([str(x) for x in pad]),
]
args_dict = {
"stride": stride,
"pad": pad,
"kernel": kern,
"dot_products": dot_products, # Ignored for error tests
"shape": shape,
"ks": gtu.product(kern), # avg_pool2d: KS = KX*KY
}
if accum is not None:
arg_str_elems.insert(0, testGen.typeStr(accum))
args_dict["acc_type"] = accum
return (arg_str.format(*arg_str_elems), args_dict)
n = 0
for a in accum_dtypes:
for s in sorted(list(strides)):
for p in sorted(list(paddings)):
for k in sorted(list(kernels)):
if error_name in [
ErrorIf.StrideSmallerOne,
ErrorIf.KernelSmallerOne,
ErrorIf.PadSmallerZero,
ErrorIf.PadLargerEqualKernel,
]:
sNew, pNew, kNew = TosaErrorIfArgGen.eiPoolingErrorIf(
rng, error_name, s, p, k
)
if None not in [sNew, pNew, kNew] and n % sparsity == 0:
arg_list.append(
get_arg_list_element(a, sNew, pNew, kNew, shape)
)
elif (
n % sparsity == 0
# padding must not exceed the kernel size
and p[0] < k[0]
and p[1] < k[0]
and p[2] < k[1]
and p[3] < k[1]
# the padded shape must exceed the kernel size
and (shape[1] + p[0] + p[1]) > k[0]
and (shape[2] + p[2] + p[3]) > k[1]
):
partial_h = shape[1] + p[0] + p[1] - k[0]
partial_w = shape[2] + p[2] + p[3] - k[1]
remainder_h = partial_h % s[0]
remainder_w = partial_w % s[1]
output_h = partial_h // s[0] + 1
output_w = partial_w // s[1] + 1
logger.debug(
f"agPooling: {shape} remainder=({remainder_h}, {remainder_w}) output=({output_h}, {output_w})"
)
if (
# the parameters must produce integer exact output
error_name != ErrorIf.PoolingOutputShapeNonInteger
and remainder_h == 0
and remainder_w == 0
) or (
error_name == ErrorIf.PoolingOutputShapeNonInteger
and (remainder_h != 0 or remainder_w != 0)
):
if (
max_dim_size is not None
and max(output_h, output_w) > max_dim_size
):
# Test will consume too much memory - skip it
continue
# Dot products = N*OH*OW*C
dp = gtu.product(
(shape[0], output_h, output_w, shape[3])
)
arg_list.append(
get_arg_list_element(a, s, p, k, dp, shape)
)
n += 1
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agCast(testGen, rng, opName, shapeList, inDtype, error_name=None):
arg_list = []
# Enumerate the output types here
if error_name == ErrorIf.WrongOutputType:
dtypeList = TosaErrorIfArgGen.eiCastErrorIf(inDtype)
elif inDtype == DType.INT8:
dtypeList = [
DType.BOOL,
DType.INT16,
DType.INT32,
DType.FP16,
DType.BF16,
DType.FP32,
]
elif inDtype == DType.INT16:
dtypeList = [
DType.BOOL,
DType.INT8,
DType.INT32,
DType.FP16,
DType.BF16,
DType.FP32,
]
elif inDtype == DType.INT32:
dtypeList = [
DType.BOOL,
DType.INT8,
DType.INT16,
DType.FP16,
DType.BF16,
DType.FP32,
]
elif inDtype == DType.BOOL:
dtypeList = [DType.INT8, DType.INT16, DType.INT32]
elif inDtype == DType.FP16:
dtypeList = [
DType.INT8,
DType.INT16,
DType.INT32,
DType.FP32,
DType.FP8E4M3,
DType.FP8E5M2,
]
elif inDtype == DType.BF16:
dtypeList = [
DType.INT8,
DType.INT16,
DType.INT32,
DType.FP32,
DType.FP8E4M3,
DType.FP8E5M2,
]
elif inDtype == DType.FP32:
dtypeList = [
DType.INT8,
DType.INT16,
DType.INT32,
DType.FP16,
DType.BF16,
DType.FP8E4M3,
DType.FP8E5M2,
]
elif inDtype in [DType.FP8E4M3, DType.FP8E5M2]:
dtypeList = [DType.FP16, DType.BF16, DType.FP32]
elif error_name == ErrorIf.WrongInputType:
# Pick some potentially correct output type for incorrect input type
dtypeList = [DType.BOOL, DType.INT8, DType.INT16, DType.FP32]
else:
raise Exception("Unexpected input dtype: {}".format(inDtype))
for dtype in dtypeList:
arg_list.append(
("out{}".format(testGen.typeStr(dtype)), {"out_type": dtype})
)
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
return arg_list
@staticmethod
def agRescale(testGen, rng, opName, shapeList, inDtype, error_name=None):
arg_list = []
# Enumerate the output types here
for outDtype in [
DType.UINT8,
DType.INT8,
DType.INT16,
DType.INT32,
DType.UINT16,
]:
if (
outDtype in [DType.UINT8, DType.INT8, DType.UINT16]
and error_name == ErrorIf.OutputZeroPointNotZero
):
continue
if (
outDtype != DType.UINT16
and error_name == ErrorIf.U16OutputZeroPointNotValid
) or (
inDtype != DType.UINT16
and error_name == ErrorIf.U16InputZeroPointNotValid
):
# ErrorIfs only valid with UINT16
continue
if (
inDtype == DType.UINT8
and outDtype not in [DType.INT8, DType.INT16]
and error_name != ErrorIf.WrongOutputType
):
# The only output dtypes for UINT8 are INT8/INT16, skip all others
continue
if (
inDtype not in [DType.INT8, DType.INT16]
and outDtype == DType.UINT8
and error_name != ErrorIf.WrongOutputType
):
# The only input dtypes for UINT8 are INT8/INT16, skip all others
continue
if (
inDtype == DType.UINT16
and outDtype != DType.INT16
and error_name != ErrorIf.WrongOutputType
):
# The only output dtype for UINT16 is INT16, skip all others
continue
if (
inDtype != DType.INT16
and outDtype == DType.UINT16
and error_name != ErrorIf.WrongOutputType
):
# The only input dtype for UINT16 is INT16, skip all others
continue
if (
error_name == ErrorIf.WrongOutputType
and not TosaErrorIfArgGen.eiRescaleWrongOutputType(inDtype, outDtype)
):
continue
for scale32 in [False, True]:
if error_name == ErrorIf.ScaleTrue and not scale32:
continue
elif error_name == ErrorIf.ScaleNotTrue and scale32:
continue
for double_round in [False, True]:
if error_name == ErrorIf.ScaleNotTrue and not double_round:
continue
for per_channel in [False, True]:
if (
inDtype == DType.INT48
and scale32
and error_name != ErrorIf.ScaleTrue
):
# Illegal condition. Must be scale32=False
continue
if (
double_round
and not scale32
and error_name != ErrorIf.ScaleNotTrue
):
# Illegal condition. ERROR_IF(!scale32 && double_round)
continue
if per_channel:
nc = shapeList[0][-1]
else:
nc = 1
in_type_width = gtu.dtypeWidth(inDtype)
out_type_width = gtu.dtypeWidth(outDtype)
# Calculate scale based on:
# scale = a *(2^output_width)/(2^input_width))
a = np.float32(rng.random(size=[nc]))
scale_arr = a * np.float32(
(1 << out_type_width) / (1 << in_type_width)
)
if scale32:
# Cap the scaling at 2^31 - 1 for scale32
scale_arr = np.clip(
scale_arr, 1.0 / (1 << 31), (1 << 31) - 1
)
else:
# Cap the scaling at 2^15 - 1 for scale16
scale_arr = np.clip(scale_arr, 1.0 / (1 << 31), 32767.0)
logger.debug(
f"agRescale: {out_type_width} {in_type_width} -> {scale_arr}"
)
multiplier_arr = np.int32(np.zeros(shape=[nc]))
shift_arr = np.int32(np.zeros(shape=[nc]))
for i in range(nc):
(
multiplier_arr[i],
shift_arr[i],
) = TosaQuantGen.computeMultiplierAndShift(
scale_arr[i], scale32
)
arg_list.append(
(
"out{}_sc{}_dr{}_pc{}".format(
testGen.typeStr(outDtype),
int(scale32),
int(double_round),
int(per_channel),
),
{
"output_dtype": outDtype,
"scale": scale32,
"double_round": double_round,
"per_channel": per_channel,
"multiplier": multiplier_arr,
"shift": shift_arr,
},
)
)
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
inDtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agMul(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
if dtype is DType.INT32:
for p in range(testGen.args.num_rand_permutations):
shift = rng.randInt(0, 32)
arg_list.append(("perm{}_shift{}".format(p, shift), {"shift": shift}))
else:
arg_list.append(("perm0_shift0", {"shift": 0}))
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agArithmeticRightShift(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
for round in (True, False):
args_dict = {
"round": round,
}
arg_list.append((f"round{round}", args_dict))
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agFFT2d(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
shape = shapeList[0]
dot_products = gtu.product(shape)
ks = 2 * shape[1] * shape[2] # 2*H*W
for inverse in (True, False):
args_dict = {
"dot_products": dot_products,
"shape": shape,
"ks": ks,
"acc_type": dtype,
"inverse": inverse,
}
arg_list.append((f"inverse{inverse}", args_dict))
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agRFFT2d(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
shape = shapeList[0]
dot_products = gtu.product(shape)
ks = shape[1] * shape[2] # H*W
args_dict = {
"dot_products": dot_products,
"shape": shape,
"ks": ks,
"acc_type": dtype,
}
arg_list.append(("", args_dict))
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
# Helper function for reshape. Gets some factors of a larger number.
@staticmethod
def getFactors(val, start=1):
factors = []
for i in range(start, int(np.sqrt(val)) + 1):
if (val % i) == 0:
factors.append(i)
return factors
@staticmethod
def agReshape(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
origShape = shapeList[0]
totalElements = gtu.product(origShape)
factors = TosaArgGen.getFactors(totalElements)
# Find new shapes up to the number of permutations asked for
# This code is NOT fast. Fortunately, the numbers are fairly small.
for p in range(testGen.args.num_rand_permutations):
# Rank from 1 to MAX_TENSOR_RANK
newRank = rng.randInt(1, (gtu.MAX_TENSOR_RANK + 1))
if len(factors) < newRank:
continue
# escape_counter limits the generation of new shapes to a reasonable time
for escape_counter in range(100):
# Generate the new shape of the chosen new rank
newShape = []
remainingElements = totalElements
shuffledFactors = rng.permutation(factors)
for i in range(1, newRank):
# pick rank-1 factors
newShape.append(shuffledFactors[0])
remainingElements = remainingElements // shuffledFactors[0]
shuffledFactors = rng.permutation(
TosaArgGen.getFactors(remainingElements)
)
newShape.append(remainingElements)
# Check for duplicates
duplicate = False
for name, args_dict in arg_list:
if args_dict["new_shape"] == newShape:
duplicate = True
break
if not duplicate:
outShape = "x".join([str(x) for x in newShape])
arg_list.append(
(
"perm{}_rank{}_out{}".format(p, newRank, outShape),
{"new_shape": newShape},
)
)
# Found an output shape for this permutation
break
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
return arg_list
@staticmethod
def agTranspose(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
ifm_shape = shapeList[0]
if error_name == ErrorIf.IndexOutsideBounds:
incorrect_large_index = range(len(ifm_shape) + 1, 2 * len(ifm_shape) + 1)
incorrect_small_index = range(-len(ifm_shape), 0)
permutations = [p for p in itertools.permutations(incorrect_large_index)]
permutations.extend(
[p for p in itertools.permutations(incorrect_small_index)]
)
elif error_name == ErrorIf.IndexUsedTwice:
# Create list with a duplicated index
perm_range = list(range(len(ifm_shape)))
index_choice = rng.choice(range(len(perm_range)))
perm_range[(index_choice + 1) % len(perm_range)] = perm_range[index_choice]
permutations = [p for p in itertools.permutations(perm_range)]
else:
# Get all permutations
permutations = [p for p in itertools.permutations(range(len(ifm_shape)))]
# Limit to possible permutations from shape dimension or argument setting
limit = min(len(permutations), testGen.args.num_rand_permutations)
# Get random permutation generator that uses all permutations
random_permutations = rng.permutation(permutations)
# Create list of required amount of permutations
arg_list = [
("perm{}".format(p), {"perms": random_permutations[p].tolist()})
for p in range(limit)
]
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agSlice(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
ifm_shape = shapeList[0]
rank = len(ifm_shape)
for p in range(testGen.args.num_rand_permutations):
start = []
size = []
valid = True
for i in range(rank):
if ifm_shape[i] > 1:
start.append(rng.randInt(0, ifm_shape[i]))
size.append(rng.randInt(0, ifm_shape[i] - start[i]))
# Invalid slice size?
if size[i] == 0:
valid = False
else:
start.append(0)
size.append(1)
if valid:
# If ERROR_IF test required then incorrect start, size will be returned
start, size = TosaErrorIfArgGen.eiSliceErrorIf(
rng, error_name, ifm_shape, start, size
)
arg_list.append(("perm{}".format(p), {"start": start, "size": size}))
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agTile(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
ifm_shape = shapeList[0]
rank = len(ifm_shape)
for p in range(testGen.args.num_rand_permutations):
# Pick a few random, but small multiple values
# because otherwise this has a tendency to generate
# enormous tensors
multiples = []
for i in range(rank):
if ifm_shape[i] > 1000:
# Multiple of 1 if ifm_shape dimension is large to reduce
# tensor size
multiples.append(1)
elif max(ifm_shape) > 1000:
multiples.append(2)
else:
multiples.append(rng.randInt(1, 4))
arg_list.append(("perm{}".format(p), {"multiples": multiples}))
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agResize(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
ifm_shape = shapeList[0]
def get_aspect_ratio_resize_params():
common_aspect_ratios = ((3, 2), (16, 9), (4, 3))
aspect_ratio = rng.choice(common_aspect_ratios)
invert = rng.choice((False, True))
letterbox = rng.choice((False, True))
scale_y_n = aspect_ratio[0] if invert else aspect_ratio[1]
scale_x_n = aspect_ratio[1] if invert else aspect_ratio[0]
scale_y_d = scale_x_d = 1
offset_x = offset_y = 0
if letterbox:
max_border = scale_y_n
border_y = rng.randInt(low=0, high=max_border)
border_x = 0
else:
# Pillarboxing
border_y = 0
max_border = scale_x_n
border_x = rng.randInt(low=0, high=max_border)
scale = (scale_y_n, scale_y_d, scale_x_n, scale_x_d)
offset = (offset_y, offset_x)
border = (border_y, border_x)
return scale, offset, border
def get_upscale_downscale_params():
valid_params = False
while not valid_params:
upscale = rng.choice((False, True))
# True if sampling begins from (0,0). Otherwise (-0.5,-0.5)
origin_sampling = rng.choice((False, True))
if upscale:
shift = rng.randInt(low=1, high=4)
scale_x_d = scale_y_d = 1
scale_x_n = scale_y_n = (
1 << shift if origin_sampling else 2 << shift
)
border_x = border_y = 0 if origin_sampling else (1 << shift) - 1
offset_x = offset_y = 0 if origin_sampling else -(1 << shift) + 1
else:
scale_x_n = 1
scale_y_n = 1
# Return list of valid scale_*_d values (max value 4) given input dim shape
def get_valid_denom(ifm_dim):
return [x for x in range(1, 5) if ifm_dim % x == 1]
# Generate list of valid downscale values and choose one randomly
valid_scale_y_ds = get_valid_denom(ifm_shape[1])
valid_scale_x_ds = get_valid_denom(ifm_shape[2])
if not valid_scale_y_ds and not valid_scale_x_ds:
# Bad parameters, skip
continue
if not valid_scale_y_ds:
scale_y_d = 1
else:
scale_y_d = rng.choice(valid_scale_y_ds)
if not valid_scale_x_ds:
scale_x_d = 1
else:
scale_x_d = rng.choice(valid_scale_x_ds)
border_x = border_y = 0
offset_y = rng.randInt(0, 16 * scale_y_n)
offset_x = rng.randInt(0, 16 * scale_x_n)
valid_params = True
scale = (scale_y_n, scale_y_d, scale_x_n, scale_x_d)
offset = (offset_y, offset_x)
border = (border_y, border_x)
return scale, offset, border
def get_rand_params():
def fix_scale_to_max_scale(scale_n, scale_d, max_scale):
scale = scale_n / scale_d
if scale > max_scale:
factor = scale / max_scale
new_scale_d = math.ceil(scale_d * factor)
assert scale_n / new_scale_d <= max_scale
scale_d = new_scale_d
return scale_d
# Scale
scale_y_n = rng.randInt(low=1, high=(1 << 11))
scale_x_n = rng.randInt(low=1, high=(1 << 11))
scale_y_d = rng.randInt(low=1, high=(16 * scale_y_n))
scale_x_d = rng.randInt(low=1, high=(16 * scale_x_n))
scale_y_d = fix_scale_to_max_scale(
scale_y_n, scale_y_d, testGen.TOSA_8K_LEVEL_MAX_SCALE
)
scale_x_d = fix_scale_to_max_scale(
scale_x_n, scale_x_d, testGen.TOSA_8K_LEVEL_MAX_SCALE
)
# Offsets and border within the scale
offset_y = rng.randInt(low=-scale_y_n, high=(16 * scale_y_n))
offset_x = rng.randInt(low=-scale_x_n, high=(16 * scale_x_n))
border_y = rng.randInt(low=(-16 * scale_y_n), high=scale_y_n)
border_x = rng.randInt(low=(-16 * scale_x_n), high=scale_x_n)
scale = (scale_y_n, scale_y_d, scale_x_n, scale_x_d)
offset = (offset_y, offset_x)
border = (border_y, border_x)
return scale, offset, border
def get_level_8k_params():
# Create 64x scale - 64/1 to 2048/32
scale_d = rng.randInt(
low=1, high=(1 << 11) / testGen.TOSA_8K_LEVEL_MAX_SCALE
)
scale_n = scale_d * testGen.TOSA_8K_LEVEL_MAX_SCALE
# Create half to fifth scaling
scale_d_alt = rng.randInt(low=2, high=6)
scale_n_alt = 1
switch = rng.choice((False, True))
if switch:
scale = (scale_n_alt, scale_d_alt, scale_n, scale_d)
else:
scale = (scale_n, scale_d, scale_n_alt, scale_d_alt)
offset_y = rng.choice((-scale[0], 0, (16 * scale[0]) - 1))
offset_x = rng.choice((-scale[2], 0, (16 * scale[2]) - 1))
offset = (offset_y, offset_x)
border_y = rng.choice((-16 * scale[0], 0, scale[0] - 1))
border_x = rng.choice((-16 * scale[2], 0, scale[2] - 1))
border = (border_y, border_x)
return scale, offset, border
for mode in [ResizeMode.NEAREST, ResizeMode.BILINEAR]:
# Exclude illegal {mode, type} configurations. Pick legal output types
if mode == ResizeMode.NEAREST and dtype == DType.INT8:
outputDTypeList = [DType.INT8]
elif mode == ResizeMode.NEAREST and dtype == DType.INT16:
outputDTypeList = [DType.INT16]
elif mode == ResizeMode.BILINEAR and dtype == DType.INT8:
outputDTypeList = [DType.INT32]
elif mode == ResizeMode.BILINEAR and dtype == DType.INT16:
outputDTypeList = [DType.INT48]
elif dtype == DType.FP16:
outputDTypeList = [DType.FP16]
elif dtype == DType.BF16:
outputDTypeList = [DType.BF16]
elif dtype == DType.FP32:
outputDTypeList = [DType.FP32]
elif dtype == DType.FP8E4M3:
outputDTypeList = [DType.FP8E4M3]
elif dtype == DType.FP8E5M2:
outputDTypeList = [DType.FP8E5M2]
elif error_name == ErrorIf.WrongInputType:
# If an incorrect input type is used then we set a 'correct'
# output type to avoid other errors
outputDTypeList = [DType.INT8, DType.INT16, DType.INT32]
else:
continue
arg_str = "mode{}_out{}_sc{}x{}x{}x{}_off{}x{}_bor{}x{}"
for outputDType in outputDTypeList:
perm = 0
while perm < testGen.args.num_rand_permutations:
# Random choice of type of params we are testing
if not testGen.args.level8k:
_rnd_param_fn = rng.choice(
(
get_rand_params,
get_upscale_downscale_params,
get_aspect_ratio_resize_params,
)
)
scale, offset, border = _rnd_param_fn()
else:
scale, offset, border = get_level_8k_params()
# Expand params for bounds-checking
(scale_y_n, scale_y_d, scale_x_n, scale_x_d) = scale
(offset_y, offset_x) = offset
(border_y, border_x) = border
# Make sure output dimensions OH and OW are integers
partial_output_y = (
(ifm_shape[1] - 1) * scale_y_n - offset_y + border_y
)
partial_output_x = (
(ifm_shape[2] - 1) * scale_x_n - offset_x + border_x
)
if error_name == ErrorIf.ResizeOutputShapeNonInteger:
# Look for non-integer test
if (
partial_output_y % scale_y_d == 0
and partial_output_x % scale_x_d == 0
):
# Skip this test as it doesn't produce NonInteger output
if perm > 0:
perm += 1
continue
else:
# Alter the scaling factors to make the output integer
while partial_output_y % scale_y_d != 0:
scale_y_d -= 1
while partial_output_x % scale_x_d != 0:
scale_x_d -= 1
# Make sure we are still within max scaling
if (
scale_y_n / scale_y_d
) > testGen.TOSA_8K_LEVEL_MAX_SCALE or (
scale_x_n / scale_x_d
) > testGen.TOSA_8K_LEVEL_MAX_SCALE:
# Skip the test as it is using too large a scaling factor
if perm > 0:
perm += 1
continue
output_y = partial_output_y // scale_y_d + 1
output_x = partial_output_x // scale_x_d + 1
if (
output_y >= testGen.args.max_resize_output_dim
or output_x >= testGen.args.max_resize_output_dim
) and error_name is None:
# Skip positive test if output dim will be too high
# Avoid high test latency and OOM issues
if not testGen.args.level8k or perm > 0:
perm += 1
continue
if (
output_y <= 0
or output_y >= gtu.MAX_RESIZE_DIMENSION
or output_x <= 0
or output_x >= gtu.MAX_RESIZE_DIMENSION
):
# Output dimensions out of scope
if error_name is not None and perm > 0:
# As long as we have one ERROR_IF test, don't worry
# about creating all the other permutations
perm += 1
continue
if error_name == ErrorIf.ResizeOutputShapeMismatch and (
(
output_y + scale_y_d >= gtu.MAX_RESIZE_DIMENSION
and output_y - scale_y_d < 1
)
or (
output_x + scale_x_d >= gtu.MAX_RESIZE_DIMENSION
and output_x - scale_x_d < 1
)
):
# Can't create a negative test with these params as it
# will create invalid output size
if perm > 0:
perm += 1
continue
scale = [scale_y_n, scale_y_d, scale_x_n, scale_x_d]
offset = [offset_y, offset_x]
border = [border_y, border_x]
# Common for all data types
if error_name is not None:
(
scale,
offset,
border,
outputDTypeNew,
) = TosaErrorIfArgGen.eiResizeErrorIf(
rng,
error_name,
mode,
dtype,
shapeList,
outputDType,
scale,
offset,
border,
)
else:
outputDTypeNew = outputDType
arg_to_append = (
arg_str.format(
"N" if mode == ResizeMode.NEAREST else "B",
testGen.typeStr(outputDTypeNew),
scale[0],
scale[1],
scale[2],
scale[3],
offset[0],
offset[1],
border[0],
border[1],
),
{
"mode": mode,
"scale": scale,
"offset": offset,
"border": border,
"output_dtype": outputDTypeNew,
},
)
if arg_to_append in arg_list:
# Skip already generated test params
continue
# Valid permutation
perm += 1
arg_list.append(arg_to_append)
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
@staticmethod
def agTable(testGen, rng, opName, shapeList, dtype, error_name=None):
arg_list = []
if dtype == DType.INT8:
table = np.int32(rng.integers(low=-128, high=128, size=[256])).tolist()
else: # INT16
table = np.int32(rng.integers(low=-32768, high=32768, size=[513])).tolist()
# Make sure all slopes are within REQUIRE min/max 16-bit int
for idx in range(len(table) - 1):
slope = table[idx + 1] - table[idx]
# Alter the next table entry to force the slope to be ok
if slope > 32767:
table[idx + 1] -= slope - 32767
if slope < -32768:
table[idx + 1] -= slope + 32768
slope = table[idx + 1] - table[idx]
assert slope <= 32767 and slope >= -32768
arg_list.append(
(
"",
{"table": table},
)
)
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
def agCondIf(testGen, rng, opName, shapeList, dtype, error_name=None):
# CondIf generates the condition values here.
# Convert to tensors in the build function, along with the
# then and else blocks
arg_list = []
for c in [False, True]:
arg_list.append(("cond{}".format(int(c)), {"condition": c}))
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list
def agWhileLoop(testGen, rng, opName, shapeList, dtype, error_name=None):
# While loop: 0 iterations, 1, more than 1
arg_list = []
for iterations in [0, 1, 4]:
arg_list.append(("iter{}".format(iterations), {"iterations": iterations}))
# Now add data generator types
arg_list = TosaArgGen._add_data_generators(
testGen,
opName,
shapeList,
dtype,
arg_list,
error_name,
)
# Return list of tuples: (arg_str, args_dict)
return arg_list