File size: 15,589 Bytes
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// SPDX-License-Identifier: Apache-2.0
// K1 segment_reduce (ttaw.ops.segment, PLAN.md 1.3): per-segment MAX over the rows of a tensor sorted by segment.
//
//   out[s, :] = max(x[off[s] : off[s + 1], :])      for s in [0, NUM_SEG); empty segments -> FILL_BITS
//
// x: [1, 1, NUM_ROWS, C] interleaved DRAM, TILE (IN_TILE = 1) or ROW_MAJOR, bf16 or fp32; off: uint32 ROW_MAJOR
// [1, >= NUM_SEG + 1] interleaved DRAM (one page; CSR offsets, non-decreasing); out: [1, 1, NUM_SEG, C] ROW_MAJOR
// interleaved DRAM, bf16 (round to nearest even) or fp32. Exact: the maximum is taken on integer keys of the
// float bit patterns (key = bits ^ ((bits >> 31) & 0x7fffffff), signed order == float order, -0 < +0), so no float
// arithmetic happens on the RISC-V (no F extension) and bf16 / fp32 inputs come out bit-exact (fp32 -> bf16 is a
// monotonic rounding, so max-then-round == round-then-max). NaN is not supported (not produced by the models).
//
// Work split (deterministic, data-dependent, no per-core runtime args): NUM_WORKERS workers = SLOTS data-movement
// RISC-Vs per core on a GW-wide core rectangle starting at (X0, Y0); worker w = core_index * SLOTS + SLOT. The cost
// of segment s is off[s] + s (its rows plus one output row; strictly increasing in s), T = off[S] + S. Worker w owns
// the segments whose cost lies in [w T / W, (w + 1) T / W): it finds them by two binary searches over off (64-byte
// probes from DRAM), then streams its contiguous row range in units of 32 rows (one tile-row in TILE mode) with
// double buffering, reduces each segment into an L1 accumulator and writes one output row per segment (a ring of
// OUT_RING staging rows). Every segment is written by exactly one worker; nothing is shared between workers.
//
// Runtime args: COMMON only [x_addr, off_addr, out_addr] (P3 rule: per-core work derives from the core coordinates;
// the arg layout is fixed by the compile-time args). Hang-protocol notes (PLAN.md 4.4): no CB is pushed or popped
// (CBs are plain L1 scratch: data 2 units, offsets window, probe block, accumulator, output ring; sizes checked by
// the host builder), no multicast, no semaphores; every DRAM read has matching 64-byte alignment of source and
// destination (Blackhole NOC_DRAM_READ_ALIGNMENT_BYTES), every read is waited on by noc_async_read_barrier (which
// also invalidates the Blackhole L1 data cache) before the data are used, staging rows are reused only after
// noc_async_writes_flushed, and the kernel ends with noc_async_write_barrier. Offsets are clamped to [0, NUM_ROWS]
// and to non-decreasing order, so a corrupt table can never make the kernel read outside x.
#include <cstdint>

#include "api/dataflow/dataflow_api.h"

constexpr uint32_t IN_TILE = get_compile_time_arg_val(0);      // 1: TILE input, 0: ROW_MAJOR input
constexpr uint32_t IN_BYTES = get_compile_time_arg_val(1);     // input element bytes: 2 (bf16) or 4 (fp32)
constexpr uint32_t OUT_BYTES = get_compile_time_arg_val(2);    // output element bytes: 2 (bf16, RNE) or 4 (fp32)
constexpr uint32_t C = get_compile_time_arg_val(3);            // channels (multiple of 32)
constexpr uint32_t NUM_ROWS = get_compile_time_arg_val(4);     // input rows (multiple of 32): the read bound
constexpr uint32_t NUM_SEG = get_compile_time_arg_val(5);      // output rows (segments)
constexpr uint32_t NUM_WORKERS = get_compile_time_arg_val(6);  // workers of the program
constexpr uint32_t SLOTS = get_compile_time_arg_val(7);        // workers per core (1 or 2)
constexpr uint32_t SLOT = get_compile_time_arg_val(8);         // this kernel's slot on its core
constexpr uint32_t X0 = get_compile_time_arg_val(9);           // core rectangle: first logical x
constexpr uint32_t Y0 = get_compile_time_arg_val(10);          //                 first logical y
constexpr uint32_t GW = get_compile_time_arg_val(11);          //                 width in cores
constexpr uint32_t CB_DATA = get_compile_time_arg_val(12);     // 2 units of input rows (+ 64 B alignment slack)
constexpr uint32_t CB_OFF = get_compile_time_arg_val(13);      // offsets window: OFF_CHUNK uint32 (+ 64 B)
constexpr uint32_t CB_PROBE = get_compile_time_arg_val(14);    // one 64-byte probe block (+ 64 B)
constexpr uint32_t CB_ACC = get_compile_time_arg_val(15);      // C int32 keys
constexpr uint32_t CB_OUT = get_compile_time_arg_val(16);      // OUT_RING output rows (+ 64 B)
constexpr uint32_t OUT_RING = get_compile_time_arg_val(17);    // staging rows in flight
constexpr uint32_t OFF_CHUNK = get_compile_time_arg_val(18);   // offsets per window (multiple of 16)
constexpr uint32_t FILL_BITS = get_compile_time_arg_val(19);   // fp32 bits written for an empty segment
constexpr uint32_t OFF_PAGE_BYTES = get_compile_time_arg_val(20);  // page (= tensor) size of off, bytes
constexpr auto x_args = TensorAccessorArgs<21>();
constexpr auto off_args = TensorAccessorArgs<x_args.next_compile_time_args_offset()>();
constexpr auto out_args = TensorAccessorArgs<off_args.next_compile_time_args_offset()>();

