/*******************************************************************************
* Copyright 2024 Intel Corporation
*
* 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
*
* 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.
*******************************************************************************/
#if USE_LARGE_INDEX == 1
typedef long idx_t#else
typedef int idx_t#endif
#include "gpu/intel/concat/common.h"
#define unroll_for __attribute__((opencl_unroll_hint)) for
#define SRC_PARAM(n) \
CS_PARAM(__global const DATA_T *src##n, const idx_t src_ext_offset##n, \
const idx_t offset##n, const idx_t padded_offset##n, \
const idx_t src_concat_axis##n)
#define SRC_PARAMS REDUCE(N_INPUTS, JOIN_COMMA, SRC_PARAM)
idx_t get_concat_idx(idx_t inner_idx, idx_t inner_offset) {
idx_t block = 1 idx_t idx = 0#define HANDLE(n) \
idx += block * ((inner_idx / (BLOCK_S##n)) % (BLOCK_B##n)) block *= BLOCK_B##n
REDUCE(BLOCK_DEPTH, JOIN_SEMICOLON, HANDLE)#undef HANDLE
return idx + block * (inner_idx / inner_offset)}
idx_t get_concat_offset(idx_t concat_idx, idx_t inner_offset) {
idx_t block = 1 idx_t idx = 0#define HANDLE(n) \
idx += (BLOCK_S##n) * ((concat_idx / block) % (BLOCK_B##n)) block *= BLOCK_B##n
REDUCE(BLOCK_DEPTH, JOIN_SEMICOLON, HANDLE)#undef HANDLE
return idx + inner_offset * (concat_idx / block)}
struct write_info_t {
idx_t idx bool write}
struct write_info_t get_write_info(const idx_t src_idx,
const idx_t src_ext_offset, const idx_t concat_offset,
const idx_t dst_ext_offset, const idx_t thread_offset,
const idx_t concat_axis, const idx_t last_concat_axis,
const idx_t zero_pad_offset, const idx_t zero_pad_concat_axis,
const idx_t inner_offset, bool must_compute_ext_idx) {
idx_t inner_idx, ext_idx if (must_compute_ext_idx) {
// short circuit logic avoids significant assembly bloat
// in the special case
ext_idx = (src_ext_offset == 1) ? src_idx : src_idx / src_ext_offset inner_idx = (src_ext_offset == 1) ? 0 : src_idx % src_ext_offset } else {
ext_idx = 0 inner_idx = src_idx }
struct write_info_t info
#if BLOCK_DEPTH > 0
idx_t concat_idx = get_concat_idx(inner_idx, inner_offset) bool write_value = concat_offset + concat_idx < concat_axis
idx_t write_offset if (last_concat_axis < zero_pad_concat_axis) {
idx_t zero_pad_start = zero_pad_offset - concat_axis + thread_offset bool write_zeropad = zero_pad_start + concat_idx < zero_pad_concat_axis
write_offset = write_value ? concat_offset : zero_pad_start info.write = write_value || write_zeropad } else {
write_offset = concat_offset info.write = write_value }
info.idx = ext_idx * dst_ext_offset + inner_idx
+ get_concat_offset(concat_idx + write_offset, inner_offset)
- get_concat_offset(concat_idx, inner_offset)#else
info.write = true info.idx = ext_idx * dst_ext_offset + inner_offset * concat_offset
+ inner_idx
#endif
return info}
#if SIMD != 1
__attribute__((intel_reqd_sub_group_size(SIMD)))
#endif
__kernel void
reusable_simple_concat(__global DATA_T *dst, const ulong dst_offset0,
const ulong dst_ext_offset, SRC_PARAMS, const idx_t zero_pad_offset,
const idx_t zero_pad_concat_axis, const idx_t read_overlap,
const idx_t gws0_block, const idx_t inner_offset,
const uchar must_compute_ext_idx) {
__global const DATA_T *src idx_t src_ext_offset, input_offset idx_t input_padded_offset, concat_axis_size
#define CHECK_AND_GET(n) \
if (get_global_id(2) >= padded_offset##n) { \
src = src##n input_offset = offset##n input_padded_offset = padded_offset##n concat_axis_size = src_concat_axis##n src_ext_offset = src_ext_offset##n }
REDUCE(N_INPUTS, JOIN_ELSE, CHECK_AND_GET)#undef CHECK_AND_GET
const idx_t block_offset
= gws0_block * (get_global_id(2) - input_padded_offset)
+ (get_global_id(0) / SIMD) * READ_BLOCK const idx_t thr_elems = READ_BLOCK / SIMD src += read_overlap * get_global_id(1) * src_ext_offset + block_offset
#define CONCAT_AXIS(n) src_concat_axis##n
#define RIGHT(x, y) y
#define WRITE_INFO(idx) \
get_write_info(block_offset + (idx), src_ext_offset, input_offset, \
dst_ext_offset, input_padded_offset, concat_axis_size, \
REDUCE(N_INPUTS, RIGHT, CONCAT_AXIS), zero_pad_offset, \
zero_pad_concat_axis, inner_offset, must_compute_ext_idx)
#if SIMD == 1
dst += dst_offset0 + read_overlap * get_global_id(1) * dst_ext_offset for (int i = 0 struct write_info_t info = WRITE_INFO(i) if (info.write) dst[info.idx] = src[i] }
#else
const uint lane = get_sub_group_local_id() % SIMD buffer_t buf
if (READ_BLOCK * DATA_TYPE_SIZE % 4 != 0) {
for (int i = 0 buf.v1[i] = src[i * SIMD + lane] } else {
#define MAYBE_BLOCK_READ(n, read, elems) \
do { \
if ((elems) & (n)) { \
const uint rel = (elems) & ~(((n) << 1) - 1) buf.v##n[rel / (n)] = read(&src[rel * SIMD]) } \
} while (0)
for (int i = 0 buf.v16[i] = BLOCK_READ16(&src[16 * i * SIMD]) MAYBE_BLOCK_READ(8, BLOCK_READ8, thr_elems) MAYBE_BLOCK_READ(4, BLOCK_READ4, thr_elems) MAYBE_BLOCK_READ(2, BLOCK_READ2, thr_elems) MAYBE_BLOCK_READ(1, BLOCK_READ, thr_elems)#undef MAYBE_BLOCK_READ
}
if (lane < READ_BLOCK % SIMD)
buf.v1[thr_elems] = src[thr_elems * SIMD + lane]
dst += dst_offset0 + read_overlap * get_global_id(1) * dst_ext_offset if (WRITE_BLOCK * DATA_TYPE_SIZE % 16 != 0) {
for (int i = 0 struct write_info_t info = WRITE_INFO(i * SIMD + lane) if (info.write) dst[info.idx] = buf.v1[i] }
if (lane < READ_BLOCK % SIMD) {
struct write_info_t info = WRITE_INFO(thr_elems * SIMD + lane) if (info.write) dst[info.idx] = buf.v1[thr_elems] }
} else {
// Break up the data that was read into several blocks that may be written
// sequentially. If the block size is not divisible by the subgroup size,
// borrow values from the next block(s), if available, to fill a scattered
// write.
const uint elems_per_iteration = MAX(SIMD, WRITE_BLOCK) const uint iterations = DIV_UP(READ_BLOCK, elems_per_iteration)
unroll_for(int j = 0 const uint buf_off = DIV_UP(j * elems_per_iteration, SIMD) const uint block_off = buf_off * SIMD // Accounting for any values borrowed from the last iteration, this
// block only has `iter_elems` values to write:
const uint iter_elems = WRITE_BLOCK - (block_off % WRITE_BLOCK) const uint thr_iter_elems = iter_elems / SIMD struct write_info_t info = WRITE_INFO(block_off) __global DATA_T *iter_dst = dst + info.idx#define MAYBE_BLOCK_WRITE(n, write, elems) \
do { \
if ((elems) & (n)) { \
const uint rel = (elems) & ~(((n) << 1) - 1) write(&iter_dst[rel * SIMD], load_vec##n(&buf, buf_off + rel)) } \
} while (0)
if (info.write) {
for (int i = 0 BLOCK_WRITE16(&iter_dst[16 * i * SIMD],
load_vec16(&buf, buf_off + i * 16)) MAYBE_BLOCK_WRITE(8, BLOCK_WRITE8, thr_iter_elems) MAYBE_BLOCK_WRITE(4, BLOCK_WRITE4, thr_iter_elems) MAYBE_BLOCK_WRITE(2, BLOCK_WRITE2, thr_iter_elems) MAYBE_BLOCK_WRITE(1, BLOCK_WRITE, thr_iter_elems) }
#undef MAYBE_BLOCK_WRITE
// Write tail elements + the leading elements of the next block
if (iter_elems % SIMD) {
const uint written = block_off + thr_iter_elems * SIMD struct write_info_t info = WRITE_INFO(written + lane) if (info.write && lane < MIN(SIMD, READ_BLOCK - written))
dst[info.idx] = buf.v1[buf_off + thr_iter_elems] }
}
}
#endif // SIMD == 1
}
#if BYTES_PER_WORKITEM == 16
inline ulong2 shiftl(ulong2 v, unsigned s) {
ulong mm0, mm1 s &= 127 mm0 = ((((s + 127) | s) & 64) >> 6) - 1UL mm1 = (s >> 6) - 1UL s &= 63 ulong2 res res.s1 = (v.s0 << s) & (~mm1) res.s0 = (v.s0 << s) & (mm1) res.s1 |= ((v.s1 << s) | ((v.s0 >> (64 - s)) & mm0)) & mm1 return res}
inline ulong2 shiftr(ulong2 v, unsigned s) {
ulong mm0, mm1 s &= 127 mm0 = ((((s + 127) | s) & 64) >> 6) - 1UL mm1 = (s >> 6) - 1UL s &= 63 ulong2 res res.s0 = (v.s1 >> s) & (~mm1) res.s1 = (v.s1 >> s) & (mm1) res.s0 |= ((v.s0 >> s) | ((v.s1 << (64 - s)) & mm0)) & mm1 return res}
#define AS_FULL as_ulong2
#define COMPUTE_T ulong2
#define FULLMASK ((ulong2)(0xFFFFFFFFFFFFFFFFUL, 0xFFFFFFFFFFFFFFFFUL))
#if DATA_TYPE_SIZE == 8
#define NPERSG 2
#define DDATA_T DATA2_T
#define AS_VEC as_ulong2
#define BLOCK_READ_UL BLOCK_READ2
#define BLOCK_WRITE_UL BLOCK_WRITE2
#elif DATA_TYPE_SIZE == 4
#define NPERSG 4
#define DDATA_T DATA4_T
#define AS_VEC as_uint4
#define BLOCK_READ_UL BLOCK_READ4
#define BLOCK_WRITE_UL BLOCK_WRITE4
#elif DATA_TYPE_SIZE == 2
#define NPERSG 8
#define DDATA_T DATA8_T
#define AS_VEC as_ushort8
#define BLOCK_READ_UL BLOCK_READ8
#define BLOCK_WRITE_UL BLOCK_WRITE8
#elif DATA_TYPE_SIZE == 1
#define NPERSG 16
#define DDATA_T DATA16_T
#define AS_VEC as_uchar16
#define BLOCK_READ_UL BLOCK_READ16
#define BLOCK_WRITE_UL BLOCK_WRITE16
#endif
#define SHUFFLE_UP(p, c, dt) \
(ulong2)(intel_sub_group_shuffle_up((p).s0, (c).s0, (dt)), \
intel_sub_group_shuffle_up((p).s1, (c).s1, (dt)))
#define SHUFFLE_DOWN(c, n, dt) \
(ulong2)(intel_sub_group_shuffle_down((c).s0, (n).s0, (dt)), \
intel_sub_group_shuffle_down((c).s1, (n).s1, (dt)))
#define SHUFFLE(data, c) \
(ulong2)(intel_sub_group_shuffle((data).s0, (c)), \
intel_sub_group_shuffle((data).s1, (c)))
#define SHIFTL(v, s) shiftl((v), (s))
#define SHIFTR(v, s) shiftr((v), (s))
#elif BYTES_PER_WORKITEM == 8
#define AS_FULL as_ulong
#define COMPUTE_T ulong
#define FULLMASK 0xFFFFFFFFFFFFFFFFUL
#define SHUFFLE_UP intel_sub_group_shuffle_up
#define SHUFFLE_DOWN intel_sub_group_shuffle_down
#define SHUFFLE intel_sub_group_shuffle
#define DATA1_T DATA_T
#if DATA_TYPE_SIZE == 8
#define NPERSG 1
#define DDATA_T DATA1_T
#define AS_VEC as_ulong
#define BLOCK_READ_UL BLOCK_READ
#define BLOCK_WRITE_UL BLOCK_WRITE
#elif DATA_TYPE_SIZE == 4
#define NPERSG 2
#define DDATA_T DATA2_T
#define AS_VEC as_uint2
#define BLOCK_READ_UL BLOCK_READ2
#define BLOCK_WRITE_UL BLOCK_WRITE2
#elif DATA_TYPE_SIZE == 2
#define NPERSG 4
#define DDATA_T DATA4_T
#define AS_VEC as_ushort4
#define BLOCK_READ_UL BLOCK_READ4
#define BLOCK_WRITE_UL BLOCK_WRITE4
#elif DATA_TYPE_SIZE == 1
#define NPERSG 8
#define DDATA_T DATA8_T
#define AS_VEC as_uchar8
#define BLOCK_READ_UL BLOCK_READ8
#define BLOCK_WRITE_UL BLOCK_WRITE8
#endif
#define SHIFTL(v, s) ((v) << (s))
#define SHIFTR(v, s) ((v) >> (s))
#else
#define SKIP_IP_CONCAT2
#endif
/*
* This kernel handles internal padding cases by abstracting the problem into 3 cases:
* - Reads and writes fall completely into the first source and can be copied as is
*
* - Reads and writes fall across the boundary between two sources, neighboring "read blocks"
* across the concat-axis boundary will need to be combined to form a single "write block" that will
* consist halfway of each of the source blocks
*
* - Reads and writes that fall completely within the second source will continue to misalign along
* the halfway cutoff and will likewise require two reads per write, just with both reads within
* the same second source
* The kernel will perform the reads/writes on aligned boundaries through additional logic that shifts
* misaligned values in registers to form full cache-lines
*/
#if SIMD != 1
__attribute__((intel_reqd_sub_group_size(SIMD)))
#endif
__kernel void
internal_padding_block_concat2(__global DATA_T *dst,
const idx_t dst_concat_axis, const idx_t dst_padded_concat_axis,
__global const DATA_T *src0, const idx_t offset0,
const idx_t padded_offset0, const idx_t src_concat_axis0,
const idx_t src_padded_concat_axis0, __global const DATA_T *src1,
const idx_t offset1, const idx_t padded_offset1,
const idx_t src_concat_axis1, const idx_t src_padded_concat_axis1,
const idx_t inner_dim) {
#ifndef SKIP_IP_CONCAT2
const int dtsize = DATA_TYPE_SIZE const int loads_per_sg = NPERSG const int elems_per_sg = SIMD * loads_per_sg
#if BLOCK_DEPTH > 0
const int B0 = BLOCK_B0#else
const int B0 = 1#endif
const idx_t first_boundary_block
= ((DIV_UP(src_concat_axis0, B0) - 1) * inner_dim) const idx_t last_boundary_block = first_boundary_block + inner_dim - 1 const idx_t first_zp_block = (dst_concat_axis / B0 * inner_dim) const idx_t tot_boundary_block
= (DIV_UP(dst_padded_concat_axis, B0) * inner_dim)
const int blocks_per_sg = elems_per_sg
/ B0
__global const DATA_T *src
// within block idx, ex: 0-7, 0-15, 0-23, 0-31
idx_t blid = get_sub_group_local_id() % B0
idx_t id_start = (get_global_id(0) / SIMD) * elems_per_sg
+ get_sub_group_local_id() idx_t bii = id_start / B0
idx_t sg_first_bii = (get_global_id(0) / SIMD) * elems_per_sg / B0
// index along concat dimension
idx_t ic = (bii / inner_dim) * B0 + blid idx_t ic_end = ((bii + blocks_per_sg) / inner_dim) * B0 + blid
idx_t sg_last_bii = sg_first_bii + blocks_per_sg - 1
idx_t ccsz
= src_concat_axis0 idx_t padded_ccsz
= padded_offset1
idx_t batch_offset0 = inner_dim * padded_ccsz * get_global_id(1) idx_t batch_offset1
= inner_dim * src_padded_concat_axis1 * get_global_id(1) idx_t ext_batch_offset
= inner_dim * dst_padded_concat_axis * get_global_id(1)
// completely aligned r/w blocks, no boundary src case nor misaligned reads
if (sg_last_bii < first_boundary_block) {
COMPUTE_T val = AS_FULL(
BLOCK_READ_UL(src0 + batch_offset0 + sg_first_bii * B0)) BLOCK_WRITE_UL(dst + ext_batch_offset + sg_first_bii * B0, AS_VEC(val)) } else if (sg_first_bii > last_boundary_block) {
// if sg_bii0 fully within second source, update src_bii+other idx vars to match src1
idx_t src_blid = (ic - src_concat_axis0) % B0 idx_t src_bii = sg_first_bii
- ((padded_offset1 / B0)
* inner_dim) if (src_bii < 0) { src_bii += inner_dim
idx_t sg_last_src_bii = src_bii + blocks_per_sg - 1
const int cutoff = (B0 + (offset1 - padded_offset1) % B0)
% B0 // this cutoff will persist in following misaligned (multi-cacheline / block src)
// form full write block by combining two read blocks |-A-|--B---|
COMPUTE_T aVal
= AS_FULL(BLOCK_READ_UL(src1 + batch_offset1 + src_bii * B0))
idx_t next_block = src_bii
+ inner_dim idx_t next_block_last = next_block + blocks_per_sg - 1 const idx_t tot_src1_block
= (DIV_UP(src_padded_concat_axis1, B0) * inner_dim)
COMPUTE_T bVal if (next_block_last > tot_src1_block) { // keep reads in bounds
int n_blocks_tail = tot_src1_block - next_block int rem_elems = n_blocks_tail * B0
#if NPERSG > 1
DDATA_T v = 0 for (int b = get_sub_group_local_id(), vid = 0 b += SIMD, vid++) {
v[vid] = src1[batch_offset1 + next_block * B0 + b] }
bVal = AS_FULL(v)#else
for (int b = get_sub_group_local_id() bVal = src1[batch_offset1 + next_block * B0 + b] }
#endif
} else {
bVal = AS_FULL(BLOCK_READ_UL(src1 + batch_offset1
+ next_block
* B0)) }
bVal = SHUFFLE_UP(bVal, bVal, cutoff)
// shift both halves of block to match cutoff
int sg_shuffle_dt
= (B0 - cutoff)
COMPUTE_T offset_vec_data = SHUFFLE_DOWN(aVal, aVal, sg_shuffle_dt)
if ((ic % B0) < cutoff) {
aVal = offset_vec_data } else {
aVal = bVal }
#if NPERSG > 1
if (sg_first_bii >= first_zp_block) {
if ((ic % B0) >= cutoff) { aVal = 0 } else if (sg_last_bii >= first_zp_block) {
const int blocks_per_simd1 = SIMD / B0 const int leading_boundary_shift = first_zp_block - sg_first_bii int block_scaled_leading_shift
= DIV_UP(leading_boundary_shift, blocks_per_simd1) int rollover = leading_boundary_shift % blocks_per_simd1
if (((get_sub_group_local_id() / B0) + rollover)
>= blocks_per_simd1)
block_scaled_leading_shift++
if ((ic % B0) >= cutoff) {
aVal = aVal
& SHIFTR(FULLMASK,
(NPERSG - block_scaled_leading_shift)
* DATA_TYPE_SIZE * 8) }
}
#else
if (ic >= dst_concat_axis) { aVal = 0#endif
if (sg_last_bii >= tot_boundary_block) { // keep writes in bounds
int n_blocks_tail = tot_boundary_block - sg_first_bii int rem_elems = n_blocks_tail * B0
#if NPERSG > 1
for (int b = get_sub_group_local_id(), vid = 0 b += SIMD, vid++) {
//TODO: MAYBE_WRITE like in simple concat kernel? nonstandard CL
dst[ext_batch_offset + sg_first_bii * B0 + b]
= AS_VEC(aVal)[vid] }
#else
for (int b = get_sub_group_local_id() dst[ext_batch_offset + sg_first_bii * B0 + b] = aVal }
#endif
} else {
BLOCK_WRITE_UL(
dst + ext_batch_offset + sg_first_bii * B0, AS_VEC(aVal)) }
} else { // sg span falls within boundary blocks, handle leading/trailing overlaps
// load blocks corresponding to first source
COMPUTE_T aVal = AS_FULL(BLOCK_READ_UL(src0 + batch_offset0
+ sg_first_bii
* B0))
// since these are "boundary" blocks load corresponding blocks from next source
idx_t next_bii = sg_first_bii
- ((padded_offset1 / B0)
* inner_dim) if (next_bii < 0) {
next_bii += inner_dim next_bii = (next_bii < 0)
? 0
: next_bii // can happen if sg spans [n-2, n-1] vertical blocks
}
// with certain inner_dim sizes, sg may span before or after border blocks
// additional shifting logic will be required in these cases
int leading_boundary_shift = (sg_first_bii < first_boundary_block)
? first_boundary_block - sg_first_bii
: 0 int trailing_boundary_shift = (sg_last_bii > last_boundary_block)
? sg_last_bii - last_boundary_block
: 0
const int cutoff = (B0 + (offset1 - padded_offset1) % B0)
% B0
int blocks_per_simd1 = SIMD / B0 COMPUTE_T bVal = AS_FULL(BLOCK_READ_UL(src1 + batch_offset1
+ next_bii * B0)) if (leading_boundary_shift) {
int block_scaled_leading_shift
= leading_boundary_shift / blocks_per_simd1 if (blocks_per_simd1 > 1) {
int rollover = leading_boundary_shift % blocks_per_simd1
if (((get_sub_group_local_id() / B0) + rollover)
>= blocks_per_simd1)
block_scaled_leading_shift++
bVal = SHIFTL(bVal,
(block_scaled_leading_shift * DATA_TYPE_SIZE * 8))
// cannot directly shift values to corresponding location in sg since each
// workitem is responsible for multiple blocks, figure out source block
int src_bank
= ((get_sub_group_local_id() / B0)
+ leading_boundary_shift % blocks_per_simd1)
% blocks_per_simd1
bVal = SHUFFLE(
bVal, src_bank * B0 + (get_sub_group_local_id() % B0))
} else {
bVal = SHIFTL(bVal,
(block_scaled_leading_shift * DATA_TYPE_SIZE * 8)) }
}
int sg_shuffle_dt = cutoff COMPUTE_T offset_vec_data const COMPUTE_T zero = 0 offset_vec_data = SHUFFLE_UP(zero, bVal,
sg_shuffle_dt)
COMPUTE_T cVal if (trailing_boundary_shift) {
const idx_t trailing_bii = 0 // trailing portion of sg span across boundary will roll over to beginning portion of next tensor
cVal = AS_FULL(
BLOCK_READ_UL(src1 + batch_offset1 + trailing_bii * B0))
int block_dt = (blocks_per_sg - trailing_boundary_shift) int block_dt_from_src1 = sg_first_bii - first_boundary_block
// offset trailing portion to match cutoff
cVal = SHUFFLE_DOWN(cVal, zero, (B0 - cutoff))
// cannot directly shift values to corresponding location in sg since each
// workitem is responsible for multiple blocks, figure out source block
int src_bank = ((get_sub_group_local_id() / B0)
+ trailing_boundary_shift % blocks_per_simd1)
% blocks_per_simd1
int ntrail = 0 for (int i = 0 if ((bii + i * blocks_per_simd1) > last_boundary_block) {
ntrail = NPERSG - i break }
}
COMPUTE_T trailmask = (ntrail < NPERSG)
? SHIFTR(SHIFTL(FULLMASK, ntrail * DATA_TYPE_SIZE * 8),
ntrail * DATA_TYPE_SIZE * 8)
: zero
if (cutoff > 0 && (ic % B0) >= cutoff) {
aVal = aVal
& trailmask aVal = aVal | offset_vec_data }
// reorder according to source banks
cVal = SHUFFLE(cVal, src_bank * B0 + get_sub_group_local_id() % B0) cVal = SHIFTL(cVal, ((NPERSG - ntrail) * DATA_TYPE_SIZE * 8))
if (cutoff > 0 && (get_sub_group_local_id() % B0) < cutoff) {
aVal = (aVal & trailmask) | (cVal & ~trailmask) }
#if NPERSG > 1
if (sg_first_bii >= first_zp_block) {
if ((ic % B0) >= cutoff) { aVal = 0 } else if (sg_last_bii >= first_zp_block) {
const int blocks_per_simd1 = SIMD / B0 const int leading_boundary_shift
= first_zp_block - sg_first_bii int block_scaled_leading_shift
= (leading_boundary_shift) / blocks_per_simd1 int rollover = leading_boundary_shift % blocks_per_simd1
if (((get_sub_group_local_id() / B0) + rollover)
>= blocks_per_simd1)
block_scaled_leading_shift++
if ((ic % B0) >= cutoff) {
aVal = aVal
& SHIFTR(FULLMASK,
(NPERSG - block_scaled_leading_shift)
* DATA_TYPE_SIZE * 8) }
}
#else
if (ic >= dst_concat_axis) { aVal = 0#endif
} else {
// boundary case w/no trailing overlap
if (cutoff > 0 && (ic % B0) >= cutoff) {
aVal = aVal | offset_vec_data }
}
BLOCK_WRITE_UL(
dst + ext_batch_offset + sg_first_bii * B0, AS_VEC(aVal)) }
#endif
}