804 lines
46 KiB
C
804 lines
46 KiB
C
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
//
|
|
// resident_gdn_protocol.h
|
|
// The sixteen PROGRAM words the GATED-DELTA-NET op family reads out of its parameter
|
|
// block. The host half is ft_tt_op_gdn.h; the two must agree word for word, and
|
|
// tests/test_gdn_ops.c parses BOTH declarations off-hardware and asserts they do.
|
|
//
|
|
// ===========================================================================
|
|
// WHAT THE FAMILY IS, AND WHY IT IS AN OPCODE RATHER THAN A COMPOSITION
|
|
// ===========================================================================
|
|
// Qwen3.5's linear layers are a Gated-Delta-Net. Per SSM layer, per decode token, with
|
|
// S = 128 the state size, 16 k-heads and 32 v-heads (Pantheon's qwen35moe_ssm.c):
|
|
//
|
|
// conv1d+SiLU over the whole conv_dim stream, primed from a rolling window
|
|
// split into q | k | v, L2-normalise each q/k head over S, GQA-repeat q/k to 32 heads
|
|
// per v-head h, with decay = exp(g_h) and beta_h:
|
|
// (a) S *= decay scalar over the whole [S,S]
|
|
// (b) pred_j = sum_i S[j][i] * k[i] a row reduce
|
|
// (c) delta_j = (v_j - pred_j) * beta
|
|
// (d) S[j][i] += k[i] * delta_j rank-1
|
|
// (e) y_j = scale * sum_i S[j][i] * q[i] READ AFTER THE WRITE
|
|
//
|
|
// Pantheon stores the state TRANSPOSED (s[h][j][i], row j contiguous over i) and that
|
|
// layout is what makes it a tile machine's problem: with rows=j and cols=i, (a) is a
|
|
// broadcast multiply, (b) and (e) are ROW REDUCES and (d) is a broadcast multiply plus an
|
|
// add. No new primitive is needed for any of them.
|
|
//
|
|
// It is nonetheless its own opcode for two reasons that are not tidiness:
|
|
//
|
|
// - DISPATCH COUNT. 32 heads x 4 tile-rows expressed as separate descriptors is ~3,800
|
|
// dispatches a token. The thing this op exists to remove is 0.225 s of host
|
|
// recurrence; 3,800 dispatches would cost more than that. The head/tile-row loops are
|
|
// INSIDE the kernel.
|
|
// - IT CARRIES STATE ACROSS TOKENS. Two of its operands are read AND written -- the conv
|
|
// window and the [S,S] fp32 state per v-head. Every other op in this backend is a pure
|
|
// function of its inputs. A wrong answer here does not stay wrong for one token, it
|
|
// compounds, which is why the acceptance gate is a 48-token completion.
|
|
//
|
|
// ===========================================================================
|
|
// (b) AND (e) ARE THE SHIPPED REDUCE, AND THAT IS MEASURED RATHER THAN ASSUMED
|
|
// ===========================================================================
|
|
// `ft_tensix_unpack_ab_reduce_row(data_cb, scaler_cb, data_tile, scaler_tile)` takes an
|
|
// INDEPENDENT scaler tile index per call; rms_norm passes 0 every iteration only because
|
|
// its scaler is one constant 1/W tile. Whether the scaler contributes PER REDUCE POSITION
|
|
// or as one broadcast constant decided whether (b) and (e) needed a kernel at all.
|
|
//
|
|
// MEASURED ON SILICON 2026-08-18 (tests/test_ops.c, FT_TT_OPS=reduce_scaler): the scaler is
|
|
// PER-POSITION. A 32-element non-uniform scaler seeded from pid+clock was recovered element
|
|
// for element, in the same index order, worst relative error 0.0077 and 0.0063 on two
|
|
// independent seeds, against 0.88 for the broadcast reading. The uniform-1/32 control
|
|
// recovered 1/32 on all 32 rows to 0.0002, so the probe could have failed and did not.
|
|
//
|
|
// So (b) is `reduce_row(cb_state, cb_k, tile, tile)` and (e) is the same call with q in the
|
|
// scaler CB. The scaler for those arms is 1.0 rather than 1/W. NO elementwise product and no
|
|
// pre-broadcast. q/k are staged ROW-REPLICATED, not as column tiles: the probe's own scaler
|
|
// tile carried s[c] at every [r][c], so it never tested the axis, and a row-replicated tile
|
|
// is ALSO the (d) rank-1 operand -- one staged tile for two uses.
|
|
//
|
|
// ===========================================================================
|
|
// WHY THERE ARE TWO PHASES AND NOT ONE MONOLITH
|
|
// ===========================================================================
|
|
// The two per-head norms in this block -- the q/k L2 norm and the output gated norm -- are
|
|
// 4 tile columns wide, over 16 and 32 row blocks. Every RMSNorm elsewhere in this model is
|
|
// n_embd/32 = 128 tile columns, and a reduce residency is placed for exactly ONE width
|
|
// (Pantheon's pn_tt_rms_norm refuses a mismatch by design). They therefore cannot run on
|
|
// the model's own norm core -- but they are ORDINARY RMS NORMS at a second width, not new
|
|
// arithmetic, so they belong to the shipped reduce family on a second narrow residency
|
|
// rather than inside this kernel. `L2(x) = x/sqrt(sum x^2)` and `RMSNorm(x,g) =
|
|
// x*g*rsqrt(mean x^2)` are the same function at `g = 1/sqrt(S)`.
|
|
//
|
|
// That leaves this family two phases, `kGdnPhaseConv` and `kGdnPhaseDelta`, run as two
|
|
// descriptors a layer with the narrow norms between and after them. Five descriptors a
|
|
// layer, ~120 a token -- which honours the dispatch-count reason above completely, since
|
|
// the thing that would have cost 3,800 was the per-head loop and that is still inside.
|
|
//
|
|
// THE SPLIT-POINT TABLE DOES NOT APPLY TO THIS SPLIT. Plan Phase 7.9 records that moving
|
|
// conv1d alone buys 1.00x and conv1d+L2+GQA is 1.33x WORSE. That table is about where the
|
|
// HOST takes over, and it is about the PCIe link. Here the host never takes over mid-block:
|
|
// q/k/v round-trip through CARD DRAM between phases, which is on-card bandwidth, not the
|
|
// 5.2 MB/s link. Nothing in the table is about a descriptor boundary.
|
|
//
|
|
// ===========================================================================
|
|
// THE ONE THING THAT STAYS ON THE HOST, AND WHY IT IS NOT A COMPROMISE
|
|
// ===========================================================================
|
|
// `decay = exp(A * softplus(z))` = `(1 + e^z)^A`, which needs a `pow` and therefore a
|
|
// `log`. The SFPU surface here is rsqrt / sigmoid / silu / exp / recip / square /
|
|
// binary_max and there is NO log and no pow. So the host computes decay and beta and sends
|
|
// them down: 32 scalars each, 256 B a layer in each direction, ~6 KB a token, ~1.2 ms.
|
|
// Against the 1,185,792 B and 0.225 s a host recurrence costs, that is 0.5%, and every
|
|
// value still crosses the link exactly once.
|
|
//
|
|
// ===========================================================================
|
|
// A CIRCULAR BUFFER MUST CLOSE ON THE CONSUMER'S INDEXED READ, NOT ONLY ON THE PUSH GRID
|
|
// ===========================================================================
|
|
// `ft_tensix_unpack_ab(cb, ..., tile)` is `read_pointer + tile * page_size` WITH NO WRAP, and
|
|
// a FIFO pointer moves back to its base on an EXACT HIT of the limit and on nothing else. So
|
|
// a compute thread that waits for `n` pages and then names tiles `0..n-1` needs the front low
|
|
// enough that the whole window still fits -- which is `pages % n == 0` for the INDEXED chunk
|
|
// of EVERY phase the buffer serves, on top of the push-grid rule
|
|
// `ft_tensix_cb_push_grid_is_safe()` already states for the producer.
|
|
//
|
|
// AND `pages % n == 0` IS NOT SUFFICIENT ON ITS OWN for a RESIDENT op. A descriptor's traffic
|
|
// is not a whole number of buffers -- this family moves 512 CB_STATE pages in PHASE DELTA and
|
|
// 1,280 in PHASE CONV -- so descriptor N+1 starts wherever N left off. The three kernels
|
|
// therefore REBASE every indexed buffer to its base at the top of every descriptor, the
|
|
// producers behind a whole-buffer reservation and the consumers unguarded (they have already
|
|
// popped everything). `ft_tensix_cb_rebase_{read,write}_pointer()` carries the argument.
|
|
//
|
|
// MEASURED ON SILICON 2026-08-18 as this family's whole wrong answer, on two buffers at once.
|
|
// CB_VECTOR held 8 pages against a DELTA window of `state_tiles + kGdnVectorConstants` = 7, so
|
|
// 24 of 32 v-heads read their v / decay / beta / scale out of the buffer and into CB_STAGE --
|
|
// exactly the 8 heads whose front was 0 or 1 scored 4e-3 against the position-0 closed form
|
|
// and the rest scored 1.0. CB_STATE held 8 against a CONV window of `depth*(taps-1)` = 3, so
|
|
// 2 blocks in 8 read past the end; that one is invisible at layer 0 of token 0 ALONE, because
|
|
// the L1 above CB_STATE is CB_SCALER and it is still zero until the first DELTA descriptor
|
|
// writes k/q replicas into it -- conv scored 6.9e-4 at layer 0 and 1e-2 to 2e-1 after it.
|
|
//
|
|
// ===========================================================================
|
|
// A BUFFER HAS ONE PRODUCER FOR THE LIFE OF THE RESIDENCY, NOT ONE PER DESCRIPTOR
|
|
// ===========================================================================
|
|
// The two push primitives keep INCOMPATIBLE BOOKS for the same `tiles_received` stream
|
|
// register. `ft_tensix_cb_push_back()` on a dataflow processor does `register += n` and keeps
|
|
// no local copy. `ft_tensix_compute_cb_push_back()` on a TRISC keeps a LOCAL
|
|
// `tiles.counters.tiles_received`, adds to that, and STORES it over the register. So the
|
|
// moment a buffer is pushed by the reader in one phase and by the compute thread in another,
|
|
// the compute's first push discards every increment the reader ever made, the register goes
|
|
// BACKWARDS, and the consumer's `(uint16_t)(received - acked)` in the stock wait-for-tiles routine wraps to
|
|
// ~65000: `cb_wait_front` on that buffer returns immediately for the rest of the run.
|
|
//
|
|
// MEASURED ON SILICON 2026-08-18. PHASE DELTA's compute-private working state shared CB_IN
|
|
// with PHASE CONV's qkv stream. PHASE CONV's PASS A survived, because it also waits on
|
|
// CB_VECTOR and CB_STATE and the reader pushes those AFTER the qkv read under the same
|
|
// barrier; PASS B's last tap waits on CB_IN and nothing else, so the compute thread free-ran
|
|
// and copied whatever the eight pages held. With an INJECTIVE stream staged (`qkv[c] = c`)
|
|
// the map read `s = t (mod 8)` for source tile `s` of output tile `t` at all 256 tiles, the
|
|
// lag growing from 8 tiles to 96 as the compute outran the reader. The one descriptor in a
|
|
// run that was CORRECT was SSM layer 0 at position 0 -- the only CONV never preceded by a
|
|
// DELTA. PHASE DELTA's working state is CB_WORK now.
|
|
//
|
|
// ===========================================================================
|
|
// THE GQA REPEAT IS AN ADDRESS COMPUTATION AND MUST NEVER BE MATERIALISED
|
|
// ===========================================================================
|
|
// The repeat is INTERLEAVED: `vh = r * n_k_heads + kh`, i.e. `kh = vh % n_k_heads`, NOT a
|
|
// blocked mapping -- Pantheon's own docs name a real gibberish bug from getting this
|
|
// backwards. In the reader it is one modulo on a head index: v-head `vh` stages the k/q
|
|
// columns of k-head `vh % n_k_heads`. Materialising it would DOUBLE q and k, which is the
|
|
// 1.33x-worse row of the split-point table.
|
|
//
|
|
#ifndef FOUNDRY_RESIDENT_GDN_PROTOCOL_H
|
|
#define FOUNDRY_RESIDENT_GDN_PROTOCOL_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifndef __cplusplus
|
|
#include <stdbool.h>
|
|
#endif
|
|
|
|
/* The SFPI GCC fork's own pointer attribute -- an access through it goes to L1 rather than
|
|
* to ordinary memory. Spelled here as well as in resident_queue_protocol.h so this header
|
|
* does not depend on include order. */
|
|
#ifndef FTQ_L1_PTR
|
|
#define FTQ_L1_PTR __attribute__((rvtt_l1_ptr))
|
|
#endif
|
|
|
|
#ifndef FTQ_INLINE
|
|
#ifdef __cplusplus
|
|
#define FTQ_INLINE inline
|
|
#else
|
|
#define FTQ_INLINE static inline
|
|
#endif
|
|
#endif
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The sixteen words. Mirrors ft_tt_op_gdn.h -- change both or neither.
|
|
//
|
|
// WHY THE COORDINATES ARE PACKED TWO TO A WORD, stated because every other family in this
|
|
// backend spends a word each. This op has FIVE DRAM endpoints (an activation in, an
|
|
// activation out, two per-layer constants and two pieces of carried state) where rope has
|
|
// three. Spelled the rope way that is 5 pairs + 6 addresses + shape = 17 words, and
|
|
// FT_TT_QUEUE_PARAM_WORDS is 16 for EVERY family -- a seventeenth word changes the shared
|
|
// slot stride and breaks on the card. The pack is forced, not preferred, and x/y are 16-bit
|
|
// NoC coordinates so nothing is lost.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** byte 0 phase, byte 1 state tiles (S/32), byte 2 v-heads, byte 3 k-heads. */
|
|
enum { kGdnShape = 0 };
|
|
|
|
/** Tile columns of the conv_dim stream: conv_dim/32. A full word -- 8192/32 = 256 does not
|
|
* fit in the byte the other shape fields use, and a shape that silently truncates is the
|
|
* failure this family can least afford. */
|
|
enum { kGdnConvTileCols = 1 };
|
|
|
|
/** byte 0 conv taps (ssm_conv, 4); the rolling window is taps-1 deep. */
|
|
enum { kGdnConvTaps = 2 };
|
|
|
|
/** Activation bank: x in the low half-word, y in the high.
|
|
*
|
|
* **ONE WORD FOR TWO BUFFERS, SO THE INPUT AND THE OUTPUT ARE ONE DRAM ENDPOINT.** That is
|
|
* a CONTRACT the stager has to honour and NOTHING ON THE DEVICE CAN CHECK: the reader reads
|
|
* the input's bank correctly and the writer then puts the answer at the OUTPUT's offset
|
|
* inside that same bank. The descriptor completes, the retire is clean, the counters are
|
|
* right, and the output buffer is never written at all — the host reads back whatever it was
|
|
* allocated with. Measured on silicon 2026-08-18 as the whole of this op's first wrong
|
|
* answer, and named only because the conv result came back bit-identical with the compute
|
|
* kernel's SiLU deleted. `ft_tt_op_gdn_parameters()` refuses a mismatched pair. */
|
|
enum { kGdnActivationXy = 3 };
|
|
/** Phase CONV: the qkv projection, one tilized row of `conv_tile_cols` tiles.
|
|
* Phase DELTA: the staged q|k|v, three tilized matrices -- see the layout section. */
|
|
enum { kGdnActivationIn = 4 };
|
|
/** Phase CONV: conv_out. Phase DELTA: y, one row per v-head. */
|
|
enum { kGdnActivationOut = 5 };
|
|
|
|
/** Per-layer constants bank. */
|
|
enum { kGdnConstantXy = 6 };
|
|
/** Phase CONV only: the depthwise conv1d weight, as `taps` TAP-MAJOR PLANES of
|
|
* `conv_tile_cols` tiles -- NOT the oracle's row-major [conv_dim, taps]. See the layout
|
|
* section below; the stager transposes. */
|
|
enum { kGdnConstantWeight = 7 };
|
|
/** Phase DELTA only: decay[v_heads] then beta[v_heads], fp32, the host softplus's answer. */
|
|
enum { kGdnConstantGate = 8 };
|
|
|
|
/** Carried-state bank. Both operands below are READ AND WRITTEN by one descriptor. */
|
|
enum { kGdnStateXy = 9 };
|
|
/** Phase CONV only: the rolling conv window, `taps-1` TAP-MAJOR PLANES laid out exactly
|
|
* like the weight. Read AND written, so the roll is layout-preserving. */
|
|
enum { kGdnStateConv = 10 };
|
|
/** Phase DELTA only: the recurrent state, [v_heads, S, S] fp32, TRANSPOSED (row j over i). */
|
|
enum { kGdnStateRecurrent = 11 };
|
|
|
|
/** fp32 bit pattern of the (e) scale, 1/sqrt(S). Sent rather than derived because a
|
|
* reciprocal square root computed two ways is two answers that must agree. */
|
|
enum { kGdnScaleBits = 12 };
|
|
|
|
/** Bytes per tile of the state and activation operands. Float32 tiles are 4096 B where the
|
|
* bf16 the rest of this backend uses are 2048; see the note on formats below. */
|
|
enum { kGdnTileBytes = 13 };
|
|
|
|
/** Reserved for variant bits. Written 0 and never read, exactly as the vector protocol
|
|
* requires of a word a family does not use. */
|
|
enum { kGdnFlags = 14 };
|
|
/** M6 (reader only): build the DELTA phase's replicated k/q tiles with local NoC copies
|
|
* instead of RISC-V stores. Same bytes. */
|
|
enum { kGdnFlagNocReplicate = 8u };
|
|
enum { kGdnFlagRedNoCopy = 16u }; /* RED ONLY: skip the faces 2-3 copy */
|
|
/** M9 (reader + writer): PHASE CONV moves only row 0 of each tile (faces 0-1, 2 x 64 B). Its
|
|
* arithmetic is elementwise, so row 0 is exact whatever rows 1-31 of a CB page hold. */
|
|
enum { kGdnFlagConvStrip = 32u };
|
|
enum { kGdnFlagRedStripHalf = 64u }; /* RED ONLY: the reader skips the face-1 half */
|
|
/** S2 (writer only): write the carried state to the OTHER phase's state word (CONV: word 11,
|
|
* DELTA: word 10) instead of back over the one it read — same bank (word 9). The speculative
|
|
* verify keeps every row's state this way, with no copy. */
|
|
enum { kGdnFlagStateOut = 128u };
|
|
/** S2c (reader + writer): THE ROW SELECT, flags bits 16-20. CONV (strip mode only) reads its
|
|
* stream input from row r and writes its stream output to row r — the window stays row 0;
|
|
* DELTA takes q/k/v from row r of the staged stream and writes y to row r. 0 = the shipped
|
|
* row-0 walk, byte for byte. Lets a k-row verify run the decode mixers on a k-row strip. */
|
|
enum { kGdnRowShift = 16 };
|
|
enum { kGdnRowMask = 31 };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MULTI-ROW CONV (PM 6, strip mode only): the same row count runs the CONV of rows
|
|
// [row, row + R) in ONE descriptor, row by row, each row's work exactly the one-row
|
|
// descriptor's: row r convolves stream row `row + r` against the window it would have seen —
|
|
// tap t is element t + r of [live window planes 0..taps-2, stream rows row, row+1, ...] (the
|
|
// reader reads a stream row into row 0, the strip layout) — and its rolled window (elements
|
|
// r+1 .. r+taps-1) goes to EXTENSION word r, its output to row `row + r`. Nothing is carried in
|
|
// L1: every operand of row r is re-read, so the arithmetic and its operands are the per-row
|
|
// walk's, bit for bit; what is saved is R-1 descriptors' issue, completion and puller turns.
|
|
// ---------------------------------------------------------------------------
|
|
// MULTI-ROW DELTA (PM 5): flags bits 21-25 carry a ROW COUNT. 0 and 1 are the one-row walk,
|
|
// byte for byte. R > 1 runs the decode recurrence over rows [row, row + R) of the staged
|
|
// stream in ONE descriptor: per head the q/k/v tiles and the state are read ONCE, each row's
|
|
// updated state is carried in L1 (CB_CARRY, compute-owned) to the next row, and every row's
|
|
// state still goes out — row r's to the address in EXTENSION word r — and its y to row
|
|
// `row + r`. The per-element arithmetic is the one-row descriptor's, in the same order, on
|
|
// the same bits: the carry pages are packed from the SAME DST section the writer's state
|
|
// pages are (the fused state emit, delta_extras bit 0, which ships), so row r+1 unpacks
|
|
// exactly what it would have read back from DRAM.
|
|
//
|
|
// THE EXTENSION BLOCK is the NEXT parameter slot (16 words at `parameters + kGdnWords`): the
|
|
// DRAM address of row r's state output in the state bank (word 9), r < R. The host writes it
|
|
// before the descriptor; every processor reads it from its own L1.
|
|
//
|
|
// The gate blocks are row-major at `gate + r * ftg_gate_stride()`: decay[v] then beta[v] per
|
|
// row, each block rounded up to the 64 B read granule (Pantheon's per-row gate blocks).
|
|
// ---------------------------------------------------------------------------
|
|
/** RED ONLY (reader): every row of a multi-row descriptor stages row `row`'s q/k/v — must
|
|
* DIFFER, which proves the rows' own operands reach the recurrence. */
|
|
enum { kGdnFlagRedRowsRow0 = 1u << 26 };
|
|
enum { kGdnRowCountShift = 21 };
|
|
enum { kGdnRowCountMask = 31 };
|
|
enum { kGdnMultiRowMax = 16 };
|
|
/** The compute-owned carry buffer's CB index: a protocol constant, not a compile-time arg, so
|
|
* the shipped build lines are unchanged. Mirrors FT_TT_OP_GDN_CB_CARRY. */
|
|
enum { kGdnCbCarry = 9 };
|
|
|
|
/** Bytes between two rows' gate blocks: decay[v] + beta[v], fp32, rounded to 64 B. */
|
|
FTQ_INLINE uint32_t ftg_gate_stride(uint32_t v_heads)
|
|
{
|
|
return (2u * v_heads * 4u + 63u) & ~63u;
|
|
}
|
|
|
|
/** TOTAL tiles this descriptor writes out -- all the writer reads. */
|
|
enum { kGdnTileCount = 15 };
|
|
|
|
enum { kGdnWords = 16 };
|
|
|
|
/** Where a byte lives in a packed word. Bytes rather than bit fields because a field a
|
|
* human has to read out of a hex dump on a stuck core is worth a nibble pair. */
|
|
enum { kGdnBytePhase = 0 };
|
|
enum { kGdnByteStateTiles = 1 };
|
|
enum { kGdnByteVHeads = 2 };
|
|
enum { kGdnByteKHeads = 3 };
|
|
enum { kGdnByteTaps = 0 };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// THE HEAD SLICE, AND WHY IT LIVES IN THE CONV-TAPS WORD
|
|
//
|
|
// PHASE DELTA divides EXACTLY by v-head: `DELTA(N heads) = 59.8 us + N x 103.4` measured over
|
|
// six points, and the op parallelises 1.96 / 3.79 / 7.12x on 2 / 4 / 8 cores. Dividing the
|
|
// heads gives the same series as replicating the whole descriptor across cores, so the work is
|
|
// embarrassingly parallel from both sides and the only term that does not divide is the 59.8 us
|
|
// host round trip. Every buffer this family places is sized per HEAD -- CB_STATE holds one
|
|
// tile-row, CB_SCALER one head's replicas -- so a member that owns four heads instead of
|
|
// thirty-two needs the IDENTICAL placement. Nothing in `resolve_cb_pages()` reads v_heads.
|
|
//
|
|
// `FT_TT_QUEUE_PARAM_WORDS` is 16 for every family and a seventeenth word moves the SHARED
|
|
// slot stride, so the slice cannot have a word of its own. The shape word's four bytes are
|
|
// full (phase, state tiles, v-heads, k-heads); the CONV-taps word uses ONE of its four and is
|
|
// meaningless in PHASE DELTA, so bytes 1 and 2 of it are where the slice goes.
|
|
//
|
|
// A COUNT OF ZERO MEANS THE WHOLE DESCRIPTOR, and that is the compatibility hinge rather than
|
|
// a convenience: every encoder that predates the slice leaves those bytes zero, and every one
|
|
// of them means "all v_heads from 0". A slice is opt-in on the HOST side and invisible to a
|
|
// descriptor that does not ask for one.
|
|
// ---------------------------------------------------------------------------
|
|
enum { kGdnByteHeadFirst = 1 };
|
|
enum { kGdnByteHeadCount = 2 };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// THE DECLARED CONV DEPTH, AND WHY THE THREE-SIDED CONTRACT NEEDED A CARD-SIDE CHECK
|
|
//
|
|
// `FT_TT_OP_GDN_CONV_DEPTH` is one number on three sides: the HOST sizes CB_VECTOR, CB_STATE
|
|
// and CB_IN from it, the READER is built with it as compile-time arg 7, and the COMPUTE
|
|
// kernel as compile-time arg 9. Only ONE DIRECTION of a drift was ever caught. If the host
|
|
// places for a depth SMALLER than the kernels were built at, `conv_block_depth()` finds the
|
|
// buffers too shallow and returns 0, and every CONV descriptor refuses -- loud. If the host
|
|
// places for a depth LARGER, every one of those checks passes: a 112-page CB_VECTOR is a
|
|
// perfectly legal home for depth-1 blocks, so the kernels quietly run at their own smaller
|
|
// depth and the descriptor completes with the right answer. Nothing is wrong except the
|
|
// number you are about to write down.
|
|
//
|
|
// THAT IS THE SHAPE THAT HAS ALREADY BITTEN THIS PROGRAM. The reduce family's constants were
|
|
// built at 2048 under a 4096 program, scored 1.0e+00, and NOTHING REFUSED ANYWHERE. Here the
|
|
// silent direction does not corrupt an answer, it corrupts a MEASUREMENT -- which is worse
|
|
// for a depth sweep, because the arm reads green and reports the control's time.
|
|
//
|
|
// So the host DECLARES its own depth in byte 3 of the CONV-taps word -- the last free byte of
|
|
// the sixteen (byte 0 is the taps, 1 and 2 are the DELTA head slice) -- and both kernels
|
|
// compare it against the constant they were compiled with. The comparison is STRICT and there
|
|
// is no "unset" escape: a zero byte is a mismatch against any real depth, so an encoder that
|
|
// forgets to declare REFUSES instead of passing, which is the opposite of the head slice's
|
|
// compatibility hinge and deliberately so. PHASE DELTA does not read the depth and its taps
|
|
// word is zero by construction, so the check lives in the CONV branch only and a DELTA
|
|
// descriptor is byte-for-byte what it was.
|
|
// ---------------------------------------------------------------------------
|
|
enum { kGdnByteConvDepth = 3 };
|
|
|
|
/**
|
|
* Does the depth the HOST sized its placement for match the one THIS image was built with?
|
|
*
|
|
* ONE implementation for the reader and the compute kernel, so the two cannot disagree about
|
|
* the disagreement. Strict equality on purpose -- see the note above on why there is no
|
|
* "0 means unset" escape.
|
|
*/
|
|
FTQ_INLINE bool ftg_conv_depth_agrees(uint32_t declared, uint32_t built)
|
|
{
|
|
return declared == built;
|
|
}
|
|
|
|
/** Which half of the block this descriptor runs. 0 is deliberately not a phase, so a zeroed
|
|
* parameter block is an invalid program rather than a plausible one. */
|
|
enum { kGdnPhaseConv = 1 }; /**< conv1d + SiLU, rolls the window, splits into q|k|v */
|
|
enum { kGdnPhaseDelta = 2 }; /**< the four-pass delta rule over every v-head */
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WHERE A v-HEAD'S CONSTANTS SIT IN CB_VECTOR
|
|
//
|
|
// The reader pushes `state_tiles` v columns and then these three, in this order; the
|
|
// compute thread indexes them at `state_tiles + kGdnVector*`. They lived as a private enum
|
|
// in BOTH kernels, each carrying a comment saying the other had to agree — which is the
|
|
// exact shape of a contract that drifts. They are here now because the HOST needs the count
|
|
// too: `ft_tt_op_gdn_place_cbs()` cannot size CB_VECTOR without it, and the reader refuses a
|
|
// descriptor whose CB_VECTOR cannot hold `state_tiles + kGdnVectorConstants` pages.
|
|
// ---------------------------------------------------------------------------
|
|
enum { kGdnVectorDecay = 0 };
|
|
enum { kGdnVectorBeta = 1 };
|
|
enum { kGdnVectorScale = 2 };
|
|
enum { kGdnVectorConstants = 3 };
|
|
|
|
/** One descriptor's shape, decoded once at the top of the op. */
|
|
typedef struct ftg_shape {
|
|
uint32_t phase;
|
|
uint32_t state_tiles; /**< S / 32 */
|
|
uint32_t v_heads;
|
|
uint32_t k_heads;
|
|
uint32_t conv_tile_cols;
|
|
uint32_t conv_taps;
|
|
uint32_t tile_bytes;
|
|
uint32_t tile_count;
|
|
uint32_t head_first; /**< PHASE DELTA: first v-head this member owns */
|
|
uint32_t head_count; /**< PHASE DELTA: v-heads this member owns; `v_heads` if unsliced */
|
|
/** PHASE CONV: the block depth the HOST sized the circular buffers for. Compared against
|
|
* each image's own compile-time constant -- see kGdnByteConvDepth. Zero in PHASE DELTA,
|
|
* which does not read the depth. */
|
|
uint32_t declared_conv_depth;
|
|
/** PHASE CONV column slice: the tile columns THIS member convolves, [col_first,
|
|
* col_first + col_count). The plane STRIDE stays `conv_tile_cols`. Whole stream if
|
|
* unsliced. See kGdnByteHeadFirst: in CONV the same two bytes carry the column range. */
|
|
uint32_t col_first;
|
|
uint32_t col_count;
|
|
/** PHASE DELTA rows: the first (the row select) and how many (1 = the one-row walk). */
|
|
uint32_t row_first;
|
|
uint32_t row_count;
|
|
} ftg_shape;
|
|
|
|
FTQ_INLINE uint32_t ftg_byte(uint32_t word, uint32_t index)
|
|
{
|
|
return (word >> (index * 8u)) & 0xFFu;
|
|
}
|
|
|
|
FTQ_INLINE uint32_t ftg_xy_x(uint32_t word)
|
|
{
|
|
return word & 0xFFFFu;
|
|
}
|
|
|
|
FTQ_INLINE uint32_t ftg_xy_y(uint32_t word)
|
|
{
|
|
return (word >> 16) & 0xFFFFu;
|
|
}
|
|
|
|
FTQ_INLINE ftg_shape ftg_decode(volatile FTQ_L1_PTR uint32_t *parameters)
|
|
{
|
|
const uint32_t shape_word = parameters[kGdnShape];
|
|
ftg_shape shape;
|
|
|
|
shape.phase = ftg_byte(shape_word, kGdnBytePhase);
|
|
shape.state_tiles = ftg_byte(shape_word, kGdnByteStateTiles);
|
|
shape.v_heads = ftg_byte(shape_word, kGdnByteVHeads);
|
|
shape.k_heads = ftg_byte(shape_word, kGdnByteKHeads);
|
|
shape.conv_tile_cols = parameters[kGdnConvTileCols];
|
|
shape.conv_taps = ftg_byte(parameters[kGdnConvTaps], kGdnByteTaps);
|
|
shape.tile_bytes = parameters[kGdnTileBytes];
|
|
shape.tile_count = parameters[kGdnTileCount];
|
|
/* The head slice. A count of ZERO is "the whole descriptor" -- see the note beside
|
|
* kGdnByteHeadFirst; it is what every encoder written before the slice emits, and it is
|
|
* what makes an unsliced descriptor byte-identical to the one that shipped. */
|
|
shape.head_first = ftg_byte(parameters[kGdnConvTaps], kGdnByteHeadFirst);
|
|
shape.head_count = ftg_byte(parameters[kGdnConvTaps], kGdnByteHeadCount);
|
|
/* The host's own CONV block depth. Read UNCONDITIONALLY and compared only in the CONV
|
|
* branch: a DELTA descriptor's taps word is zero and DELTA does not read the depth. */
|
|
shape.declared_conv_depth = ftg_byte(parameters[kGdnConvTaps], kGdnByteConvDepth);
|
|
/* PHASE CONV READS BYTES 1 AND 2 AS A COLUMN RANGE. The convolution divides by CHANNEL:
|
|
* its weight and window are tap-major planes of `conv_tile_cols` tiles, so a member owns
|
|
* a range of tile columns in EVERY plane at the full plane stride. A count of zero is the
|
|
* whole stream — every CONV encoder before the slice left these bytes zero, because the
|
|
* encoder refused a CONV head slice — and a CONV descriptor owns every head. */
|
|
shape.col_first = 0u;
|
|
shape.col_count = shape.conv_tile_cols;
|
|
if (shape.phase == (uint32_t)kGdnPhaseConv) {
|
|
shape.col_first = shape.head_first;
|
|
if (shape.head_count != 0u)
|
|
shape.col_count = shape.head_count;
|
|
shape.head_first = 0u;
|
|
shape.head_count = 0u;
|
|
}
|
|
if (shape.head_count == 0u) {
|
|
shape.head_first = 0u;
|
|
shape.head_count = shape.v_heads;
|
|
}
|
|
shape.row_first = (parameters[kGdnFlags] >> kGdnRowShift) & (uint32_t)kGdnRowMask;
|
|
shape.row_count = (parameters[kGdnFlags] >> kGdnRowCountShift) & (uint32_t)kGdnRowCountMask;
|
|
if (shape.row_count == 0u)
|
|
shape.row_count = 1u;
|
|
return shape;
|
|
}
|
|
|
|
/**
|
|
* Is this a shape this binary can execute?
|
|
*
|
|
* Checked on the DEVICE and not only on the host, for the reason the reduce family's
|
|
* equivalent gives: the failure a bad shape produces is not a wrong answer but a blocked
|
|
* triad -- a writer waiting on an output tile nobody will produce, until its bound expires.
|
|
* A refusal names it in one word instead.
|
|
*
|
|
* The GQA divisibility check is here rather than only on the host because getting the
|
|
* repeat backwards is a KNOWN bug in this architecture, not a hypothetical one.
|
|
*/
|
|
FTQ_INLINE bool ftg_shape_is_supported(const ftg_shape *shape)
|
|
{
|
|
if (shape->phase != kGdnPhaseConv && shape->phase != kGdnPhaseDelta)
|
|
return false;
|
|
if (shape->state_tiles == 0u || shape->state_tiles > 8u)
|
|
return false;
|
|
if (shape->v_heads == 0u || shape->k_heads == 0u)
|
|
return false;
|
|
if (shape->v_heads % shape->k_heads != 0u)
|
|
return false;
|
|
if (shape->tile_count == 0u)
|
|
return false;
|
|
/* The head slice has to name heads this descriptor's operands actually have. A slice
|
|
* that runs off the end would address a state region belonging to no head and scatter a
|
|
* y row into a tile the layout never allocated -- and both are clean retires. */
|
|
if (shape->head_count == 0u || shape->head_first + shape->head_count > shape->v_heads)
|
|
return false;
|
|
/* MULTI-ROW: at most kGdnMultiRowMax rows, all of them inside one tile. DELTA since PM 5;
|
|
* CONV since PM 6 (strip mode only — the reader and writer refuse it otherwise). */
|
|
if (shape->row_count > 1u &&
|
|
(shape->row_count > (uint32_t)kGdnMultiRowMax || shape->row_first + shape->row_count > 32u))
|
|
return false;
|
|
if (shape->phase == kGdnPhaseConv) {
|
|
if (shape->conv_tile_cols == 0u)
|
|
return false;
|
|
/* The column slice has to name columns the stream has. */
|
|
if (shape->col_count == 0u || shape->col_first + shape->col_count > shape->conv_tile_cols)
|
|
return false;
|
|
/* A one-tap conv has no window to roll, which would make the carried state
|
|
* write-only -- almost certainly a shape that was meant to be something else. */
|
|
if (shape->conv_taps < 2u || shape->conv_taps > 8u)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// THE DRAM LAYOUTS, SETTLED. Every address arithmetic below is HERE and not in a kernel,
|
|
// because the host stager and the reader are the two ends of a contract nothing checks at
|
|
// run time: a stager that disagrees produces plausible numbers from the first token instead
|
|
// of an error. The reference is `pn_qwen35moe_ssm_recurrence()` in Pantheon's
|
|
// `src/models/qwen35moe_ssm.c`, which defines the op value for value.
|
|
//
|
|
// A "TILIZED [rows, cols] MATRIX" below means what `ft_tt_tilize_strip()` produces:
|
|
// `ceil(rows/32)` row blocks in order, each `ceil(cols/32)` face-ordered 32x32 tiles in
|
|
// order, so element [r][c] lands at tile `(r/32)*ceil(cols/32) + c/32`, position
|
|
// [r%32][c%32]. That is the layout every activation and every weight in this backend already
|
|
// uses; nothing here is a private convention.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// kGdnActivationIn, PHASE DELTA -- ONE ROW-0 STRIP, head-major: q | k | v
|
|
// ---------------------------------------------------------------------------
|
|
// q : `k_heads * state_tiles` tiles L2-normalised, NOT GQA-repeated
|
|
// k : `k_heads * state_tiles` tiles L2-normalised, NOT GQA-repeated
|
|
// v : `v_heads * state_tiles` tiles
|
|
//
|
|
// A head's S values live in `state_tiles` CONSECUTIVE tiles, all of them in ROW 0 -- i.e. the
|
|
// whole block is the tilized `[1, heads*S]` strip `ft_tt_tilize_strip(rows = 1)` produces, and
|
|
// head `h` starts at tile `h * state_tiles`. Rows 1..31 are zero and nothing reads them.
|
|
//
|
|
// **THIS IS THE SAME GEOMETRY AS PHASE CONV'S OUTPUT, AND THAT IS THE WHOLE POINT.** conv_out
|
|
// is a `[1, conv_dim]` strip whose channel `c` sits at tile `c/32`, row 0; the delta rule's
|
|
// q, k and v ARE channels [0, key_dim), [key_dim, 2*key_dim) and [2*key_dim, conv_dim) of that
|
|
// same strip. So PHASE CONV's output buffer IS PHASE DELTA's input buffer with no copy, no
|
|
// re-tilization and no host round trip, and the per-head L2 norm that sits between them is an
|
|
// ordinary `row_blocks = 2*k_heads, tile_cols = state_tiles` reduce over its first
|
|
// `2 * k_heads * state_tiles` tiles, written back over its own input.
|
|
//
|
|
// It used to be three tilized `[heads, S]` matrices with ONE ROW PER HEAD, 12 tiles at
|
|
// Ornith's shape against 256 now. That packing was 32x denser in DRAM and cost nothing to
|
|
// read -- a head still needed the whole `state_tiles`-tile row block to get one row of it --
|
|
// but it was a DIFFERENT GEOMETRY from the strip every projection, every reduce and every
|
|
// elementwise op in this backend produces, so the host had to de-tilize conv_out, L2-normalise
|
|
// on the CPU and re-tilize, which is 49,408 B a layer over PCIe and 86% of a decode token.
|
|
// Head-major row-0 tiles are what let the block run WHOLE on the card.
|
|
//
|
|
// The three blocks are not the same size -- q and k are per K-HEAD and v is per V-HEAD.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// kGdnConstantWeight and kGdnStateConv, PHASE CONV -- TAP-MAJOR PLANES, not [conv_dim, taps]
|
|
// ---------------------------------------------------------------------------
|
|
// The compute thread multiplies a whole tile of the qkv stream by a whole tile of the weight,
|
|
// so a weight tile has to line channel c up with its OWN tap-t coefficient at the SAME
|
|
// position. That makes the operand `taps` PLANES of `conv_tile_cols` tiles, plane t holding
|
|
// w[.][t] laid out exactly like the stream -- and the oracle's `layer->ssm_conv1d` is
|
|
// row-major `[conv_dim][taps]`, `w[c*taps + tap]`. THE STAGER MUST TRANSPOSE IT; a reshape
|
|
// gives a wrong answer with no error anywhere. This header used to say "[conv_dim, taps]",
|
|
// which is the oracle's order and not the card's.
|
|
//
|
|
// The rolling window is the same shape, `taps-1` planes, and it is READ AND WRITTEN in that
|
|
// layout -- so the transpose is a one-time staging cost and the roll is layout-preserving.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// kGdnConstantGate, PHASE DELTA -- A FLAT fp32 ARRAY, the one operand that is not tiles
|
|
// ---------------------------------------------------------------------------
|
|
// `decay[v_heads]` then `beta[v_heads]`, contiguous fp32, indexed by head. The reader pulls it
|
|
// with ONE NoC read whose length is rounded UP to the 64-byte DRAM granule, so the allocation
|
|
// must be at least `(2*v_heads*4 + 63) & ~63` bytes or the read runs off the end of it.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// kGdnActivationOut and kGdnStateRecurrent
|
|
// ---------------------------------------------------------------------------
|
|
// y is a ROW-0 STRIP, head-major: v-head `h`'s S values are ROW 0 of tiles
|
|
// [h*state_tiles, +state_tiles). Same geometry as the delta input and as conv_out, for the
|
|
// same reason -- the gated RMS norm that follows is then a `row_blocks = v_heads,
|
|
// tile_cols = state_tiles` reduce over the strip, and the `silu(z) *` that follows THAT is an
|
|
// elementwise op against `ssm_z`, which is a strip because a projection cannot produce
|
|
// anything else. The writer still transposes each head's answer out of the column the (e)
|
|
// reduce produces; what changed is only WHERE it lands.
|
|
//
|
|
// The recurrent state is [v_heads, S, S] fp32, TRANSPOSED per head (the oracle's
|
|
// `s[h*S*S + j*S + i] == S_h[i][j]`, row j contiguous over i), each head tilized on its own
|
|
// into `state_tiles * state_tiles` tiles in row-major tile order. Head-major.
|
|
// ===========================================================================
|
|
|
|
/** Which of the three staged streams, in the order they sit at kGdnActivationIn. */
|
|
enum { kGdnStreamQ = 0 };
|
|
enum { kGdnStreamK = 1 };
|
|
enum { kGdnStreamV = 2 };
|
|
|
|
/** Tiles of the q, k or v block at kGdnActivationIn. q and k are per-K-HEAD. */
|
|
FTQ_INLINE uint32_t ftg_delta_stream_tiles(const ftg_shape *shape, uint32_t stream)
|
|
{
|
|
const uint32_t heads = (stream == (uint32_t)kGdnStreamV) ? shape->v_heads : shape->k_heads;
|
|
|
|
return heads * shape->state_tiles;
|
|
}
|
|
|
|
/** First tile of the q, k or v block at kGdnActivationIn. */
|
|
FTQ_INLINE uint32_t ftg_delta_stream_base(const ftg_shape *shape, uint32_t stream)
|
|
{
|
|
const uint32_t k_block = shape->k_heads * shape->state_tiles;
|
|
|
|
if (stream == (uint32_t)kGdnStreamV)
|
|
return 2u * k_block;
|
|
return stream * k_block;
|
|
}
|
|
|
|
/** First tile of head `head`'s S values inside the stream block that carries it. */
|
|
FTQ_INLINE uint32_t ftg_delta_head_tile(const ftg_shape *shape, uint32_t stream, uint32_t head)
|
|
{
|
|
return ftg_delta_stream_base(shape, stream) + head * shape->state_tiles;
|
|
}
|
|
|
|
/** Tiles the whole staged q|k|v input occupies -- what a stager allocates. */
|
|
FTQ_INLINE uint32_t ftg_delta_input_tiles(const ftg_shape *shape)
|
|
{
|
|
return ftg_delta_stream_base(shape, (uint32_t)kGdnStreamV) +
|
|
ftg_delta_stream_tiles(shape, (uint32_t)kGdnStreamV);
|
|
}
|
|
|
|
/** Tile `column` of tap plane `tap`, in the conv weight OR in the rolling window. */
|
|
FTQ_INLINE uint32_t ftg_conv_plane_tile(const ftg_shape *shape, uint32_t tap, uint32_t column)
|
|
{
|
|
return tap * shape->conv_tile_cols + column;
|
|
}
|
|
|
|
/** Tile of the carried state holding rows [32*tile_row, +32) and columns [32*tile_col, +32)
|
|
* of v-head `head`. Head-major, then row-major over the tiles inside the head. */
|
|
FTQ_INLINE uint32_t ftg_state_tile(const ftg_shape *shape, uint32_t head, uint32_t tile_row,
|
|
uint32_t tile_col)
|
|
{
|
|
return head * shape->state_tiles * shape->state_tiles + tile_row * shape->state_tiles +
|
|
tile_col;
|
|
}
|
|
|
|
/** Output tile of y carrying v-head `head`'s elements [32*tile_row, +32). y is a ROW-0 STRIP,
|
|
* head-major, so the head's row inside that tile is always 0 (`kGdnYRow`). */
|
|
FTQ_INLINE uint32_t ftg_y_tile(const ftg_shape *shape, uint32_t head, uint32_t tile_row)
|
|
{
|
|
return head * shape->state_tiles + tile_row;
|
|
}
|
|
|
|
/** The row a strip's data occupies inside every tile of it -- the delta input's q, k and v and
|
|
* the y output alike. Named rather than spelled `0` because it used to be `head % 32` on both
|
|
* sides, and both the reader's source row and the writer's destination row are arguments. */
|
|
enum { kGdnStripRow = 0 };
|
|
|
|
/**
|
|
* The k-head whose q/k columns v-head `v_head` reads: the INTERLEAVED GQA repeat.
|
|
*
|
|
* `vh = r * n_k_heads + kh` in Pantheon's qwen35moe_ssm.c, so `kh = vh % n_k_heads`. This
|
|
* is the whole of the repeat -- an address computation, never a materialised tensor. It is
|
|
* a named function rather than an inline modulo because the blocked mapping
|
|
* (`vh / kv_groups`) is the plausible wrong answer and has produced gibberish before.
|
|
*/
|
|
FTQ_INLINE uint32_t ftg_k_head_for(uint32_t v_head, uint32_t k_heads)
|
|
{
|
|
return v_head % k_heads;
|
|
}
|
|
|
|
/** Tiles of carried recurrent state one v-head owns: [S,S] in 32x32 tiles. */
|
|
FTQ_INLINE uint32_t ftg_state_tiles_per_head(const ftg_shape *shape)
|
|
{
|
|
return shape->state_tiles * shape->state_tiles;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HOW MANY TILES PHASE DELTA PUTS IN DST BETWEEN ONE ACQUIRE AND ONE COMMIT
|
|
//
|
|
// MEASURED ON SILICON 2026-08-18. The DELTA descriptor costs 3,368 us and is bound by
|
|
// NEITHER compute (95 GFLOP/s on a Tensix that can issue ~1,400) NOR bandwidth (1.27 GB/s
|
|
// against 3-5% of one core's DRAM). It is bound by STRUCTURE: ~2,688 single-tile
|
|
// `wait_for_dest -> one tile op -> commit_dst -> wait_front` round trips a descriptor, each
|
|
// with an unpacker `*_init` in front, at ~1,250 ns where the tile arithmetic is ~64 cycles.
|
|
// The fixed part of that round trip is per ACQUIRE, not per tile: a `ZEROACC CLR_HALF` over
|
|
// the whole DST half, two cross-thread semaphore hops and a full ADDR_MOD + MOP reprogram,
|
|
// none of which get bigger when four tiles ride in one section instead of one.
|
|
//
|
|
// `reduce_row_sum` in the compute kernel already batches this way -- one acquire, `tiles`
|
|
// accumulating reduce ops, one commit -- so the pattern is not new to the family; the three
|
|
// per-tile loops around it simply never used more than DST index 0.
|
|
//
|
|
// THE CEILING IS FOUR AND IT IS NOT A TUNING CHOICE. DST is 16 tiles of 32x32; DST_SYNC half
|
|
// gives MATH one half at a time, and `dst_accum_mode 1` -- MANDATORY for this family, since
|
|
// it is the only op carrying state ACROSS TOKENS -- halves it again. Four is also exactly
|
|
// Ornith's `state_tiles`, so every DELTA loop batches whole. A `dst_batch_max` above the
|
|
// ceiling would silently alias DST tiles onto each other and produce a smoothly wrong
|
|
// recurrence, so the clamp is here rather than in either kernel's copy of the arithmetic.
|
|
//
|
|
// THE BATCH MUST ALSO DIVIDE `state_tiles`, because every buffer it touches closes on the
|
|
// CONSUMER'S INDEXED READ (see the CB grid note above): a block that does not divide the
|
|
// tile-row leaves CB_WORK's front somewhere other than where the next block expects it.
|
|
// ---------------------------------------------------------------------------
|
|
enum { kGdnDstBatchCeiling = 4 };
|
|
|
|
/**
|
|
* DST tiles one PHASE DELTA block writes, from the shared compile-time `dst_batch_max`.
|
|
*
|
|
* ONE implementation for the reader, the compute kernel and the host, because it is the same
|
|
* three-sided contract `FT_TT_OP_GDN_CONV_DEPTH` is and that one drifted. `1` is the identity
|
|
* value: every batched loop collapses to the per-tile sequence that shipped, which is what
|
|
* makes a `dst_batch_max = 1` build the control arm for a `dst_batch_max = 4` one.
|
|
*/
|
|
FTQ_INLINE uint32_t ftg_dst_batch(uint32_t state_tiles, uint32_t dst_batch_max)
|
|
{
|
|
uint32_t batch = dst_batch_max;
|
|
|
|
if (batch == 0u || state_tiles == 0u)
|
|
return 1u;
|
|
if (batch > (uint32_t)kGdnDstBatchCeiling)
|
|
batch = (uint32_t)kGdnDstBatchCeiling;
|
|
if (batch > state_tiles)
|
|
batch = state_tiles;
|
|
while (batch > 1u && (state_tiles % batch) != 0u)
|
|
batch--;
|
|
return batch;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HOW MANY CB_OUT PAGES THE WRITER PUTS BEHIND ONE NoC WRITE BARRIER
|
|
//
|
|
// A NoC write barrier is a PER-CORE DRAIN, not a per-transaction wait: it spins until every
|
|
// write this core has issued has been ACKED by its destination. That is why the block
|
|
// reader takes one for a whole block instead of one per tile pair and is worth 5.9x
|
|
// (docs/tt_kernel_model.md 12), and it is why "one barrier per stream group, never per tile"
|
|
// is a rule of this backend rather than a preference. PHASE DELTA's writer took ONE PER
|
|
// TILE -- `v_heads * state_tiles * (state_tiles + 1)` = 640 a descriptor at Ornith's shape.
|
|
//
|
|
// THE GROUP CANNOT SIMPLY BE MADE BIGGER, and the reason is the SOURCE of the write. Every
|
|
// write's source is the CB_OUT page itself, so a page popped while its write is still in
|
|
// flight is a page the compute thread is free to pack over -- a barrier batched without
|
|
// moving the pops behind it trades a drain for silent corruption. So the pops move behind
|
|
// the barrier and CB_OUT has to hold TWO groups: one being drained, one being filled.
|
|
//
|
|
// RUNGS, NOT A FREE INTEGER, because the host sizes CB_OUT from this same function and the
|
|
// two must agree:
|
|
//
|
|
// 0 one barrier per TILE -- the sequence that shipped, and what a SHORT BUILD LINE
|
|
// folds to, since an out-of-range compile-time arg reads a
|
|
// literal 0 with no diagnostic (FT_TT_OP_GDN_CONV_DEPTH)
|
|
// 1 one barrier per TILE-ROW -- `state_tiles` state pages then one y page: exactly the
|
|
// stream group the writer's own split is defined over
|
|
// 2 one barrier per HEAD -- `state_tiles` tile-rows of the same
|
|
//
|
|
// Rung 0 is the identity, which is what makes a rung-0 build the exact control arm for a
|
|
// rung-1 one. The kernel CLAMPS the result to half the placed page count, so a build at
|
|
// rung 2 under a host that did not grow CB_OUT degrades to the shipped sequence instead of
|
|
// hanging on an unsatisfiable `cb_wait_front` -- the failure the DST-batch round hit on
|
|
// silicon when `reserve_back(cb_out, 4)` met a two-page buffer.
|
|
//
|
|
// PHASE CONV STAYS AT RUNG 0 ON EVERY ARM of this round. It drains the same CB_OUT, so a
|
|
// CONV descriptor that does not move is the measurement that says the RESIZE on its own
|
|
// bought nothing and the barrier count is what moved DELTA.
|
|
// THE CEILING IS AN ARENA BOUND, NOT A TASTE ONE. CB_OUT is placed at twice the group, and
|
|
// at S = 256 (8 state tiles, 8 taps) an unclamped rung 2 asks for 144 pages -- 576 KiB, which
|
|
// puts that shape's ten buffers 13 pages over the 330-page arena and makes
|
|
// `ft_tt_op_gdn_place_cbs()` return ENOMEM for a shape the family supports. Measured
|
|
// off-hardware by test_gdn_ops.c before it ever reached a card. 20 pages is rung 2 at
|
|
// Ornith exactly, so the ceiling costs the shape being measured nothing.
|
|
enum { kGdnWriterBlockCeiling = 20 };
|
|
|
|
FTQ_INLINE uint32_t ftg_writer_block(uint32_t state_tiles, uint32_t rung)
|
|
{
|
|
uint32_t block;
|
|
|
|
if (state_tiles == 0u || rung == 0u)
|
|
return 1u;
|
|
block = (rung == 1u) ? state_tiles + 1u : state_tiles * (state_tiles + 1u);
|
|
if (block > (uint32_t)kGdnWriterBlockCeiling)
|
|
block = (uint32_t)kGdnWriterBlockCeiling;
|
|
return block;
|
|
}
|
|
|
|
#endif // FOUNDRY_RESIDENT_GDN_PROTOCOL_H
|