File size: 18,486 Bytes
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
// SPDX-License-Identifier: Apache-2.0
// msda_k (tt/msda_k.py, OPT_PLAN #1 / #2 / #10): the data-movement half of the fixed-point MSDA kernel. The same
// source runs on both data-movement RISC-Vs; PART 0 (NCRISC, "reader") owns the points [0, P/2), PART 1 (BRISC,
// "writer") the points [P/2, P) and also writes the output tiles. Each RISC has its own CBs (cb_geo, cb_in, cb_loc),
// so there is no cross-RISC handshake.
//
// Work item w (contiguous range per core, core index from the logical coordinates: common RT args only, probe P3):
//   j = w / NB (head block), b = w % NB (32-query block); value map m = j * SUB + b / (NB / SUB);
//   loc  int32 TILE [1, H, NB * 32, 32], tile j * NB + b: row r, column 2p / 2p + 1 = x / y of point p as
//        round(pixel * 2^16) (pixel = (g + 1) size / 2 - 0.5, align_corners=False);
//   attn fp32 TILE  [1, H, NB * 32, 32], tile j * NB + b: the softmax weight of point p in column p;
//   out  bf16 TILE  [1, 1, NB * 32, H * 32], tile b * H + j.
// value: VSRC 0 = (N, h, w, 32) ROW_MAJOR, page m * h * w + y * w + x; VSRC 1 = the head split [B, heads, rows, 32]
// TILE as nlp_create_qkv_heads writes it (map m = j * SUB + s: slice j, rows s * h * w ..; TPH tiles per slice), the
// stick of a pixel = row r of its tile: two 32-byte face rows (faces 2 (r / 16) and + 1), no untilize needed.
// Per item and RISC: cb_geo <- 3 fp32 tiles (fx, fy, attn with the other RISC's point columns zeroed) and
// cb_in <- 2P bf16 tiles (step k = (p - p0) * 4 + corner; corners NW, NE, SW, SE): row r of step k is the 32-channel
// value stick of that corner, read as two 32-byte halves straight into the tile's face rows; corners outside the map
// (and padding rows) read the zeros page. The compute kernel turns geo into the 4 corner weights on the SFPU.
#include <stdint.h>

#include "api/dataflow/dataflow_api.h"
#include "ttnn/cpp/ttnn/kernel_lib/reduce_helpers_dataflow.hpp"

