Skip to content

11 movement

Both data-movement CPUs of each core carry one stream (Movement). The program has no reader and no writer of its own: one source, movement.cpp, is built for both CPUs, the movers. Mover A (RISCV_0) issues on NoC 0, mover B (RISCV_1) on NoC 1. Each core reads its tiles in blocks; a pattern given on the command line says which mover reads block k (AAAB: mover A three of every four blocks, mover B the fourth), each mover reads into its own half of a buffer pair, and compute takes the blocks in order, each from the half its owner filled.

Handler Arguments What a core does
READ a0 input_address, a2 tiles, a3 tiles_per_block, a4 pattern word, a5 repetitions the movers read the core's tiles pages (from page rank x tiles) a5 times through the input pair; compute takes each block and drops it
COPY as READ, with a1 output_address, once compute copies each block from the input pair into the same half of the output pair, and the movers write it to the same pages of the output

The host checks every word: for each pattern a COPY of a block and a half per core (a ragged last block) goes through both streams, and the output read back must equal the input. Then it times READ from the card's clock, by the difference of R and 4R repetitions (best of three), and prints each pattern's rate beside pattern A, mover A alone.

The program, from the host

import komorebi as km

program = km.Program(
    "movement",
    kernels="kernels/",  # movement.cpp for both movers, compute.cpp
    buffers=[
        km.BufferPair(0, 1, "bfloat16", 2048, pages=64),  # input: mover A's, mover B's
        km.BufferPair(16, 17, "bfloat16", 2048, pages=32),  # output
    ],
    compile_args={"movement": tensor.accessor_args()},
    handlers="kernels/movement.h",  # kHandlerRead = 1, kHandlerCopy = 2
)
pattern = km.Pattern("AAAB")
run.submit("read", group=(0, cores), args=[address, 0, 256, 16, pattern.word, 1])
// movement.cpp, the same source on both movers
km::Movement movement;
movement.run([&](const km_command_t& command) {
    const km::Stream read(km::kMoveRead, TensorAccessor(arguments, command.a0),
                          rank * tiles, tiles, tiles_per_block, repetitions, kInput,
                          km::Pattern::of(command.a4));
    return movement.move(read);  // this mover's blocks of the stream
});

// compute.cpp: block k from the half its owner filled
const uint32_t half = km::block_half(kInput, pattern, block);

Files

File What it is
kernels/movement.cpp the data-movement kernel of both movers: a km::Movement that moves the read stream (and, for COPY, the write stream)
kernels/compute.cpp takes the blocks in order; COPY copies each through the vector unit's registers
kernels/movement.h handlers, the buffer indices of the two pairs and the command arguments
movement.py checks the copies, times the reads, prints one line a pattern and one JSON line

What it needs

A Komorebi install and a card prepared as in Running on a card.

python movement.py                       # pattern AB beside A, every core of the grid
python movement.py --pattern AAAB B --cores 8 --tiles 256 --block 32
python movement.py --pattern A B AB AAAB --cores 4 --tiles 48 --block 8 --check-only

--cores 0 (the default) is every core of the grid. The script exits non-zero if a copy differs from its input.

Numbers

A Blackhole p150a, AICLK 1,350 MHz, host load 4.7, the whole grid (120 cores), 256 tiles of 2 KB a core in blocks of 16, every copy exact (python movement.py --pattern AB AAAB B):

Pattern GB/s Beside A
A 427.8 1.00
AB 485.9 1.14
AAAB 502.6 1.17
B 247.4 0.58

On the full grid NoC 0 reads DRAM faster than NoC 1, so the best share gives mover B one block in four. On fewer cores the routes differ and so does the best pattern; the reference has the measured table.