constexpr uint32_t UNIT_ROWS = 32;                              // rows per streamed unit (one tile-row)
constexpr uint32_t CT = C / 32;                                 // 32-column tiles per row
constexpr uint32_t TILE_ELEMS = 1024;
constexpr uint32_t IN_PAGE_BYTES = IN_TILE ? TILE_ELEMS * IN_BYTES : C * IN_BYTES;
constexpr uint32_t PAGES_PER_UNIT = IN_TILE ? CT : UNIT_ROWS;
constexpr uint32_t UNIT_BYTES = PAGES_PER_UNIT * IN_PAGE_BYTES;
constexpr uint32_t OUT_ROW_BYTES = C * OUT_BYTES;
constexpr uint32_t OFF_ALIGNED_BYTES = (OFF_PAGE_BYTES + 63u) & ~63u;
constexpr uint32_t NUM_UNITS = NUM_ROWS / UNIT_ROWS;

static_assert(C % 32 == 0, "C must be a multiple of 32");
static_assert(NUM_ROWS % UNIT_ROWS == 0, "NUM_ROWS must be a multiple of 32");
static_assert(IN_BYTES == 2 || IN_BYTES == 4, "bf16 or fp32 input");
static_assert(OUT_BYTES == 2 || OUT_BYTES == 4, "bf16 or fp32 output");
static_assert(OFF_CHUNK % 16 == 0 && OFF_CHUNK >= 16, "OFF_CHUNK must be a multiple of 16");
static_assert(OUT_RING >= 1, "OUT_RING >= 1");
static_assert(IN_PAGE_BYTES % 64 == 0, "input pages must be 64-byte multiples (DRAM read alignment)");
static_assert(OUT_ROW_BYTES % 16 == 0, "output rows must be 16-byte multiples (DRAM write alignment)");

#define COMPILER_BARRIER() asm volatile("" ::: "memory")

static inline uint32_t align64(uint32_t a) { return (a + 63u) & ~63u; }

// float bits <-> signed key (an involution); signed comparison of keys == float comparison (no NaN)
static inline int32_t to_key(uint32_t bits) {
    const int32_t b = static_cast<int32_t>(bits);
    return b ^ ((b >> 31) & 0x7fffffff);
}
static inline uint32_t from_key(int32_t k) { return static_cast<uint32_t>(k ^ ((k >> 31) & 0x7fffffff)); }

static inline uint16_t to_bf16_rne(uint32_t bits) {
    return static_cast<uint16_t>((bits + 0x7fffu + ((bits >> 16) & 1u)) >> 16);
}

