Skip to content

X280 threads reference

Status: implemented on one Blackhole p150a, work clusters 1, 2 and 3, each with its command ring to the dispatcher (its sources 2, 3 and 4). The library's ring calls are also checked on the CPU against a fake kernel.

The guide is X280 threads: what a thread is for, how regions change hands, the rules. This page lists the interface. The sources are src/komorebi_native/x280/kmx/ (kmx.h, kmx_layout.h, kmx_ring.h, kmx.c) and src/komorebi/thread.py.

C: #include "kmx.h"

Plain C, no libc. The library's start-up is the image's main; the thread supplies kmx_main.

Call Core Does Returns
int kmx_main(int core) 0 the thread's entry, written by the program its code, unless kmx_result gave one
const kmx_args* kmx_args_get(void) any the argument block: nwords, words[], nregions, region[], deadline, flags never 0
void* kmx_region(int id, uint64_t* bytes) any region id as memory, *bytes its size 0 for an unknown id
void kmx_invalidate(int id) any drops the cluster's cached lines of the region: before reading what the grid or the host wrote -
void kmx_flush(int id) any writes the region's cached lines back to DRAM: after writing what the grid or the host will read -
void kmx_invalidate_at(int id, uint64_t offset, uint64_t bytes), kmx_flush_at any the same for the lines holding bytes of the region from offset -
int kmx_spawn(int core, void (*function)(int core, void* arg), void* arg) 0 starts function(core, arg) on core 1, 2 or 3 and returns KMX_OK; KMX_FULL while that core runs its last spawn; KMX_BAD for another core
int kmx_join(int core) 0 waits until core returned from its last spawn (at once if none) KMX_OK; KMX_LATE / KMX_STOPPED (library error late / stopped); KMX_BAD
int kmx_parallel(void (*function)(int core, void* arg), void* arg) 0, not nested spawns function on cores 1 to 3, runs function(0, arg), joins them KMX_OK; KMX_FULL if a spawned core is busy (nothing started); KMX_LATE or KMX_STOPPED; KMX_STOPPED with error parallel from another core or inside a kmx_parallel
uint32_t kmx_clusters(void) any the clusters running this image in the program, bit c for cluster c a mask
int kmx_lead(void) any the lowest of them: the one that calls kmx_parallel_all 1, 2 or 3
int kmx_parallel_all(void (*function)(int cluster, int core, uint64_t arg), uint64_t arg) the lead's 0, not nested runs function(cluster, core, arg) on every core of kmx_clusters(); the first call waits until every other cluster serves KMX_OK; KMX_LATE / KMX_STOPPED; error parallel elsewhere
int kmx_serve(void) 0 runs the lead's kmx_parallel_all calls until the lead's kmx_main returns KMX_OK; KMX_NONE on the lead; KMX_LATE / KMX_STOPPED
int kmx_push(int cluster, int id, uint64_t offset, const void* source, uint64_t bytes) any stores bytes from source at offset of region id of cluster's thread; returns once the last line's store is acknowledged; on core 0 outside kmx_parallel, over the four cores KMX_OK; KMX_BAD (a cluster not in kmx_clusters(), an unknown region, out of range, not 64-byte multiples, a window, or before the lead's first call on another cluster); the lead's first push to another cluster waits as kmx_parallel_all does
int kmx_port_cores(void) any entries of the port tables: the grid's cores (0: the program declared no ports) a count
int kmx_port_owner(int core) any the X280 core of this cluster that serves core 0 to 3, or -1 (another cluster's, or none)
int kmx_port_tell(int core, uint32_t first, uint32_t second, uint32_t third) the one serving core stores a tell into the core's KM_CONTROL_X280 line; the core sees the newest KMX_OK; KMX_BAD for a core this X280 core does not serve
int kmx_port_asked(int core, kmx_message* message) the same core's newest ask, if not answered yet; never waits KMX_OK and message; KMX_NONE; KMX_BAD
int kmx_port_answer(int core, uint32_t sequence, uint32_t first, uint32_t second, uint32_t third) the same answers ask sequence; never waits KMX_OK; KMX_BAD (also for sequence 0)
int kmx_port_told(int core, kmx_message* message) the same core's newest tell, if newer than the last one looked at (latest wins) KMX_OK and message; KMX_NONE; KMX_BAD
int kmx_port_run(const km_command_t* command, void (*function)(int cluster, int x280_core, uint64_t arg), uint64_t arg, kmx_event* event) the lead's 0, not nested submits command, runs function(cluster, x280_core, arg) on every X280 core as kmx_parallel_all does, ends the serve at the command's event KMX_OK and event once every function returned; KMX_FULL (nothing submitted, or 32 events kept); KMX_LATE / KMX_STOPPED; KMX_BAD without ports; error parallel elsewhere
int kmx_port_open(void) any, inside kmx_port_run's function whether the command still runs; the lead's X280 core 0 looks for its event every fourth call 1, or 0 once the event came, a stop or the deadline (and outside a run)
int kmx_pull_run(const km_command_t* command, const kmx_pull* settings, kmx_event* event) the lead's 0, not nested submits command and serves its pulls on every X280 core until its event: units 0 to settings->units - 1 in ranges of settings->per, by settings->mode (KMX_PULL_COUNT, _LOG, _REPLAY; log region settings->log) or settings->pick as kmx_port_run; KMX_BAD also for a bad mode, per 0, a log region under KMX_PULL_LOG_BYTES(1), or a log that ran out of room (the command ran)
int kmx_submit(const km_command_t* command, uint32_t* handle) 0 stores command as the next command of the ring KMX_OK and the handle (1, 2, ...); KMX_FULL when no slot is free; KMX_STOPPED without a ring
int kmx_wait(uint32_t handle, uint64_t deadline, kmx_event* event) 0 waits for the handle's event until kmx_time() passes deadline; other handles' events are kept for kmx_poll, wake events are passed over KMX_OK; KMX_LATE / KMX_STOPPED (library error late / stopped); KMX_FULL when 32 events are kept
int kmx_poll(kmx_event* event) 0 one event, a kept one first; never waits KMX_OK or KMX_NONE
int kmx_cluster(void) any the work cluster the thread runs on 1, 2 or 3
uint64_t kmx_time(void) any the 50 MHz timer (KMX_TICKS_PER_SECOND) ticks
int kmx_stop_requested(void) any true once the host asked for a stop or the thread's deadline passed 0 or 1
void kmx_progress(uint32_t where) any writes where and its time into the result block as a progress marker -
void kmx_result(uint32_t code, const void* words, uint32_t word_count) 0 the thread's code and up to 48 words error result if word_count > 48 (the first 48 kept)

Ring calls, kmx_spawn, kmx_join, kmx_parallel, kmx_parallel_all, kmx_port_run and kmx_serve from X280 cores 1 to 3 command the library error parallel and return KMX_STOPPED. kmx_message: sequence, w[3]. A command for kmx_submit is a km_command_t (komorebi/abi/km_abi.h): hdr = handler | flags << 8, group = first | count << 16, arguments a0 to a9; the library writes sequence and the landing words. Flags: KM_FLAG_EXCLUSIVE (submitted only when no group in flight overlaps it), KM_FLAG_RESULT (the group leader's writer.result(value) is returned in the event's ticks), KM_FLAG_QUEUE (the kernel chooses first). A command that cannot be submitted now is rejected at once (KM_EVENT_REJECTED, auxiliary 11) and the kernel then sends one KMX_EVENT_WAKE (handle 0) when a slot frees or the queue submits a job: the command may be submitted again after it. The kernel's other rejections are its README's ("Rejected commands"), and STOP, EXIT, RUN and CANCEL from a thread are rejected with auxiliary 1.

kmx_event: handle, kind (KM_EVENT_DONE, KM_EVENT_TIMEOUT, KM_EVENT_REJECTED, KMX_EVENT_WAKE from kmx_poll only), auxiliary (the first core of a done command or the slot of a timeout in its low 8 bits, the low 16 bits of the command's sequence number above them; the reason of a rejection), ticks (submit to completion in timer ticks, or the result word of a KM_FLAG_RESULT command).

Return codes (kmx_layout.h): KMX_OK 0, KMX_FULL 1, KMX_STOPPED 2, KMX_LATE 3, KMX_NONE 4, KMX_BAD 5.

The image

Rule Value
size at most 126,976 bytes, globals included
globals a non-zero initialiser and __attribute__((section(".data"))): the image has no .bss
stack 32 KiB per core
libraries none: a struct assignment or a large zero initialiser may become a memset call that does not link
start-up floating point and vector on; the L2 prefetchers set to the documented values unless prefetch="keep"; argument block, regions and bank checked; then kmx_main on core 0, spawns served on cores 1 to 3; after it, every region given back
build Thread.sources with Thread.includes and Thread.defines, compiled with the dispatcher's toolchain into the build cache

Memory

The thread's block in the cluster's local memory (LIM), bytes from its base:

Offset Bytes What Written by
0x000 0x240 kmx_args: run value, magic kmx1, version 4 (KMX_VERSION), deadline (ticks), region and word counts, check line address and value, flags (KMX_FLAG_KEEP_PREFETCH 1), the cluster (cluster, at 0x38); region table at 0x040 (16 x {offset, bytes}); words at 0x140 (64 x uint32) host, before the start
0x240 64 stop line: word 0 = 0x53544F50 asks for a stop host
0x280 256 result block thread
0x380 4 x 64 spawn lines, one per core, core core's at + 64 * core: 64-bit words gen, fn, arg (core 0), done (that core); core 0's word 0 ends the service cores
0x480 64 kmx_ring_descriptor, zero for no ring host, before the start
0x4C0 64 kmx_peers: a value for this load (nonce), the lead, the mask of clusters running the image, the offset all of the kmx_parallel_all lines, then each cluster's tile and its bank's DRAM tile (x \| y << 16, indexed by cluster); zero: no peers host, before the start
0x500 64 kmx_ports: at (LIM offset of the ports area, 0: no ports), control (the cores' control block address), cores (table entries), flags (KMX_PORTS_NOC1 1), state (kmx_port_run's: n << 1 while run n serves, \| 1 once it ended), next, lost, pull, pick, arg (kmx_pull_run's, written by the lead) host, before the start; state the lead
all 4 x 64 after the block the host writes (and the ring's events): the call line the lead writes into each other cluster (32-bit words nonce, gen, fn, arg, exit), and in the lead's, cluster cluster's done line at + 64 * cluster (nonce, the last gen done; 0: it serves) the lead; the others

The ports area at at (after the kmx_parallel_all lines, written by the host before any thread starts, KMX_PORTS_BYTES = 8,832): each core's tile at + 4 i (x | y << 16), its serving X280 core at 0x200 + i (cluster << 2 | x280_core, 0: none), its mailbox line at 0x280 + 64 i (ask at + 0x00, tell at + 0x10, written by the core).

Result block: state (1 boot, 2 running, 3 done, 4 failed), error (the first library error: 0 none, 1 args, 2 bank, 3 late, 4 stopped, 5 parallel, 6 result), code, n, where, t_where (ticks from the start to the last progress marker), ticks (start to the end of kmx_main), then 48 result words at byte 64.

Windows on a work cluster: the ring's 16 (KMX_RING_WINDOW), cluster c's LIM through 16 + c (KMX_PEER_WINDOW + c - 1) while the thread runs, core h's push through 20 + h (KMX_PUSH_WINDOW + h) during a push, core i's control block through 24 + i (KMX_PORT_WINDOW + i) for the run when this cluster serves it.

Regions are whole 4 KiB pages of the work cluster's DRAM bank (on the p150a bank 6 for cluster 1, bank 7 for clusters 2 and 3); page 0 of a bank's pages is the check line, then each thread's own regions, then the shared ones. The thread sees region id at the cached alias of the bank plus its offset; a Tensix kernel reaches it with get_noc_addr_from_bank_id<true>(bank, address). A thread reads only its own bank: data for a thread on the other bank is pushed there by its producer.

kmx_ring_descriptor (one 64-byte line): cmd (command_depth 64-byte slots, 64-byte aligned), consumed (the kernel's count of commands taken), events (event_depth 16-byte events), read (a 64-byte line, word 0 the events the thread took), command_depth, event_depth (powers of two, at most 1,024), flags (KMX_RING_CACHED 1: each flush or invalidate is a sweep; KMX_RING_REMOTE 2: cmd and read are NoC addresses in the tile reserved[0] names, x | y << 16, inside one 2 MiB window that the start-up opens in slot KMX_RING_WINDOW and closes after kmx_main), reserved[0]. Other addresses are as the thread's cluster sees them. A line that names a ring but is out of range ends the start-up with error args. The session's ring: 16 command lines and the read line in the dispatcher's LIM after its layout (KMD_LIM_BYTES, cluster c's ring c - 1 rings further), 64 events after the kmx block in the thread's LIM (KMD_WORK_* from KMX_LIM_BYTES). The protocol is the host ring's (the dispatcher's README, "Protocol with the host").

Python

Program.thread(cluster=1, clusters=None, **kwargs) declares the thread (one per cluster) and returns a komorebi.thread.Thread; with clusters, a list of one per cluster, the same fields on each:

Field Meaning
sources C files, linked with the kmx library
regions {name: size in bytes or an array of initial contents}; ids in dict order
shared names of the program's shared regions the thread uses; ids after its own; with regions, 1 to 16 in all
words up to 64 unsigned 32-bit words
cluster the work cluster, 1 to 3 (bank 6 for 1, bank 7 for 2 and 3)
host_fallback function(regions, words, run) -> (code, words): the thread on the host, on uint32 copies of the regions, submitting commands through run (the session's Run) where the thread uses its ring
deadline seconds for the whole thread, at most 60 (default 10)
includes, defines extra include directories and -D definitions
prefetch "documented" (default) or "keep"

Program.region(name, value) declares a shared region (a size in bytes or an array of initial contents). Every thread naming it must be on one bank, and every shared region must be named by a thread; session.load rejects the program otherwise.

Program.ports(serve=None, noc=0) declares X280 ports (X280 ports): serve {core: (cluster, x280_core)} (x280_core 0 to 3), or None for every core of the grid dealt over the threads' X280 cores in grid order, cluster by cluster (komorebi.thread.port_plan); noc the NoC of the X280 cores' stores into the cores. Once per program (ValueError again, for noc other than 0 or 1, or an empty serve). session.load rejects ports with thread="host" or without a thread, a core outside the grid, a cluster without a thread and an X280 core outside 0 to 3. At the load it writes each served core's KM_X280_TO line (its X280 core's tile and mailbox address) into the control blocks, and every cluster's ports area before any thread starts.

session.load(program, thread="card" | "host") writes the regions before the grid starts, then starts every image (or readies the fallbacks) once the dispatcher is ready. run.threads holds a ThreadRun per cluster; run.thread is the lowest cluster's:

Member Meaning
run.thread_result(timeout) waits for the thread to return (asks it to stop at the timeout), stops it, returns a ThreadResult; with thread="host" runs the fallback; raises ThreadFailed when the thread is stuck
run.thread_results(timeout) the same for every thread, {cluster: ThreadResult}; on the first that does not return in time or ends with a library error, stops the others and raises ThreadFailed (cluster, results; its message names a stuck thread)
run.thread.read(name) the region's bytes (uint8), only after run.stop()
run.thread.regions.address(name) (bank, bank address) for a Tensix kernel
run.thread.ring the RingPlacement the host wrote into the ring line (None with thread="host")
run.stop() stops every thread still running, then the run

ThreadResult: ran (false when it was stopped at the timeout), state, error (names, as in the result block), code, words, where, where_microseconds, microseconds, stuck (it did not return within 1 s of the stop flag either: a core outside the library or halted by a trap) and detail (then the runtime's error message for a stop that failed).

RingPlacement(cmd, consumed, events, read, command_depth, event_depth, cached=False, remote=None), work_ring(kernel_lim, kernel_coordinates, work_lim, work_coordinates, cluster=1) (the session's placement of one cluster's ring and the kernel's end of it), kernel_work_args(ends) (the kernel's work_* words for every ring) and args_block(thread, base, check_value, run_value=0, ring=None, table=None, peers=None) build the LIM block; Peers(nonce, lead, tiles, banks) is the peers line (tiles and banks: {cluster: x | y << 16}) and peer_groups({cluster: image}) the clusters that run one image. They need no device; the session fills Peers from the clusters' tiles and their banks' DRAM tiles for every thread on the card.

Measured

On work cluster 1 of a p150a at 1,750 MHz: the bank read at about 1.3 GB/s and written at 1.3 to 1.8 GB/s with four cores; a flush or invalidate sweep at 5 to 8 GB/s of region (experiments/x280_threads/). At the session's 1,000 MHz on each of clusters 1 to 3: about 1.2 GB/s read and written per cluster, alone or together (three at once 3.5 GB/s); a push into the other bank 2.3 GB/s, a pull 0.024 GB/s; invalidate 4.6 GB/s, flush 4.3 GB/s (experiments/work_clusters/). An empty kmx_parallel call: 0.71 µs; kmx_parallel_all over three clusters, the map, heavy map and prefix sum on 1, 4, 8 and 12 cores: X280 threads and examples/08_parallel_thread/ (--twelve).