constexpr uint32_t PART = get_compile_time_arg_val(1);
constexpr uint32_t cb_in = get_compile_time_arg_val(2);
constexpr uint32_t cb_geo = get_compile_time_arg_val(3);
constexpr uint32_t cb_loc = get_compile_time_arg_val(4);
constexpr uint32_t cb_e = get_compile_time_arg_val(5);
constexpr uint32_t cb_out = get_compile_time_arg_val(6);
constexpr int32_t HV = get_compile_time_arg_val(7);
constexpr int32_t WV = get_compile_time_arg_val(8);
constexpr uint32_t NQ = get_compile_time_arg_val(9);
constexpr uint32_t NB = get_compile_time_arg_val(10);
constexpr uint32_t H = get_compile_time_arg_val(11);
constexpr uint32_t SUB = get_compile_time_arg_val(12);
constexpr uint32_t P = get_compile_time_arg_val(13);
constexpr uint32_t NWORK = get_compile_time_arg_val(14);
constexpr uint32_t NC = get_compile_time_arg_val(15);
constexpr uint32_t GX = get_compile_time_arg_val(16);
constexpr uint32_t WRITES_OUT = get_compile_time_arg_val(17);
constexpr uint32_t MAKES_E = get_compile_time_arg_val(18);
// 0: ROW_MAJOR maps (64-byte pages); 1: TILE heads; 2 (BEVFORMER_MSDA_PAIRS): pixel-pair maps [N, h, w + 1, 64]
// (tt/pairmap_k.py: 128-byte page (m, y, i) = the sticks of x = i - 1 and x = i): ONE read per point and row lands
// the NW + NE (or SW + SE) sticks as a row of a [32 x 64] row-major staging block (tilized by the compute kernel)
constexpr uint32_t VSRC = get_compile_time_arg_val(19);
constexpr uint32_t TPH = get_compile_time_arg_val(20);    // VSRC 1: tiles per head slice (SUB maps of HV x WV rows)
// SKIP: items strided over the cores (w = core, core + NC, ..: padding blocks spread evenly) and points with all four
// corners outside the map (or rows past NQ) issue no reads: their weight is set to 0, and their tile rows keep
// earlier (finite) gathers; the step tiles are zeroed once. 0: contiguous item ranges, zeros-page reads.
constexpr uint32_t SKIP = get_compile_time_arg_val(21);
// LOC_HS 0: loc is the head split [1, H, NB * 32, 32] (tile j * NB + b, columns 2p / 2p + 1); LOC_HS > 0
// (BEVFORMER_MSDA_LOC=compact): loc is the merged [1, 1, NB * 32, H * LOC_HS] map (the sampling projection's own
// layout: head j in columns j * LOC_HS .. + 2P), LOC_TW tiles per row block: tile b * LOC_TW + j * LOC_HS / 32,
// column (j * LOC_HS) % 32 + 2p
constexpr uint32_t LOC_HS = get_compile_time_arg_val(22);
constexpr uint32_t LOC_TW = get_compile_time_arg_val(23);
// SM 1 (BEVFORMER_MSDA_SOFTMAX=kernel): attn is the compact attention-logit map [1, 1, NB * 32, H * P] (head j in
// columns j * P ..; AT_TW tiles per row block); the PART-0 RISC rebuilds the head's logit tile (columns 0..P-1 the
// logits, the rest PAD_LOGIT: what the head-block projection writes) into cb_lg for the compute kernel's softmax,
// and makes the reduce scalers and the two half masks once. Rows past NQ get logits 0 (finite; their values are 0).
constexpr uint32_t SM = get_compile_time_arg_val(24);
constexpr uint32_t AT_TW = get_compile_time_arg_val(25);
constexpr uint32_t PAD_BITS = get_compile_time_arg_val(26);   // fp32 bits of PAD_LOGIT
constexpr uint32_t cb_lg = 9, cb_scmax = 10, cb_scsum = 11, cb_msk = 12, cb_scr = 18;
constexpr auto v_args = TensorAccessorArgs<27>();
constexpr auto l_args = TensorAccessorArgs<v_args.next_compile_time_args_offset()>();
constexpr auto a_args = TensorAccessorArgs<l_args.next_compile_time_args_offset()>();
constexpr auto o_args = TensorAccessorArgs<a_args.next_compile_time_args_offset()>();

constexpr uint32_t PH = P / 2;
constexpr uint32_t P0 = PART * PH;
constexpr uint32_t NS = PH * 4;  // steps (tiles) per item and RISC
constexpr uint32_t TB16 = 2048, TB32 = 4096;
constexpr uint32_t VPAGE = VSRC == 0 ? 64 : (VSRC == 2 ? 128 : 2048);  // RM stick; pixel pair; TILE: a bf16 tile
static_assert(VSRC != 2 || !SKIP, "msda_k: pair maps and SKIP are exclusive");
constexpr uint32_t BPS = NB / SUB;
constexpr uint32_t MAP = static_cast<uint32_t>(HV * WV);
constexpr int32_t LOC_LIM = 1 << 28;  // |pixel| < 4096 after the clamp: no overflow below
static_assert(P % 2 == 0 && P <= 8, "msda_k: P even, <= 8 (x / y of a point share a face)");
static_assert(NB % SUB == 0, "msda_k: blocks split evenly over the value maps of a head");
static_assert(LOC_HS == 0 || (LOC_HS >= 2 * P && 32 % LOC_HS == 0), "msda_k: LOC_HS holds 2P columns, divides 32");

static inline uint32_t tile_elem(uint32_t r, uint32_t c) {
    return (((r >> 4) << 1) + (c >> 4)) * 256 + ((r & 15) << 4) + (c & 15);
}

// q in [0, 65536): the fp32 bit pattern of q / 65536 (exact)
static inline uint32_t q16_to_f32(uint32_t q) {
    if (q == 0) {
        return 0;
    }
    const uint32_t n = 31 - __builtin_clz(q);
    return ((127 - 16 + n) << 23) | ((q << (23 - n)) & 0x7FFFFF);
}

// one corner stick (32 channels) -> the face rows at dst (channels 0..15) and dst + 512 (16..31). pos: RM page
// (VSRC 0) or slice * TPH * 32 + row (VSRC 1).
// VSRC 3 (BEVFORMER_DEC_VALUE=fused, D20 round 3): the value is the projection itself, TILE [1, 1, SUB h w, H * 32]
// (no head split): the head block j of map row pos is tile (pos / 32) * H + j; g_hj = the current item's head
static uint32_t g_hj = 0;