void kernel_main() {
    const uint32_t x_addr = get_common_arg_val<uint32_t>(0);
    const uint32_t off_addr = get_common_arg_val<uint32_t>(1);
    const uint32_t out_addr = get_common_arg_val<uint32_t>(2);

    const uint32_t core = (get_absolute_logical_y() - Y0) * GW + (get_absolute_logical_x() - X0);
    const uint32_t w = core * SLOTS + SLOT;
    if (w >= NUM_WORKERS) {
        return;
    }

    const auto xa = TensorAccessor(x_args, x_addr, IN_PAGE_BYTES);
    const auto oa = TensorAccessor(off_args, off_addr, OFF_PAGE_BYTES);
    const auto ya = TensorAccessor(out_args, out_addr, OUT_ROW_BYTES);

    const uint32_t data_l1 = align64(get_write_ptr(CB_DATA));
    const uint32_t off_l1 = align64(get_write_ptr(CB_OFF));
    const uint32_t probe_l1 = align64(get_write_ptr(CB_PROBE));
    const uint32_t out_l1 = align64(get_write_ptr(CB_OUT));
    int32_t* acc = reinterpret_cast<int32_t*>(get_write_ptr(CB_ACC));

    // ---- offsets: single 64-byte probes (binary search) and a streamed window (the worker's segments)
    auto probe = [&](uint32_t s) -> uint32_t {
        const uint32_t byte = s * 4u;
        noc_async_read(oa.get_noc_addr(0, byte & ~63u), probe_l1, 64);
        noc_async_read_barrier();
        COMPILER_BARRIER();
        const uint32_t v = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(probe_l1)[(byte & 63u) >> 2];
        return v < NUM_ROWS ? v : NUM_ROWS;
    };
    uint32_t win0 = 0xffffffffu;  // first offset index held in the window (multiple of 16)
    auto off_at = [&](uint32_t s) -> uint32_t {
        if (win0 == 0xffffffffu || s < win0 || s >= win0 + OFF_CHUNK) {
            win0 = s & ~15u;
            const uint32_t start = win0 * 4u;
            uint32_t bytes = OFF_CHUNK * 4u;
            if (start + bytes > OFF_ALIGNED_BYTES) {
                bytes = OFF_ALIGNED_BYTES - start;
            }
            noc_async_read(oa.get_noc_addr(0, start), off_l1, bytes);
            noc_async_read_barrier();
            COMPILER_BARRIER();
        }
        const uint32_t v = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(off_l1)[s - win0];
        return v < NUM_ROWS ? v : NUM_ROWS;
    };

    // ---- this worker's segments [s_lo, s_hi): costs off[s] + s in [w T / W, (w + 1) T / W)
    const uint32_t total = probe(NUM_SEG) + NUM_SEG;
    auto search = [&](uint32_t target) -> uint32_t {  // smallest s in [0, NUM_SEG] with off[s] + s >= target
        uint32_t lo = 0, hi = NUM_SEG;
        while (lo < hi) {
            const uint32_t mid = (lo + hi) >> 1;
            if (probe(mid) + mid >= target) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    };
    const uint32_t t0 = static_cast<uint32_t>((static_cast<uint64_t>(w) * total) / NUM_WORKERS);
    const uint32_t t1 = static_cast<uint32_t>((static_cast<uint64_t>(w + 1) * total) / NUM_WORKERS);
    const uint32_t s_lo = (w == 0) ? 0 : search(t0);
    const uint32_t s_hi = (w + 1 == NUM_WORKERS) ? NUM_SEG : search(t1);
    if (s_lo >= s_hi) {
        return;
    }

    // ---- input rows: units of 32 rows, two L1 slots, the next unit prefetched while the current one is reduced
    uint32_t row_last = off_at(s_lo);  // largest row this worker reads (+1), monotone over its segments
    {
        const uint32_t r_end = off_at(s_hi);
        if (r_end > row_last) {
            row_last = r_end;
        }
    }
    const uint32_t unit_last = row_last > 0 ? (row_last - 1) / UNIT_ROWS : 0;
    auto issue_unit = [&](uint32_t u, uint32_t slot) {
        const uint32_t dst = data_l1 + slot * UNIT_BYTES;
        const uint32_t first_page = IN_TILE ? u * CT : u * UNIT_ROWS;
        for (uint32_t p = 0; p < PAGES_PER_UNIT; ++p) {
            noc_async_read(xa.get_noc_addr(first_page + p), dst + p * IN_PAGE_BYTES, IN_PAGE_BYTES);
        }
    };
    uint32_t cur_unit = 0xffffffffu, cur_slot = 0;
    bool next_ready = false;  // the unit cur_unit + 1 is in flight into slot cur_slot ^ 1
    auto ensure_unit = [&](uint32_t u) {
        if (u == cur_unit) {
            return;
        }
        if (next_ready && u == cur_unit + 1) {
            noc_async_read_barrier();
            cur_slot ^= 1u;
        } else {
            noc_async_read_barrier();  // drain a stale prefetch before its slot is reused
            issue_unit(u, cur_slot);
            noc_async_read_barrier();
        }
        COMPILER_BARRIER();
        cur_unit = u;
        next_ready = false;
        if (u + 1 <= unit_last && u + 1 < NUM_UNITS) {
            issue_unit(u + 1, cur_slot ^ 1u);
            next_ready = true;
        }
    };

    auto reset_acc = [&]() {
        for (uint32_t c = 0; c < C; ++c) {
            acc[c] = static_cast<int32_t>(0x80000000u);
        }
    };
    auto accumulate_row = [&](uint32_t r) {  // acc = max(acc, x[r, :]) on keys
        const uint32_t i = r - cur_unit * UNIT_ROWS;
        const uint32_t base = data_l1 + cur_slot * UNIT_BYTES;
        if constexpr (IN_TILE) {
            const uint32_t face_row = (i >> 4) << 1;  // faces 0/1 (rows 0-15) or 2/3 (rows 16-31)
            for (uint32_t tc = 0; tc < CT; ++tc) {
                for (uint32_t h = 0; h < 2; ++h) {
                    const uint32_t e0 = tc * TILE_ELEMS + (face_row + h) * 256 + (i & 15u) * 16;
                    int32_t* a = acc + tc * 32 + h * 16;
                    if constexpr (IN_BYTES == 4) {
                        volatile tt_l1_ptr uint32_t* src = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(base) + e0;
                        for (uint32_t k = 0; k < 16; ++k) {
                            const int32_t key = to_key(src[k]);
                            a[k] = key > a[k] ? key : a[k];
                        }
                    } else {
                        volatile tt_l1_ptr uint16_t* src = reinterpret_cast<volatile tt_l1_ptr uint16_t*>(base) + e0;
                        for (uint32_t k = 0; k < 16; ++k) {
                            const int32_t key = to_key(static_cast<uint32_t>(src[k]) << 16);
                            a[k] = key > a[k] ? key : a[k];
                        }
                    }
                }
            }
        } else {
            if constexpr (IN_BYTES == 4) {
                volatile tt_l1_ptr uint32_t* src = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(base) + i * C;
                for (uint32_t c = 0; c < C; ++c) {
                    const int32_t key = to_key(src[c]);
                    acc[c] = key > acc[c] ? key : acc[c];
                }
            } else {
                volatile tt_l1_ptr uint16_t* src = reinterpret_cast<volatile tt_l1_ptr uint16_t*>(base) + i * C;
                for (uint32_t c = 0; c < C; ++c) {
                    const int32_t key = to_key(static_cast<uint32_t>(src[c]) << 16);
                    acc[c] = key > acc[c] ? key : acc[c];
                }
            }
        }
    };

    // ---- output rows: a ring of OUT_RING staging rows, reused after noc_async_writes_flushed
    uint32_t emitted = 0;
    auto emit = [&](uint32_t s, bool empty) {
        const uint32_t ring = emitted % OUT_RING;
        if (ring == 0 && emitted > 0) {
            noc_async_writes_flushed();
        }
        const uint32_t dst = out_l1 + ring * OUT_ROW_BYTES;
        if constexpr (OUT_BYTES == 4) {
            volatile tt_l1_ptr uint32_t* o = reinterpret_cast<volatile tt_l1_ptr uint32_t*>(dst);
            for (uint32_t c = 0; c < C; ++c) {
                o[c] = empty ? FILL_BITS : from_key(acc[c]);
            }
        } else {
            volatile tt_l1_ptr uint16_t* o = reinterpret_cast<volatile tt_l1_ptr uint16_t*>(dst);
            const uint16_t fill = to_bf16_rne(FILL_BITS);
            for (uint32_t c = 0; c < C; ++c) {
                o[c] = empty ? fill : to_bf16_rne(from_key(acc[c]));
            }
        }
        COMPILER_BARRIER();
        noc_async_write(dst, ya.get_noc_addr(s), OUT_ROW_BYTES);
        ++emitted;
    };

    // ---- the reduction
    uint32_t seg_begin = off_at(s_lo);
    for (uint32_t s = s_lo; s < s_hi; ++s) {
        uint32_t seg_end = off_at(s + 1);
        if (seg_end < seg_begin) {
            seg_end = seg_begin;  // non-decreasing offsets (a corrupt table reads nothing extra)
        }
        if (seg_end == seg_begin) {
            emit(s, true);
        } else {
            reset_acc();
            for (uint32_t r = seg_begin; r < seg_end; ++r) {
                ensure_unit(r / UNIT_ROWS);
                accumulate_row(r);
            }
            emit(s, false);
        }
        seg_begin = seg_end;
    }
    noc_async_read_barrier();  // a prefetch past the last row may still be in flight
    noc_async_write_barrier();
}