Tensix runtime reference¶
Status: implemented. Runs on one Blackhole card under the dispatcher
(01_mapcollect) and under the test feeder. This page
describes the headers in src/komorebi_native/include/komorebi/. The design behind
them is in the Tensix runtime.
A program has three persistent kernels on every core it uses: a reader and a writer on the core's two data-movement CPUs, and the compute kernel on the three TRISC threads. The easy layout runs the reader on RISCV_0 with NoC0 and the writer on RISCV_1 with NoC1, but the two are roles, not "the CPU that reads" and "the CPU that writes": either CPU can read, write and send to other cores. What differs is the NoC each uses, that a circular buffer has one CPU filling it and the other draining it, and that the writer reports the command. The runtime owns each kernel's loop: it takes the next command, and the program supplies the body that runs it, usually a switch on the handler.
#include "komorebi/device/km_worker.hpp"
void kernel_main() { // reader.cpp
km::Reader reader;
reader.run([&](const km_command_t& command) {
switch (km::handler(command)) {
case kHandlerMap:
if (!reader.reserve(kCbIn, tiles)) return; // gave up: the tile is stopping
... // read the tiles, then cb_push_back
break;
default:
break;
}
});
}
void kernel_main() { // writer.cpp: the body says whether to report
km::Writer writer;
writer.run([&](const km_command_t& command) {
if (km::handler(command) != kHandlerMap) return true;
if (!writer.wait_front(kCbOut, 1)) return false; // nothing to report
... // write, flush, cb_pop_front
return true;
});
}
void kernel_main() { // compute.cpp: the body runs on all three threads
km::Compute compute;
compute.run([&](const km_command_t& command) {
if (km::handler(command) != kHandlerMap) return;
bool ok = compute.wait_front(kCbIn, 1); // the same answer on all three threads
...
});
}
Headers¶
| Header | What it holds | tt-metal headers |
|---|---|---|
abi/km_abi.h |
the command, flags, reserved handlers, the control block offsets, events (plain C, shared with the X280 and the host) | none |
device/km_command.hpp |
command fields, sequence order, in_group, partition, report_to, the ring scan |
none |
device/km_core.hpp |
runtime buffers, state words, gave-up and where codes, start-up arguments, the clock, poll_stop, command_gave_up, small_write, km::Core |
yes |
device/km_worker.hpp |
km::Reader, km::Writer, km::Compute |
yes |
device/km_completion.hpp |
km::complete: a core's completion report |
yes |
device/km_trace.hpp |
km::Trace: per-command timestamps |
yes |
device/km_group.hpp |
groups of a group, reduce schedules, the arrival buffer (ports) | none |
device/km_port.hpp |
emit, arrive, take, reduce (ports) | yes |
device/km_broadcast.hpp |
broadcast (broadcasts) | yes |
device/km_movement.hpp |
both data movers on one stream: patterns, buffer pairs, streams, km::move, km::Movement (movement) |
yes |
The headers without tt-metal can be compiled on the host; the CPU tests do so.
The command¶
One 64-byte line of four 16-byte chunks (km_command_t). Word 0 of every chunk
carries the sequence number. The command has landed when all four agree
(KM_LANDED), and a core runs only a command that has landed.
| Word | Field | Read with |
|---|---|---|
sequence |
sequence number, from 1, increasing (compared across the wrap) | km::newer(sequence, other) |
hdr |
handler \| flags << 8 \| slot << 16 |
km::handler(command), km::flags(command), km::slot(command) |
group |
first \| count << 16: cores first to first + count - 1 in core order |
km::first(command), km::count(command), km::in_group(command, me) |
a0 to a9 |
the handler's ten argument words | command.a0 ... command.a9 |
land1 to land3 |
copies of sequence (landing) |
KM_LANDED(&command) |
| Handler | Meaning |
|---|---|
1 to 239 (KM_HANDLER_USER_MAX) |
the program's handlers |
0 (KM_HANDLER_NOP) |
no work; passed to the bodies like any other |
255 (KM_HANDLER_EXIT) |
every core leaves its kernels; completes as a group of all workers, whatever group it names |
| 250 to 254 | commands to the dispatcher; never reach a core |
| Flag | Meaning |
|---|---|
KM_FLAG_EXCLUSIVE (2) |
never in flight together with a command whose group overlaps (km::groups_overlap). Required for commands that use ports or broadcasts |
KM_FLAG_RESULT (4) |
the leader acknowledges with {ack, result}; see Writer::result |
KM_FLAG_QUEUE (8) |
dispatcher: the group allocator chooses first |
A command's header names its ring slot. A command found in another slot is not run and
is counted in kStSlotWrong. Other helpers in km_command.hpp:
km::partition(items, index, count)gives{start, len}, rankindex's contiguous part ofitemsitems. The firstitems % countcores take one item more.km::report_to(command, me, row)gives{parent, want}, the completion tree described below.
The three kernels¶
km::Core (reader and writer)¶
| Call | What it does |
|---|---|
args() |
the start-up arguments (km::Args, below) |
me() |
this core's index in the core order |
arg(index) |
the program's start-up argument index (runtime argument kArgs + index) |
control(offset), state() |
a word of the control block, the state words |
stop() |
the tile's stop flag is set (call invalidate_l1_cache() first) |
core_noc(index, address) |
the NoC address of address on core index of the core table |
disp_noc(address) |
the NoC address of address at the dispatcher |
poll(ready, limit, grace = 0) |
km::poll_stop on this tile's stop flag |
gave_up(word, code, where, slot, sequence) |
km::command_gave_up for this tile (kStReader or kStWriter) |
km::Reader¶
| Call | What it does |
|---|---|
run(body) |
starts up, then calls body(command) for each command of this core until EXIT or a wait gives up. The body returns nothing |
reserve(cb, pages) |
timed cb_reserve_back of a contiguous item. False: the reader gave up (recorded) |
wait_stop(command) |
waits only for the stop flag, up to the idle limit, and commands a give-up at stall. True on stop. For tests that make a core stall |
The reader's start-up runs once and in this order: 1. Clears the ring, the DONE counters, the state words, the stop flag, the broadcast areas and the ports. 2. Loads the core table. 3. Checks in: each core of the group writes to its leader. A leader waits for its cores, then writes READY to the dispatcher. 4. Sends the hello, if a mailbox is given.
Then for each command it waits for the two token pages and scans the ring. It copies
the command into KM_CONTROL_JOB and hands a token to compute and to the writer.
km::Writer¶
| Call | What it does |
|---|---|
run(body) |
per command: body(command) returns whether to report. The writer then waits for its writes (noc_async_write_barrier), waits until compute has left its body for this command, and runs km::complete. An EXIT command completes, then the writer leaves |
wait_front(cb, pages, times = 1) |
timed cb_wait_front of a contiguous item, timeout times wait limits. False: gave up (recorded) |
result(value) |
the command's result word: sent with the leader's ack when the command has KM_FLAG_RESULT, 0 if never called. On other cores of the group it is not sent |
A body that returns false reports nothing, so the command does not complete. A body returns false only after a give-up, when the tile is stopping anyway. A handler the writer has nothing to do for returns true; otherwise its group never completes.
km::Compute¶
| Call | What it does |
|---|---|
run(body) |
per command: all three threads copy the command and call body(command). After the body, unpack marks the command done in its DONE slot, which the writer waits for |
me(), arg(index) |
this core's index; the program's compute argument index (after kComputeArgs) |
wait_front(cb, pages, times = 1) |
timed cb_wait_front. Unpack decides and passes the answer to math and pack, so all three threads take the same path |
reserve_back(cb, pages) |
timed cb_reserve_back, decided by pack. False on pack only: gave up (recorded); true on unpack and math |
Groups and completion (km_completion.hpp)¶
km::complete(core, command, result) runs in the writer after the body. Within one grid
row of row cores (or with row 0), every core of the group reports to the first core. Across
rows, cores report to their row's leader, and row leaders report to the first
core. A core that receives reports waits (with a timeout) for want of them in
DONE[slot] and clears it. A core other than the first then increments its parent's
DONE[slot] with an atomic. The first core writes the ack word, and with
KM_FLAG_RESULT also the result, into the dispatcher's ack line of the slot. Nothing is
acknowledged once the tile's stop flag is set.
Start-up arguments¶
Reader and writer: runtime arguments 0 to kArgs - 1 (19 words, km::Args), then
the program's own.
| Word | Meaning |
|---|---|
control |
control block address, the same on every core (a multiple of 16) |
me |
this core's index in the core order |
cores |
DRAM address of the core table: core index to NoC x \| y << 16 |
dispatcher_coordinates, ack_base |
the dispatcher's NoC x, y and its ack words (slot j at ack_base + 16 j) |
mailbox |
the hello's address at the dispatcher; 0 sends none |
leader, rank, peers |
start-up group: leader's index, this core's check-in slot, cores the leader waits for |
ready_slot, ready_base |
the leader's READY word at the dispatcher |
tag |
per-process random tag, 16 bits; check-ins carry it |
workers |
cores 0 to workers - 1 run commands (the group of an EXIT) |
depth |
ring slots scanned, 1 to 16 (0: 16) |
row |
cores per grid row for completion; 0: one level always |
wait_limit |
timeout of a wait for another core, in 1024-cycle units |
idle_limit |
timeout of a wait for the next command, in 1024-cycle units; 0: none |
trace, trace_cap |
DRAM address of the trace (a page per core; 0: none), sequences traced |
Compute: kCArgControl, kCArgMe, kCArgWait, kCArgIdle (4 words), then the
program's. The host gives the limits in seconds: session.load(..., wait=2.0,
idle=120.0) becomes abi.limit_units(seconds, mhz).
Timed waits¶
Every wait of the runtime is a km::poll_stop(stop, ready, limit, grace). It polls
ready() and returns kGaveNone when it holds. It returns kGaveLimit after
limit units (0: no limit). It returns kGaveStop when the stop flag is set: at once
with grace 0, otherwise after grace more units, so a wait already about to end
can still end. A wait that gives up commands its first give-up and sets the tile's stop flag, so all three kernels leave. The writer reports a command only after the
reader's and compute's bodies for it have returned, so a give-up in any of a core's
three kernels fails the command it happens in.
| Kernel | Wait | Limit | Where |
|---|---|---|---|
| reader | a leader for its cores' check-ins (start-up) | wait | checkin |
| reader | free token pages for the next command | idle; wait after a stop | token |
| reader | the next landed command of this core | idle | next |
| reader | reserve |
wait | data |
| reader | wait_stop |
idle | stall |
| reader | after a give-up: token pages for the EXIT it hands on (not recorded) | wait | - |
| writer | the next token | idle; wait after a stop | writer |
| writer | wait_front |
wait x times |
data |
| writer | compute and the reader have left their bodies for this command | wait | cdone |
| writer | its cores' completion reports | wait | complete |
| compute | the next token (unpack) | idle; wait after a stop | ctoken |
| compute | wait_front (unpack) |
wait x times |
cinput |
| compute | reserve_back (pack) |
wait | coutput |
| reader | km::arrive: a port's count |
wait | port |
| writer | km::emit: write and atomic flush |
wait | emit |
| writer, compute | a wait behind a port's wait (reduce_result, take) |
2 x wait (km::kBehindPort) |
data, cinput |
| reader or writer | broadcast calls | wait | broadcast* (broadcasts) |
The NoC barriers have no timeout: noc_async_read_barrier,
noc_async_write_barrier, noc_async_writes_flushed and noc_async_atomic_barrier.
They end once the transactions to valid addresses complete. The buffer wrappers'
checks are what keep those addresses valid. A program's own waits go through
poll and gave_up in the same way.
Gave-up and where codes¶
A gave-up word is code | where << 8 | slot << 16, and its sequence number is in the
next word. For port and emit the top byte holds the port. Only the first give-up
of each kernel is kept, because it names the cause. The host decodes the words with
komorebi.state.gave_up; the names below are the ones it prints.
| Code | Name | Meaning |
|---|---|---|
| 0 | - | did not give up |
1 (kGaveStop) |
stop |
the stop flag was set |
2 (kGaveLimit) |
limit |
the wait timed out |
3 (kGaveBadItem) |
bad_item |
an item of several pages would pass the buffer's end, or a buffer is too small for its schedule; nothing was accessed |
4 (kGaveBadBroadcast) |
bad_broadcast |
a broadcast's arguments do not fit the command |
| Where | Name | Kernel: waiting for |
|---|---|---|
| 1 | checkin |
reader (leader): its cores' start-up check-ins |
| 2 | token |
reader: free token pages |
| 3 | next |
reader: a command |
| 4 | stall |
a handler waiting for the stop flag (tests) |
| 5 | complete |
writer: its cores' completion reports |
| 6 | writer |
writer: a token |
| 7 | data |
reader or writer: a buffer wait |
| 8 | ctoken |
compute (unpack): a token |
| 9 | cinput |
compute (unpack): input pages |
| 10 | coutput |
compute (pack): output space |
| 11 | cdone |
writer: compute's and the reader's bodies to return |
| 12 | broadcastbegin |
broadcast: arguments or buffer at a command |
| 13 | broadcastdone |
broadcast sender: DONE markers, its NoC flushes |
| 14 | broadcastvalid |
broadcast receiver: VALID |
| 15 | broadcastfree |
broadcast: its compute's pops, its last markers |
| 16 | - | free |
| 17 | port |
reader: a port's count (km::arrive) |
| 18 | emit |
writer: a put or a bump to flush (km::emit) |
How each kernel left is one byte of kStEnds (reader, writer, compute):
1. kEndExit: left on an EXIT command.
2. kEndGaveUp: left after its own wait gave up.
3. kEndAbort: left on the EXIT the reader hands on after a give-up.
State words¶
48 words from KM_CONTROL_STATE, read back after the stop (komorebi.state). Counters
saturate.
| Word | Meaning |
|---|---|
0 to 15 (kStLast) |
last sequence consumed, per ring slot |
16 to 31 (kStReported) |
last sequence this core reported, per slot |
| 32, 33 / 34, 35 / 36, 37 | gave-up word and its sequence: reader / writer / compute |
38 kStRuns, 41 kStCompleted, 42 kStSeen |
commands the reader took, the writer finished, compute took |
39 kStPasses, 40 kStNextCycles |
ring passes and cycles / 16 spent waiting for commands |
43 kStWrong |
bytes unpack, math, pack: job copies that did not match the token |
44 kStEnds |
how each kernel left |
45 kStMagic |
0x4B000000 \| tag << 8 \| me: the reader started with this tag |
46 kStSlotWrong |
commands not run: found in a slot their header does not name |
47 kStTraceDropped |
commands past the trace's capacity |
Control block¶
Every core has one control block in L1, at the same address everywhere (args().control,
a multiple of 16). Offsets are in bytes, and every offset has a static assert in
km_abi.h.
| Offset | Name | Contents |
|---|---|---|
| 0x000 | KM_CONTROL_RING |
16 ring slots of one command each (64 bytes) |
| 0x400 | KM_CONTROL_RESERVED |
reserved, 16 bytes |
| 0x410 | KM_CONTROL_STOP |
stop flag: nonzero makes every wait give up |
| 0x440 | KM_CONTROL_DONE |
16 slots of 16 bytes: word 0 counts completion reports, word 1 (kDoneCompute) is the last sequence whose compute body returned, word 2 (kDoneReader) the last whose reader body returned |
| 0x540 | KM_CONTROL_BOOT |
start-up check-ins at a leader, one 16-byte slot per rank |
| 0x640 | KM_CONTROL_STATE |
48 state words |
| 0x700 | KM_CONTROL_JOB |
the command being run, copied by the reader for compute and writer |
| 0x740 | KM_CONTROL_OUT |
4 source slots of 16 bytes for small NoC writes: check-in, READY, hello, ack |
| 0x780 | KM_CONTROL_CORES |
core table: 128 words, core index to NoC x \| y << 16 |
| 0x980 | KM_CONTROL_PORT |
8 ports of 16 bytes: count, wanted count (ports) |
| 0xA00 | KM_CONTROL_BROADCAST |
2 broadcast areas of 0x140 bytes: VALID, out slot, give-up, statistics, DONE-BY words (broadcasts) |
| 0xC80 | KM_CONTROL_MESSAGES |
routed messages: the tail as last read, head, sent, consumed, quota, idle words, status-line slot, first give-up, reservation answer (dynamic) |
| 0xD40 | KM_CONTROL_TAILS |
routed messages on a counter tile: its READY line, two read slots, and one 16-byte tail line per core (counter tiles) |
| 0x1580 | KM_CONTROL_X280 |
X280 port: the X280 core's tell and answer, the core's out slot, its X280 core's tile and mailbox (X280 ports) |
| 0x15C0 | KM_CONTROL_BYTES |
size of the block |
Runtime buffers: circular buffers 29 and 30 carry the tokens (kCbTokenCompute,
kCbTokenWriter, 64 bytes each). Buffer 31 (kCbTrace) holds the trace when tracing
is on.
Trace¶
With a trace address, each core keeps 4 wall-clock words per sequence number, for
sequences 1 to trace_cap:
- kTraceSeen: the reader took the command.
- kTraceDone: the writer's outputs were written.
- kTraceSent: completion was sent.
- kTraceExtra: the reader's ring passes for this command.
The writer writes the page to DRAM when it leaves. Commands past the capacity are
counted in kStTraceDropped.
What a program may and may not do¶
A program may:
- use handlers 1 to 239, the ten argument words, and its own start-up arguments
(arg(index));
- use any circular buffer except 29 to 31;
- wait on its own conditions with poll(ready, args().wait_limit) and command a
give-up with gave_up(...), under a where code of the runtime (kWhereData,
kWhereStall) or of a call it uses;
- write to DRAM and to other cores' L1 through the NoC from the reader and the writer;
- send a result word with writer.result(value) on a KM_FLAG_RESULT command.
A program must:
- take and release pages of its buffers only through the wrappers (Reader::reserve,
Writer::wait_front, Compute::wait_front, Compute::reserve_back) before the
matching cb_push_back or cb_pop_front. A buffer is a multiple of its item, and
every push and pop on it is a multiple of the item;
- give every wait of its own a timeout and check the stop flag, through poll;
- return from a body after a give-up (the wrappers return false) without touching the
buffer it was waiting for;
- in compute, take the same path on all three threads: branch only on the command and
on what wait_front returned;
- send a small write between two cores' L1 only with km::small_write, slot to slot.
The two addresses must share their low four bits, or the write is silently wrong;
- give commands that use ports or broadcasts KM_FLAG_EXCLUSIVE;
- fence (invalidate_l1_cache()) between the stores that build data in L1 and the
cb_push_back or NoC send that hands it on: the core's stores pass through a small
write-through cache, and a send that starts before they land carries old bytes.
A program must not: - write the control block except through the runtime's calls; - loop without a limit, or wait on a NoC barrier for an address it has not checked; - write to the NoC from the compute kernel; - return false from the writer's body except after a give-up or in a test that means the command to time out: the group then never completes.