Part 3. Data · lesson 16 of 42
16. Reading from DRAM¶
A data-movement kernel can copy one tile of a tensor from DRAM into the core's L1, where
the core can use it. Which of the two
data-movement kernels does it is the program's choice (lesson 4, Hello, core); in this lesson it is
reader.cpp. This lesson relies on lesson 15, Tiles and tensors, lesson 7, Arguments and
lesson 3, The card.
Why the core must copy¶
A core works only on what is in its own L1 (lesson 3, The card). Data in DRAM is out of its reach until it is copied in. A data-movement CPU does the copy, over the NoC: it asks for tile number so-and-so of a tensor and says where in L1 the tile is to land.
A copy takes time and is not finished when the call returns. The kernel has to wait for it, and then has to tell the others that the tile has arrived.
The general form¶
The kernel needs three things: a place in L1 for the tile, a way to name the tensor, and the copy itself.
In Python, before load:
program = km.Program(
NAME,
kernels=FOLDER,
handlers=HEADER,
buffers=[km.Buffer(index=INDEX, format=DTYPE, page_bytes=BYTES, pages=PAGES)],
compile_args={"reader": TENSOR.accessor_args()},
)
In reader.cpp:
constexpr auto ARGUMENTS = TensorAccessorArgs<0>();
...
const auto tensor = TensorAccessor(ARGUMENTS, ADDRESS);
reader.reserve(INDEX, PAGES);
noc_async_read_page(TILE, tensor, get_write_ptr(INDEX));
noc_async_read_barrier();
cb_push_back(INDEX, PAGES);
km.Buffer(...)sets aside room in the L1 of every core.INDEXis its number, 0 to 31, andDTYPEandBYTESsay what one tile of the tensor is:"uint32"and 4096 bytes for the tiles of the last lesson.PAGESis how many tiles fit; this lesson uses one. Thecb_in the C++ names below belongs to the kind of buffer that lesson 19, Circular buffers explains. For now a buffer is a numbered room in L1.compile_argshands the kernel what it needs to find the tensor's pages. In the kernel,TensorAccessorArgs<0>()receives it, andTensorAccessor(ARGUMENTS, ADDRESS)is an object that turns a tile number into a place in DRAM.ADDRESSis where the tensor starts. Python has it astensor.address; it travels in a command argument.reader.reserve(INDEX, PAGES)waits until the buffer has room forPAGEStiles.noc_async_read_page(TILE, tensor, where)starts a copy of tile numberTILEof the tensor to the L1 addresswhere.get_write_ptr(INDEX)is the address of the room in the buffer. The call returns at once; the copy goes on.noc_async_read_barrier()waits until every copy that was started is finished.cb_push_back(INDEX, PAGES)says "these tiles are now in the buffer". Whoever waits for them is let through.
One line at a time¶
The tensor first, in Python:
tile_numbers = np.arange(4, dtype=np.uint32).reshape(4, 1, 1)
positions = np.arange(32 * 32, dtype=np.uint32).reshape(1, 32, 32)
source = session.tensor(
(tile_numbers * 1000 + positions).reshape(128, 32), "uint32", "tile"
)
Four tiles; the number at place p of tile t is 1000 * t + p, so that a number tells
where it came from. (positions is 0 to 1023, and the numpy shapes line up tile numbers
and positions.)
program = km.Program(
"reading_from_dram",
kernels=HERE / "kernels",
handlers=HERE / "kernels" / "handlers.h",
buffers=[
km.Buffer(index=0, format="uint32", page_bytes=source.page_bytes, pages=1)
],
compile_args={"reader": source.accessor_args()},
)
The program names the buffer and gives reader.cpp what it needs to find source. The
buffer holds one tile of 4096 bytes in the format of the tensor.
reader.run([&](const km_command_t& command) {
if (km::handler(command) == kHandlerFetch) {
const uint32_t source_address = command.a0;
const uint32_t tile_number = command.a1;
const auto source = TensorAccessor(source_arguments, source_address);
if (!reader.reserve(kTileBuffer, 1)) {
return;
}
noc_async_read_page(tile_number, source, get_write_ptr(kTileBuffer));
noc_async_read_barrier();
cb_push_back(kTileBuffer, 1);
}
});
The command's arguments say which tensor and which tile. reader.cpp makes the accessor,
waits for room, starts the copy, waits for it to finish, and announces the tile.
writer.run([&](const km_command_t& command) {
if (km::handler(command) == kHandlerFetch) {
const uint32_t position = command.a2;
if (!writer.wait_front(kTileBuffer, 1)) {
return false;
}
const auto tile = (
reinterpret_cast<volatile tt_l1_ptr uint32_t*>(
get_read_ptr(kTileBuffer)
)
);
writer.result(tile[position]);
cb_pop_front(kTileBuffer, 1);
}
return true;
});
writer.cpp is the one that can send a result (lesson 6, A result), so it takes the number out of
the tile. wait_front waits until reader.cpp has announced the tile; get_read_ptr is the
address of the tile in L1; the reinterpret_cast says "read it as an array of uint32_t";
tile[position] is the number at that place. cb_pop_front frees the room for the next
command.
for tile, position in [(2, 0), (2, 5), (3, 0)]:
handle = run.submit(
"fetch", group=(0, 1), args=[source.address, tile, position], result=True
)
number = run.wait(handle, timeout=10.0).result
print(f"tile {tile}, number {position}: {number}")
assert number == tile * 1000 + position
Three commands, each fetching one tile and asking for one number of it.
The whole program¶
"""Reading from DRAM: a core copies a tile into its L1; the writer reads a number."""
from pathlib import Path
import numpy as np
import komorebi as km
HERE = Path(__file__).resolve().parent
with km.Session() as session:
tile_numbers = np.arange(4, dtype=np.uint32).reshape(4, 1, 1)
positions = np.arange(32 * 32, dtype=np.uint32).reshape(1, 32, 32)
source = session.tensor(
(tile_numbers * 1000 + positions).reshape(128, 32), "uint32", "tile"
)
program = km.Program(
"reading_from_dram",
kernels=HERE / "kernels",
handlers=HERE / "kernels" / "handlers.h",
buffers=[
km.Buffer(index=0, format="uint32", page_bytes=source.page_bytes, pages=1)
],
compile_args={"reader": source.accessor_args()},
)
run = session.load(program)
for tile, position in [(2, 0), (2, 5), (3, 0)]:
handle = run.submit(
"fetch", group=(0, 1), args=[source.address, tile, position], result=True
)
number = run.wait(handle, timeout=10.0).result
print(f"tile {tile}, number {position}: {number}")
assert number == tile * 1000 + position
run.stop()
// The reader kernel: copies one tile from DRAM into the buffer in L1.
#include "api/dataflow/dataflow_api.h"
#include "komorebi/device/km_worker.hpp"
#include "handlers.h"
void kernel_main() {
constexpr auto source_arguments = TensorAccessorArgs<0>();
km::Reader reader;
reader.run([&](const km_command_t& command) {
if (km::handler(command) == kHandlerFetch) {
const uint32_t source_address = command.a0;
const uint32_t tile_number = command.a1;
const auto source = TensorAccessor(source_arguments, source_address);
if (!reader.reserve(kTileBuffer, 1)) {
return;
}
noc_async_read_page(tile_number, source, get_write_ptr(kTileBuffer));
noc_async_read_barrier();
cb_push_back(kTileBuffer, 1);
}
});
}
// The writer kernel: waits for the tile in L1 and sends one number of it back.
#include "api/dataflow/dataflow_api.h"
#include "komorebi/device/km_worker.hpp"
#include "handlers.h"
void kernel_main() {
km::Writer writer;
writer.run([&](const km_command_t& command) {
if (km::handler(command) == kHandlerFetch) {
const uint32_t position = command.a2;
if (!writer.wait_front(kTileBuffer, 1)) {
return false;
}
const auto tile = (
reinterpret_cast<volatile tt_l1_ptr uint32_t*>(
get_read_ptr(kTileBuffer)
)
);
writer.result(tile[position]);
cb_pop_front(kTileBuffer, 1);
}
return true;
});
}
The compute file is the empty one.
Each number is 1000 * tile + position: the tile that was asked for arrived, and its
number at that place. The script checks it. It asks only for positions below 16 because
that is where the row-by-row order of the host array and the order of the numbers in the tile
agree; beyond them a tile keeps its numbers in four blocks of 16 x 16.
Common mistakes¶
- Starting the copy and not waiting.
noc_async_read_pagereturns at once, and withoutnoc_async_read_barrier()the tile may not be in L1 yet when it is used. - Typing
page_bytesby hand. It must be the size of one tile of the tensor; writesource.page_bytesinstead of 4096 and it cannot go wrong when the dtype changes. - Forgetting
cb_pop_frontinwriter.cpp. The first command works. The second finds the buffer still full,reader.cppwaits for room that never comes, and the command ends withCommandTimeout(lesson 10, When it goes wrong). - Taking a number of the tile at a place that is not in the first row of the first 16 columns, and expecting the host array's order. The numbers inside a tile are kept in four blocks of 16 x 16.
Next: lesson 17, Writing to DRAM sends a tile the other way.