Skip to content

Part 1. One core · lesson 8 of 42

8. Handlers

A handler is one piece of work inside a kernel, and the command says which one to run by its number. This lesson relies on lesson 4, Hello, core, lesson 6, A result and lesson 7, Arguments.

Why handlers

A program rarely does one thing. The kernels are loaded once (lesson 5, Commands), so the one kernel has to contain every piece of work the program may ask for, and each command has to say which one it wants. That is what the number you gave to submit first, the 0 of lesson 4, was for. A handler is a piece of work with a number and a name.

All three kernels of a core receive every command. Each looks at the handler and does the part that belongs to it. A kernel that has nothing for a handler ignores it, as the empty the kernels in reader.cpp and compute.cpp have so far ignored everything.

The general form

The numbers and names live in one C++ header, which the kernels include and Python reads:

constexpr uint32_t kHandlerNAME = NUMBER;

In the kernel, inside the function you give to run:

if (km::handler(command) == kHandlerNAME) {
    WORK
}

In Python:

program = km.Program("...", kernels=FOLDER, handlers=HEADER)
handle = run.submit("name", group=(0, 1))
  • NAME is a word that starts with a capital letter, such as Add or BigStep. NUMBER is its number, from 1 to 239. The prefix kHandler is required.
  • km::handler(command) is the number of the handler that this command asked for.
  • WORK is what the kernel does for that handler.
  • HEADER is the path of the header file. Program reads it, and from then on submit accepts the handler's name in place of its number. The name is the C++ name without the prefix, in lower case with underscores between the words: kHandlerBigStep is "big_step", kHandlerAdd is "add".
  • The number 0 is the one handler you do not write. A command with handler 0 runs the kernels, and no if matches it.

One line at a time

// The handlers of this program: the numbers that the kernels and Python agree on.
#pragma once

#include <stdint.h>

constexpr uint32_t kHandlerAdd = 1;
constexpr uint32_t kHandlerMultiply = 2;

Two handlers, Add with number 1 and Multiply with number 2. Both kernels and Python agree on these numbers because they all read this one file.

writer.run([&](const km_command_t& command) {
    const uint32_t left = command.a0;
    const uint32_t right = command.a1;
    if (km::handler(command) == kHandlerAdd) {
        writer.result(left + right);
    }
    if (km::handler(command) == kHandlerMultiply) {
        writer.result(left * right);
    }
    return true;
});

writer.cpp reads the two arguments, then checks the handler. A command for Add makes it the sum, a command for Multiply the product. The two ifs do not overlap, so at most one of them runs.

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

handlers= is the one new argument of Program: the header.

handle = run.submit("add", group=(0, 1), args=[6, 7], result=True)

"add" is the handler's name in place of the number. The same line with "multiply" asks for the other piece of work.

What `writer.cpp` of this program does with each command.

The whole program

"""Handlers: one kernel, two pieces of work; each command names the one it wants."""

from pathlib import Path

import komorebi as km

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

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

with km.Session() as session:
    run = session.load(program)
    handle = run.submit("add", group=(0, 1), args=[6, 7], result=True)
    print(f"6 add 7      = {run.wait(handle, timeout=10.0).result}")
    handle = run.submit("multiply", group=(0, 1), args=[6, 7], result=True)
    print(f"6 multiply 7 = {run.wait(handle, timeout=10.0).result}")
    run.stop()
// The handlers of this program: the numbers that the kernels and Python agree on.
#pragma once

#include <stdint.h>

constexpr uint32_t kHandlerAdd = 1;
constexpr uint32_t kHandlerMultiply = 2;
// The writer kernel: the handler of the command decides what it computes.
#include "komorebi/device/km_worker.hpp"
#include "handlers.h"

void kernel_main() {
    km::Writer writer;
    writer.run([&](const km_command_t& command) {
        const uint32_t left = command.a0;
        const uint32_t right = command.a1;
        if (km::handler(command) == kHandlerAdd) {
            writer.result(left + right);
        }
        if (km::handler(command) == kHandlerMultiply) {
            writer.result(left * right);
        }
        return true;
    });
}

reader.cpp and compute.cpp are the empty ones; the header need not be included in them, since they do not look at the handler.

6 add 7      = 13
6 multiply 7 = 42

Two commands with the same arguments, 6 and 7, and different handlers, and two different answers from the same loaded kernel.

Common mistakes

  • A name that is not in the header. run.submit("nothing", ...) stops with KeyError: "handlers has no handler 'nothing'". The same happens when you forgot handlers= in Program: Python then knows no names, only numbers.
  • A handler number the kernel has no if for. run.submit(77, ...) is accepted, runs the kernels, and the result is 0. No message tells you that nothing matched.
  • A number outside 1 to 239. run.submit(240, ...) stops with ValueError: handler 240: not NOP and not in 1..KM_HANDLER_USER_MAX. The numbers above 239 belong to Komorebi.

Next: lesson 9, Many commands sends many commands at once.