Foundry/tt/ft_tt_group_shared_operand.h

215 lines
12 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_group_shared_operand.h
* @brief What a core group needs on top of `ft_tt_group.h` before it can express
* "these N cores cooperate on ONE matmul": a shared operand source, a barrier, and a
* rectangle to hold them.
*
* ---------------------------------------------------------------------------
* WHAT ft_tt_group.h ALREADY DOES, AND THE THREE THINGS IT DOES NOT
* ---------------------------------------------------------------------------
* `ft_tt_core_group` already gives a group disjoint CORES, disjoint DRAM BANKS, a queue
* per core, and group-level COMPLETION ("did every member reach item N, with this run's
* token"). That is enough for N cores doing N INDEPENDENT things. It is not enough for N
* cores doing ONE thing, and the gap is exactly three items:
*
* 1. **A group is not a rectangle, and a multicast needs one.**
* `ft_tt_group_partition()` hands out CONSECUTIVE live Tensix indices, and
* `ft_tt_coord_tensix_by_index()`'s stride is the live column count — so eight
* consecutive indices routinely wrap a row, and the bounding box of a wrapped run
* contains cores the group does not own. A multicast writes every node in the box.
* `ft_tt_group_rectangle_coordinates()` is the partitioner that cannot do that.
* It is a NEW entry point rather than a change to `ft_tt_group_partition()`, whose
* "consecutive indices, remainder to the first groups" contract is right for the
* independent-workload case and is depended on by tests/test_queue.c's synthetic
* 12-column grid.
*
* 2. **A shared OPERAND SOURCE.** A group has no notion of a member's ROLE. Sharing an A
* tile means one member READS it (from DRAM, once) and every other member RECEIVES it,
* at the SAME L1 address on all of them — the hardware requires the address to match,
* and nothing in `ft_tt_core_group` says a per-core address must agree across cores.
* `ft_tt_group_shared_operand` is that contract: who the leader is, where the operand
* lands, and how big it is.
*
* 3. **A BARRIER, which is not the completion block.** Group completion answers "has every
* member finished item N" and it is read by the HOST, after the fact, over PCIe. A
* cooperative matmul needs a member to answer "is every member ready for me to
* overwrite the shared buffer" ON THE CARD, per K-block, in under a microsecond. Those
* are different questions with different readers and the second one does not exist yet.
* It is the two-semaphore rendezvous below — a ready semaphore on the leader that
* receivers atomically increment, and an arrival semaphore on every member that the
* leader multicasts VALID into. The protocol is in
* kernels/ft_tensix_noc_multicast.h's header block.
*
* ---------------------------------------------------------------------------
* AND ONE THING THAT ALREADY WORKS AND SHOULD NOT BE CHANGED
* ---------------------------------------------------------------------------
* The COMPLETION RULE. `ft_tt_group_check_completion()`'s "every member's own slot reached
* sequence N with this run's token" is already the right shape for a cooperative op: the
* members produce different output tiles of one logical result, and "the group finished
* output block N" is exactly per-slot agreement. A sum would not be — one core doing 2N
* items and another doing none sums to the same 2N (ft_tt_group.h). The one thing worth
* saying out loud is that ABANDONMENT stays visible: a member whose wait gave up does not
* post, so the group wait times out and `out_reached` names the lowest sequence anyone got
* to. That is the diagnosis you want and it needs no change.
*
* ---------------------------------------------------------------------------
* WHAT THIS FILE DELIBERATELY DOES NOT DO
* ---------------------------------------------------------------------------
* It does not allocate L1. The four addresses come from the caller, because a resident op
* owns its own L1 map (ft_tt_op_l1_layout.h) and a second allocator would be a second
* source of truth for the same bytes. What this file does is REFUSE a layout that cannot
* work: overlapping regions, a mis-aligned operand, a receiver set that is not a
* rectangle.
*
* @status Off-hardware in tests/test_multicast_rectangle.c; exercised on the card by
* tests/test_multicast_bench.c.
* @issues None
* @todo None
*/
#ifndef FT_TT_GROUP_SHARED_OPERAND_H
#define FT_TT_GROUP_SHARED_OPERAND_H
#include <stdint.h>
#include "ft_tt_coord.h"
#include "ft_tt_launch.h"
#include "ft_tt_multicast.h"
/** The two values the arrival semaphore takes. The vendor's semaphore values, mirrored in
* kernels/ft_tensix_noc_multicast.h as FT_TENSIX_SEMAPHORE_*; the three must agree. */
#define FT_TT_GROUP_SEMAPHORE_INVALID 0u
#define FT_TT_GROUP_SEMAPHORE_VALID 1u
/** A semaphore is one 4-byte word, and the NoC's register-class alignment is 4 bytes
* (NOC_REG_ALIGNMENT_BYTES). */
#define FT_TT_GROUP_SEMAPHORE_BYTES 4u
/**
* Where the shared-operand machinery lives in each member's L1. The caller supplies it;
* this module only checks it.
*
* Every address is the SAME on every member. That is a hardware requirement for the
* operand (a multicast writes one address on all destinations) and a deliberate choice
* for the semaphores, so that one runtime-arg block serves every member and a per-member
* address cannot drift.
*/
typedef struct ft_tt_group_shared_operand_layout {
/** Where the shared operand lands on every member. 16-byte aligned. */
uint32_t operand_address;
/** How big it is. Non-zero and a multiple of 16 — a zero here is the zero-length NoC
* transfer (DEV_NOTES.md §1) and is refused, not treated as "nothing to send". */
uint32_t operand_bytes;
/** On the LEADER: the word receivers atomically increment to say they are ready. */
uint32_t ready_semaphore_address;
/** On EVERY member: the word the leader multicasts VALID into when the operand has
* landed. */
uint32_t arrival_semaphore_address;
/** On the LEADER: a word holding FT_TT_GROUP_SEMAPHORE_VALID, which is what the
* leader multicasts FROM. It is a staged constant rather than an immediate because a
* semaphore multicast is a WRITE of an L1 word, not an atomic. */
uint32_t valid_word_address;
} ft_tt_group_shared_operand_layout;
/** A group's shared-operand contract: the rectangle, the leader, and the layout. */
typedef struct ft_tt_group_shared_operand {
ft_tt_multicast_rectangle rectangle;
ft_tt_group_shared_operand_layout layout;
/** TRANSLATED coordinate of the core that reads the operand and multicasts it. */
ft_tt_noc_coord leader;
/** How many cores RECEIVE the operand. Equals rectangle.destination_count. */
uint32_t receiver_count;
/** How many cores the cooperating group has, leader included — `receiver_count` when
* the leader is one of the receivers' rectangle, one more when it is a dedicated
* feeder outside it. */
uint32_t member_count;
} ft_tt_group_shared_operand;
/**
* Derive `rows * columns` TRANSLATED Tensix coordinates forming a RECTANGLE, starting at
* live index `first_index`.
*
* This is the partitioner `ft_tt_group_partition()` is not: it refuses a block that would
* wrap a row instead of silently handing back a set whose bounding box contains cores it
* does not own.
*
* Every size comes from the LIVE grid — there is no core, row or column count in this
* module — because "14x10" is exactly the constant that feels safe while partitioning a
* grid and is wrong on the 120-core p150s other people run (ft_tt_group.h).
*
* @param grid from ft_tt_arc_telemetry_read_grid(); never a constructed one
* @param first_index live Tensix index of the rectangle's first (lowest-index) core
* @param rows at least 1
* @param columns at least 1
* @param out_members receives `rows * columns` coordinates in live-index order
* @param capacity how many entries `out_members` has
* @return FT_TT_OK;
* FT_TT_EINVAL for a NULL argument or a zero dimension;
* FT_TT_ENOMEM when `capacity` is smaller than `rows * columns`;
* FT_TT_ERROR when the block would wrap a row or leave the live grid — with the
* column count and the offending index in the log, because "why not" is the
* question a caller then has
*
* Thread-safe: yes Reentrant: yes Signal-safe: no (logs on failure)
*/
int ft_tt_group_rectangle_coordinates(const ft_tt_grid_info *grid, uint32_t first_index,
uint32_t rows, uint32_t columns,
ft_tt_noc_coord *out_members, uint32_t capacity);
/**
* Bind the RECEIVERS and an L1 layout into a shared-operand contract, refusing every shape
* that cannot work.
*
* **The leader is passed separately from the receivers on purpose.** The rectangle is what
* the hardware broadcasts into, and the leader may be either inside it (the natural
* cooperating-matmul layout: N cores, one of them also does the DRAM read, so
* `destination_count` is N-1 because a non-loopback multicast does not write to self) or
* outside it (a dedicated feeder core, `destination_count` is N). Folding the leader into
* the member array would have made the first shape the only expressible one, and the
* second is exactly what tests/test_multicast_bench.c needs so that both arms of its A/B
* run on the SAME set of cores.
*
* Refuses: receivers that are not a rectangle (ft_tt_multicast_rectangle_from_cores());
* a rectangle that leaves zero destinations once the leader is excluded; an operand that is
* zero-length, mis-aligned or outside L1; a semaphore that is not 4-byte aligned; and ANY
* overlap between the operand region, the two semaphores and the valid word — because the
* leader multicasts into the arrival semaphore and into the operand region in the same
* round, and an overlap makes one of them silently destroy the other.
*
* @param receivers TRANSLATED coordinates, from ft_tt_coord.h against live telemetry
* @param receiver_count how many; at least 1
* @param leader the core that reads the operand and multicasts it
* @param out written only on success
* @return FT_TT_OK, FT_TT_EINVAL, or FT_TT_ERROR (the reason is logged)
*/
int ft_tt_group_shared_operand_bind(const ft_tt_noc_coord *receivers, uint32_t receiver_count,
ft_tt_noc_coord leader,
const ft_tt_group_shared_operand_layout *layout,
ft_tt_group_shared_operand *out);
/**
* Put the barrier into its start-of-run state on the card: the leader's ready semaphore
* to 0, every member's arrival semaphore to INVALID, and the leader's valid word to VALID.
*
* Call it before every run, not once at setup. A ready semaphore left non-zero by an
* abandoned run lets the next leader believe receivers are ready that have not booted —
* and unlike the completion block, whose zero is not a sequence any descriptor may carry,
* a stale semaphore value is INDISTINGUISHABLE from a fresh one. That is why this is a
* reset rather than an init.
*
* @param noc an accessor over a claimed window
* @param receivers the same array passed to ft_tt_group_shared_operand_bind()
* @param receiver_count the same count
* @return FT_TT_OK, or the first failing write's code (logged)
*
* Thread-safe: no Reentrant: no Signal-safe: no
*/
int ft_tt_group_shared_operand_reset(const ft_tt_noc_access *noc,
const ft_tt_group_shared_operand *operand,
const ft_tt_noc_coord *receivers,
uint32_t receiver_count);
#endif /* FT_TT_GROUP_SHARED_OPERAND_H */