Skip to content

Part 2. Many cores · lesson 14 of 42

14. Placing groups

A group is a run of core numbers, so you place it by choosing where the run starts and how long it is. This lesson relies on lesson 11, A core group, lesson 13, The leader and lesson 3, The card.

Why placing matters

Until now every group started at core 0. A program that uses the card well chooses its cores: a whole row for work that is the same everywhere, the left panel for one stage of a job and the right for the next, a block of cores for a block of data. A group is always the numbers FIRST up to FIRST + COUNT - 1, so what shapes are possible follows from how the cores are numbered. lesson 3, The card gave the numbering: 12 cores in a row, 10 rows, 0 to 119 row by row, the first six of each row in the left panel and the last six in the right.

The general form

first = ROW * 12 + COLUMN
handle = run.submit(KIND, group=(first, COUNT))
  • ROW is the row, 0 to 9, from the top.
  • COLUMN is the place in the row, 0 to 11: 0 to 5 is the left panel, 6 to 11 the right.
  • first is the number of the core at the start of the group. It is the leader.
  • COUNT is how many cores follow it, as long as the group ends at core 119 or before. Within a row COLUMN + COUNT must not pass 12 if the group is to stay in its row.
  • A shape that is not one run of core numbers is several groups: one command for each, all submitted before the first wait (lesson 9, Many commands).

Pictures of the placements

Five groups on the card. Lit cores are in the group; L marks a leader.

One line at a time

writer.run([&](const km_command_t& command) {
    if (km::handler(command) == kHandlerWhere) {
        writer.result(writer.me());
    }
    return true;
});

Each core of the group sets its own number as the result. Python reads the leader's, which is the group's first core, so the result shows where a group started.

CORES_PER_ROW = 12
placements = {
    "row 2": [(2 * CORES_PER_ROW, CORES_PER_ROW)],
    "rows 2 and 3": [(2 * CORES_PER_ROW, 2 * CORES_PER_ROW)],
    "left panel of row 2": [(2 * CORES_PER_ROW, 6)],
    "right panel of row 2": [(2 * CORES_PER_ROW + 6, 6)],
    "right panel of rows 2 to 4": [
        (row * CORES_PER_ROW + 6, 6) for row in (2, 3, 4)
    ],
}
for name, groups in placements.items():
    handles = [run.submit("where", group=group, result=True) for group in groups]
    events = run.wait(handles, timeout=10.0)
    leaders = [event.result for event in events]
    print(f"{name:27} {len(groups)} group(s), leader cores {leaders}")
    assert leaders == [first for first, _ in groups]

CORES_PER_ROW is 12. The dictionary lists five placements by name: for each, a list of groups, each a pair (first, count). The loop sends one command to each group of the placement, waits for all of them, and prints the leaders' core numbers. The last line of the loop is the program's own check: each leader is the first core of its group.

The whole program

"""Placing groups: a row, two rows, a panel, and a rectangle made of several groups."""

from pathlib import Path

import komorebi as km

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

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

with km.Session() as session:
    run = session.load(program)

    CORES_PER_ROW = 12
    placements = {
        "row 2": [(2 * CORES_PER_ROW, CORES_PER_ROW)],
        "rows 2 and 3": [(2 * CORES_PER_ROW, 2 * CORES_PER_ROW)],
        "left panel of row 2": [(2 * CORES_PER_ROW, 6)],
        "right panel of row 2": [(2 * CORES_PER_ROW + 6, 6)],
        "right panel of rows 2 to 4": [
            (row * CORES_PER_ROW + 6, 6) for row in (2, 3, 4)
        ],
    }
    for name, groups in placements.items():
        handles = [run.submit("where", group=group, result=True) for group in groups]
        events = run.wait(handles, timeout=10.0)
        leaders = [event.result for event in events]
        print(f"{name:27} {len(groups)} group(s), leader cores {leaders}")
        assert leaders == [first for first, _ in groups]
    run.stop()
// The handlers of this program: the numbers that the kernels and Python agree on.
#pragma once

#include <stdint.h>

constexpr uint32_t kHandlerWhere = 1;
// The writer kernel: every core of the group sets its own number as the result; the
// leader's reaches Python.
#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) == kHandlerWhere) {
            writer.result(writer.me());
        }
        return true;
    });
}
row 2                       1 group(s), leader cores [24]
rows 2 and 3                1 group(s), leader cores [24]
left panel of row 2         1 group(s), leader cores [24]
right panel of row 2        1 group(s), leader cores [30]
right panel of rows 2 to 4  3 group(s), leader cores [30, 42, 54]

Each command was done, which means every core of every group reported. The last placement has three groups, so three events and three leaders. The group of two rows, (24, 24), works like the group of one row: the leader is its first core.

Common mistakes

  • Expecting a rectangle from one group. group=(30, 18) is not "6 columns of 3 rows": it is cores 30 to 47: the right panel of row 2 and then all of row 3, because the run goes on to the next number.
  • A group that does not fit the card. group=(110, 20) stops at submit with ValueError: group (110, 20) is not inside 120 cores.
  • Counting rows or columns from 1. Both start at 0.

That is the end of Part 2. A command can now go to any run of cores, each core knows its rank, takes its piece of the work by it, and the leader speaks for the group. Part 3 gives the cores something to work on: data.

Next: lesson 15, Tiles and tensors.