blob: 6dcd16fa2791be05ab900a1ef87078d16b72ffbd [file] [edit]
#!/usr/bin/env python3
# Copyright 2025 Google LLC
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
import abc
from gemm_compiler import arm_template
class Aarch32(arm_template.Arm):
"""All non SIMD features for aarch32."""
def __init__(self, m: int, n: int):
super().__init__(m, n)
self.decrement = 4
def register_bytes(self):
return 16
def n_step(self):
return 4
def prefix(self):
return 'v'
def astride_register(self):
return 'r4'
def kc_register(self):
return 'r2'
def k_register(self):
return 'r0'
def cm_stride_register(self):
return 'r12'
def am_registers(self):
return [self.a_ptr_register()] + [
'r7',
'r9',
'r10',
'r11',
]
def a_ptr_register(self):
return 'r3'
def c_ptr_register(self):
return 'r6'
def cm_registers(self):
return [self.c_ptr_register()] + [
'r4',
'r8',
'r14',
]
def w_ptr_register(self):
return 'r5'
def min_register(self):
return 'q8'
def max_register(self):
return 'q9'
def nc_register(self):
return 'r1'
def mr_register(self):
return 'r0'
def register_map_byte(self, reg):
return reg.replace('x', 'w')
def header(self):
self.asm_string += """// Copyright 2025 Google LLC
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.
#include "src/xnnpack/assembly.h"\n\n"""
self.asm_string += 'BEGIN_FUNCTION ' + self.function_name()
self.asm_string += """
# Free up GP registers. Decrement sp by 36.
push {r4, r5, r6, r7, r8, r9, r10, r11, r14}
# Preserve callee saved q4-q7 registers. Decrement sp by 64.
vpush {d8-d15}
# Load weight's ptr.
ldr r5, [sp, #104]
# Load c ptr.
ldr r6, [sp, #108]
# Load params.
ldr r4, [sp, #124]
# Load min/max values.
vld1.8 {q8, q9}, [r4]\n"""
self.quantization_params()
def jump_to_label(self, label):
return f'b {label}\n'
def read_a_registers(self):
return
def do_loop(self, k: int, iterations: int, no_interleaved_instructions: bool):
"""One unrolled iteration of the inner loop.
Args:
k: iteration number.
iterations: total number of iterations.
no_interleaved_instructions: Do not interleaved instructions for the next
loop iteration. Usually used for the last loop iteration.
Returns:
A string containing the assembly instructions for one unrolled iteration
of the inner loop.
"""
second_half = k + 1 > iterations - self.m
last_iter = k == iterations - 1
for l in self.weights_asm()['loop']:
if last_iter and no_interleaved_instructions:
continue
self.asm_string += l.format(
W_ptr=self.w_ptr_register(),
W=str(int(self.w_registers()[(k + 1) % 2]) * 2),
)
for l in self.compute_asm()['loop']:
for nr in range(0, self.n):
if nr == self.n // 2:
if 'after' in self.weights_asm() and not last_iter:
for r in self.weights_asm()['after']:
self.asm_string += r.format(
W=str(int(self.w_registers()[(k + 1) % 2]) * 2),
W_2=self.w_registers()[(k + 1) % 2],
)
if second_half and not no_interleaved_instructions:
for r in self.input_asm()['loop']:
pos = k - (iterations - self.m)
self.asm_string += r.format(
AM_ptr=self.am_registers()[pos],
AM=str(int(self.a_registers(pos)) * 2),
)
for mr in range(0, self.m):
self.asm_string += l.format(
ACC=self.acc_registers()[mr * 2 + nr],
W=str(int(self.w_registers()[k % 2]) * 2 + nr),
A=str(int(self.a_registers(mr)) * 2 + (k // 4)),
POS=k % 4,
)
def extend_inputs(self):
if 'after' in self.input_asm():
for mr in range(0, self.m):
for l in self.input_asm()['after']:
self.asm_string += l.format(
AM=str(int(self.a_registers(mr)) * 2), AM_2=self.a_registers(mr)
)
def extend_inputs_and_weights(self):
if 'after' in self.weights_asm():
for l in self.weights_asm()['after']:
self.asm_string += l.format(
W=str(int(self.w_registers()[0]) * 2),
W_2=self.w_registers()[0],
)
self.extend_inputs()
@abc.abstractmethod
def k_unroll(self) -> int:
raise NotImplementedError
def inner_loop_in_order(self):
"""An inner loop for in-order cores.
Returns:
A string containing the assembly instructions for the entire inner loop.
"""
self.asm_string += f"""\n# Are there at least {self.k_unroll()} bytes?
subs r0, r0, #8
blo .Lfinal_iteration\n"""
self.asm_string += '\n.Linner_loop:\n'
# weights
if 'before' in self.weights_asm():
self.asm_string += self.weights_asm()['before']
inner_loop_label = '.Linner_loop'
self.extend_inputs_and_weights()
# The main loop which interleaves loads of the A and B data for the next
# iteration with the loop.
for k in range(self.k_unroll()):
self.do_loop(k, self.k_unroll(), False)
# loop counter
self.cmp_k_and_jump_if_less(
label=inner_loop_label, decrement=self.k_unroll(), cond='bhs'
)
# The final iteration, which is identical except that it does not load the
# As and Bs for the next iteration.
self.asm_string += '\n.Lfinal_iteration:\n'
self.extend_inputs_and_weights()
for k in range(self.k_unroll()):
self.do_loop(k, self.k_unroll(), True)
self.asm_string += 'adds r0, r0, #8\n'
self.asm_string += 'bne .Lepilogue\n'
self.asm_string += '\n'
def load_strides(self):
"""Load a & cm_strides parameters.
The offsets come from the aarch32 calling convention.
Returns:
A string containing the assembly instructions to load a & cm_stride into
general purpose registers.
"""
self.asm_string += '# Load a and cm stride registers.\n'
a_stride_reg = self.astride_register()
cm_stride_reg = self.cm_stride_register()
self.asm_string += f'ldr {a_stride_reg}, [sp, #100]\n'
self.asm_string += f'ldr {cm_stride_reg}, [sp, #112]\n'
def clamp_inputs_and_outputs(self, labels, input_registers, output_registers):
clamping = {
'clamp': """
cmp {mr_reg}, #{M}
movlo {AM_1}, {AM_0}
movlo {CM_1}, {CM_0}
movls {AM_2}, {AM_1}
movls {CM_2}, {CM_1}\n""",
}
outer = self.m
# clamp a & c
end_index = self.m if (self.m % 2 == 1) else self.m - 1
for mr in range(2, end_index, 2):
self.asm_string += clamping['clamp'].format(
mr_reg=self.mr_register(),
AM_0=input_registers[mr - 2],
AM_1=input_registers[mr - 1],
AM_2=input_registers[mr],
CM_0=output_registers[mr - 2],
CM_1=output_registers[mr - 1],
CM_2=output_registers[mr],
M=mr,
)
if end_index != self.m:
self.asm_string += """
cmp {mr_reg}, #{M}
movlo {AM_1}, {AM_0}
movlo {CM_1}, {CM_0}\n""".format(
mr_reg=self.mr_register(),
AM_0=input_registers[end_index - 1],
AM_1=input_registers[end_index],
CM_0=output_registers[end_index - 1],
CM_1=output_registers[end_index],
M=end_index + 1,
)
def initialize_k_register(self):
self.asm_string += f'subs {self.k_register()}, {self.kc_register()}, #8\n'
def epilogue(self) -> str:
self.asm_string += """
.Lreturn:
# Restore callee saved q4-q7 registers.
vpop {d8-d15}
# Restore the callee saved GP registers.
pop {r4, r5, r6, r7, r8, r9, r10, r11, r14}
bx lr
"""
self.inner_loop_epilogue()
self.asm_string += """
END_FUNCTION {function_name}""".format(function_name=self.function_name())
def inner_loop_epilogue(self):
"""Remainder handling for the inner loop.
Process up to k_unroll - 1 channels. This is placed at the very end of the
function for control flow reasons. Instructions for the following channel
may not be interleaved as we do not know how many channels must be
processed. The epilogue iteration is identical except that it does not load
the As and Bs for the next iteration and checks k after each mlal batch.
Returns:
A string containing the assembly instructions for the inner loop
epilogue.
"""
k_register = self.k_register()
self.asm_string += '\n.Lepilogue:\n'
self.asm_string += f'and {k_register}, {k_register}, #7\n'
self.asm_string += f'\n# Load {self.m} As and B0\n'
# change this to input & weight's asm.
for mr in range(self.m):
am_ptr = self.am_registers()[mr]
am = str(int(self.a_registers(mr)) * 2)
self.asm_string += f'vld1.8 d{am}, [{am_ptr}]\n'
self.asm_string += f'add {am_ptr}, {k_register}\n'
self.extend_inputs()
k_unroll = self.k_unroll() - 1
for k in range(k_unroll):
self.epilogue_iteration(k, k_unroll)
self.asm_string += 'b .Linner_loop_end\n'
def epilogue_iteration(self, k: int, iterations: int):
"""Process one channel of the loop epilogue.
Args:
k: iteration number.
iterations: total number of iterations.
Returns:
A string containing the assembly instructions for one iteration of the
inner loop epilogue.
"""
self.asm_string += ''
for l in self.weights_asm()['loop']:
self.asm_string += l.format(
W_ptr=self.w_ptr_register(),
W=str(int(self.w_registers()[0]) * 2),
)
if 'after' in self.weights_asm():
for r in self.weights_asm()['after']:
self.asm_string += r.format(
W=str(int(self.w_registers()[0]) * 2),
W_2=self.w_registers()[0],
)
for l in self.compute_asm()['loop']:
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += l.format(
ACC=self.acc_registers()[mr * 2 + nr],
W=str(int(self.w_registers()[0]) * 2 + nr),
A=str(int(self.a_registers(mr)) * 2 + (k // 4)),
POS=k % 4,
)
if (k % 2) == 0:
if k != iterations - 1:
k_register = self.k_register()
k_val = k + 2
self.asm_string += f'cmp {k_register}, #{k_val}\n'
self.asm_string += 'blo .Linner_loop_end\n'
else:
self.asm_string += 'beq .Linner_loop_end\n'
def cmp_k_and_jump_if_less(self, label, decrement, cond):
k_register = self.k_register()
self.asm_string += f"""subs {k_register}, {k_register}, #{decrement}
{cond} {label}\n"""