template <typename A>
static inline void gather(const A& v_acc, uint32_t pos, bool ok, uint64_t zeros, uint32_t dst) {
    if (!ok) {
        noc_async_read(zeros, dst, 32);
        noc_async_read(zeros, dst + 512, 32);
        return;
    }
    uint64_t s;
    uint32_t hi;
    if constexpr (VSRC == 0) {
        s = v_acc.get_noc_addr(pos);
        hi = 32;
    } else {
        const uint32_t r = pos & 31;
        const uint32_t t = VSRC == 3 ? (pos >> 5) * H + g_hj : pos >> 5;
        s = v_acc.get_noc_addr(t, ((r >> 4) << 10) + ((r & 15) << 5));
        hi = 512;
    }
#ifndef MSDA_K_NOREAD
    noc_async_read(s, dst, 32);
    noc_async_read(s + hi, dst + 512, 32);
#endif
}

static inline void zero_l1(uint32_t addr, uint32_t bytes) {
    const uint64_t z = get_noc_addr(MEM_ZEROS_BASE);
    for (uint32_t o = 0; o < bytes; o += MEM_ZEROS_SIZE) {
        noc_async_read(z, addr + o, MEM_ZEROS_SIZE);
    }
}

void kernel_main() {
    const uint32_t ci = get_absolute_logical_y() * GX + get_absolute_logical_x();
    if (ci >= NC) {
        return;
    }
    const uint32_t w0 = SKIP ? ci : ci * NWORK / NC;
    const uint32_t w1 = SKIP ? NWORK : (ci + 1) * NWORK / NC;
    const uint32_t wstep = SKIP ? NC : 1;
    if (w0 >= w1) {
        return;
    }
    const auto v_acc = TensorAccessor(v_args, get_common_arg_val<uint32_t>(0), VPAGE);
    const auto l_acc = TensorAccessor(l_args, get_common_arg_val<uint32_t>(1), TB32);
    const auto a_acc = TensorAccessor(a_args, get_common_arg_val<uint32_t>(2), TB32);
    const auto o_acc = TensorAccessor(o_args, get_common_arg_val<uint32_t>(3), TB16);
    const uint64_t zeros = get_noc_addr(MEM_ZEROS_BASE);

    if constexpr (MAKES_E) {
        // E_p (bf16): row p all ones, so W @ E_p broadcasts column p of W over the 32 channels
        cb_reserve_back(cb_e, P);
        const uint32_t e_addr = get_write_ptr(cb_e);
        zero_l1(e_addr, P * TB16);
        noc_async_read_barrier();
        volatile tt_l1_ptr uint16_t* e = reinterpret_cast<volatile tt_l1_ptr uint16_t*>(e_addr);
        for (uint32_t p = 0; p < P; ++p) {
            for (uint32_t c = 0; c < 32; ++c) {
                e[p * (TB16 / 2) + tile_elem(p, c)] = 0x3F80;
            }
        }
        cb_push_back(cb_e, P);
    }
    if constexpr (SM && MAKES_E) {
        // the softmax's reduce scalers, as ttnn's softmax reader makes them; the half masks (fp32 1 / 0 per column:
        // RISC h owns the points [h P / 2, (h + 1) P / 2)); the logit pages pre-filled with PAD_LOGIT
        dataflow_kernel_lib::calculate_and_prepare_reduce_scaler<cb_scmax, ckernel::PoolType::MAX,
                                                                 ckernel::ReduceDim::REDUCE_ROW>();
        dataflow_kernel_lib::calculate_and_prepare_reduce_scaler<cb_scsum, ckernel::PoolType::SUM,
                                                                 ckernel::ReduceDim::REDUCE_ROW>();
        cb_reserve_back(cb_msk, 2);
        volatile tt_l1_ptr uint32_t* m = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(get_write_ptr(cb_msk));
        for (uint32_t h = 0; h < 2; ++h) {
            for (uint32_t r = 0; r < 32; ++r) {
                for (uint32_t c = 0; c < 32; ++c) {
                    m[h * 1024 + tile_elem(r, c)] = (c >= h * PH && c < (h + 1) * PH) ? 0x3F800000u : 0u;
                }
            }
        }
        cb_push_back(cb_msk, 2);
        cb_reserve_back(cb_lg, 2);
        volatile tt_l1_ptr uint32_t* lg = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(get_write_ptr(cb_lg));
        for (uint32_t i = 0; i < 2048; ++i) {
            lg[i] = PAD_BITS;
        }
    }
    // the geo pages: zero once (columns >= P and the right faces stay zero / finite forever)
    cb_reserve_back(cb_geo, 6);
    zero_l1(get_write_ptr(cb_geo), 6 * TB32);
    if constexpr (SKIP) {
        cb_reserve_back(cb_in, 2 * NS);
        zero_l1(get_write_ptr(cb_in), 2 * NS * TB16);
    }
    cb_reserve_back(cb_loc, 1);
    const uint32_t lbuf = get_write_ptr(cb_loc);
    noc_async_read_barrier();
    volatile tt_l1_ptr int32_t* loc = reinterpret_cast<volatile tt_l1_ptr int32_t*>(lbuf);

    uint32_t prev = 0xFFFFFFFF;
    for (uint32_t w = w0; w < w1; w += wstep) {
        const uint32_t j = w / NB;
        const uint32_t b = w - j * NB;
        // VSRC 0: page of the map's pixel 0; VSRC 1: the slice's first tile row + the map's first row in it
        const uint32_t vbase = VSRC == 0   ? (j * SUB + b / BPS) * MAP
                               : VSRC == 2 ? (j * SUB + b / BPS) * HV * (WV + 1)
                               : VSRC == 3 ? (b / BPS) * MAP
                                           : j * TPH * 32 + (b / BPS) * MAP;
        g_hj = j;
        cb_reserve_back(cb_geo, 3);
        cb_reserve_back(cb_in, NS);
        const uint32_t g_addr = get_write_ptr(cb_geo);
        const uint32_t in_base = get_write_ptr(cb_in);
        const uint32_t ltile = LOC_HS == 0 ? j * NB + b : b * LOC_TW + (j * LOC_HS) / 32;
        const uint32_t lcol = LOC_HS == 0 ? 0 : (j * LOC_HS) % 32;
        noc_async_read(l_acc.get_noc_addr(ltile), lbuf, TB32);
        if constexpr (SM) {
            if constexpr (MAKES_E) {
                noc_async_read(a_acc.get_noc_addr(b * AT_TW + (j * P) / 32), get_write_ptr(cb_scr), TB32);
            }
        } else {
            noc_async_read(a_acc.get_noc_addr(j * NB + b), g_addr + 2 * TB32, TB32);
        }
        noc_async_read_barrier();
        if constexpr (SM && MAKES_E) {
            // the head's logit tile: columns 0..P-1 <- compact columns (j P) % 32 + p; columns >= P keep PAD
            cb_reserve_back(cb_lg, 1);
            volatile tt_l1_ptr uint32_t* lg = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(get_write_ptr(cb_lg));
            volatile tt_l1_ptr uint32_t* src = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(get_write_ptr(cb_scr));
            const uint32_t acol = (j * P) % 32;
            for (uint32_t r = 0; r < 32; ++r) {
                const bool ok = b * 32 + r < NQ;
                for (uint32_t p = 0; p < P; ++p) {
                    lg[tile_elem(r, p)] = ok ? src[tile_elem(r, acol + p)] : 0u;
                }
            }
            cb_push_back(cb_lg, 1);
        }
        volatile tt_l1_ptr uint32_t* fx = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(g_addr);
        volatile tt_l1_ptr uint32_t* fy = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(g_addr + TB32);
        volatile tt_l1_ptr uint32_t* at = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(g_addr + 2 * TB32);
        // the other RISC's points carry zero weight in this copy (their fx / fy here are stale but finite); SM: the
        // compute kernel applies the half mask to the softmax instead
        if constexpr (!SM) {
            for (uint32_t r = 0; r < 32; ++r) {
                for (uint32_t p = 0; p < P; ++p) {
                    if (p < P0 || p >= P0 + PH) {
                        at[tile_elem(r, p)] = 0;
                    }
                }
            }
        }
        for (uint32_t r = 0; r < 32; ++r) {
            const uint32_t roff = (((r >> 4) << 1) << 9) + ((r & 15) << 5);  // face (r / 16, 0), row r % 16
            const bool row_ok = b * 32 + r < NQ;
            for (uint32_t p = P0; p < P0 + PH; ++p) {
                const uint32_t dst = in_base + (p - P0) * 4 * TB16 + roff;
                const uint32_t ew = tile_elem(r, p);
                if (!row_ok) {
                    fx[ew] = 0;
                    fy[ew] = 0;
                    if constexpr (!SM) {
                        at[ew] = 0;
                    }
                    if constexpr (SKIP) {
                        continue;
                    }
                    if constexpr (VSRC == 2) {
                        const uint32_t blk = in_base + (p - P0) * 4 * TB16 + r * 128;
                        noc_async_read(zeros, blk, 128);
                        noc_async_read(zeros, blk + 2 * TB16, 128);
                        continue;
                    }
                    for (uint32_t c = 0; c < 4; ++c) {
                        noc_async_read(zeros, dst + c * TB16, 32);
                        noc_async_read(zeros, dst + c * TB16 + 512, 32);
                    }
                    continue;
                }
                int32_t vx = loc[tile_elem(r, lcol + 2 * p)];
                int32_t vy = loc[tile_elem(r, lcol + 2 * p + 1)];
                vx = vx > LOC_LIM ? LOC_LIM : (vx < -LOC_LIM ? -LOC_LIM : vx);
                vy = vy > LOC_LIM ? LOC_LIM : (vy < -LOC_LIM ? -LOC_LIM : vy);
                const int32_t x0 = vx >> 16;
                const int32_t y0 = vy >> 16;
                fx[ew] = q16_to_f32(static_cast<uint32_t>(vx) & 0xFFFF);
                fy[ew] = q16_to_f32(static_cast<uint32_t>(vy) & 0xFFFF);
                const bool xv0 = x0 >= 0 && x0 < WV, xv1 = x0 + 1 >= 0 && x0 + 1 < WV;
                const bool yv0 = y0 >= 0 && y0 < HV, yv1 = y0 + 1 >= 0 && y0 + 1 < HV;
                if constexpr (SKIP) {
                    static_assert(!(SKIP && SM), "msda_k: SKIP and SM are exclusive");
                    if (!((yv0 || yv1) && (xv0 || xv1))) {
                        at[ew] = 0;
                        continue;
                    }
                }
                if constexpr (VSRC == 2) {
                    // staging blocks of point p: top (NW | NE) at + 0, bottom (SW | SE) at + 2 tiles; row r: 128 B
                    const uint32_t blk = in_base + (p - P0) * 4 * TB16 + r * 128;
                    const bool xp = x0 >= -1 && x0 < WV;                 // page i = x0 + 1 in [0, WV]
                    const uint32_t pg = vbase + static_cast<uint32_t>(y0 * (WV + 1) + x0 + 1);
                    if (xp && yv0) {
                        noc_async_read(v_acc.get_noc_addr(pg), blk, 128);
                    } else {
                        noc_async_read(zeros, blk, 128);
                    }
                    if (xp && yv1) {
                        noc_async_read(v_acc.get_noc_addr(pg + WV + 1), blk + 2 * TB16, 128);
                    } else {
                        noc_async_read(zeros, blk + 2 * TB16, 128);
                    }
                    continue;
                }
                const uint32_t pos = vbase + static_cast<uint32_t>(y0 * WV + x0);
                gather(v_acc, pos, yv0 && xv0, zeros, dst);
                gather(v_acc, pos + 1, yv0 && xv1, zeros, dst + TB16);
                gather(v_acc, pos + WV, yv1 && xv0, zeros, dst + 2 * TB16);
                gather(v_acc, pos + WV + 1, yv1 && xv1, zeros, dst + 3 * TB16);
            }
        }
        noc_async_read_barrier();
        cb_push_back(cb_geo, 3);
        cb_push_back(cb_in, NS);
        if constexpr (WRITES_OUT) {
            if (prev != 0xFFFFFFFF) {
                const uint32_t jp = prev / NB;
                const uint32_t bp = prev - jp * NB;
                cb_wait_front(cb_out, 1);
                noc_async_write(get_read_ptr(cb_out), o_acc.get_noc_addr(bp * H + jp), TB16);
                noc_async_writes_flushed();
                cb_pop_front(cb_out, 1);
            }
        }
        prev = w;
    }
    if constexpr (WRITES_OUT) {
        const uint32_t jp = prev / NB;
        const uint32_t bp = prev - jp * NB;
        cb_wait_front(cb_out, 1);
        noc_async_write(get_read_ptr(cb_out), o_acc.get_noc_addr(bp * H + jp), TB16);
        noc_async_write_barrier();
        cb_pop_front(cb_out, 1);
    }
}