blob: 5563b96952a3b7294a95147f748b521983318fdd [file] [log] [blame]
# Copyright (C) 2020 Arm Limited or its affiliates. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed 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
#
# 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.
# Description:
# Register level (low-level) command stream generation for Ethos-U55. Takes a high-level command stream and generates
# all the register settings. Calculates dependencies between commands and inserts wait operations. And generates a bit
# stream suitable for interpretation by the Ethos-U55 processor.
from collections import defaultdict
from enum import Enum, IntEnum
from .high_level_command_stream import CommandType
from .ethos_u55_regs.ethos_u55_regs import *
from .tensor import MemArea, TensorBlockTraversal
from .operation import NpuBlockType
from .numeric_util import quantise_float32, round_up, round_away_zero, round_up_to_int, clamp_sigmoid, clamp_tanh
from .data_type import BaseType
import numpy as np
from .shared_buffer_allocation import SharedBufferAllocation
from .architecture_features import SharedBufferArea, SHRAMElements, ArchitectureFeatures
from .nn_graph import TensorFormat, SchedulingStrategy
from .range_set import (
MemoryAccessSet,
AccessDirection,
)
from .mark_tensors import (
reshape_operations,
)
from .architecture_features import Block, Kernel, Rect
from . import scaling
class RegisterMachine:
def __init__(self):
self.n_banks = 1
self.registers = [defaultdict(lambda: None) for _ in range(self.n_banks)]
self.bank_idx = 0
def set_register(self, reg, value):
is_changed = self.registers[self.bank_idx][reg] != value
self.registers[self.bank_idx][reg] = value
# is_changed = True # force command
return is_changed
def switch_bank(self):
self.bank_idx = (self.bank_idx + 1) % self.n_banks
class CmdMode(IntEnum):
NoPayload = 0x0000
Payload32 = 0x4000
Mask = 0xC000
CmdOpMask = 0x03FF
class BasePointerIndex(IntEnum):
ReadOnly = 0 # base address slot index for weights and scaling
Scratch = 1 # base address slot index for scratch memory area
# TODO: Replace with definitions from ethos_u55_regs
class IFM2Broadcast(IntEnum):
BroadcastHdim = 1 << 0
BroadcastWdim = 1 << 1
BroadcastCdim = 1 << 2
ReverseOperandOrder = 1 << 6
UseIFM2Scalar = 1 << 7
class CommandStreamEmitter:
def __init__(self):
self.cmd_stream = []
self.reg_machine = [RegisterMachine(), RegisterMachine()]
self.last_absolute_wait = defaultdict(int)
def get_reg_machine(self, cmd):
if "DMA" in cmd.name:
return self.reg_machine[1]
else:
return self.reg_machine[0]
def size_in_bytes(self):
sz = 0
for cmd in self.cmd_stream:
sz += len(cmd) * 4
return sz
def to_list(self):
return [elem for cmd in self.cmd_stream for elem in cmd]
def print_cmds(self):
print("Code: Command: Param: Payload:")
for words_for_one_command in self.cmd_stream:
code = words_for_one_command[0] & 0x0000FFFF # lower 16 bits
param = words_for_one_command[0] >> 16 # higher 16 bits
payload_mode = CmdMode(code & CmdMode.Mask)
# code and command
s = " 0x%04x " % code
if payload_mode == CmdMode.NoPayload:
s += str(cmd0(code & CmdMode.CmdOpMask))
else:
s += str(cmd1(code & CmdMode.CmdOpMask))
s = s.ljust(40)
s += "%5d" % param
# payload
if payload_mode == CmdMode.Payload32:
s += " 0x%08x (%d)" % (words_for_one_command[1], words_for_one_command[1])
else:
s += " -"
print(s)
def cmd0_with_param(self, cmd, param):
if isinstance(param, Enum):
param = int(param.value)
else:
param = int(param)
param = param & 0xFFFF
command = cmd.value | (param << 16)
if not self.get_reg_machine(cmd).set_register(cmd, (command, param)):
return
# This is not a redundant command, actually write it
self.cmd_stream.append((command,))
def cmd1_with_offset(self, cmd, offset, param=0x0):
offset = int(offset) & 0xFFFFFFFFF
command = cmd.value | CmdMode.Payload32.value | (param << 16)
if not self.get_reg_machine(cmd).set_register(cmd, (command, offset)):
return
# This is not a redundant command, actually write it
self.cmd_stream.append((command, offset))
def cmd_wait(self, cmd, param, absolute_wait_time):
if absolute_wait_time <= self.last_absolute_wait[cmd]:
return
self.last_absolute_wait[cmd] = absolute_wait_time
param = int(param)
command = ((param & 0xFFFF) << 16) | cmd.value
self.cmd_stream.append((command,))
def cmd_do_operation(self, cmd, param=0):
param = int(param)
command = ((param & 0xFFFF) << 16) | cmd.value
self.cmd_stream.append((command,))
self.get_reg_machine(cmd).switch_bank()
def calc_command_dependencies(cmd_stream, arch):
cmd_starts = {}
cmd_ends = {}
memory_accesses = {}
# Keep track of accumulated number of commands in command stream.
# First element kernel ops: (# of blocks, # of commands)
# Second element DMA ops: (# of commands)
pos = np.array((np.array((0, 0)), np.array([0])))
dependencies = {}
for cmd in cmd_stream:
cmd_starts[cmd] = pos
op_count = cmd.get_operation_count()
# Keep track of both num blocks and commands
cmd_add = 0 if (op_count[0] == 0) else 1
pos = np.array((pos[0] + np.array((op_count[0], cmd_add)), pos[1] + np.array([op_count[1]])))
cmd_ends[cmd] = np.array((pos[0], pos[1]))
memory_accesses[cmd] = cmd.get_memory_accesses()
for idx, cmd in enumerate(cmd_stream):
curr_accesses = memory_accesses[cmd]
# Keep track of command dependency.
# First element kernel ops: (# of blocks, # of commands)
# Second element DMA ops: (# of commands)
dep_offsets = np.array((np.array((-1, -1)), np.array([-1])))
dep_cmds = [None] * CommandType.Size.value
if idx > 0:
# Look at the previous commands in backwards order
for prev_cmd in cmd_stream[idx - 1 :: -1]:
assert prev_cmd is not cmd
if dep_cmds[prev_cmd.cmdtype] is None:
is_dependency = False
if cmd.cmdtype == CommandType.NpuStripe and prev_cmd.cmdtype == CommandType.NpuStripe:
# Special handling here, as dpu -> dpu operations require additional care
if not SharedBufferAllocation.is_compatible(prev_cmd.ps.shared_buffer, cmd.ps.shared_buffer):
is_dependency = True
elif memory_accesses[prev_cmd].conflicts(curr_accesses):
is_dependency = True
else:
if memory_accesses[prev_cmd].conflicts(curr_accesses):
is_dependency = True
if is_dependency:
new_offset = cmd_ends[prev_cmd][prev_cmd.cmdtype]
if new_offset[0] > dep_offsets[prev_cmd.cmdtype][0]:
dep_cmds[prev_cmd.cmdtype] = prev_cmd
dep_offsets[prev_cmd.cmdtype] = new_offset
# Check if we've got dependencies for all commands, in which case we can early out
for dep in dep_cmds:
if dep is None:
break
else:
break # all handled
# Convert absolute to relative dependencies, using None to signal the special case of no
# dependency of this kind
res = [None] * CommandType.Size.value
for i in range(CommandType.Size.value):
if dep_cmds[i] is not None:
res[i] = cmd_starts[cmd][i] - dep_offsets[i]
dependencies[cmd] = cmd_starts[cmd], res
return dependencies
def get_op_kernel(ps):
if ps.primary_op is None:
return None
strides = ps.primary_op.attrs.get("strides", (1, 1, 1, 1))
dilation = ps.primary_op.attrs.get("dilation", (1, 1, 1, 1))
if ps.weight_tensor:
if ps.npu_block_type in set((NpuBlockType.VectorProduct, NpuBlockType.ElementWise)):
k_h = 1
k_w = 1
else:
k_h = ps.weight_tensor.shape[0]
k_w = ps.weight_tensor.shape[1]
else:
k_h = ps.primary_op.attrs.get("filter_height", 1)
k_w = ps.primary_op.attrs.get("filter_width", 1)
return Kernel(k_w, k_h, strides[2], strides[1], dilation[2], dilation[1])
def full_shape(shape, fill):
return ([fill] * (4 - len(shape))) + shape
def has_prev_op_dependency(prev_cmd, cmd):
if prev_cmd is None:
return False
if (prev_cmd.cmdtype == cmd.cmdtype == CommandType.NpuStripe) and (prev_cmd.ps != cmd.ps):
if prev_cmd.ofm_tensor == cmd.ifm_tensor:
return True
else:
return prev_cmd.ofm_tensor.equivalence_id == cmd.ifm_tensor.equivalence_id
return False
def get_op_ofm_rect(cmd):
start = full_shape(cmd.ofm_box.start_coord, 0)
end = full_shape(cmd.ofm_box.end_coord, 1)
return Rect(start[-2], start[-3], start[-1], end[-2] - 1, end[-3] - 1, end[-1] - 1)
def get_op_ifm_rect(cmd):
start = full_shape(cmd.ifm_box.start_coord, 0)
end = full_shape(cmd.ifm_box.end_coord, 1)
return Rect(start[-2], start[-3], start[-1], end[-2] - 1, end[-3] - 1, end[-1] - 1)
def get_op_ifmofm_block_depth(arch, cmd):
# Note: NOT equivalent to the normal ifm block depth calculation since
# it takes into account 'depthless' block operations by returning full
# depth
if cmd.ps.npu_block_type in (NpuBlockType.ConvolutionDepthWise, NpuBlockType.Pooling, NpuBlockType.ElementWise):
return cmd.ofm_box.get_size_shape()[-1]
return arch.calc_ifm_block_depth(cmd.ifm_box.get_size_shape()[-1], cmd.ifm_tensor.dtype.bits)
def get_op_padding_lt(cmd):
if cmd.ps.npu_block_type not in (
NpuBlockType.ConvolutionDepthWise,
NpuBlockType.Pooling,
NpuBlockType.ConvolutionMxN,
):
return (0, 0)
explicit_padding = list(cmd.ps.primary_op.attrs["explicit_padding"]) # (top, left, bottom, right)
# Check if this is for horizontal ifm streaming
if not (cmd.is_first_h_stripe and cmd.is_last_h_stripe):
explicit_padding[0] = cmd.pad_top
explicit_padding[2] = cmd.pad_bottom
return (explicit_padding[1], explicit_padding[0])
def generate_register_command_stream(nng, sg, arch, verbose=False):
emit = CommandStreamEmitter()
base_ptr_idx_map = {
MemArea.Sram: BasePointerIndex.Scratch,
MemArea.OnChipFlash: BasePointerIndex.ReadOnly,
MemArea.OffChipFlash: BasePointerIndex.ReadOnly,
MemArea.Dram: BasePointerIndex.ReadOnly,
}
# Maps an AccumulatorType enum to the corresponding acc_format value
acc_format_map = {
SHRAMElements.Acc16: acc_format.FP_S5_10.value,
SHRAMElements.Acc32: acc_format.INT_32BIT.value,
SHRAMElements.Acc40: acc_format.INT_40BIT.value,
}
# Maps an elementwise op type to an elementwise_mode enum value used by NPU_OP_ELEMENTWISE
elementwise_mode_map = {
"MulAct": elementwise_mode.MUL.value,
"AddAct": elementwise_mode.ADD.value,
"SubAct": elementwise_mode.SUB.value,
"Minimum": elementwise_mode.MIN.value,
"Maximum": elementwise_mode.MAX.value,
"LeakyRelu": elementwise_mode.LRELU.value,
"Abs": elementwise_mode.ABS.value,
}
cmd_stream = []
for cmd in sg.high_level_command_stream:
if cmd.cmdtype == CommandType.NpuStripe and cmd.ps.npu_block_type == NpuBlockType.Default:
print("Warning: Skipping register command stream generation for", cmd.ps)
else:
cmd_stream.append(cmd)
dependencies = calc_command_dependencies(cmd_stream, arch)
# Initialise operator dependency state
prev_ifm_rect = cur_ifm_rect = None
prev_ifm_block_depth = cur_ifm_block_depth = None
prev_ofm_rect = cur_ofm_rect = None
prev_ofm_block = cur_ofm_block = None
prev_kernel = cur_kernel = None
prev_cmd = None
def emit_wait_commands(cmd):
# The command is fully set up, emit whatever wait commands we need
absolute_dep, relative_dep = dependencies[cmd]
if relative_dep[CommandType.NpuStripe] is not None:
if cmd.cmdtype == CommandType.DMA:
param = relative_dep[CommandType.NpuStripe][1]
if param <= 3:
emit.cmd_wait(cmd0.NPU_OP_KERNEL_WAIT, param, absolute_dep[CommandType.NpuStripe][1])
else:
param = relative_dep[CommandType.NpuStripe][0]
param = min(param, 0xFFFF) # Clamp to allowable wait amount
if relative_dep[CommandType.DMA] is not None:
param = relative_dep[CommandType.DMA][0]
param = min(param, 0xF) # Clamp to allowable wait amount
emit.cmd_wait(cmd0.NPU_OP_DMA_WAIT, param, absolute_dep[CommandType.DMA][0])
prev_cmd = None # Clear any dependency
# Start by issuing REGION commands since they remain the same
emit.cmd0_with_param(cmd0.NPU_SET_IFM_REGION, BasePointerIndex.Scratch)
emit.cmd0_with_param(cmd0.NPU_SET_IFM2_REGION, BasePointerIndex.Scratch)
emit.cmd0_with_param(cmd0.NPU_SET_OFM_REGION, BasePointerIndex.Scratch)
for cmd in cmd_stream:
if cmd.cmdtype == CommandType.DMA:
start_coord = cmd.box.start_coord
src_addr = cmd.in_tensor.address_for_coordinate(start_coord)
dst_addr = cmd.out_tensor.address_for_coordinate(start_coord)
if cmd.in_tensor.compressed_values is not None:
stream_index = cmd.in_tensor.compressed_stream_index_from_coord(start_coord)
sz = cmd.in_tensor.size_of_compressed_stream(stream_index)
else:
sz = cmd.in_tensor.address_for_coordinate(cmd.box.end_coord, is_top_box=True) - src_addr
# TODO: Yoda support needs to use feature_maps_not_in_fast_storage and force_outputs_to_fast_storage
emit.cmd0_with_param(cmd0.NPU_SET_DMA0_SRC_REGION, base_ptr_idx_map[cmd.in_tensor.mem_area])
emit.cmd1_with_offset(cmd1.NPU_SET_DMA0_SRC, src_addr)
emit.cmd0_with_param(cmd0.NPU_SET_DMA0_DST_REGION, base_ptr_idx_map[cmd.out_tensor.mem_area])
emit.cmd1_with_offset(cmd1.NPU_SET_DMA0_DST, dst_addr)
emit.cmd1_with_offset(cmd1.NPU_SET_DMA0_LEN, sz)
dma_channel = 0
mode = 0 # From external to external
emit_wait_commands(cmd)
emit.cmd_do_operation(cmd0.NPU_OP_DMA_START, dma_channel * 16 + mode)
elif cmd.cmdtype == CommandType.NpuStripe:
ps = cmd.ps
primary_op = ps.primary_op
npu_block_type = ps.npu_block_type
# Specifies if global scale from the NPU_SET_OFM_SCALE register should be used instead of per-channel scale
use_global_scale = False
# Specifies type of rounding to be used.
rounding_mode = rounding.TFL
fmf = primary_op.attrs.get("fused_memory_function", None)
faf = primary_op.attrs.get("fused_activation_function", None)
# Specifies which operand to apply scaling to in bitexact elementwise ADD/SUB
op_to_scale = 0
# Update state history
prev_ifm_rect = cur_ifm_rect
prev_ifm_block_depth = cur_ifm_block_depth
prev_ofm_rect = cur_ofm_rect
prev_ofm_block = cur_ofm_block
prev_kernel = cur_kernel
block_config = ps.block_config
emit.cmd0_with_param(cmd0.NPU_SET_OFM_BLK_HEIGHT_M1, block_config[0] - 1)
emit.cmd0_with_param(cmd0.NPU_SET_OFM_BLK_WIDTH_M1, block_config[1] - 1)
emit.cmd0_with_param(cmd0.NPU_SET_OFM_BLK_DEPTH_M1, block_config[3] - 1)
shared_buffer = ps.shared_buffer
if npu_block_type == NpuBlockType.ElementWise:
ifm2_broadcast = 0
if cmd.ifm_tensor.shape == []:
# The scalar has to be the ifm2 tensor so switch the ifms
cmd.ifm_tensor, cmd.ifm2_tensor = cmd.ifm2_tensor, cmd.ifm_tensor
cmd.ifm_box, cmd.ifm2_box = cmd.ifm2_box, cmd.ifm_box
# Set ReverseOperandOrder bit to IFM2_BROADCAST
ifm2_broadcast |= IFM2Broadcast.ReverseOperandOrder
# Calculate scales needed for arithmetic elementwise operators
if primary_op.type in set(("AddAct", "MulAct", "SubAct",)):
input_scale = cmd.ifm_tensor.quantization.scale_f32
input2_scale = cmd.ifm2_tensor.quantization.scale_f32
output_scale = cmd.ofm_tensor.quantization.scale_f32
use_global_scale = True
if primary_op.type == "MulAct":
if (faf == "Sigmoid") or (faf == "Tanh"):
output_scale = 1 / 0x3000
ofm_scale, shift = scaling.elementwise_mul_scale(input_scale, input2_scale, output_scale)
emit.cmd1_with_offset(cmd1.NPU_SET_OFM_SCALE, ofm_scale, shift)
else: # AddAct/SubAct
if (faf == "Sigmoid") or (faf == "Tanh"):
output_scale = 1 / 0x3000
if input_scale == input2_scale:
opa_scale, opb_scale, ofm_scale, shift = scaling.simplified_elementwise_add_sub_scale(
input_scale, input2_scale, output_scale
)
opa_shift = 0 # Unused for this case
else:
# Use advanced implementation only when input scales differ
bitdepth = cmd.ifm_tensor.dtype.bits
(
opa_scale,
opa_shift,
ofm_scale,
shift,
op_to_scale,
) = scaling.advanced_elementwise_add_sub_scale(
input_scale, input2_scale, output_scale, bitdepth
)
opb_scale = 0 # Unused for this case
if ifm2_broadcast & IFM2Broadcast.ReverseOperandOrder:
# If the operand order is reversed we also have to swap which operand is scaled
if op_to_scale == scaling.OperandToScale.OPa:
op_to_scale = scaling.OperandToScale.OPb
else:
op_to_scale = scaling.OperandToScale.OPa
emit.cmd1_with_offset(cmd1.NPU_SET_OPA_SCALE, opa_scale, opa_shift)
emit.cmd1_with_offset(cmd1.NPU_SET_OPB_SCALE, opb_scale)
emit.cmd1_with_offset(cmd1.NPU_SET_OFM_SCALE, ofm_scale, shift)
if primary_op.type in set(("LeakyRelu", "Abs",)):
output_scale = cmd.ofm_tensor.quantization.scale_f32
use_global_scale = True
if primary_op.type == "LeakyRelu":
output_scale *= primary_op.attrs["alpha"]
ofm_scale, shift = scaling.quantise_scale(output_scale)
emit.cmd1_with_offset(cmd1.NPU_SET_OFM_SCALE, ofm_scale, shift)
# For elementwise set the required SHRAM to be equal to the total size of SHRAM
shram_required = arch.shram_total_banks
emit.cmd0_with_param(cmd0.NPU_SET_IFM_IB_END, shram_required)
# Acc buffers not needed so set AB_START to size of SHRAM
emit.cmd0_with_param(cmd0.NPU_SET_AB_START, arch.shram_total_banks)
# Is not a unary operator
if cmd.ifm2_tensor is not None:
if cmd.ifm2_tensor.shape == []:
# IFM2 is a constant, set UseIFM2Scalar bit to IFM2_BROADCAST
ifm2_broadcast |= IFM2Broadcast.UseIFM2Scalar
else:
ifm_box_shape = cmd.ifm_box.get_size_shape()
ifm2_box_shape = cmd.ifm2_box.get_size_shape()
if len(cmd.ifm_tensor.shape) > 1 and ifm_box_shape[1] != ifm2_box_shape[1]:
# Broadcast in 'H' dimension
assert cmd.ifm2_tensor.shape[1] == 1
ifm2_broadcast |= IFM2Broadcast.BroadcastHdim
if len(cmd.ifm_tensor.shape) > 2 and ifm_box_shape[2] != ifm2_box_shape[2]:
# Broadcast in 'W' dimension
assert cmd.ifm2_tensor.shape[2] == 1
ifm2_broadcast |= IFM2Broadcast.BroadcastWdim
if len(cmd.ifm_tensor.shape) > 3 and ifm_box_shape[3] != ifm2_box_shape[3]:
# Broadcast in 'C' dimension
assert cmd.ifm2_tensor.shape[3] == 1
ifm2_broadcast |= IFM2Broadcast.BroadcastCdim
# Set IFM2_IB_START to the latter half of the IB space
ifm_ib_start = shared_buffer.bank_locations[SharedBufferArea.IFM]
emit.cmd0_with_param(
cmd0.NPU_SET_IFM2_IB_START, (shram_required - ifm_ib_start) / 2 + ifm_ib_start
)
emit.cmd0_with_param(cmd0.NPU_SET_IFM2_BROADCAST, ifm2_broadcast)
else:
emit.cmd0_with_param(
cmd0.NPU_SET_IFM_IB_END,
shared_buffer.bank_locations[SharedBufferArea.IFM]
+ shared_buffer.banks_required[SharedBufferArea.IFM],
)
emit.cmd0_with_param(cmd0.NPU_SET_AB_START, shared_buffer.bank_locations[SharedBufferArea.Accumulators])
emit.cmd0_with_param(cmd0.NPU_SET_ACC_FORMAT, acc_format_map[shared_buffer.use_accumulator_element])
emit.cmd0_with_param(cmd0.NPU_SET_IFM_UPSCALE, 0)
if npu_block_type in set(
(NpuBlockType.ConvolutionMxN, NpuBlockType.ConvolutionDepthWise, NpuBlockType.Pooling)
):
# Set up padding
explicit_padding = list(primary_op.attrs["explicit_padding"]) # (top, left, bottom, right)
# Check if this is for horizontal ifm streaming
if not (cmd.is_first_h_stripe and cmd.is_last_h_stripe):
explicit_padding[0] = cmd.pad_top
explicit_padding[2] = cmd.pad_bottom
# Indexing from end since a 1x1 Avgpool might have been added with non 4-dimensional input/output,
# because of activation function needed to be fused.
if cmd.ifm_box.start_coord[-2] > 0:
explicit_padding[1] = 0
if cmd.ifm_box.end_coord[-2] < cmd.ifm_tensor.shape[-2]:
explicit_padding[3] = 0
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_TOP, explicit_padding[0])
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_LEFT, explicit_padding[1])
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_BOTTOM, explicit_padding[2])
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_RIGHT, explicit_padding[3])
stride = primary_op.attrs["strides"][2] - 1
stride |= (primary_op.attrs["strides"][1] - 1) << 1
if npu_block_type == NpuBlockType.Pooling:
k_height, k_width = primary_op.attrs["ksize"][1:3]
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_HEIGHT_M1, k_height - 1)
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_WIDTH_M1, k_width - 1)
valid_padding = sum(explicit_padding) == 0
if primary_op.type in set(("AvgPool", "AvgPoolAct")) and valid_padding:
# For valid padding vela has to output scaling values
if faf == "Sigmoid" or faf == "Tanh":
rescale = 0x3000 * cmd.ifm_tensor.quantization.scale_f32
rescale_bits = len(bin(round_up_to_int(rescale))) - 2 + 1
scale, shift = scaling.quantise_pooling_scale(k_height * k_width, rescale_bits)
scale = int(round_away_zero(scale * rescale))
else:
# In case avg pool fused with concat or other memory operation, rescaling might be needed.
# k_height == k_width == 1 is allways true in this case
# Normally the scale is maximised, to get maximum precision, which means that
# if rescale != 1, scale need to consider the number of bits needed for rescaling
rescale = cmd.ifm_tensor.quantization.scale_f32 / cmd.ofm_tensor.quantization.scale_f32
rescale_bits = 0
if k_height == k_width == 1:
if fmf == "ConcatSliceWrite":
rounding_mode = rounding.NATURAL
if rescale > 1:
rescale_bits = len(bin(round_up_to_int(rescale))) - 2 + 1
elif rescale < 1:
rescale_bits = -(len(bin(round_up_to_int(1 / rescale))) - 2 - 1)
scale, shift = scaling.quantise_pooling_scale(k_height * k_width, rescale_bits)
scale = int(round_away_zero(scale * rescale))
emit.cmd1_with_offset(cmd1.NPU_SET_OFM_SCALE, scale, shift)
# Valid-padded average pool should use the global scale from
# NPU_SET_OFM_SCALE register, which is set above.
use_global_scale = True
else: # Convolution
assert cmd.weight_tensor.block_traversal != TensorBlockTraversal.Default
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_HEIGHT_M1, cmd.weight_tensor.shape[0] - 1)
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_WIDTH_M1, cmd.weight_tensor.shape[1] - 1)
if cmd.weight_tensor.block_traversal == TensorBlockTraversal.PartKernelFirst:
# Part-kernel-first weight ordering
assert npu_block_type == NpuBlockType.ConvolutionMxN
stride |= 1 << 2
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_STRIDE, stride)
elif npu_block_type in set((NpuBlockType.VectorProduct,)):
# Vector product is implemented using a 1x1 convolution so need
# to setup the appropriate padding and kernel info
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_TOP, 0)
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_LEFT, 0)
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_BOTTOM, 0)
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PAD_RIGHT, 0)
# kernel stride reg = 0 means stride(1,1) + depth first weight
# order + dilation(0,0) + kernel_split_size=8
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_STRIDE, 0)
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_HEIGHT_M1, 0)
emit.cmd0_with_param(cmd0.NPU_SET_KERNEL_WIDTH_M1, 0)
if npu_block_type in set(
(NpuBlockType.ConvolutionMxN, NpuBlockType.ConvolutionDepthWise, NpuBlockType.VectorProduct)
):
# Emit Weight base address commands, only maps the area required for
# this command's weights from the larger tensor.
stream_index = cmd.weight_tensor.compressed_stream_index_from_coord(cmd.weight_box.start_coord)
weight_addr = cmd.weight_tensor.address_for_coordinate(cmd.weight_box.start_coord)
weight_len = cmd.weight_tensor.size_of_compressed_stream(stream_index)
# Select weight/scale region depending on where permanent storage was defined
weight_region = base_ptr_idx_map[cmd.weight_tensor.mem_area]
if arch.permanent_storage_mem_area == MemArea.Sram:
weight_region = BasePointerIndex.ReadOnly
emit.cmd0_with_param(cmd0.NPU_SET_WEIGHT_REGION, weight_region)
emit.cmd1_with_offset(cmd1.NPU_SET_WEIGHT_BASE, weight_addr)
emit.cmd1_with_offset(cmd1.NPU_SET_WEIGHT_LENGTH, weight_len)
# Emit Scale & Bias base address commands, with length matching the amount required by
# the weight tensors.
if cmd.scale_tensor is not None:
# Get address and size of the scale/bias data area
scale_addr = cmd.scale_tensor.address_for_coordinate(cmd.weight_box.start_coord[-1:])
scale_len = (
cmd.scale_tensor.address_for_coordinate(cmd.weight_box.end_coord[-1:], True) - scale_addr
)
# Emit base address for NPU to access scale & bias data
scale_region = base_ptr_idx_map[cmd.scale_tensor.mem_area]
if arch.permanent_storage_mem_area == MemArea.Sram:
scale_region = BasePointerIndex.ReadOnly
emit.cmd0_with_param(cmd0.NPU_SET_SCALE_REGION, scale_region)
emit.cmd1_with_offset(cmd1.NPU_SET_SCALE_BASE, scale_addr)
emit.cmd1_with_offset(cmd1.NPU_SET_SCALE_LENGTH, round_up(scale_len, 16))
ofm_quant = cmd.ofm_tensor.quantization
ofm_quant_qmin = cmd.ofm_tensor.quantization.quant_min
ofm_quant_qmax = cmd.ofm_tensor.quantization.quant_max
ifm_min = cmd.ifm_tensor.quantization.min
ifm_max = cmd.ifm_tensor.quantization.max
# Emit commands for any fused activation function
if faf == None:
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION, activation.NONE)
# Even if no activation function, values need to be set to override previous values
faf_min = ofm_quant_qmin
faf_max = ofm_quant_qmax
elif faf == "Relu":
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION, activation.NONE)
faf_min = quantise_float32(0.0, ofm_quant.scale_f32, ofm_quant.zero_point)
faf_max = ofm_quant_qmax
elif faf == "Relu6":
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION, activation.NONE)
faf_min = quantise_float32(0.0, ofm_quant.scale_f32, ofm_quant.zero_point)
faf_max = quantise_float32(6.0, ofm_quant.scale_f32, ofm_quant.zero_point)
elif faf == "ReluN1To1":
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION, activation.NONE)
faf_min = quantise_float32(-1.0, ofm_quant.scale_f32, ofm_quant.zero_point)
faf_max = quantise_float32(1.0, ofm_quant.scale_f32, ofm_quant.zero_point)
elif faf == "Tanh":
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION, activation.TANH)
faf_min = quantise_float32(clamp_tanh(ifm_min), ofm_quant.scale_f32, ofm_quant.zero_point)
faf_max = quantise_float32(clamp_tanh(ifm_max), ofm_quant.scale_f32, ofm_quant.zero_point)
elif faf == "Sigmoid":
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION, activation.SIGMOID)
faf_min = quantise_float32(clamp_sigmoid(ifm_min), ofm_quant.scale_f32, ofm_quant.zero_point)
faf_max = quantise_float32(clamp_sigmoid(ifm_max), ofm_quant.scale_f32, ofm_quant.zero_point)
else:
raise Exception("Unsupported fused_activation_function = " + faf)
# Activation range needs to be set based upon the quantisation range and the fused activation range
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION_MIN, max(ofm_quant_qmin, faf_min))
emit.cmd0_with_param(cmd0.NPU_SET_ACTIVATION_MAX, min(ofm_quant_qmax, faf_max))
out_shape = cmd.ofm_box.get_size_shape()
if len(out_shape) >= 4:
emit.cmd0_with_param(cmd0.NPU_SET_OFM_HEIGHT_M1, out_shape[-3] - 1)
else:
emit.cmd0_with_param(cmd0.NPU_SET_OFM_HEIGHT_M1, 0)
if len(out_shape) >= 2:
emit.cmd0_with_param(cmd0.NPU_SET_OFM_WIDTH_M1, out_shape[-2] - 1)
else:
emit.cmd0_with_param(cmd0.NPU_SET_OFM_WIDTH_M1, 0)
emit.cmd0_with_param(cmd0.NPU_SET_OFM_DEPTH_M1, out_shape[-1] - 1)
if npu_block_type in set((NpuBlockType.ConvolutionMxN, NpuBlockType.VectorProduct)):
in_shape = cmd.ifm_box.get_size_shape()
emit.cmd0_with_param(cmd0.NPU_SET_IFM_DEPTH_M1, in_shape[-1] - 1)
else:
emit.cmd0_with_param(cmd0.NPU_SET_IFM_DEPTH_M1, out_shape[-1] - 1)
for tens, box, ptr_ops, stride_ops, zero_point_op in (
(
cmd.ifm_tensor,
cmd.ifm_box,
(cmd1.NPU_SET_IFM_BASE0, cmd1.NPU_SET_IFM_BASE1, cmd1.NPU_SET_IFM_BASE2, cmd1.NPU_SET_IFM_BASE3),
(cmd1.NPU_SET_IFM_STRIDE_C, cmd1.NPU_SET_IFM_STRIDE_Y, cmd1.NPU_SET_IFM_STRIDE_X),
cmd0.NPU_SET_IFM_ZERO_POINT,
),
(
cmd.ifm2_tensor,
cmd.ifm2_box,
(
cmd1.NPU_SET_IFM2_BASE0,
cmd1.NPU_SET_IFM2_BASE1,
cmd1.NPU_SET_IFM2_BASE2,
cmd1.NPU_SET_IFM2_BASE3,
),
(cmd1.NPU_SET_IFM2_STRIDE_C, cmd1.NPU_SET_IFM2_STRIDE_Y, cmd1.NPU_SET_IFM2_STRIDE_X),
cmd0.NPU_SET_IFM2_ZERO_POINT,
),
(
cmd.ofm_tensor,
cmd.ofm_box,
(cmd1.NPU_SET_OFM_BASE0, cmd1.NPU_SET_OFM_BASE1, cmd1.NPU_SET_OFM_BASE2, cmd1.NPU_SET_OFM_BASE3),
(cmd1.NPU_SET_OFM_STRIDE_C, cmd1.NPU_SET_OFM_STRIDE_Y, cmd1.NPU_SET_OFM_STRIDE_X),
cmd0.NPU_SET_OFM_ZERO_POINT,
),
):
if tens == None:
continue
need_zero_point = (faf != None) or (fmf == "ConcatSliceWrite")
if (
primary_op.type in set(("AvgPool", "AvgPoolAct")) and not need_zero_point
) or tens.quantization == None:
# Actual integer operation, just set scale to 1 and zero point to 0
emit.cmd0_with_param(zero_point_op, 0)
else:
assert tens.quantization.zero_point is not None, "need an actual zero point set"
emit.cmd0_with_param(zero_point_op, int(tens.quantization.zero_point))
if tens.shape == []:
# Empty shape, elementwise constant
ifm2_scalar = tens.quant_values.astype(np.uint8)
assert ifm2_scalar.size == 1
emit.cmd0_with_param(cmd0.NPU_SET_IFM2_SCALAR, ifm2_scalar.item(0))
continue
height_0, height_1, width_0, addresses = tens.addresses_for_rolling_buffer(
box.start_coord, box.end_coord
)
if npu_block_type != NpuBlockType.VectorProduct:
if tens == cmd.ifm_tensor:
emit.cmd0_with_param(cmd0.NPU_SET_IFM_HEIGHT0_M1, height_0 - 1)
emit.cmd0_with_param(cmd0.NPU_SET_IFM_HEIGHT1_M1, height_1 - 1)
emit.cmd0_with_param(cmd0.NPU_SET_IFM_WIDTH0_M1, width_0 - 1)
elif tens == cmd.ofm_tensor:
emit.cmd0_with_param(cmd0.NPU_SET_OFM_HEIGHT0_M1, height_0 - 1)
emit.cmd0_with_param(cmd0.NPU_SET_OFM_HEIGHT1_M1, height_1 - 1)
emit.cmd0_with_param(cmd0.NPU_SET_OFM_WIDTH0_M1, width_0 - 1)
elif tens == cmd.ifm2_tensor:
emit.cmd0_with_param(cmd0.NPU_SET_IFM2_HEIGHT0_M1, height_0 - 1)
emit.cmd0_with_param(cmd0.NPU_SET_IFM2_HEIGHT1_M1, height_1 - 1)
emit.cmd0_with_param(cmd0.NPU_SET_IFM2_WIDTH0_M1, width_0 - 1)
else:
if len(out_shape) == 2:
# TODO: N is put in W-dimension for now
# Should be spread over H and W, but then block size selectetion,
# and stride calculation should be changed
if tens == cmd.ifm_tensor:
emit.cmd0_with_param(cmd0.NPU_SET_IFM_WIDTH0_M1, out_shape[-2] - 1)
elif tens == cmd.ofm_tensor:
emit.cmd0_with_param(cmd0.NPU_SET_OFM_WIDTH0_M1, out_shape[-2] - 1)
else:
assert False
for idx, addr in enumerate(addresses):
if addr is None:
addresses[idx] = 0
emit.cmd1_with_offset(ptr_ops[0], addresses[0])
emit.cmd1_with_offset(ptr_ops[1], addresses[1])
emit.cmd1_with_offset(ptr_ops[2], addresses[2])
emit.cmd1_with_offset(ptr_ops[3], addresses[3])
strides = tens.get_strides()
emit.cmd1_with_offset(stride_ops[0], strides[1]) # stride between 16-byte channel blocks (C)
emit.cmd1_with_offset(stride_ops[2], strides[3]) # stride between horisontal values (W)
emit.cmd1_with_offset(stride_ops[1], strides[2]) # stride between vertical values (H)
if tens.format == TensorFormat.NHCWB16:
# Check that all BasePointer addresses are aligned to 16 bytes
assert (int(addresses[0]) % 16) == 0
assert (int(addresses[1]) % 16) == 0
assert (int(addresses[2]) % 16) == 0
assert (int(addresses[3]) % 16) == 0
ofm_dtype = cmd.ofm_tensor.dtype
assert ofm_dtype.type & BaseType.Int
prec = 0
if ofm_dtype.size_in_bits() == 8:
prec = 0
elif ofm_dtype.size_in_bits() == 16:
prec = 2
else:
assert 0
if ofm_dtype.type & BaseType.Signed:
prec += 1
if use_global_scale:
# Set global scale bit, as opposed to using per channel scale
prec |= 1 << 8
if cmd.ofm_tensor.format == TensorFormat.NHCWB16:
prec |= 1 << 6
prec |= rounding_mode.value << 14
emit.cmd0_with_param(cmd0.NPU_SET_OFM_PRECISION, prec)
prec = None
weight_bits = 8
if cmd.weight_tensor is not None:
weight_bits = cmd.weight_tensor.dtype.size_in_bits()
ifm_dtype = cmd.ifm_tensor.dtype
assert weight_bits == 8, "Unsupported weight bit depth"
assert ifm_dtype.size_in_bits() in {8, 16}
if ifm_dtype.size_in_bits() == 8:
if ifm_dtype.type & BaseType.Signed:
prec = ifm_precision.W8_S8
else:
prec = ifm_precision.W8_U8
elif ifm_dtype.size_in_bits() == 16:
if ifm_dtype.type & BaseType.Signed:
prec = ifm_precision.W8_S16
else:
prec = ifm_precision.W8_U16
ifm_prec = prec.value
ifm2_prec = ifm_prec
if cmd.ifm_tensor.format == TensorFormat.NHCWB16:
ifm_prec |= 1 << 6
ifm_prec |= op_to_scale << 8
emit.cmd0_with_param(cmd0.NPU_SET_IFM_PRECISION, ifm_prec)
if cmd.ifm2_tensor is not None:
if cmd.ifm2_tensor.format == TensorFormat.NHCWB16:
ifm2_prec |= 1 << 6
emit.cmd0_with_param(cmd0.NPU_SET_IFM2_PRECISION, ifm2_prec)
emit_wait_commands(cmd)
# Get op parameters
cur_ifm_block_depth = get_op_ifmofm_block_depth(arch, cmd)
cur_ofm_block = Block(ps.block_config[1], ps.block_config[0], ps.block_config[3])
cur_ofm_rect = get_op_ofm_rect(cmd)
cur_ifm_rect = get_op_ifm_rect(cmd)
cur_kernel = get_op_kernel(cmd.ps)
cur_padLT = get_op_padding_lt(cmd)
if (prev_kernel is not None) and (cur_kernel is not None) and has_prev_op_dependency(prev_cmd, cmd):
if cmd.ifm_tensor.shape == prev_cmd.ofm_tensor.shape:
blockdep = arch.calc_block_dep(
prev_ifm_rect,
prev_ofm_rect,
prev_ifm_block_depth,
prev_ofm_block,
prev_kernel,
cur_ifm_rect,
cur_ofm_rect,
cur_ifm_block_depth,
cur_ofm_block,
cur_kernel,
cur_padLT,
)
else:
blockdep = 0
else:
blockdep = ArchitectureFeatures.MAX_BLOCKDEP
# Set between every op (dependent or not)
blockdep = min(blockdep, arch.max_blockdep)
emit.cmd0_with_param(cmd0.NPU_SET_BLOCKDEP, blockdep)
prev_cmd = cmd
if npu_block_type == NpuBlockType.ConvolutionMxN:
emit.cmd_do_operation(cmd0.NPU_OP_CONV)
elif npu_block_type == NpuBlockType.ConvolutionDepthWise:
emit.cmd_do_operation(cmd0.NPU_OP_DEPTHWISE)
elif npu_block_type == NpuBlockType.VectorProduct:
# Vector product is implemented using a 1x1 convolution
emit.cmd_do_operation(cmd0.NPU_OP_CONV)
elif npu_block_type == NpuBlockType.Pooling:
param = "Max" not in primary_op.type
emit.cmd_do_operation(cmd0.NPU_OP_POOL, param=param)
elif npu_block_type == NpuBlockType.ElementWise:
param = elementwise_mode_map[primary_op.type]
emit.cmd_do_operation(cmd0.NPU_OP_ELEMENTWISE, param)
else:
print("Warning: Skipping register command stream generation for", ps)
# Fill in final part of command stream:
emit.cmd_do_operation(cmd0.NPU_OP_STOP, param=0xFFFF)
sg.register_command_stream = emit.to_list()
if verbose:
emit.print_cmds()
print("number of commands", len(emit.cmd_stream))
print("command stream length in words", len(sg.register_command_stream))