Ports, emit and reduce¶
Status: implemented in the Tensix runtime and run on one Blackhole card through a
Session and Run, under the dispatcher, and under the test feeder
(02_merge_tree: python merge.py --jobs 20 --count 8
--form both).
A static breakpoint that sends a value to one receiver. Who sends to whom, into which
slot, and how many values a wait expects all follow from the command (first, count,
the arguments) and the core's rank. Nothing is queued and nothing is addressed by
content.
#include "komorebi/device/km_port.hpp"
constexpr km::Arrival arrival = {kCbArrive, kArrivePages}; // the program's arrival buffer
// reader, writer and compute, per command
const km::Group group = km::group_of(command, me);
// reader: after its own inputs, every arrival this core waits for
if (!km::reduce_arrive(reader, command, arrival, group, form)) return;
// compute: own part in DST 0, then the parts in rank order
ok = ok && km::reduce(compute, arrival, group, form, [](uint32_t slot) {
copy_init(kCbArrive);
copy_tile(kCbArrive, slot, 1);
add_binary_tile_init();
add_binary_tile(0, 1, 0);
});
// writer
if (group.rank != 0) {
return km::reduce_emit(writer, command, arrival, group, form, kCbOut, 1);
}
if (!km::reduce_result(writer, kCbOut, 1)) return false; // then write the result
On the wire¶
| Step | Kernel | Call | What it does |
|---|---|---|---|
| put | writer (NoC1) | km::emit |
the pages at the front of a buffer go into a slot of the receiver's arrival buffer; timed wait for the write to be acknowledged |
| bump | writer | km::emit |
an atomic increment of the receiver's port; timed wait for its response (a write flush does not cover an atomic) |
| wait | reader | km::arrive(..., port, wanted) |
timed wait until the port has counted wanted; clears it; pushes the whole arrival buffer to compute |
| take | compute | km::take, km::untake |
waits for the arrival buffer, reads slots by page index, pops it whole |
- Ports. Every core has
KM_PORTS(8) port slots of 16 bytes in its control block atKM_CONTROL_PORT: the count senders increment, and the count a wait wants. Ports are cleared at start-up and by the wait that consumed them. A sender counts the atomic's acknowledgement and never reads its returned value, so a lost answer (atomic_answers) cannot affect a port. - The arrival buffer is one circular buffer of
pagespages,perto a slot (km::Arrival {cb, pages, per},per= 1 by default): slotslotis pagesslot * pertoslot * per + per - 1, and a part sent withemitis at mostperpages. Every core runs the same program, so a slot has the same L1 address everywhere (taken from the buffer's configuration). It is always passed to compute whole, allpagespages pushed and popped, so its addresses never drift. KM_FLAG_EXCLUSIVE. A command that uses ports must carry it: two commands whose groups overlap are then never in flight together, so nothing of one command lands in the next one's slots or ports.
Groups¶
km::Group {base, stride, size, rank}: rank rank is core base + rank * stride.
| Call | Group |
|---|---|
km::group_of(command, me) |
the whole group, in core order |
km::digit_group(command, me, stride, radix) |
the cores whose group index differs from this core's only in the digit of weight stride and base radix (for example the parts of one output block along the matrix's inner dimension) |
Reduce¶
| Form | Sender | Receiver | Port, slot | Slots |
|---|---|---|---|---|
km::kReduceOwner |
every core of the group but rank 0 | rank 0 waits for size - 1 on port 0, combines slots 0 to size - 2 in rank order | port 0, slot rank - 1 | at least size - 1 |
km::kReduceTree |
in each round, a rank that is an odd multiple of 2^round sends to rank - 2^round and is done | a multiple of 2^(round+1) waits for 1 on port round if rank + 2^round < size |
port round, slot round |
one per round, ceil(log2 size), at most 7 |
The combine order is fixed by rank in both forms, so a result does not depend on
arrival order; on exact-valued inputs both forms give the same bits.
km::reduce_send(g, form), km::reduce_wants(g, form, port) and
km::reduce_slots(g, form) give the schedule itself (komorebi/device/km_group.hpp,
free of tt-metal headers). A buffer with too few slots gives up with bad_item.
When a core does not send¶
Every wait times out at the start-up wait limit and sees the stop flag. The port's
wait gives up first: waits that lie behind it (the writer's wait for a part that needs
arrivals, take, reduce_result) time out at km::kBehindPort (2) wait limits. A
give-up commands where = port (or emit on the sender) with the port number in the
gave-up word's top byte, and leaves the wanted count in the port, so the host reads the
missing count (commands.waiting_ports in the test feeder). The tile stops, the
dispatcher's or feeder's deadline stops the run, every kernel leaves, and the next
launch runs.
X280 ports¶
A port between a Tensix core and the X280 core that serves it, without the dispatcher
(komorebi/device/km_x280.hpp; the X280 core's side and the declaration are in
X280 threads). A message is a sequence number and three
words, one 16-byte NoC write each way:
#include "komorebi/device/km_x280.hpp"
km::X280Port port(reader); // owned by the reader; X280Port(writer, km::kStWriter): the writer
km::X280Msg message;
if (port.call(ask, 0, 0, message)) { // ask, then wait for its answer
use(message.w[0]);
}
port.tell(hash, total, largest); // to the X280 core; it sees the newest
if (port.told(message)) { ... } // the X280 core's newest tell, if newer; never waits
| Call | Waits | Returns |
|---|---|---|
served() |
no | whether the program gave this core an X280 core |
told(message) |
no | true and the X280 core's newest tell, if newer than the last one looked at |
wait_told(message) |
timed | the same, waiting for one; false: gave up (recorded) |
tell(first_word, second_word, third_word) |
for the last store to leave | false: gave up (recorded) |
ask(first_word, second_word, third_word) |
as tell |
the ask's sequence number, 0: gave up |
answered(sequence, message) |
no | true and the answer to ask sequence, if it landed |
wait_answer(sequence, message) |
timed | the same, waiting; false: gave up (recorded) |
call(first_word, second_word, third_word, message) |
timed | ask, then wait_answer |
next(first) |
timed | the next range of a pull (kmx_pull_run): its count and first, 0 when none is left or a wait gave up (pull_gave_up()); asks for the range after it before returning |
Latest wins. A tell overwrites the line it lands in: a side that looks less often
than the other tells sees only the newest, and tells in between are lost. Ask and
answer are paired by the sequence number, one ask outstanding at a time; between the
first next() of a pull and the one that returns 0, that ask is the pull's.
On the wire. The core's KM_CONTROL_X280 area (64 bytes): the X280 core's tell at
KM_X280_TELL (+ 0x00) and answer at KM_X280_ANSWER (+ 0x10), stored by the X280 core
through its window; the core's out slot KM_X280_OUT (+ 0x20), the source of its
writes; KM_X280_TO (+ 0x30): the X280 core's tile (x | y << 16) and the mailbox
address in its local memory, written by the host at the load (0: no X280 core). The core
writes its ask to the mailbox at KM_BOX_ASK (+ 0x00) and its tell at KM_BOX_TELL
(+ 0x10) on its kernel's NoC. Each line has one writer; no atomics. A pull's ask is
{KM_PULL_ASK, 0, 0}, answered {first, count, pulls so far} (count 0: none left).
When a side does not answer. Every wait times out at the wait limit and sees the
stop flag. A give-up commands where = kWhereX280Answer (37), kWhereX280Tell (38)
or kWhereX280Send (36) for a store that did not leave, kWhereX280None (39) when the
program gave the core no X280 core. The command then ends as any command whose core gave up.