Skip to content

Part 1. One core · lesson 4 of 42

4. Hello, core

A Komorebi program is a folder with three C++ files, one for each of three roles, and a Python file that loads them onto the cores and sends the cores a command. This lesson takes the smallest such program apart, line by line. It relies on lesson 1, What Komorebi is, lesson 2, Setup and the first run and lesson 3, The card.

Why there are three files

Every Tensix core has five CPUs (lesson 3, The card). Komorebi runs one small C++ function on each of three sets of them:

  • reader.cpp runs on the first of the two CPUs that move data,
  • writer.cpp runs on the second of them,
  • compute.cpp runs on the three that do arithmetic.

Such a function is called a kernel. It is the code that runs on a core, as opposed to the Python that runs on your computer. Each is a file of its own, all in one folder. Komorebi always loads all three onto the cores, so a program always has all three files, even when a role has nothing to do. In this lesson none has: the program does nothing on the core except to be asked.

The two data-movement CPUs are alike. Either can read from DRAM, write to DRAM, and send to and receive from other cores, and a kernel on one of them may do all of these. "Reader" and "writer" are only the traditional names of the two. A program often has the first bring data in and the second send it out, and the tutorial does the same from lesson 16, Reading from DRAM on, but that is the program's choice. Three things are not a choice:

  • the two CPUs use different NoCs, one each, fixed for the program: by default the first uses NoC 0 and the second NoC 1;
  • a queue of data in L1 has exactly one CPU that fills it and one that empties it;
  • in Komorebi the second CPU is the one that reports the command to Python, and the one that can attach a result to it (lesson 6, A result).

The general form of a kernel file

Each of the three files has the same shape. Words in capitals are the parts you choose.

#include "komorebi/device/km_worker.hpp"

void kernel_main() {
    km::ROLE name;
    name.run([&](const km_command_t& command) {
        BODY
    });
}
  • #include "komorebi/device/km_worker.hpp" brings in everything a kernel needs from Komorebi.
  • void kernel_main() is the function the card calls. The name is fixed.
  • km::ROLE name; makes the object that stands for the role: km::Reader, km::Compute or km::Writer, the one that belongs to the file. name is yours; the tutorial uses the role's own word: reader, compute, writer.
  • name.run(...) is a loop that you do not write. It waits until a command arrives for this core, calls the function you gave it once, and waits again. It goes on until the program is stopped.
  • [&](const km_command_t& command) { BODY } is that function: a C++ lambda that takes the command that arrived. BODY is the work. The command is the only parameter, and you do not use it yet.

In writer.cpp, BODY must be return true;. That true is writer.cpp saying "I am done with this command, report it". The functions of reader.cpp and compute.cpp return nothing.

This is the whole writer.cpp of the program in this lesson:

writer.run([&](const km_command_t& command) {
    return true;
});

The function has one line. A command arrives, the function returns true, the core reports.

The general form of the Python file

program = km.Program(NAME, kernels=FOLDER)

with km.Session() as session:
    run = session.load(program)
    handle = run.submit(KIND, group=(FIRST, COUNT))
    event = run.wait(handle, timeout=SECONDS)
    run.stop()
  • NAME is a name for the program, a string. It labels messages.
  • FOLDER is the folder with the three kernel files. The programs of this tutorial write it HERE / "kernels", where HERE is the folder that holds the Python file itself, so that the program runs from any directory.
  • KIND is a number that says what kind of work the command is. It is always 0 in this lesson; lesson 8, Handlers gives it a meaning.
  • FIRST and COUNT say which cores the command is for: COUNT cores, starting at core number FIRST. Together they are the command's group.
  • SECONDS is how long wait may wait before it gives up with an error.

One line at a time

program = km.Program("hello", kernels=HERE / "kernels")

A Program is a description of what the cores will run: a name and the folder with the three files. Nothing is compiled and the card is not touched yet.

with km.Session() as session:

A Session is the open card. with opens it at the start of the block and closes it at the end, also when an error ends the block early. Everything that follows is inside the block.

run = session.load(program)

load compiles the three files, copies the kernels onto all 120 cores, and starts them. The kernels now run and wait for commands. What load returns, run, is the program loaded and running: the handle on it for the rest of the program.

handle = run.submit(0, group=(0, 1))

submit sends one command to the cores. A command is a message from Python to cores that says: run your kernels once, now. submit does not wait for the cores; it returns at once with a handle, a ticket that names this command, so that you can ask about it later.

The parts of the line that sends a command.

Only core 0 runs the command. Every core sees every command, but a core that is not in the group lets it pass.

event = run.wait(handle, timeout=10.0)

wait blocks until the command is finished, then returns an event: the card's report about that command. If the command takes longer than timeout seconds, wait raises an error instead.

run.stop()

stop tells the kernels on every core to finish, and waits until they have. After stop no command can be sent.

print(f"the command is {event.kind}")

event.kind says how the command ended. done means that every core of the group reported that it finished.

What happens, in order

The life of the one command of this program.

The whole program

"""Hello, core: the smallest program. One command to one core, and its answer."""

from pathlib import Path

import komorebi as km

HERE = Path(__file__).resolve().parent

program = km.Program("hello", kernels=HERE / "kernels")

with km.Session() as session:
    run = session.load(program)
    handle = run.submit(0, group=(0, 1))
    event = run.wait(handle, timeout=10.0)
    run.stop()
    print(f"the command is {event.kind}")
// The reader kernel. It has nothing to do.
#include "komorebi/device/km_worker.hpp"

void kernel_main() {
    km::Reader reader;
    reader.run([&](const km_command_t& command) {});
}
// The compute kernel. It has nothing to do.
#include "komorebi/device/km_worker.hpp"

void kernel_main() {
    km::Compute compute;
    compute.run([&](const km_command_t& command) {});
}
// The writer kernel. It has nothing to do yet, but it must say that it is done.
#include "komorebi/device/km_worker.hpp"

void kernel_main() {
    km::Writer writer;
    writer.run([&](const km_command_t& command) {
        return true;
    });
}

Run it from the folder tutorial/04_hello_core:

python hello.py
the command is done

It prints one line. The program did nothing on the core, and that is the point: the command arrived, three empty functions ran, the core reported, and Python heard it. Everything the later lessons do is a way to make those three functions do something.

Common mistakes

  • A missing file. If the folder lacks one of the three, load stops with FileNotFoundError: a: no kernel source [.../compute.cpp, .../writer.cpp], naming the missing files.
  • A group outside the card. group=(0, 500) stops at submit with ValueError: group (0, 500) is not inside 120 cores. The cores are numbered 0 to 119.
  • A writer that returns false, or does not return true. The core never reports, and after about a second the card gives up on the command and wait raises CommandTimeout. lesson 10, When it goes wrong is about this.

Next: lesson 5, Commands looks at what a command is, and at what it costs.