215 lines
12 KiB
C
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 */
|