Skip to content

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 at KM_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 pages pages, per to a slot (km::Arrival {cb, pages, per}, per = 1 by default): slot slot is pages slot * per to slot * per + per - 1, and a part sent with emit is at most per pages. 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, all pages pages 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.