blob: 69c95cf884df4970248b7f1f25e17ce3a05270bc [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.
import math
from gemm_compiler import avx512f_template
class Avx512Vnni(avx512f_template.Avx512F):
"""All SIMD features for avx512vnni."""
def __init__(self, m: int, n: int, c: int):
super().__init__(m, n, c)
def isa(self):
return 'avx512vnni'
def element_size(self):
return 1
def a_registers(self, idx):
return 'zmm2'
def scale_registers(self):
return ['zmm10', 'zmm11', 'zmm2', 'zmm3']
def w_registers(self):
return ['zmm6', 'zmm7', 'zmm8', 'zmm9']
def acc_registers(self):
return [
'mm5',
'mm12',
'mm14',
'mm15',
'mm16',
'mm17',
'mm18',
'mm19',
'mm20',
'mm21',
'mm22',
'mm23',
'mm24',
'mm25',
'mm26',
'mm27',
'mm28',
'mm29',
'mm30',
'mm4',
'mm8',
'mm9',
]
def function_name(self):
c = self._c
return (
f'xnn_qd8_f32_qc8w_gemm_minmax_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c{c}__asm_amd64_{self.isa()}'
)
def zp_scale(self, pos):
regs = ['10', '11']
return regs[pos]
def channels(self):
return self._c
# kc = round_up_po2(kc, channels)
def adjust_kc(self):
channels = self.channels()
self.asm_string += """
add {kc_reg}, {channels}
and {kc_reg}, {neg_channels}\n""".format(
kc_reg=self.kc_register(), channels=channels - 1, neg_channels=-channels
)
def quantization_params_register(self):
return self.k_register()
def pre_header(self):
alignment = int(math.log2(self.register_bytes()))
header_c4 = ''
header_c8 = f"""
.p2align {alignment}, 0x0
.PERMUTATION:
.long 0
.long 2
.long 4
.long 6
.long 8
.long 10
.long 12
.long 14
.long 16
.long 18
.long 20
.long 22
.long 24
.long 26
.long 28
.long 30
"""
match self._c:
case 4:
self.asm_string += header_c4
case 8:
self.asm_string += header_c8
case _:
raise NotImplementedError
def input_asm(self):
loop_c4 = 'vpbroadcastd {AM}, [{AM_ptr} + {a_offset}]\n'
loop_c8 = 'vbroadcasti32x2 {AM}, qword ptr [{AM_ptr} + {a_offset}]\n'
match self._c:
case 4:
loop = loop_c4
case 8:
loop = loop_c8
case _:
raise NotImplementedError
in_asm = {
'loop': [loop],
'compute': ['vpdpbusd z{ACC}, {A}, {W}\n'],
}
return in_asm
def weights_asm(self):
w_asm = {
'loop': [
'vmovaps {W}, [{W_ptr} + {offset}]\n',
],
'after': [
'add {W}, {w_step}\n',
],
}
return w_asm
def compute_asm(self):
c_asm = {
'loop': ['vpdpbusd z{ACC}, {A}, {W}\n'],
}
return c_asm
# Quantization parameters are pushed to the stack at this offset.
def quantization_params_offset(self):
return 8
def accumulator_reg_offset(self, M, nr):
match self._c:
case 4:
return 0
case 8:
return nr * M
case _:
raise NotImplementedError
def convert_to_float(self):
accumulators = self.acc_registers()
self.asm_string += '\n# Convert from int32 to float.\n'
for nr in range(0, self.n):
for mr in range(0, self.m):
reg_ofset = self.accumulator_reg_offset(self.m, nr)
other_reg = accumulators[self.m * nr + mr + reg_ofset]
ACC = accumulators[self.m * nr + mr]
self.asm_string += f'vcvtdq2ps z{ACC}, z{other_reg}\n'
# TODO: This is a qd8 conversion, move it to a child class?
def convert_to_output(self):
W = self.w_ptr_register()
if self._c == 8:
shift_add = """vpsrlq {tmp}, z{acc}, 32
vpaddd z{acc}, z{acc}, {tmp}\n"""
accumulators = self.acc_registers()
for nr in range(0, self.n * 2):
for mr in range(0, self.m):
self.asm_string += shift_add.format(
acc=accumulators[self.m * nr + mr], tmp=self.w_registers()[0]
)
perm_reg = self.w_registers()[0]
self.asm_string += (
f'vmovaps {perm_reg}, zmmword ptr [rip + .PERMUTATION]\n'
)
perm = 'vpermt2ps z{acc0}, {perm_reg}, z{acc1}\n'
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += perm.format(
perm_reg=perm_reg,
acc0=accumulators[2 * self.m * nr + mr],
acc1=accumulators[2 * self.m * nr + self.m + mr],
)
self.convert_to_float()
accumulators = self.acc_registers()
self.asm_string += '# Load quantization_params pointer from stack\n'
self.asm_string += (
'mov {quantization_params_reg}, [rsp + {offset}]\n'.format(
quantization_params_reg=self.quantization_params_register(),
offset=self.stack_size() + self.quantization_params_offset(),
)
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += (
'vmulps z{ACC}, z{ACC}, dword ptr [{quantization_params_reg} +'
' {offset}]{{1to16}}\n'.format(
ACC=accumulators[nr * self.m + mr],
offset=4 + mr * 8,
quantization_params_reg=self.quantization_params_register(),
)
)
output_scale = 'vmovaps {W_SCALE}, [{W} + {offset}]\n'
# output scales
for nr in range(0, self.n):
self.asm_string += output_scale.format(
W=W,
offset=self.register_bytes() * nr,
W_SCALE=self.scale_registers()[nr],
)
self.increment_ptr(ptr=W, step=self.register_bytes() * self.n)
# biases
for nr in range(0, self.n):
self.asm_string += output_scale.format(
W=W, offset=self.register_bytes() * nr, W_SCALE=self.w_registers()[nr]
)
self.increment_ptr(ptr=W, step=self.register_bytes() * self.n)
# Intel gets points here for its fma instructions which can accumulate into
# any of the registers. For once, Intel has saner instructions than Arm.
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += 'vfmadd213ps z{ACC}, {SCALE}, {BIAS}\n'.format(
ACC=accumulators[nr * self.m + mr],
SCALE=self.scale_registers()[nr],
BIAS=self.w_registers()[nr],
)
self.comment('Min/max clamping.')
self.clamp()
return self.asm_string
# TODO: This is qd8 specific, move it to a child class?
def outer_loop_prepare(self):
# outside the outer loop
zp_scale_load_push = (
"""mov {tmp_reg}, [{quantization_params_reg} + {zp_offset}]
vpbroadcastd {tmp_s_reg}, {tmp_reg}
vmovaps zmmword ptr [rsp + {offset}], {tmp_s_reg}\n"""
)
self.comment('Load quantization_params pointer from stack')
self.asm_string += (
'mov {quantization_params_reg}, [rsp + {offset}]\n'.format(
quantization_params_reg=self.quantization_params_register(),
offset=self.stack_size() + self.quantization_params_offset(),
)
)
for mr in range(0, self.m, 1):
self.asm_string += zp_scale_load_push.format(
tmp_reg=self.register_map_dword(self.tmp_gp_registers()[0]),
quantization_params_reg=self.quantization_params_register(),
tmp_s_reg=self.w_registers()[0],
offset=self.stupid_offset() + mr * 64,
zp_offset=mr * 8,
)
def init_accumulators(self):
self.comment('Initialize accumulators with k_sum * input zero point.')
accumulators = self.acc_registers()
W = self.w_ptr_register()
ksum_x16 = 'vmovaps {KSUM}, [{W} + {offset}]\n'
vksum = 'vpmulld z{ACC}, {KSUM}, zmmword ptr [rsp + {offset}]\n'
for nr in range(0, self.n):
self.asm_string += ksum_x16.format(
W=self.w_ptr_register(),
KSUM=self.w_registers()[nr],
offset=self.register_bytes() * nr,
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += vksum.format(
ACC=accumulators[nr * self.m + mr],
KSUM=self.w_registers()[nr],
pos=int((mr % 2) * 2),
offset=self.stupid_offset() + mr * 64,
)
self.increment_ptr(ptr=W, step=self.register_bytes() * self.n)
self.interleave_zeros(self.m, self.n)
def stupid_offset(self):
size = self.m * 16 + self.c_ptr_stack_offset()
return math.ceil(size / 64) * 64
def stack_size(self):
size = self.stupid_offset() + self.m * 64
# round up to multiple of 64.
return math.ceil(size / 64) * 64
def interleave_zeros(self, M, N):
match self._c:
case 4:
return
case 8:
W = self.w_ptr_register()
accumulators = self.acc_registers()
bias_registers = self.bias_registers()
c = self._c * self.element_size()
self.comment('Interleave with zeros.')
unpack_lo = 'vpmovzxdq z{acc0}, y{acc1}\n'
unpack_hi = """vextracti64x4 y{acc0}, z{acc1}, 1
vpmovzxdq z{acc0}, y{acc0}
"""
for mr in range(0, M):
for nr in reversed(range(0, N)):
self.asm_string += unpack_hi.format(
acc0=accumulators[mr + 2 * M * nr + M],
acc1=accumulators[mr + nr * M],
)
self.asm_string += unpack_lo.format(
acc0=accumulators[mr + 2 * M * nr],
acc1=accumulators[mr + nr * M],
)
case _:
raise NotImplementedError
def inner_loop_spill_gp(self, tail: bool = False) -> str:
return self._inner_loop_spill_gp(self._c // 4 * self.n, tail)
def inner_loop_small_M_N(self, tail: bool = False) -> str:
return self._inner_loop_small_M_N(self._c // 4 * self.n, tail)
class Avx512VnniQc4w(Avx512Vnni):
def function_name(self):
c = self._c
return (
f'xnn_qd8_f32_qc4w_gemm_minmax_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c{c}__asm_amd64_{self.isa()}'
)
def weights_asm(self):
w_asm = {
'loop': [],
'loop_2': ["""vmovaps {W_1}, [{W_ptr} + {offset}]
vpslld {W}, {W_1}, 4
vpandd {W}, {W}, z{mask}
vpandd {W_1}, {W_1}, z{mask}\n"""],
'after': ['add {W}, {w_step}\n'],
}
return w_asm
def outer_loop_prepare(self):
super().outer_loop_prepare()
self.asm_string += """
mov {quantization_params_reg}, [rsp + 88]
# Load 0xF0 for masking the weights
vbroadcastsd z{mask}, qword ptr [rip + .MASK]\n
""".format(
quantization_params_reg=self.quantization_params_register(),
mask=self.mask_register(),
)
def convert_to_float(self):
accumulators = self.acc_registers()
self.asm_string += '\n# Convert from int32 to float.\n'
for nr in range(0, self.n):
for mr in range(0, self.m):
reg_ofset = self.accumulator_reg_offset(self.m, nr)
other_reg = accumulators[self.m * nr + mr + reg_ofset]
ACC = accumulators[self.m * nr + mr]
self.asm_string += f'vpsrad z{other_reg}, z{other_reg}, 4\n'
self.asm_string += f'vcvtdq2ps z{ACC}, z{other_reg}\n'
def pre_header(self):
super().pre_header()
self.asm_string += """.MASK:
.quad -1085102592571150096\n"""
def w_register_bytes(self):
return self.register_bytes() // 2
class Avx512VnniQS8QC8W(Avx512Vnni):
def function_name(self):
c = self._c
return (
f'xnn_qs8_qc8w_gemm_minmax_fp32_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c{c}__asm_amd64_{self.isa()}'
)
def quantization_params_offset(self):
# TODO: Reorganize class hierarchy so that this override is not needed, qd8
# should be a child/sibling of this.
return 0
def sign_mask_register(self):
# mask register is unused here, just use that one.
return self.mask_register()
def pre_header(self):
super().pre_header()
self.asm_string += """.SIGN_MASK:
.quad -9187201950435737472 # 0x8080808080808080\n"""
def load_params(self, reg):
return """
movsx eax, word ptr [{reg}]
vpbroadcastd zmm31, eax
vpbroadcastb x{min}, byte ptr [{reg} + 2]
movsx eax, word ptr [{reg} + 4]
vpbroadcastd z{max}, eax
vpsubd z{max}, z{max}, zmm31
vcvtdq2ps z{max}, z{max}
""".format(
reg=reg,
prefix=self.prefix(),
min=self.min_register(),
max=self.max_register(),
)
def init_accumulators(self):
self.comment('Initialize accumulators with bias')
accumulators = self.acc_registers()
W = self.w_ptr_register()
vksum = 'vmovaps z{ACC}, [{W} + {offset}]\n'
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += vksum.format(
ACC=accumulators[nr * self.m + mr],
W=self.w_ptr_register(),
offset=self.register_bytes() * nr,
)
self.increment_ptr(ptr=W, step=self.register_bytes() * self.n)
self.interleave_zeros(self.m, self.n)
def input_asm(self):
loop_c4 = (
'vpxord {AM}, z{sign_mask}, dword ptr [{AM_ptr} + {a_offset}]{{1to16}}\n'
)
loop_c8 = (
'vpxorq {AM}, z{sign_mask}, qword ptr [{AM_ptr} + {a_offset}]{{1to8}}\n'
)
match self._c:
case 4:
loop = loop_c4
case 8:
loop = loop_c8
case _:
raise NotImplementedError
in_asm = {
'loop': [loop],
'compute': ['vpdpbusd z{ACC}, {A}, {W}\n'],
}
return in_asm
def convert_to_output(self):
W = self.w_ptr_register()
if self._c == 8:
shift_add = """vpsrlq {tmp}, z{acc}, 32
vpaddd z{acc}, z{acc}, {tmp}\n"""
accumulators = self.acc_registers()
for nr in range(0, self.n * 2):
for mr in range(0, self.m):
self.asm_string += shift_add.format(
acc=accumulators[self.m * nr + mr], tmp=self.w_registers()[0]
)
perm_reg = self.w_registers()[0]
self.asm_string += (
f'vmovaps {perm_reg}, zmmword ptr [rip + .PERMUTATION]\n'
)
perm = 'vpermt2ps z{acc0}, {perm_reg}, z{acc1}\n'
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += perm.format(
perm_reg=perm_reg,
acc0=accumulators[2 * self.m * nr + mr],
acc1=accumulators[2 * self.m * nr + self.m + mr],
)
self.convert_to_float()
accumulators = self.acc_registers()
output_scale = 'vmovaps {W_SCALE}, [{W} + {offset}]\n'
# output scales
for nr in range(0, self.n):
self.asm_string += output_scale.format(
W=W,
offset=self.register_bytes() * nr,
W_SCALE=self.scale_registers()[nr],
)
self.increment_ptr(ptr=W, step=self.register_bytes() * self.n)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += 'vmulps z{ACC}, z{ACC}, {SCALE}\n'.format(
ACC=accumulators[nr * self.m + mr],
SCALE=self.scale_registers()[nr],
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += 'vminps z{ACC}, z{ACC}, z{MAX}\n'.format(
ACC=accumulators[nr * self.m + mr],
MAX=self.max_register(),
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += 'vcvtps2dq z{ACC}, z{ACC}\n'.format(
ACC=accumulators[nr * self.m + mr],
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += 'vpaddd z{ACC}, z{ACC}, zmm31\n'.format(
ACC=accumulators[nr * self.m + mr],
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += 'vpmovsdb x{ACC}, z{ACC}\n'.format(
ACC=accumulators[nr * self.m + mr],
)
for nr in range(0, self.n):
for mr in range(0, self.m):
self.asm_string += 'vpmaxsb x{ACC}, x{ACC}, x{MIN}\n'.format(
ACC=accumulators[nr * self.m + mr],
MIN=self.min_register(),
)
return self.asm_string
def store(self):
tmp_gp_regs = self.tmp_gp_registers()
accumulators = self.acc_registers()
cm_registers = self.cm_registers()
nc_reg = self.nc_register()
pop_c = self.m > self.max_m_before_spilling()
c_reg_offset = self.max_m_before_spilling()
if pop_c:
self.asm_string += '\n' + '# Pop output pointers from the stack.\n'
c_reg_offset = 0
pop_c_str = 'mov {C_REG}, [rsp + {offset}]\n'
for mr in range(0, self.m):
sp_offset = (mr) * 16 + self.c_ptr_stack_offset()
self.asm_string += pop_c_str.format(
C_REG=cm_registers[mr], offset=sp_offset
)
self.asm_string += """
# Check whether full or partial store.
cmp {nc}, {n_step}
jl .Ltail\n""".format(n_step=self.n * self.n_step(), nc=nc_reg)
for mr in range(0, self.m):
self.asm_string += """
vmovups [{c_reg}], x{ACC}""".format(
ACC=accumulators[mr], c_reg=cm_registers[mr + c_reg_offset]
)
for nr in range(1, self.n):
self.asm_string += """
vmovups [{c_reg} + {offset}], x{ACC}""".format(
ACC=accumulators[self.m * nr + mr],
c_reg=cm_registers[mr + c_reg_offset],
offset=self.register_bytes()
* nr
// 4, # TODO: Find a better way to compute this stride than dividing by 4.
)
self.asm_string += '\n'
for mr in range(0, self.m):
self.asm_string += 'add {cm}, {cn_stride}\n'.format(
cn_stride=self.n
* self.register_bytes()
// 4, # TODO: Find a better way to compute this stride than dividing by 4.
cm=cm_registers[mr + c_reg_offset],
)
if pop_c:
self.asm_string += '\n' + '# Write output pointers to the stack.\n'
pop_c_str = 'mov [rsp + {offset}], {C_REG}\n'
for mr in range(0, self.m):
sp_offset = (mr) * 16 + self.c_ptr_stack_offset()
self.asm_string += pop_c_str.format(
C_REG=cm_registers[mr], offset=sp_offset
)
check = """
sub {nc}, {n_step}
jne .Louter_loop
jmp .Lreturn\n""".format(n_step=self.n * self.n_step(), nc=nc_reg)
self.asm_string += check
self.asm_string += '\n.Ltail:'
if self.n * self.n_step() == 64:
self.asm_string += """
mov {tmp1}, -1
shlx {tmp1}, {tmp1}, {nc_reg}
not {tmp1}
kmovw k1, {tmp1_lo}
shr {tmp1}, 16
kmovw k2, {tmp1_lo}
shr {tmp1}, 16
kmovw k3, {tmp1_lo}
shr {tmp1}, 16
kmovw k4, {tmp1_lo}\n
""".format(
nc_reg=nc_reg,
tmp1=tmp_gp_regs[1],
tmp1_lo=self.register_map_dword(tmp_gp_regs[1]),
)
for mr in range(0, self.m):
self.asm_string += (
'vmovdqu8 xmmword ptr [{c_reg}]{{k1}}, z{ACC}\n'.format(
ACC=accumulators[mr], c_reg=cm_registers[mr + c_reg_offset]
)
)
self.asm_string += (
'vmovdqu8 xmmword ptr [{c_reg} + 16]{{k2}}, z{ACC}\n'.format(
ACC=accumulators[mr + self.m],
c_reg=cm_registers[mr + c_reg_offset],
)
)
self.asm_string += (
'vmovdqu8 xmmword ptr [{c_reg} + 32]{{k3}}, z{ACC}\n'.format(
ACC=accumulators[mr + 2 * self.m],
c_reg=cm_registers[mr + c_reg_offset],
)
)
self.asm_string += (
'vmovdqu8 xmmword ptr [{c_reg} + 64]{{k4}}, z{ACC}\n'.format(
ACC=accumulators[mr + 3 * self.m],
c_reg=cm_registers[mr + c_reg_offset],
)
)
elif self.n * self.n_step() == 32:
self.asm_string += """
mov {tmp1}, -1
shlx {tmp1}, {tmp1}, {nc_reg}
not {tmp1}
kmovw k1, {tmp1_lo}
shr {tmp1_lo}, 16
kmovw k2, {tmp1_lo}\n""".format(
nc_reg=nc_reg,
tmp1_lo=self.register_map_dword(tmp_gp_regs[1]),
tmp1=tmp_gp_regs[1],
)
for mr in range(0, self.m):
self.asm_string += (
'vmovdqu8 xmmword ptr [{c_reg}]{{k1}}, z{ACC}\n'.format(
ACC=accumulators[mr], c_reg=cm_registers[mr + c_reg_offset]
)
)
self.asm_string += (
'vmovdqu8 xmmword ptr [{c_reg} + 16]{{k2}}, z{ACC}\n'.format(
ACC=accumulators[mr + self.m],
c_reg=cm_registers[mr + c_reg_offset],
)
)
else:
self.asm_string += """
mov {tmp1}, -1
shlx {tmp1}, {tmp1}, {nc_reg}
not {tmp1}
kmovw k1, {tmp1_lo}\n""".format(
nc_reg=nc_reg,
tmp1=tmp_gp_regs[1],
tmp1_lo=self.register_map_dword(tmp_gp_regs[1]),
)
for mr in range(0, self.m):
self.asm_string += (
'vmovdqu8 xmmword ptr [{c_reg}]{{k1}}, x{ACC}\n'.format(
ACC=accumulators[mr], c_reg=cm_registers[mr + c_reg_offset]
)
)
def outer_loop_prepare(self):
self.asm_string += """
# Load 0x80 for xoring the weights
vbroadcastsd z{sign_mask}, qword ptr [rip + .SIGN_MASK]\n
""".format(
sign_mask=self.sign_mask_register(),
)
class Avx512VnniQS8QC4W(Avx512VnniQc4w, Avx512VnniQS8QC8W):
def function_name(self):
c = self._c
return (
f'xnn_qs8_qc4w_gemm_minmax_fp32_ukernel_{self.m}x{self.n * self.n_step()}'
+ f'c{c}__asm_amd64_{self.isa()}'
)
def sign_mask_register(self):
return "mm30"