blob: e824a2117cb4d52b62c5a1fd940b31db01938375 [file] [edit]
#!/usr/bin/env python3
# Copyright 2024 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.
from gemm_compiler import neonfma_template
class NeonDot(neonfma_template.NeonFma):
"""All SIMD features for Aarch64 neondot."""
def isa(self):
return 'neondot'
def a_registers(self, idx):
registers = ['2', '3', '4', '5', '11', '26', '27']
assert idx < len(registers)
return registers[idx]
def w_registers(self):
return ['6', '7', '8', '9']
def acc_registers(self):
return [
'12',
'13',
'14',
'15',
'16',
'17',
'18',
'19',
'20',
'21',
'22',
'23',
'24',
'25',
'26',
'27',
'28',
'29',
'30',
'31',
]
def function_name(self):
ld = self.unroll_factor * 32
return (
f'xnn_qd8_f32_qc8w_gemm_minmax_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c4__asm_aarch64_{self.isa()}_ld{ld}_2'
)
def zp_scale(self, pos):
regs = ['28', '29', '30', '31']
return regs[pos]
# kc = round_up_po2(kc, channels)
def adjust_kc(self):
channels = 4
m = pow(2, 64) - channels
not_channels = f'0x{m:016X}'
self.comment('Round kc up to channels.')
self.asm_string += """add {kc_reg}, {kc_reg}, #{channels}
and {kc_reg}, {kc_reg}, #{not_channels}\n\n""".format(
kc_reg=self.kc_register(),
channels=channels - 1,
not_channels=not_channels,
)
def quantization_params(self):
quantization_params_reg = self.quantization_params_register()
self.asm_string += f'ldr {quantization_params_reg}, [sp, 272]\n'
def quantization_params_register(self):
return 'x24'
def compute_asm(self):
c_asm = {
'loop': ['sdot v{ACC}.4s, v{W}.16b, v{A}.4b[{POS}]\n'],
}
return c_asm
def cvtf(self, acc):
self.asm_string += f'scvtf v{acc}.4s, v{acc}.4s\n'
def input_asm(self):
match self.unroll_factor:
case 1:
return {
'loop': [
'ldr s{AM}, [{AM_ptr}], 4\n',
]
}
case 2:
return {
'loop': [
'ldr d{AM}, [{AM_ptr}], 8\n',
]
}
case 4:
return {
'loop': [
'ldr q{AM}, [{AM_ptr}], 16\n',
]
}
case _:
raise NotImplementedError
def convert_to_output(self):
accumulators = self.acc_registers()
self.comment('Convert from int32 to float.')
for nr in range(0, self.n * self.m):
self.cvtf(accumulators[nr])
self.comment('Multiply by input scale.')
for nr in range(0, self.n):
for mr in range(0, self.m):
self.fmul_lane(
a=accumulators[mr * self.n + nr],
b=accumulators[mr * self.n + nr],
c=self.zp_scale(mr // 2),
lane=int((mr % 2) * 2) + 1,
)
self.comment('Load weights scale.')
# output scales
for nr in range(0, self.n, 2):
self.load_simd_register_pair(
q0=self.a_registers(nr),
q1=self.a_registers(nr + 1),
ptr=self.w_ptr_register(),
offset=self.register_bytes() * nr,
)
self.increment_ptr(
ptr=self.w_ptr_register(), step=self.register_bytes() * self.n
)
# biases
self.comment('Load biases.')
for nr in range(0, self.n, 2):
self.load_simd_register_pair(
q0=self.w_registers()[nr],
q1=self.w_registers()[nr + 1],
ptr=self.w_ptr_register(),
offset=self.register_bytes() * nr,
)
self.increment_ptr(
ptr=self.w_ptr_register(), step=self.register_bytes() * self.n
)
# do mul + add here instead of fmla.
# fmla accumulaltes into the additional term, in this case the bias. This
# means that the bias must be copied before the fmla.
# From the Cortex X1 optimization guide, fmov takes 1 cycle with a
# throughput of 4 and fmla takes 4 cycles with a throughput of 4. This means
# 5 cycles for four movs + fmla. fadd takes 2 cycles with a throughput of 4
# and fmul takes 3 cycles with a throughput of 4, for a total of 5 cycles
# for 4 results.
self.comment("Multiply by weight's scale.")
for nr in range(0, self.n):
for mr in range(0, self.m):
self.fmul(
a=accumulators[mr * self.n + nr],
b=accumulators[mr * self.n + nr],
c=self.a_registers(nr),
)
self.comment('Add bias.')
for nr in range(0, self.n):
for mr in range(0, self.m):
self.fadd(
a=accumulators[mr * self.n + nr],
b=accumulators[mr * self.n + nr],
c=self.w_registers()[nr],
)
self.comment('Min/max clamping.')
self.clamp()
def init_accumulators(self):
self.comment('Initialize accumulators with k_sum * input zero point.')
accumulators = self.acc_registers()
if self.m > 2:
self.load_simd_register_pair(
q0=self.zp_scale(0),
q1=self.zp_scale(1),
ptr=self.quantization_params_register(),
offset=0,
)
else:
self.load_simd_register(
q=self.zp_scale(0), ptr=self.quantization_params_register(), offset=0
)
if self.m > 6:
self.load_simd_register_pair(
q0=self.zp_scale(2),
q1=self.zp_scale(3),
ptr=self.quantization_params_register(),
offset=32,
)
elif self.m > 4:
self.load_simd_register(
q=self.zp_scale(2), ptr=self.quantization_params_register(), offset=32
)
for nr in range(0, self.n - 1, 2):
self.load_simd_register_pair(
q0=self.a_registers(nr),
q1=self.a_registers(nr + 1),
ptr=self.w_ptr_register(),
offset=self.register_bytes() * nr,
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.mul_lane(
a=accumulators[mr * self.n + nr],
b=self.a_registers(nr),
c=self.zp_scale(mr // 2),
lane=int((mr % 2) * 2),
)
self.increment_ptr(
ptr=self.w_ptr_register(), step=self.register_bytes() * self.n
)
def mask_register(self):
return '10'
def tmp_w_register(self):
w_registers = self.w_registers()
w_registers_size = len(w_registers)
return w_registers[w_registers_size - 1]
class NeonDotQC4W(NeonDot):
"""All SIMD features for Aarch64 neondot with 4-bit weights."""
def weights_asm(self):
w_asm = {
'loop': ["""ldr q{tmp_W}, [{W_ptr}], 32
shl v{W}.16b, v{tmp_W}.16b, #4
and v{W_1}.16b, v{tmp_W}.16b, v{mask}.16b\n"""],
'loop_2': ["""ldr q{tmp_W}, [{W_ptr}], 16
shl v{W}.16b, v{tmp_W}.16b, #4
and v{W_1}.16b, v{tmp_W}.16b, v{mask}.16b\n"""],
}
return w_asm
def quantization_params(self):
self.asm_string += """# Load 0xF0 for masking the weights
ldr {quantization_params_reg}, [sp, 272]
movi v{mask}.16b, #240
""".format(
quantization_params_reg=self.quantization_params_register(),
mask=self.mask_register(),
)
def function_name(self):
ld = self.unroll_factor * 32
return (
f'xnn_qd8_f32_qc4w_gemm_minmax_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c4__asm_aarch64_{self.isa()}_ld{ld}_2'
)
def cvtf(self, acc):
self.asm_string += f'scvtf v{acc}.4s, v{acc}.4s, #4\n'
class NeonDotQS8QC8W(NeonDot):
def function_name(self):
ld = self.unroll_factor * 32
return (
f'xnn_qs8_qc8w_gemm_minmax_fp32_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c4__asm_aarch64_{self.isa()}_ld{ld}_2'
)
def load_min_max(self):
params_register = self.params_register()
min_reg = self.min_register()
max_reg = self.max_register()
self.asm_string += f"""
# Load min/max values.
add {params_register}, {params_register}, 2
ld2r {{{min_reg}.16b, {max_reg}.16b}}, [{params_register}]
sub {params_register}, {params_register}, 2\n"""
def init_accumulators(self):
return super(NeonDot, self).init_accumulators()
def cvts(self, acc):
self.asm_string += f'fcvtns v{acc}.4s, v{acc}.4s\n'
def output_n(self) -> int:
return self.n // 4
def dup_s16(self, ptr, q):
self.asm_string += f'ld1r {{v{q}.8h}}, [{ptr}]\n'
def convert_to_output(self):
accumulators = self.acc_registers()
self.comment('Convert from int32 to float.')
for nr in range(0, self.n * self.m):
self.cvtf(accumulators[nr])
self.comment('Load weights scale.')
# output scales
for nr in range(0, self.n, 2):
self.load_simd_register_pair(
q0=self.a_registers(nr),
q1=self.a_registers(nr + 1),
ptr=self.w_ptr_register(),
offset=self.register_bytes() * nr,
)
self.increment_ptr(
ptr=self.w_ptr_register(), step=self.register_bytes() * self.n
)
self.comment("Multiply by weight's scale.")
for nr in range(0, self.n):
for mr in range(0, self.m):
self.fmul(
a=accumulators[mr * self.n + nr],
b=accumulators[mr * self.n + nr],
c=self.a_registers(nr),
)
self.comment('Reconvert to int32.')
for nr in range(0, self.n * self.m):
self.cvts(accumulators[nr])
self.comment('Convert to int16.')
for nr in range(0, self.n, 2):
for mr in range(0, self.m):
self.sqxtn(
a=accumulators[mr * self.n + nr],
b=accumulators[mr * self.n + nr],
atype='sint32',
)
for nr in range(1, self.n, 2):
for mr in range(0, self.m):
self.sqxtn2(
a=accumulators[mr * self.n + nr - 1],
b=accumulators[mr * self.n + nr],
atype='sint32',
)
self.dup_s16(ptr=self.params_register(), q=self.tmp_w_register())
self.comment('Add output zero point.')
for nr in range(0, self.n, 2):
for mr in range(0, self.m):
self.sqadd(
a=accumulators[mr * self.n + nr],
b=accumulators[mr * self.n + nr],
c=self.tmp_w_register(),
)
self.comment('Convert to int8.')
for mr in range(0, self.m):
self.sqxtn(
a=accumulators[mr * self.n],
b=accumulators[mr * self.n],
atype='sint16',
)
for mr in range(0, self.m):
self.sqxtn2(
a=accumulators[mr * self.n],
b=accumulators[mr * self.n + 2],
atype='sint16',
)
self.comment('Min/max clamping.')
self.clamp()
def clamp_min(self, reg, prefix):
max_reg = self.max_register()
self.asm_string += (
f'smin {prefix}{reg}.16b, {max_reg}.16b, {prefix}{reg}.16b\n'
)
def clamp_max(self, reg, prefix):
min_reg = self.min_register()
self.asm_string += (
f'smax {prefix}{reg}.16b, {min_reg}.16b, {prefix}{reg}.16b\n'
)
def store(self):
accumulators = self.acc_registers()
cm_registers = self.cm_registers()
nc_reg = self.nc_register()
nc_lo = self.register_map_byte(nc_reg)
nc = self.n * self.n_step()
self.asm_string += """
# Check whether full or partial store.
cmp {nc}, {n_step}
b.lo .Ltail_{N_2}\n""".format(n_step=nc, N_2=nc // 2, nc=nc_reg)
for mr in range(0, self.m):
self.store_simd_register(
r=accumulators[mr * self.n],
prefix='q',
ptr=cm_registers[mr],
post_increment=16,
)
for mr in range(0, self.m):
am_ptr = self.am_registers()[mr]
kc_register = self.kc_register()
self.asm_string += f'sub {am_ptr}, {am_ptr}, {kc_register}\n'
check = """
sub {nc}, {nc}, {n_step}
b.ne .Louter_loop
b .Lreturn""".format(n_step=nc, nc=nc_reg)
self.asm_string += check
nc = nc // 2
self.asm_string += """
\n.Ltail_8:
tbz {nc_lo}, 3, .Ltail_4\n""".format(nc_lo=nc_lo)
for mr in range(0, self.m):
self.store_simd_register(
r=accumulators[mr * self.n],
prefix='d',
ptr=cm_registers[mr],
post_increment=8,
)
for mr in range(0, self.m):
self.asm_string += 'ext v{ACC}.16b, v{ACC}.16b, v{ACC}.16b, 8\n'.format(
ACC=accumulators[mr * self.n]
)
self.asm_string += """
\n.Ltail_4:
tbz {nc_lo}, 2, .Ltail_2\n""".format(nc_lo=nc_lo)
for mr in range(0, self.m):
self.store_simd_register_lane(
r=accumulators[mr * self.n],
prefix='s',
ptr=cm_registers[mr],
lane=0,
post_increment=4,
)
for mr in range(0, self.m):
self.asm_string += 'ext v{ACC}.16b, v{ACC}.16b, v{ACC}.16b, 4\n'.format(
ACC=accumulators[mr * self.n]
)
self.asm_string += """
\n.Ltail_2:
tbz {nc_lo}, 1, .Ltail_1\n""".format(nc_lo=nc_lo)
for mr in range(0, self.m):
self.store_simd_register_lane(
r=accumulators[mr * self.n],
prefix='h',
ptr=cm_registers[mr],
lane=0,
post_increment=2,
)
for mr in range(0, self.m):
self.asm_string += 'ext v{ACC}.16b, v{ACC}.16b, v{ACC}.16b, 2\n'.format(
ACC=accumulators[mr * self.n]
)
self.asm_string += """
\n.Ltail_1:
tbz {nc_lo}, 0, .Lreturn\n""".format(nc_lo=nc_lo)
for mr in range(0, self.m):
self.store_simd_register_lane(
r=accumulators[mr * self.n],
prefix='b',
ptr=cm_registers[mr],
lane=0,
post_increment=0,
)
class NeonDotQS8QC4W(NeonDotQC4W, NeonDotQS8QC8W):
def function_name(self):
ld = self.unroll_factor * 32
return (
f'xnn_qs8_qc4w_gemm_minmax_fp32_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c4__asm_aarch64_{self.isa()}_ld{ld}_2'
)