The Tensix runtime¶
Status: implemented and run on one Blackhole card: the persistent kernel, commands,
groups and completion, static breakpoints (ports and reduce, broadcast) and the
trace, and dynamic breakpoints (one dynamic run at a time), through a Session and
under the test feeder.
Each program has one persistent kernel on every Tensix core it uses. The worker starts once and then serves work: it takes the next command, switches on the handler the command names, runs that handler and reports completion. Dispatch is what sends the commands, from the host, from its own engines or from an X280 thread (threads reference). The model is what the handlers and breakpoints mean.
| Part | As built | Reference |
|---|---|---|
| Command | One 64-byte line: sequence number, handler, flags and ring slot, group, ten argument words. The dispatcher forwards it whole. | runtime |
| Landing | Word 0 of each of the four 16-byte chunks carries the sequence number; a core runs a command only when all four agree. | runtime |
| Worker | One persistent kernel set per program. The runtime owns each kernel's loop; the program gives the body. The reader copies each command into the tile's control block and hands a token to the compute and writer kernels, which read the command from that copy. Every wait has a timeout and observes a stop flag. | runtime |
| Groups | A contiguous range of cores in core order, named in the command. Completion is per group: cores report to their row's leader, row leaders to the first core, which acknowledges with one write. Several groups run at once. | runtime |
| Static breakpoints | A value to one core: emit writes into a fixed slot, waits for the write, increments the receiver's port; arrive is a timed counted wait. reduce by owner and by tree. A block to a row or a column: broadcast with a window. |
ports, broadcasts |
| Dynamic breakpoints | A queue per core in DRAM, positions reserved by an atomic add on the receiver; packets of up to 32 messages; bins per destination; a limited population of live messages (a send quota); sent and consumed counters in a status line when a core goes idle. Generic reader and writer kernels own these, so a program's handler code lives in the compute kernel. Through a Session and under the test feeder; one dynamic run at a time. |
dynamic |
| Trace | Per-command wall-clock timestamps per core, read back after the run. | runtime |
Commands and handlers¶
A command is the unit that travels from the dispatcher to the cores. Its first words say which handler to run, for which group and in which ring slot; the rest are that handler's arguments. Handlers 1 to 239 are the program's; EXIT ends the worker.
Groups and completion¶
A group is a contiguous range of cores in a fixed core order, and is named in the command that starts it. Cores report completion to the group's leader; the leader pushes one word to the dispatcher. A core reports only after its writer's outputs have landed and its compute body has returned, so a command with no output is not complete while compute still runs it. Because completion is per group, several groups can be busy at once, and a group's size is not tied to a column of the grid. Where a group is addressed by multicast, each rectangle must be a physically contiguous, Tensix-only span in NoC0 coordinates, so a group is cut at panel boundaries. The layout module of the Python package computes these rectangles.
A command with KM_FLAG_RESULT carries one 32-bit result word back with its
acknowledgement. The leader's writer gives it with writer.result(value) before the command
completes, and the leader acknowledges with {ack, result} as one 8-byte write into
the slot's acknowledgement line, so the result is there whenever the acknowledgement
is. A core that is not the leader may call it too; its value is not sent. A flagged
command whose leader never gave a result is acknowledged with 0, and a command without
the flag is acknowledged with the ack word alone.
Breakpoints¶
A static breakpoint is known before the run: who sends, who receives and how many
arrive, all from the command and the core's rank. A value to one core is a write into
a slot at a fixed address on the receiver, a wait until that write has landed, and an
increment of a port the receiver waits on; there is no header and no queue
(ports). A block to many cores is a broadcast: one sender
multicasts it along a row or down a column of the group, with a window of blocks in
flight, and each receiver writes a progress marker to the sender when its compute is done with a block
(broadcasts). Commands that use either carry KM_FLAG_EXCLUSIVE, so two
commands whose groups overlap are never in flight together.
A dynamic breakpoint depends on data. Each receiver has a queue in DRAM large enough for every live message. A sender collects messages per destination, reserves positions in the receiver's queue with one atomic add and writes the messages there; it never waits for the receiver, so a full inbox cannot deadlock the grid. The receiver reads its queue in order, as packets of up to 32 messages. The number of live messages is limited by a send quota. When a core has been idle for a while it writes its sent and consumed counters in a status line at the submitter, and the run is over when every line says idle with the same counters on two successive sweeps and the sums match. Dynamic handlers is the page for a program's author.
Debugging¶
The watcher cannot run while the X280 runtime is open, so the runtime is built to be debugged without it: every wait has a timeout, each core has a "gave up" word, "where" codes show where a core stopped, and the trace timestamps phases per core.
A program takes and releases pages of its circular buffers only through the runtime's
wrappers (Reader::reserve, Writer::wait_front, Compute::wait_front,
Compute::reserve_back), never through the raw cb_* calls. A wrapper's wait is
timed and sees the stop flag, and it checks that the item it hands out stays inside
the buffer. An item of several pages is one contiguous region: tt-metal's compute side
wraps its pointer when it reaches or passes the buffer's end, the data-movement side
only when it lands exactly on it, so a straddling item would send the data-movement
pointer out of the buffer for good. The buffer must therefore be a multiple of the
item, and every push and pop on it a multiple of the item; a wrapper that sees an item
pass the end gives up with the code bad_item before any access.
The one kind of wait without a timeout is the wait on the NoC itself
(noc_async_read_barrier, noc_async_write_barrier, noc_async_writes_flushed). It
ends once the transactions to valid addresses complete; bounding it would let a kernel
leave with a transaction still in flight. The wrappers' checks are what keep those
addresses valid.
A NoC write between two cores' L1 is only correct when its source and destination
addresses share their low four bits. Measured on a Blackhole p150a
(experiment): when they differ, a 4-byte
write lands at the destination address but carries the bytes found at the destination's
offset within the source's 16-byte line, and a non-posted write is acknowledged as
usual. The mismatch is silent and wrong, never an error. The runtime's small writes
therefore go through one helper, km::small_write, that takes slot numbers, not
addresses, on both ends: from a 16-byte slot of the sender's control block to a 16-byte
slot above a base on the receiver. The control block and every base the host hands to
the kernels must be multiples of 16.