Broadcast¶
Status: implemented for the persistent-kernel runtime; run on one card through a Session and
Run, under the dispatcher, and under the test feeder
(03_broadcast: python broadcast.py --rect 0,0,12,10
--blocks 16 --pages 4 --window 2 --jobs 8).
A broadcast is a run of cores of one group in one grid column, or in one grid row inside one
panel. One core of the broadcast, the sender, reads a block (pages pages) from DRAM into
slot block % window of its broadcast buffer and multicasts it to the same slot on every
other core of the broadcast, then writes a VALID marker on each. Each receiver hands the
block to its compute kernel and, when compute has popped it, writes its own marker in the
sender's DONE-BY table. The sender writes a slot again only when every receiver has
popped the block that was there. The sender's own compute takes every block too.
A program has up to two broadcasts at once (broadcast id 0 and 1), each with its own buffer, window, VALID word and DONE-BY table: a matrix product sends A blocks along rows and W blocks down columns.
Which kernel runs a broadcast¶
| NoC | Kernel | Sender | Direction of the multicast |
|---|---|---|---|
| NoC0 | reader (RISCV_0) | top of a column, left end of a row | +y, +x |
| NoC1 | writer (RISCV_1) | bottom of a column, right end of a row | -y, -x |
Every core of a broadcast runs the broadcast in the same kernel, so a broadcast buffer has one producer per core. The sender sits where its NoC's direction starts; a multicast from the other end would go the long way round the torus. The receivers' 4-byte DONE markers go against the direction; a window of two or more hides them.
Device calls¶
#include "komorebi/device/km_broadcast.hpp"
// Reader or writer, inside the handler body, once per command:
// reader: the km::Reader or km::Writer; command: the command;
// kArgBroadcast: index of the broadcast's first program word
km::Broadcast broadcast(reader, command, kArgBroadcast);
if (!broadcast.ok()) return; // the broadcast's words do not fit this command or buffer
if (broadcast.included()) {
for (uint32_t block = 0; block < blocks; block++) {
if (broadcast.sender()) {
uint32_t address = broadcast.reserve(block); // slot block % window, the same
// address everywhere
if (address == 0) return;
// read the block's pages into address (noc_async_read_page ...)
if (!broadcast.cast(block)) return; // reads landed, receivers done with the
// block `window` before, multicast, VALID,
// push to own compute
} else if (!broadcast.arrive(block)) { // VALID >= block + 1, push to own compute
return;
}
}
if (!broadcast.finish()) return; // compute popped every block, padding, last markers
}
// Compute:
km::broadcast_receive(compute, cb, pages); // a block's pages, read by index 0 .. pages - 1
km::broadcast_done(cb, pages); // done with the block (the DM kernel writes its marker)
km::broadcast_end(compute, cb, pages, window, blocks); // after the last block: pops the padding
Every call times out at the run's wait limit and checks the tile's stop flag. A call that returns false or 0 has recorded the give-up; the kernel leaves the body.
Rules for a program¶
- Blocks of a command go in order, block 0 to the last, on every core of the broadcast,
and compute takes and pops every one of them, then calls
broadcast_end. - The broadcast buffer is a circular buffer of exactly
window * pagespages, used only through these calls.finishpads it to whole windows, so every command starts at slot 0 and slot addresses are the same on every core. - A command with broadcasts names exactly the group the broadcast words were built for, and
carries
KM_FLAG_EXCLUSIVE: overlapping groups are never in flight together. - Window (
window): 1 to 8. Receivers per broadcast: up to 15. Blocks per command: 0 to 32,767, with no limit on the pages they carry in all; the buffer,window * pagespages, holds at most 65,535. A broadcast that multicasts no block in a command still callsfinishon every core. - A core reports only after its compute has popped every block (
finishchecks it for its own kernel).
Host: broadcast words¶
komorebi.layout computes the broadcasts as pure functions:
| Function | Gives |
|---|---|
broadcasts(grid, grid_panels, rect, along, noc) |
the broadcasts of a rectangle (x0, y0, columns, rows), along "column" or "row", cores in rank order (rank 0 sends) |
broadcast_rect(broadcast, noc0, noc_xy, noc) |
the multicast corners of the receivers; rejects a broadcast whose cores are not consecutive in NoC0 |
broadcast_args(grid, panels, group, rect, along, noc, noc0, noc_xy, (broadcast, cb, window, pages)) |
per core of the group, the 11 words of km::BroadcastArgs (rank 0xFF outside the rectangle) |
blackhole_noc0_x(harvest_mask) |
NoC0 x of each logical column, from the chip's Tensix harvesting mask |
panels(noc0_x) |
runs of columns with consecutive NoC0 x |
The words go into the reader's or writer's program arguments, in the field order of
km::BroadcastArgs; examples/03_broadcast/broadcast.py shows the whole host side.
When a core stalls¶
A receiver's markers are sent from inside its broadcast calls (arrive, finish),
so a receiver whose data-movement kernel stops calling them stops sending markers, even for
blocks its compute pops afterwards. The sender's wait for its marker
passes the wait limit: it commands the give-up and stops its tile; the other cores of
the broadcast give up waiting for the next VALID. Each core's broadcast area keeps the first
give-up: where (broadcast_done: the sender waiting for markers or for its own NoC barriers,
broadcast_valid: a receiver waiting for a block, broadcast_free: waiting for its own compute
or for its last markers to leave, broadcast_begin: bad words), the broadcast and the block. The command misses its deadline,
the feeder or the dispatcher reports the timeout, and every kernel returns; no
reset is needed.
Control block¶
Two broadcast areas of 0x140 bytes from KM_CONTROL_BROADCAST (0xA00), in 16-byte slots: VALID,
the source slot of this core's outgoing marker, the first give-up (where | broadcast << 8,
block, sequence number), statistics (blocks, wait cycles), and the DONE-BY words of
ranks 1 to 15. Markers carry the command's sequence number in the high half, so a word
left by an earlier command never counts.