Foundry/tt/ft_tt_group.h

261 lines
14 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_group.h
* @brief Core groups — the SubDevices equivalent. Disjoint sets of Tensix cores, each
* with its own allocator and its own completion tracking, so two workloads run
* concurrently on one card without either one being able to see the other's state.
*
* ---------------------------------------------------------------------------
* DERIVE FROM LIVE — an acceptance criterion, not a style note
* ---------------------------------------------------------------------------
* There is no core, column or row count in this file. Every size comes from
* `ft_tt_coord_tensix_core_count()` against a grid read off ARC telemetry. "14x10" is
* exactly the constant that feels safe while partitioning a grid and is wrong on the
* 120-core p150s other people run — TT confirmed the 140→120 reduction is a
* COMPATIBILITY measure, not yield binning, so those parts are a shipping configuration
* and not damaged silicon. A hardcoded 140 does not fail there; it addresses cores that
* are not present, silently.
*
* `tests/test_queue.c` partitions a SYNTHETIC 12-column grid into 120 cores with no code
* change and asserts the shape, which is where a constant creeping back in goes red.
*
* Note also that `ft_tt_coord_tensix_by_index()`'s stride IS the live column count, so a
* core index recorded on this card does not name the same core on another. Indices are
* for reproducing a run on ONE part; coordinates are what a group actually holds.
*
* ---------------------------------------------------------------------------
* WHAT "INDEPENDENT ALLOCATORS" MEANS HERE
* ---------------------------------------------------------------------------
* Two things, and only one of them needed building:
*
* - **L1 is free.** Groups own disjoint CORES, and L1 is per-core, so two groups cannot
* collide in L1 at all. Every group uses the same per-core map (ft_tt_op_l1_layout.h),
* which is what lets one kernel binary serve every group.
* - **DRAM is by BANK.** Each group is handed a disjoint stride through the LIVE bank
* count and its own `ft_tt_allocator`. DRAM banks are already separate arenas inside
* the allocator, so disjoint banks is disjoint memory with nothing to enforce at
* allocation time — a group physically cannot hand out another group's bytes.
*
* @status Green. Off-hardware in tests/test_queue.c, including the synthetic 12-column
* grid that must partition into 120 cores with no code change; on the card in
* tests/test_phase5_resident.c, two groups running independent workloads concurrently,
* and in tests/test_phase6_triad.c for group-level completion — two resident triads, a
* sequence number their WRITERS post, and the check refusing while one member is behind.
* @issues None
* @todo None
*/
#ifndef FT_TT_GROUP_H
#define FT_TT_GROUP_H
#include <stdint.h>
#include "ft_tt_buffer.h"
#include "ft_tt_coord.h"
#include "ft_tt_launch.h"
#include "ft_tt_queue.h"
/**
* One group: a disjoint set of cores, their queues, and the memory they may use.
*
* `queues` is parallel to the core set — `queues[i]` belongs to
* `ft_tt_launch_core_set_at(&group->cores, i)`. Both point into caller storage carved by
* ft_tt_group_partition().
*/
typedef struct ft_tt_core_group {
uint32_t id; /**< 0..group_count-1, for logs */
uint32_t first_tensix_index; /**< the live-grid index this group starts at */
uint32_t tensix_index_count; /**< how many consecutive indices it holds */
ft_tt_launch_core_set cores; /**< borrowed storage */
ft_tt_queue *queues; /**< borrowed storage, `tensix_index_count` entries */
ft_tt_allocator allocator; /**< this group's own DRAM arenas */
uint32_t dram_bank_first; /**< first LIVE bank this group may allocate from */
uint32_t dram_bank_stride; /**< and the stride between the ones it owns */
uint32_t dram_bank_count; /**< how many banks that comes to */
/** L1 address of the group completion block ON THE GROUP'S FIRST CORE, or 0 if this
* group does not track group-level completion. Set by
* ft_tt_group_completion_init(). */
uint32_t completion_address;
/** Which FT_TT_PROC_* are resident on every core of this group, as a bit mask. 0 means
* "ask the control block", which is right for the BRISC-only kernel and WRONG for a
* triad: `state` there has exactly one writer, so a reader that has exited while the
* compute thread is still spinning out a bound reads EXIT and is a lie. A shutdown
* that believed it would relaunch a second kernel onto a core that still has one. */
uint32_t resident_processor_mask;
} ft_tt_core_group;
/**
* Split a run of live Tensix indices into `group_count` disjoint groups.
*
* Sizes are derived: `index_count / group_count`, with the remainder given one extra core
* each to the first groups, so a 120-core part and a 140-core part both partition without
* a code change and neither leaves cores unassigned.
*
* Coordinates come from `ft_tt_launch_core_set_add_tensix()`, which derives them from the
* live grid and CLASSIFIES each one before it can enter a launch — a wrong NoC endpoint
* is a hung chip and a physical power cycle, not a failed call (spec §7.3).
*
* @param grid from ft_tt_arc_telemetry_read_grid(); never a constructed one
* @param first_index first live Tensix index to hand out
* @param index_count how many, or 0 for "everything from `first_index` to the end of
* the live grid"
* @param group_count at least 1, at most `index_count`
* @param groups `group_count` caller-owned structs, zeroed and filled
* @param placements caller-owned, at least `index_count` entries; carved among groups
* @param queues caller-owned, at least `index_count` entries; carved among groups
* @param storage_count how many entries `placements` and `queues` have
* @return FT_TT_OK; FT_TT_EINVAL on a bad argument; FT_TT_ENOMEM if the
* storage is smaller than `index_count`; or a coordinate error
*
* Thread-safe: no Reentrant: no Signal-safe: no
*/
int ft_tt_group_partition(const ft_tt_grid_info *grid, uint32_t first_index, uint32_t index_count,
uint32_t group_count, ft_tt_core_group *groups,
ft_tt_launch_core_placement *placements, ft_tt_queue *queues,
uint32_t storage_count);
/**
* Give each group its own allocator over its own DRAM banks.
*
* Separate from partitioning because a caller that only wants cores should not have to
* supply a device context, and because the allocator refuses a grid it considers
* unusable — which is a different failure from "these indices do not exist".
*
* @param ctx may be NULL; only used for ft_tt_mem_info accounting
* @return FT_TT_OK, or the first failing group's code
*/
int ft_tt_group_init_allocators(ft_tt_core_group *groups, uint32_t group_count,
const ft_tt_grid_info *grid, ft_tt_ctx *ctx);
/**
* The `index`-th DRAM bank this group owns, in LIVE bank numbering.
*
* @param out_bank written only on success
* @return FT_TT_OK, or FT_TT_EINVAL when the group owns fewer banks than that
*/
int ft_tt_group_dram_bank(const ft_tt_core_group *group, uint32_t index, uint32_t *out_bank);
/**
* Check that no coordinate appears in two groups, and that no group is empty.
*
* This is the core-level form of the pairwise-distinctness check that caught the
* multicore seed collision: when N units are supposed to be doing N different things,
* check them against EACH OTHER and not only against a model. A partitioner that
* accidentally hands two groups the same core produces two callers that both believe
* they own it, and the symptom is a wrong answer somewhere else entirely.
*
* @param out_reason may be NULL; a static string naming the first collision
* @return FT_TT_OK, or FT_TT_ERROR
*/
int ft_tt_group_check_disjoint(const ft_tt_core_group *groups, uint32_t group_count,
const char **out_reason);
/**
* Set up every core's queue control block in a group, with one nonce for the whole group.
*
* @param control_address the same per-core L1 address for every core; FT_TT_OP_QUEUE_BASE
* @param capacity ring slots, a power of two
* @param run_nonce ft_tt_queue_make_nonce()
* @return FT_TT_OK, or the first failing core's code
*/
int ft_tt_group_queues_init(const ft_tt_noc_access *noc, ft_tt_core_group *group,
uint32_t control_address, uint32_t capacity, uint32_t run_nonce);
/* ================================================================
* Group-level completion — "the group finished item N"
* ================================================================
*
* Until this existed, completion was per CORE and the only group-level answer available was
* a SUM of `completed_count` across the group's cores. A sum cannot express what a group
* scheduler needs, and worse, it is satisfiable by the wrong thing: one core doing 2N items
* and another doing none sums to the same 2N as every core doing N. That is the
* "`RUN_MSG_DONE` is 0" family of lie again — a number that looks like evidence of work and
* is evidence of arithmetic (dev notes §3).
*
* So the kernels write it. Each member's WRITER puts {sequence, token} into ITS OWN slot of
* a block on the group's first core, after that descriptor's work; the host asks "has every
* slot reached N, with THIS run's token for N". One writer per slot, one NoC transaction
* per post, no atomics, no ordering to argue about — and because the block is on a core
* rather than in host memory, another group on the device can read it too.
*/
/**
* Write the group completion block on the group's FIRST core: the magic, the member count,
* and every slot zeroed.
*
* The zero is not the poison the per-core protocol uses, and does not need to be: the
* group check asks for a SEQUENCE, and 0 is not a sequence any descriptor may carry. What
* a stale block from a previous run could offer is a large sequence, and that is what the
* TOKEN check refuses.
*
* @param group_address L1 address on the leader; FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET
* @return FT_TT_OK; FT_TT_EINVAL; FT_TT_ENOMEM if the group has more members
* than FT_TT_QUEUE_GROUP_MAX_MEMBERS
*
* Thread-safe: no Reentrant: no Signal-safe: no
*/
int ft_tt_group_completion_init(const ft_tt_noc_access *noc, ft_tt_core_group *group,
uint32_t group_address);
/**
* Fill the per-processor kernel binding for one member of a group, including where its
* group post goes.
*
* Separate from ft_tt_queue_kernel_args() because the GROUP is what knows the leader's
* coordinates and this member's slot, and the queue is what knows the addresses.
*
* @param slot this core's index within the group
* @param processor FT_TT_PROC_*; only the processor that publishes completion gets a
* non-zero group address, because a second poster would give one slot
* two writers
* @param publisher the FT_TT_PROC_* that publishes completion for this program: DM1 for
* the resident triad, DM0 for the BRISC-only kernel
*/
int ft_tt_group_kernel_binding(const ft_tt_core_group *group, uint32_t slot, unsigned processor,
unsigned publisher, uint32_t idle_spin_limit,
uint32_t cb_wait_spin_limit,
ft_tt_queue_kernel_binding *out_binding);
/** What one member's slot currently says. The slot LAYOUT belongs to the queue protocol
* (ft_tt_queue.h), so this is that type under the name group callers already use — one
* definition, not two that have to be kept in step. */
typedef ft_tt_queue_group_slot ft_tt_group_member_completion;
/**
* Read every member's slot out of the leader's L1 in ONE device access.
*
* @param out_members at least `ft_tt_launch_core_set_count(&group->cores)` entries
* @param capacity how many entries it has
*/
int ft_tt_group_completion_read(const ft_tt_noc_access *noc, const ft_tt_core_group *group,
ft_tt_group_member_completion *out_members, uint32_t capacity);
/**
* Did EVERY member reach group sequence `sequence`, with this run's token for it?
*
* Both halves are needed and each fails a different lie: the sequence alone is satisfiable
* by a previous run's residue, and the token alone cannot say how far the group got.
*
* @param expected_token the token the host stamped on the descriptor carrying `sequence`;
* ft_tt_queue_token_at() of the member's own queue
* @param out_reason may be NULL; a static string naming the first disagreement
* @param out_short_slot may be NULL; the first member that is behind
* @return FT_TT_OK, or FT_TT_ERROR
*/
int ft_tt_group_check_completion(const ft_tt_core_group *group,
const ft_tt_group_member_completion *members, uint32_t count,
uint32_t sequence, uint32_t expected_token,
const char **out_reason, uint32_t *out_short_slot);
/**
* Poll until every member reaches `sequence`, or until `timeout_ms` elapses.
*
* @param out_reached may be NULL; the lowest sequence any member had, on success or timeout
* @return FT_TT_OK; FT_TT_EIO on timeout; FT_TT_EINVAL for timeout_ms 0
*/
int ft_tt_group_wait_completion(const ft_tt_noc_access *noc, const ft_tt_core_group *group,
uint32_t sequence, uint32_t timeout_ms, uint32_t *out_reached);
#endif /* FT_TT_GROUP_H */