/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file resident_matmul_multicast_protocol.h * @brief The host<->device wire contract for a RESIDENT matmul triad whose shared A block * is MULTICAST once per block instead of read from DRAM by every member: the roles, the * runtime-arg tail, the packed rectangle word, and the refusal codes. * * **Included by BOTH sides verbatim**, like cooperative_matmul_protocol.h and for the * same reason: every wire contract in this directory that was duplicated is one edit * away from a kernel reading a different word than the host wrote. So this header * contains NOTHING but preprocessor definitions, enums and pure inline arithmetic — no * includes beyond , no types that need one. * * --------------------------------------------------------------------------- * WHAT THIS IS FOR, IN ONE NUMBER * --------------------------------------------------------------------------- * At Mt = 1 the matmul reader re-reads the whole A column once per output column, so A is * **1.76 GB of the 2.70 GB a token reads — 65%** — and every member of a split group reads * it from the ONE DRAM bank holding that strip. That is why four cores buy 3.44x and four * times more cores buy only 1.4x more, and why N=16 is byte-exact and SLOWER end to end. * A multicast turns the A side from O(N) reads of that bank into O(1). * * The mechanism already existed as `cooperative_matmul_reader.c` and was unreachable: it * is a ONE-SHOT program, and running it per op would put the image upload back at 218 µs * per member per op — **147 ms a token at N=4** against a 0.109 s token. This header is * that rendezvous expressed as a tail on the RESIDENT queue's contract, so it costs a * descriptor rather than a program load. * * --------------------------------------------------------------------------- * ceil(RECEIVERS / 4) PACKETS, NOT ONE * --------------------------------------------------------------------------- * The standing hazard is **never more than four destinations in one multicast packet**. * Measured twice on silicon: a 5-destination broadcast leaves every receiver AND the * sender able to accept re-launches indefinitely, and then wedges a core at the NEXT * firmware boot (`0x00047000`, BRISC stalled inside `device_setup()`); N=8 issued as two * disjoint <=4-destination broadcasts through that same park-and-reboot shape came back * 20/20 PASS. The cause is believed to be our per-run teardown rather than the packet, and * the rule stands until a residency that is NEVER re-booted proves otherwise. * * So the leader carries a PARTITION of its receivers — up to * FT_TT_RESIDENT_COOP_MAX_GROUPS rectangles, each naming at most four destinations, built * host-side by ft_tt_group_broadcast_partition.h. At N=4 that is one packet; at N=8, two. * * The per-block order is one flush for the whole partition, not one per piece: * * LEADER wait ready == receivers -> ready = 0 -> stage the block counter -> * read A and its own B from DRAM -> read barrier -> * payload multicast to piece 0..G-1 -> WRITES-FLUSHED BARRIER -> * counter multicast into every piece's arrival semaphore -> push both CBs * RECEIVER arrival = INVALID -> issue its own B read -> increment the leader's ready -> * wait arrival == this block's counter -> read barrier -> push both CBs * * The flush between the payload and the semaphore is **mandatory on Blackhole and only * Blackhole**: the two use different command-buffer FIFOs which are not guaranteed to * drain in issue order, so without it a receiver can be told the block arrived before it * did and read the PREVIOUS block's bytes with nothing reporting an error. ONE flush * covers all G payloads because it waits for requests SENT, and the pieces share one NoC * and one static VC — once every payload request has left, ordering behind it is the * network's job. * * --------------------------------------------------------------------------- * THE ARRIVAL WORD CARRIES THE BLOCK COUNTER, NOT A CONSTANT `VALID` * --------------------------------------------------------------------------- * This is the one deliberate divergence from upstream's protocol and from * cooperative_matmul_protocol.h, and it exists because a RESIDENT triad can drift in a way * a one-shot program cannot. * * A multicast writes ONE address into every destination, so the A block must land at the * same L1 address on every member — which it does only while every member has pushed the * same number of CB_IN0 pages since launch. In a one-shot program that is guaranteed by * construction. In a residency it is a property of the WHOLE DESCRIPTOR STREAM: one * descriptor executed by some members and not others, or split unevenly so members walk * different block counts, desynchronises the CB write pointers permanently and every later * multicast lands in the wrong half of the receivers' buffers. * * Two things stop that, and they are prevention and detection rather than one hopeful * check: * * - **Prevention, host-side and structural:** cooperation is ALL-OR-NOTHING for a * residency. A non-SOLO member that is handed a descriptor with the cooperate word * clear refuses it (kErrCoopRoleMismatch) instead of quietly running it solo, so a * group cannot drift one descriptor at a time. The host enables cooperation only when * EVERY weight splits evenly across the group. * - **Detection, device-side:** the leader multicasts its own cooperative block counter * and each receiver waits for the value IT expects. A desynchronised group then times * out on a bounded, askable wait — a refusal the host can see and re-arm from — instead * of computing a wrong answer that every status word calls clean. The counter starts at * 1 and skips 0 on wrap, so it can never collide with FT_TENSIX_SEMAPHORE_INVALID. * * Each member additionally checks its own CB_IN0 write pointer against the position its * own counter says it must be at, which catches the local half of the same failure before * anything is issued. * * @status Wire contract for resident_matmul_reader.c's cooperative path. * @issues None * @todo None */ #ifndef FT_TT_RESIDENT_MATMUL_MULTICAST_PROTOCOL_H #define FT_TT_RESIDENT_MATMUL_MULTICAST_PROTOCOL_H #include /* ------------------------------------------------------------------------------------ * Roles. One binary plays all three; the role is a runtime arg, fixed for the residency. * * The values are cooperative_matmul_protocol.h's FTCM_ROLE_*, deliberately, so a reader * moving between the one-shot kernel and this one is never reading two spellings of the * same three states. tests/test_group_broadcast_partition.c does not assert this — the * two headers are independent contracts — but nothing gains from them disagreeing. * ---------------------------------------------------------------------------------- */ /** No cooperation: the descriptor's A block comes from DRAM, exactly as it always has. * This is what a member_count == 1 residency runs and what every member ran before. */ #define FT_TT_RESIDENT_COOP_ROLE_SOLO 0u /** Reads the shared A block from DRAM and multicasts it to every piece of the partition. * Computes a slice of its own like any other member. */ #define FT_TT_RESIDENT_COOP_ROLE_LEADER 1u /** Receives the shared A block; reads only its own B panel from DRAM. */ #define FT_TT_RESIDENT_COOP_ROLE_RECEIVER 2u /* ------------------------------------------------------------------------------------ * What the descriptor's cooperate word says. * * Three values rather than a flag, because the RED PROOF has to live on the shipping path. * A red build would prove its claim about a different program; a value in a word the real * dispatch already writes proves it about this one — the same argument PN_TT_RED_SPLIT and * PN_TT_RED_BAD_DESCRIPTOR are built on. * ---------------------------------------------------------------------------------- */ /** The A block comes from DRAM. What every descriptor written before this word existed * says, and what a member_count == 1 residency runs. */ #define FT_TT_RESIDENT_COOP_DESCRIPTOR_SOLO 0u /** The leader reads A once and multicasts it. */ #define FT_TT_RESIDENT_COOP_DESCRIPTOR_COOPERATE 1u /** * RED PROOF ONLY: cooperate, but the leader SKIPS the payload multicast and still tells * the receivers the block arrived. Every receiver then computes from the PREVIOUS block's * bytes. * * It proves two things at once, which is why it is the one red arm worth having here: * - the byte comparison can still go red on the cooperative path; * - the multicast is the ONLY source of A on a receiver. If a receiver were somehow still * reading A from DRAM — a leftover read, a mis-set role, a branch that did not fold — * this arm would produce IDENTICAL output, and identical output is exactly what a * vacuous gate looks like. */ #define FT_TT_RESIDENT_COOP_DESCRIPTOR_RED_SKIP_PAYLOAD 2u /** Most pieces one leader's partition may hold. Four rectangles of four destinations * reach sixteen members, which is FT_TT_OP_COOP_MAX_MEMBERS. */ #define FT_TT_RESIDENT_COOP_MAX_GROUPS 4u /** The standing operational limit on destinations in ONE packet, restated device-side so * the kernel refuses a host that ignored it rather than trusting the plan. */ #define FT_TT_RESIDENT_COOP_MAX_DESTINATIONS 4u /* ------------------------------------------------------------------------------------ * The runtime-arg TAIL, read ONLY by the matmul reader. * * Args 0..9 are resident_queue_protocol.h's, identical on all five processors, and * `ftq_context_init()` reads exactly those ten in a statement order that is load-bearing * for the Phase 6.6 acceptance diff. Nothing here is read there: the cooperative tail has * its own reader in the matmul kernel, so the other four resident families' images are * untouched by this file existing. * * The whole tail is fixed for the life of a residency — a group's roles, geometry and * semaphore addresses do not change between descriptors — so it belongs in the arg block * and not in the 16-word parameter block, which is per descriptor and already full. * ---------------------------------------------------------------------------------- */ enum { kArgCoopRole = 10 }; enum { kArgCoopLeaderX = 11 }; enum { kArgCoopLeaderY = 12 }; /** On the LEADER. Receivers increment it; the leader waits for it to reach the receiver * count and then clears it. The same L1 address on every member, which is what lets one * arg block serve all three roles. */ enum { kArgCoopReadySemaphore = 13 }; /** On EVERY member, the same address: the word the leader multicasts the block counter * into. */ enum { kArgCoopArrivalSemaphore = 14 }; /** On the LEADER. The word it multicasts FROM, restaged with the block counter each * block. */ enum { kArgCoopCounterWord = 15 }; /** Receivers the leader waits for, EXCLUDING itself. Equals the partition's destination * total, and the host is required to have checked that they agree. */ enum { kArgCoopReceiverCount = 16 }; /** Pieces in the partition, 1..FT_TT_RESIDENT_COOP_MAX_GROUPS. */ enum { kArgCoopGroupCount = 17 }; /** The pieces, packed one word each — see ftcoop_pack_rectangle(). */ enum { kArgCoopGroup0 = 18 }; enum { kArgCoopGroup1 = 19 }; enum { kArgCoopGroup2 = 20 }; enum { kArgCoopGroup3 = 21 }; /** Spins one rendezvous wait takes before abandoning. Never 0: an unbounded wait is the * one thing a resident kernel may not be configured into. */ enum { kArgCoopSpinLimit = 22 }; enum { kArgCoopCount = 23 }; /* ------------------------------------------------------------------------------------ * The packed rectangle word * * Four 6-bit coordinates and a destination count in one 32-bit arg, because the arg block * is 32 words and five words per piece would not fit four pieces beside the ten the queue * already takes. Six bits is not a packing choice: it is the width of the NoC node-id * field, and a coordinate that does not fit does not fail — it TRUNCATES onto a different * live endpoint, which on this board is a hung NoC and a walk to the machine. * ---------------------------------------------------------------------------------- */ #define FT_TT_RESIDENT_COOP_COORDINATE_BITS 6u #define FT_TT_RESIDENT_COOP_COORDINATE_MASK 0x3Fu #define FT_TT_RESIDENT_COOP_COUNT_SHIFT 24u #define FT_TT_RESIDENT_COOP_COUNT_MASK 0xFFu /** Pack one piece. Returns 0 — never a legal piece, because a legal piece names at least * one destination — when any field is out of range, so a caller that does not check gets * a word the kernel refuses rather than a rectangle at the wrong place. */ static inline uint32_t ftcoop_pack_rectangle(uint32_t x_low, uint32_t y_low, uint32_t x_high, uint32_t y_high, uint32_t destination_count) { if (x_low > x_high || y_low > y_high) return 0u; if (x_high > FT_TT_RESIDENT_COOP_COORDINATE_MASK || y_high > FT_TT_RESIDENT_COOP_COORDINATE_MASK) return 0u; if (destination_count == 0u || destination_count > FT_TT_RESIDENT_COOP_MAX_DESTINATIONS) return 0u; return x_low | (y_low << FT_TT_RESIDENT_COOP_COORDINATE_BITS) | (x_high << (2u * FT_TT_RESIDENT_COOP_COORDINATE_BITS)) | (y_high << (3u * FT_TT_RESIDENT_COOP_COORDINATE_BITS)) | (destination_count << FT_TT_RESIDENT_COOP_COUNT_SHIFT); } static inline uint32_t ftcoop_rectangle_x_low(uint32_t packed) { return packed & FT_TT_RESIDENT_COOP_COORDINATE_MASK; } static inline uint32_t ftcoop_rectangle_y_low(uint32_t packed) { return (packed >> FT_TT_RESIDENT_COOP_COORDINATE_BITS) & FT_TT_RESIDENT_COOP_COORDINATE_MASK; } static inline uint32_t ftcoop_rectangle_x_high(uint32_t packed) { return (packed >> (2u * FT_TT_RESIDENT_COOP_COORDINATE_BITS)) & FT_TT_RESIDENT_COOP_COORDINATE_MASK; } static inline uint32_t ftcoop_rectangle_y_high(uint32_t packed) { return (packed >> (3u * FT_TT_RESIDENT_COOP_COORDINATE_BITS)) & FT_TT_RESIDENT_COOP_COORDINATE_MASK; } static inline uint32_t ftcoop_rectangle_destinations(uint32_t packed) { return (packed >> FT_TT_RESIDENT_COOP_COUNT_SHIFT) & FT_TT_RESIDENT_COOP_COUNT_MASK; } /** The FIRST cooperative block counter. Never FT_TENSIX_SEMAPHORE_INVALID (0), because the * receiver clears its arrival word to that value before announcing readiness and a * counter that could equal it would let a stale clear read as an arrival. */ #define FT_TT_RESIDENT_COOP_FIRST_COUNTER 1u /** The next counter, skipping 0 on wrap for the reason above. */ static inline uint32_t ftcoop_next_counter(uint32_t counter) { const uint32_t next = counter + 1u; return next == 0u ? FT_TT_RESIDENT_COOP_FIRST_COUNTER : next; } #endif /* FT_TT_RESIDENT_MATMUL_MULTICAST_PROTOCOL_H */