Foundry/tt/ft_tt_dram_stripe.h

194 lines
10 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_dram_stripe.h
* @brief Which DOOR of a DRAM bank each concurrent reader uses — the port/row rotation that
* turns one saturated endpoint into several, with no data moved and no kernel change.
*
* ---------------------------------------------------------------------------
* THE STRUCTURAL FACT THIS MODULE EXISTS FOR
* ---------------------------------------------------------------------------
* A Blackhole GDDR bank exposes THREE NoC ports, and they are **three doors into ONE memory,
* not three memories** (ft_tt_coord.h; the west/east split is
* ft_tt_arch_blackhole.c:93). The same byte at the same DRAM address is reachable through
* port 0, 1 or 2. **So choosing a port moves no data.** It is purely a choice of endpoint
* coordinate, which makes it the cheapest bandwidth lever in the backend: an addressing
* change, host-side, that no kernel and no weight layout can see.
*
* ---------------------------------------------------------------------------
* THE THREE CEILINGS, MEASURED 2026-08-14 (plan Phase 6.7)
* ---------------------------------------------------------------------------
* ENDPOINT ~45 GB/s one (bank, port) pair, however many readers aim at it. NoC-side
* NoC ROW ~60 GB/s SHARED by every endpoint on one NoC y. Two endpoints in one COLUMN
* do not interact at all; two on one ROW share this
* BANK ~64 GB/s the memory itself. Two ports of one bank already reach 58.5 of it,
* so the third door buys little per bank and a lot per card
* CARD 502 GB/s 8 x 63.9, with no room left for a NoC term at the card level
*
* A reader set aimed at ONE port of a bank is therefore capped at ~45 GB/s while the bank
* behind it can serve ~64. Rotating the port across the readers lifts the cap to the bank's
* own limit. Doing the same across all 8 banks measured **216 -> 296.8 GB/s, +33%, for free**
* — because a single port INDEX occupies only 4 of the 12 NoC rows (port p of bank b and of
* bank b+4 share a y), and rotating spreads the traffic over distinct rows.
*
* ---------------------------------------------------------------------------
* WHAT THIS MODULE IS NOT
* ---------------------------------------------------------------------------
* It does not stripe an operand across BANKS. That requires the bytes to actually live in
* more than one bank, so it is a placement decision a LOADER makes — `ft_tt_dram_shard.h`,
* which calls this module for the doors once it has decided the banks. The rotation here
* raises a per-operand ceiling from one endpoint (~45) to one bank (~64); bank striping is
* what raises it past that.
*
* ---------------------------------------------------------------------------
* THE LADDER'S FIRST TWO RUNGS, MEASURED IN ISOLATION 2026-08-15
* ---------------------------------------------------------------------------
* N Tensix cores reading the SAME 96 KiB block from the SAME bank at the SAME address, the
* arms differing only in which door each reader was handed (tests/test_multicast_bench.c):
*
* N 1 2 3 4 aggregate
* one door 3092 4442 6662 8882 ns 44.3 GB/s from N=2 up
* rotated 3092 3625 4622 6191 ns 63.5-63.8 GB/s from N=3 up
*
* The un-rotated arm is pinned at the ENDPOINT ceiling however many cores it has; the rotated
* arm reaches 99.4% of the BANK's own measured 63.9. N=1 is byte-identical in both, which is
* the control that this is addressing and nothing else.
*
* @status Off-hardware in tests/test_cooperative_matmul_plan.c; MEASURED on the card through
* ft_tt_op_matmul_cooperative.h by tests/test_cooperative_matmul.c, and in isolation by
* tests/test_multicast_bench.c.
* @issues None
* @todo None
*/
#ifndef FT_TT_DRAM_STRIPE_H
#define FT_TT_DRAM_STRIPE_H
#include <stdint.h>
#include "ft_tt_coord.h"
/** Storage capacity for the doors of one bank — NOT a statement about any part. Blackhole
* has 3 (ft_tt_arch_blackhole.c). ft_tt_dram_port_set_build() refuses a grid whose
* architecture declares more, naming both numbers, rather than overflowing the array. */
#define FT_TT_DRAM_STRIPE_MAX_PORTS 4u
/** One door into one bank. */
typedef struct ft_tt_dram_endpoint {
uint32_t logical_bank;
uint32_t port;
/** In the coordinate system the set was built for. TRANSLATED is what a transfer is
* AIMED with; this is the value that reaches a runtime-arg block. */
ft_tt_noc_coord coord;
/**
* NoC 0 y — the row whose ~60 GB/s two endpoints SHARE.
*
* Carried separately and always in NOC 0, because the row limit is a property of the
* physical node and the translated grid is an alias with its own y. Reading a row off a
* translated coordinate would be reading the alias, not the wire.
*/
uint32_t noc_row;
} ft_tt_dram_endpoint;
/** Every door of one bank, in port order. */
typedef struct ft_tt_dram_port_set {
uint32_t logical_bank;
uint32_t port_count;
ft_tt_dram_endpoint port[FT_TT_DRAM_STRIPE_MAX_PORTS];
} ft_tt_dram_port_set;
/**
* Resolve every NoC port of one DRAM bank, from LIVE telemetry.
*
* Both coordinate systems are asked for per port: `coord` in `system` (what a transfer is
* aimed with) and `noc_row` always in NOC 0 (what the row-sharing rule is expressed in).
* Nothing here is probed or hardcoded — a wrong DRAM endpoint does not fail a read, it hangs
* the chip (DEV_NOTES.md §1).
*
* @param grid topology from ARC telemetry
* @param logical_bank 0 .. ft_tt_coord_dram_bank_count()-1
* @param system the space `coord` is wanted in; usually FT_TT_COORD_TRANSLATED
* @param out written only on success
* @return FT_TT_OK; FT_TT_EINVAL on a NULL argument or a bank out of range;
* FT_TT_ENOMEM if the architecture declares more ports than this module can hold;
* otherwise as ft_tt_coord_dram_port(). Every reason is logged
*
* Thread-safe: yes Reentrant: yes Signal-safe: no (logs on failure)
*/
int ft_tt_dram_port_set_build(const ft_tt_grid_info *grid, uint32_t logical_bank,
ft_tt_coord_system system, ft_tt_dram_port_set *out);
/**
* The door reader `reader_index` of a concurrent group should use: `reader_index % port_count`.
*
* Round-robin rather than anything cleverer, because the measured rule is BALANCE: the
* outstanding-read budget is per endpoint, and an unbalanced map cost 30% and was worse than
* either neighbouring balanced reader count (plan Phase 6.7). Modulo is the assignment that
* differs by at most one reader between doors for every group size.
*
* @return the endpoint, or NULL if `set` is NULL or empty
*
* Thread-safe: yes Reentrant: yes Signal-safe: yes
*/
const ft_tt_dram_endpoint *ft_tt_dram_port_set_reader_endpoint(const ft_tt_dram_port_set *set,
uint32_t reader_index);
/**
* How many pairs of the given endpoints sit on ONE NoC row — the measured ~60 GB/s two
* endpoints share where two in one column do not interact at all.
*
* DISTINCT endpoints only: the same (bank, port) appearing twice is one endpoint used by two
* readers, which is the normal case and not a collision.
*
* This is a PERFORMANCE fact, not a correctness one — a colliding pair returns the right
* bytes, slowly — so it is reported rather than refused, and the caller decides. Anything
* that plans concurrent traffic should check it and say so in its own report.
*
* @param endpoints the endpoints in concurrent use
* @param count how many
* @param out_first_left may be NULL; index of the first colliding pair's first member
* @param out_first_right may be NULL; and its second
* @return the number of colliding DISTINCT pairs; 0 on a NULL/empty array
*
* Thread-safe: yes Reentrant: yes Signal-safe: yes
*/
uint32_t ft_tt_dram_stripe_shared_rows(const ft_tt_dram_endpoint *endpoints, uint32_t count,
uint32_t *out_first_left, uint32_t *out_first_right);
/**
* Choose a DOOR for each reader whose BANK is already decided by where its bytes live.
*
* This is the assignment problem the two measured striping rules pose together, and neither
* can be satisfied by a formula:
*
* ROWS two endpoints on one NoC y share ~60 GB/s, and port p of bank b and of bank b+4
* ARE one row — so `port = reader % 3` collides as soon as the bank list contains
* a b and a b+4, which any list of more than four banks does.
* BALANCE the outstanding-read budget is per endpoint, and an unbalanced map measured 30%
* worse than either neighbouring balanced core count.
*
* Greedy in reader order, deterministic: each reader takes the door of its own bank that adds
* the least contention, and the cost order is the LADDER's, not the intuitive one — a fresh
* door on a used ROW beats a used DOOR on a fresh row, because two streams through one
* endpoint share ~45 GB/s where two endpoints of one row share ~60.
*
* Row collisions are therefore expected, not a failure: this part has 24 DRAM endpoints on
* only 12 rows, so any assignment of more than 12 concurrent streams MUST double up. The
* caller checks with ft_tt_dram_stripe_shared_rows() and says what it found.
*
* @param grid topology from ARC telemetry
* @param banks `count` logical bank indices, one per reader, in reader order
* @param count readers; 0 is refused
* @param system the space `coord` is wanted in; usually FT_TT_COORD_TRANSLATED
* @param out `count` endpoints, written only on success
* @return FT_TT_OK; FT_TT_EINVAL on a NULL argument or a zero count; otherwise as
* ft_tt_dram_port_set_build()
*
* Thread-safe: yes Reentrant: yes Signal-safe: no (logs on failure)
*/
int ft_tt_dram_stripe_choose_ports(const ft_tt_grid_info *grid, const uint32_t *banks,
uint32_t count, ft_tt_coord_system system,
ft_tt_dram_endpoint *out);
#endif /* FT_TT_DRAM_STRIPE_H */