blob: ba53ba7af2a293fde44a8e1e3bfc98862d491f00 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ruff: noqa: F401, RUF012
"""
This module serves two purposes:
(1) Demonstrates how to write Python code that exercises various
Hexagon-related algorithms / features.
(2) Benchmark the resulting primfuncs.
Current limitations:
- Input shapes are limited to NHWC --> NHWC_8h8w32c.
- Testing parameters (input shapes, dtypes, etc.) currently
support only one value for each parameter.
- height, width, channel must be integer multiples of 8, 8, and 32,
respectively. I.e., partial blocks aren't currently
supported by this script.
- Requires that I/O tensors reside in "global.VTCM" memory,
rather than "global" memory.
This prevents benchmarking with I/O tensors that are too
large to fit into availble VTCM.
- The script only develops one primfunc.
Future revisions to this script are expected to add more
primfuncs and demonstrate more coding strategies.
"""
import copy
import os
from typing import List
import numpy as np
import pytest
import tvm.testing
from tvm import te, tir, topi
from tvm.contrib.hexagon import allocate_hexagon_array
from tvm.contrib.hexagon.session import Session
from tvm.topi import testing
from . import benchmark_util as bu
from .infrastructure import get_hexagon_target
# Pytest seems to require that fixture names exist in the current module.
# E.g., it doesn't allow: @pytest.mark.usefixtures("bu.benchmark_group")
BENCHMARK_GROUP = bu.benchmark_group
_SHOULD_SKIP_BENCHMARKS, _SKIP_BENCHMARKS_REASON = bu.skip_benchmarks_flag_and_reason()
def _ceil_div(numerator, denominator):
return (numerator + (denominator - 1)) // denominator
def _int8_nhwc_8h8w32c_map(n_batch, height, width, channel):
return [
n_batch,
height // 8,
width // 8,
channel // 32,
te.AXIS_SEPARATOR,
height % 8,
width % 8,
channel % 32,
]
def _int8_nhwc_8h8w32c_shape(n_batch, height, width, channel) -> List[int]:
return [
n_batch,
_ceil_div(height, 8),
_ceil_div(width, 8),
_ceil_div(channel, 32),
8,
8,
32,
]
def _int8_nhwc_8h8w32c_xform_immediate(arr_in: np.ndarray) -> np.ndarray:
"""
Return a deep copy of 'arr_in', transformed from a NWHC to
NHWC-8h8wc32 shape. Any newly created array elements have value 0.
"""
stage1 = copy.copy(arr_in)
(
n_batch,
height,
width,
channel,
) = stage1.shape
(
h_minor,
w_minor,
c_minor,
) = [8, 8, 32]
h_major = _ceil_div(height, h_minor)
w_major = _ceil_div(width, w_minor)
c_major = _ceil_div(channel, c_minor)
# This handles cases where the dimensions of arr_in are not cleanly divided
# by the minor block size, i.e. [8, 8, 32].
#
# Any additional array elements that this creates will ahve value 0.
# We shouldn't actually care what value is used for those elements, because they
# shouldn't be treated as meaningful by any of our algorithms.
if (height % h_minor) or (width % w_minor) or (channel % c_minor):
stage1.resize(
(n_batch, h_major * h_minor, w_major * w_minor, c_major * c_minor), refcheck=False
)
stage2 = stage1.reshape(n_batch, h_major, h_minor, w_major, w_minor, c_major, c_minor)
stage3 = stage2.transpose(0, 1, 3, 5, 2, 4, 6)
return stage3
def _create_test_input(shape, dtype: str) -> np.ndarray:
np_dtype = np.dtype(dtype)
min_value = np.iinfo(np_dtype).min
max_value = np.iinfo(np_dtype).max
return np.random.randint(low=min_value, high=max_value, size=tuple(shape), dtype=np.int8)
@pytest.mark.usefixtures("BENCHMARK_GROUP")
class TestMaxPool2D:
"""maxpool2D base test class"""
csv_column_order = [
# Identifies which TE-compute / TIRScript is used as the basis for the
# benchmarked primfunc. Only needs to be meaningful to humans.
"basic_kernel",
# When applicable, indicates the particular variation of schedules
# apply by the Python code. Decoding this may require looking at this
# script's source code.
"sched_type",
# Values directly based on test parameters...
"input_shape_4d",
"block_shape",
"dtype",
"kernel",
"stride",
"dilation",
"padding",
"io_tensor_mem_scope",
# Reserved columns defined by the BenchmarksTable class.
"row_status",
"timings_min_usecs",
"timings_max_usecs",
"timings_median_usecs",
"timings_mean_usecs",
"timings_stddev_usecs",
# For benchmarks that produce files on the host file system, this indicates
# their location. Useful for post-mortem investigation of benchmark results.
"host_files_dir_path",
# Miscellaneous comments about the benchmark.
"comments",
]
dtype = tvm.testing.parameter("int8")
# FIXME(cconvey): The script currently fails when height, width, or channel is not an
# integer multiple of 8, 8, or 32, respectively.
n_batch = tvm.testing.parameter(1)
height = tvm.testing.parameter(*[x * 8 for x in [1, 4, 16]])
width = tvm.testing.parameter(*[x * 8 for x in [1, 4, 16]])
channel = tvm.testing.parameter(*[x * 32 for x in [1, 2]])
kernel = tvm.testing.parameter((1, 1), (3, 3))
stride = tvm.testing.parameter((1, 1))
dilation = tvm.testing.parameter((1, 1))
padding = tvm.testing.parameter((0, 0, 0, 0))
io_tensor_mem_scope = tvm.testing.parameter("global.vtcm")
@pytest.mark.skipif(_SHOULD_SKIP_BENCHMARKS, reason=_SKIP_BENCHMARKS_REASON)
@tvm.testing.requires_hexagon
def test_maxpool2d_nhwc(
self,
n_batch,
height,
width,
channel,
dtype,
kernel,
stride,
dilation,
padding,
io_tensor_mem_scope,
hexagon_session: Session,
):
"""Test maxpool2d NHWC"""
keys_dict = {
"basic_kernel": "max_pool2d",
"sched_type": 1,
"input_shape_4d": [n_batch, height, width, channel],
"block_shape": [8, 8, 32],
"dtype": dtype,
"kernel": kernel,
"stride": stride,
"dilation": dilation,
"padding": padding,
"io_tensor_mem_scope": io_tensor_mem_scope,
}
desc = bu.get_benchmark_decription(keys_dict)
# Create the host-side directory for this benchmark run's files / logs...
host_files_dir_name = bu.get_benchmark_id(keys_dict)
host_files_dir_path = os.path.join(self.working_dir, host_files_dir_name)
os.mkdir(host_files_dir_path)
keys_dict["host_files_dir_path"] = host_files_dir_path
log_file_path = os.path.join(host_files_dir_path, "out.txt")
with open(log_file_path, "w") as log_file:
print(f"CONFIGURATION: {desc}")
log_file.write(f"CONFIGURATION: {desc}\n")
try:
input_tensor_shape_4d = [n_batch, height, width, channel]
input_tensor_shape_7d = _int8_nhwc_8h8w32c_shape(n_batch, height, width, channel)
data = te.placeholder(tuple(input_tensor_shape_4d), dtype=dtype)
output = topi.nn.pool2d(
data, kernel, stride, dilation, padding, "max", layout="NHWC"
)
primfunc = te.create_prim_func([data, output])
sch = tvm.s_tir.Schedule(primfunc, debug_mask="all")
sch.transform_layout(
block="tensor", buffer="placeholder", index_map=_int8_nhwc_8h8w32c_map
)
built_module = tvm.compile(
sch.mod,
target=get_hexagon_target("v69"),
)
# Save a local copy of the Hexagon object code (in the form of a .so file)
# to allow post-mortem inspection.
host_dso_binary_path = os.path.join(host_files_dir_path, "test_binary.so")
built_module.write_to_file(host_dso_binary_path)
print(f"SAVED BINARY TO HOST PATH: {host_dso_binary_path}")
hexagon_mod = hexagon_session.load_module(built_module)
# Generate the input tensor's data.
# Note that we'll eventually need it in two different layouts:
# (1) NHWC as an argument to testing.poolnd_python.
# (2) NHWC_8h8w32c for as an argument to our Hexagon primfunc.
# a_numpy_4d = np.random.randint(low=-128, high=127,
# size=input_tensor_shape_4d, dtype=np.int8)
a_numpy_4d = _create_test_input(input_tensor_shape_4d, dtype)
ref_output_4d = testing.poolnd_python(
a_numpy_4d.astype("int32"),
kernel,
stride,
dilation,
padding[0:2],
padding[2:],
pool_type="max",
dtype="int32",
layout="NHWC",
).astype(dtype)
output_tensor_shape_4d = ref_output_4d.shape
a_numpy_7d = _int8_nhwc_8h8w32c_xform_immediate(a_numpy_4d)
a_hexagon_7d = allocate_hexagon_array(
hexagon_session.device,
tensor_shape=input_tensor_shape_7d,
axis_separators=[4],
dtype=dtype,
mem_scope=io_tensor_mem_scope,
)
c_hexagon_4d = allocate_hexagon_array(
hexagon_session.device,
tensor_shape=output_tensor_shape_4d,
axis_separators=[],
dtype=dtype,
mem_scope=io_tensor_mem_scope,
)
a_hexagon_7d.copyfrom(a_numpy_7d)
if dtype == "int8":
rel_tolerance = 0
abs_tolerance = 0
else:
assert False, f"TODO: decide acceptable tolerances for dtype {dtype}"
timer = hexagon_mod.time_evaluator(
"main", hexagon_session.device, number=10, repeat=1
)
timing_result = timer(a_hexagon_7d, c_hexagon_4d)
try:
tvm.testing.assert_allclose(
ref_output_4d, c_hexagon_4d.numpy(), rtol=rel_tolerance, atol=abs_tolerance
)
except AssertionError as exception:
raise bu.NumericalAccuracyException(str(exception))
except bu.NumericalAccuracyException as exception:
print()
print("FAIL: Numerical accuracy error. See log file.")
log_file.write("\n")
log_file.write(f"FAIL: {exception}\n")
self.benchmark_table.record_fail(
**keys_dict, comments="Numerical accuracy error. See log file."
)
except bu.UnsupportedException as exception:
print()
print(f"SKIP: {exception}")
log_file.write("\n")
log_file.write(f"SKIP: {exception}\n")
self.benchmark_table.record_skip(
**keys_dict, comments=f"Unsupported configuration: {exception}"
)
self.benchmark_table.record_success(timing_result, **keys_dict)
if __name__ == "__main__":
tvm.testing.main()