630 lines
38 KiB
C
630 lines
38 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||
/* Copyright (C) 2026 David (Corey) Herrera */
|
||
/**
|
||
* @file ft_tt_op_gdn.h
|
||
* @brief Host driver for the GATED-DELTA-NET block — Qwen3.5's linear-layer recurrence.
|
||
* Places the triad's circular buffers and encodes the parameter block. The two phases
|
||
* (conv1d+SiLU, then the delta rule) share one binary and one parameter layout.
|
||
*
|
||
* The device half is `kernels/resident_gdn_protocol.h`; the two must agree word for word and
|
||
* `tests/test_gdn_ops.c` pins the pair off-hardware by parsing that header.
|
||
*
|
||
* THE DRAM LAYOUTS ARE NOT HERE, ON PURPOSE. Where q|k|v sit at `kGdnActivationIn`, that the
|
||
* conv weight is TAP-MAJOR PLANES rather than the oracle's row-major `[conv_dim, taps]`, that
|
||
* the gate block is a flat fp32 array and not tiles, and how y and the carried state are
|
||
* tilized, are all stated ONCE — in `resident_gdn_protocol.h`'s layout section, beside the
|
||
* `ftg_*` accessors both dataflow kernels compute their offsets with. A host stager reads
|
||
* that section. Duplicating it here is how the CB map in this file came to contradict the
|
||
* kernels for a whole round.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* WHAT THE OP IS, VALUE FOR VALUE
|
||
* ---------------------------------------------------------------------------
|
||
* The oracle is `pn_qwen35moe_ssm_recurrence()` in Pantheon's `src/models/qwen35moe_ssm.c`,
|
||
* which the CPU arm and every device arm call — so a device arm that disagrees with it
|
||
* disagrees about the matmuls and nothing else. Per SSM layer, per decode token, with
|
||
* `S = 128`, 16 k-heads, 32 v-heads and `conv_dim = 2*key_dim + val_dim = 8192`:
|
||
*
|
||
* PHASE CONV
|
||
* out[c] = SiLU( qkv[c]*w[c][k-1] + sum_{t<k-1} window[c][t]*w[c][t] ) c < conv_dim
|
||
* window[c] rolls: drop the oldest tap, append qkv[c]
|
||
* then the stream splits q | k | v at key_dim, 2*key_dim.
|
||
*
|
||
* PHASE DELTA, per v-head h, with decay = exp(g_h), beta_h, scale = 1/sqrt(S):
|
||
* (a) S *= decay
|
||
* (b) pred_j = sum_i S[j][i] * k[i]
|
||
* (c) delta_j = (v_j - pred_j) * beta
|
||
* (d) S[j][i] += k[i] * delta_j
|
||
* (e) y_j = scale * sum_i S[j][i] * q[i] READ AFTER THE WRITE
|
||
*
|
||
* Step (e) reading the UPDATED state is not a detail: doing it before (d) is a different
|
||
* recurrence that happens to look plausible for one token.
|
||
*
|
||
* The two per-head NORMS of this block — the q/k L2 norm between the phases, and the output
|
||
* gated norm after them — are NOT in this op. They are ordinary RMS norms at a second width
|
||
* (4 tile columns against the model's 128) and belong to the shipped reduce family on a
|
||
* second narrow residency. `resident_gdn_protocol.h` carries that argument in full.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* WHY THE STATE IS fp32 AND WHAT THE BAR ACTUALLY IS
|
||
* ---------------------------------------------------------------------------
|
||
* The carried state is fp32 in card DRAM: 128x128 per head x 32 heads x 24 layers =
|
||
* 50.25 MiB, already charged to the capacity plan's run-time reserve. bf16 would halve the
|
||
* traffic and is not on the table, because this is a RECURRENCE — the CPU oracle
|
||
* accumulates its dots in fp64 on purpose and its own comment names a token-60-90
|
||
* divergence onset for doing them in float32.
|
||
*
|
||
* THE BAR A TENSIX ARM HAS TO MEET IS CUDA'S, NOT THE CPU'S. A Tensix cannot do fp64, so
|
||
* bit-equality with the oracle was never available; the CUDA arm is fp32 and is gated at
|
||
* 1e-3 relative, and that is the number this op is held to. Decided before the gate was
|
||
* written rather than after it failed.
|
||
*
|
||
* AND THE FPU SEES bf16 REGARDLESS OF THE CB FORMAT — measured, and it is the constraint
|
||
* that shapes the whole design. Float32 circular buffers exist (4096 B/tile against bf16's
|
||
* 2048, `kernels/cb_tables/vector_add_float32.cbtable` ships one), but a Float32 CB unpacks
|
||
* into `Float16_b` in the source registers unless EVERY CB of the program is Float32.
|
||
* So fp32 buys accurate STORAGE and fp32 destination accumulation; the multiplies inside
|
||
* the dots run on bf16 mantissas at MATH_FIDELITY 4 whatever the CB says. That is why
|
||
* `dst_accum_mode 1` is mandatory in this family's CB table rather than optional.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* WHAT THE READER AND THE WRITER OWE, because the compute surface cannot express it
|
||
* ---------------------------------------------------------------------------
|
||
* The broadcast headers hold BROADCAST_COL only — `mul`/`sub` against column 0 of a second
|
||
* tile. There is no row broadcast and no scalar broadcast. That is sufficient, because the
|
||
* TRANSPOSED state layout makes every per-head scalar a per-ROW scalar, but it fixes what
|
||
* has to be staged:
|
||
*
|
||
* - `k` and `q` ROW-REPLICATED — [r][i] = k[i] for EVERY row r. THIS PARAGRAPH USED TO SAY
|
||
* "as COLUMN tiles (value at [i][0])" AND THAT WAS AN OVER-READ OF THE SILICON PROBE.
|
||
* The probe that proved the reduce scaler is PER-POSITION staged its scaler with
|
||
* `tests/test_ops.c build_column_scaler_tile()`, which writes `plain[row*32 + column] =
|
||
* s[column]` — the value for reduce position `c` is present at every `[r][c]`, so the
|
||
* measurement cannot tell "column 0 of row c" from "row 0, column c" from "replicated".
|
||
* Row-replicated is the layout that was actually measured, so it is the one the triad
|
||
* stages; column-0-only would have fed every reduce position past the first a zero.
|
||
* - `v` as a TRUE COLUMN tile (value at [j][0]), so `v - pred` is a plain eltwise sub of
|
||
* two columns — a reduce result's non-column-0 lanes are packed as zero, so `pred` is a
|
||
* genuine column vector;
|
||
* - `decay` and `beta` as CONSTANT column tiles (the same scalar in all 32 rows), so
|
||
* `bcast_col` applies a per-head scalar to a whole tile, and `scale` the same way,
|
||
* splatted from `kGdnScaleBits` without arithmetic;
|
||
* - the row-replicated `k` tile is SIMULTANEOUSLY the (b) reduce's scaler operand and the
|
||
* (d) rank-1 operand, so ONE tile serves both uses. That collapse is why this family has
|
||
* no separate replica buffer and why CB index 4 is free for the reader's private
|
||
* scratch. The rank-1 staging is not optional and the two documented alternatives are
|
||
* both gone: there is no FMA on this machine (ELWMUL hardcodes `acc_to_dest` to 0, which
|
||
* is why `resident_rope_compute.c` stages two products into separate CBs and adds them),
|
||
* and `matmul_tiles` here is legal only at `ct_dim == rt_dim == 1`, so the `kt_dim = 1`
|
||
* matmul the scoping offered as the alternative is not a shape this backend compiles.
|
||
* - the WRITER transposes `y` from column 0 back into a ROW, because the gated norm that
|
||
* follows wants one row per v-head. 32 strided word moves a head.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* WHAT CROSSES THE LINK, WHICH IS THE POINT OF THE OP
|
||
* ---------------------------------------------------------------------------
|
||
* Per SSM layer the host currently pulls 49,408 B and the recurrence costs 0.225 s/token,
|
||
* 35% of a 0.636 s token. With the block on the card the only things crossing are `alpha`
|
||
* and `beta` DOWN and `decay` and `beta` UP — 32 scalars each, 256 B a layer in each
|
||
* direction, ~6 KB a token, ~1.2 ms. Every value still crosses exactly once.
|
||
*
|
||
* The partial moves are traps and the numbers say so: after conv1d alone the host still
|
||
* pulls 49,408 B (a depthwise conv is width-preserving, so moving it alone is 1.00x), and
|
||
* after conv1d + L2 + the GQA repeat it pulls 65,536 — 1.33x WORSE than doing nothing,
|
||
* because a materialised repeat doubles q and k. The block moves whole or not at all.
|
||
*
|
||
* @status Parameter layout and CB placement. The reader/compute/writer triad IS written
|
||
* (`kernels/resident_gdn_{reader,compute,writer}.c`), builds, and is embedded in the
|
||
* program blob — but it has NO CALLER, so nothing in this file has ever executed on the
|
||
* card. `tests/test_gdn_ops.c` is the whole of the evidence behind it.
|
||
* @issues None
|
||
* @todo None
|
||
*/
|
||
|
||
#ifndef FT_TT_OP_GDN_H
|
||
#define FT_TT_OP_GDN_H
|
||
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
|
||
#include "ft_tt_launch.h"
|
||
#include "ft_tt_op_dispatch.h"
|
||
#include "ft_tt_op_l1_layout.h"
|
||
#include "ft_tt_queue.h"
|
||
|
||
/* ================================================================
|
||
* The parameter block — mirrors kernels/resident_gdn_protocol.h
|
||
* ================================================================ */
|
||
|
||
/** Words of one GDN parameter block. A word a phase does not use is written 0 by the host
|
||
* and never read by the kernel.
|
||
*
|
||
* THE COORDINATES ARE PACKED TWO TO A WORD and every other family here spends a word each.
|
||
* This op has FIVE DRAM endpoints where rope has three; spelled the rope way that is 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. */
|
||
typedef enum ft_tt_op_gdn_field {
|
||
FT_TT_OP_GDN_SHAPE = 0, /**< b0 phase, b1 S/32, b2 v-heads, b3 k-heads */
|
||
FT_TT_OP_GDN_CONV_TILE_COLS = 1, /**< conv_dim/32; a full word — 256 is not a byte */
|
||
FT_TT_OP_GDN_CONV_TAPS = 2, /**< b0 ssm_conv; the window is taps-1 deep */
|
||
FT_TT_OP_GDN_ACTIVATION_XY = 3, /**< x low half-word, y high */
|
||
FT_TT_OP_GDN_ACTIVATION_IN = 4, /**< CONV: qkv. DELTA: the staged q|k|v */
|
||
FT_TT_OP_GDN_ACTIVATION_OUT = 5, /**< CONV: conv_out. DELTA: y, one row per v-head */
|
||
FT_TT_OP_GDN_CONSTANT_XY = 6,
|
||
FT_TT_OP_GDN_CONSTANT_WEIGHT = 7, /**< CONV only: the conv1d weight [conv_dim, taps] */
|
||
FT_TT_OP_GDN_CONSTANT_GATE = 8, /**< DELTA only: decay[H] then beta[H], fp32 */
|
||
FT_TT_OP_GDN_STATE_XY = 9,
|
||
FT_TT_OP_GDN_STATE_CONV = 10, /**< CONV only: the rolling window; READ AND WRITTEN */
|
||
FT_TT_OP_GDN_STATE_RECURRENT = 11,/**< DELTA only: [H,S,S] fp32; READ AND WRITTEN */
|
||
FT_TT_OP_GDN_SCALE_BITS = 12, /**< fp32 bits of 1/sqrt(S) */
|
||
FT_TT_OP_GDN_TILE_BYTES = 13,
|
||
FT_TT_OP_GDN_FLAGS = 14, /**< variant bits, FT_TT_OP_GDN_FLAG_* */
|
||
FT_TT_OP_GDN_TILE_COUNT = 15, /**< TOTAL tiles out — all the writer reads */
|
||
FT_TT_OP_GDN_WORDS = 16
|
||
} ft_tt_op_gdn_field;
|
||
|
||
/** Which half of the block a descriptor runs. Mirrors kGdnPhase* on the device. 0 is
|
||
* deliberately not a phase, so a zeroed parameter block is invalid rather than plausible. */
|
||
#define FT_TT_OP_GDN_PHASE_CONV 1u
|
||
#define FT_TT_OP_GDN_PHASE_DELTA 2u
|
||
|
||
/** CB indices, matching the compile-time args the GDN triad is built with.
|
||
*
|
||
* INDEX 2 AND INDEX 4 ARE NOT WHAT THE PAPER DESIGN SAID, and the difference is the whole
|
||
* of the SCALER/REPLICA collapse above. Index 2 carries the ROW-REPLICATED k and q — one
|
||
* tile serving both the reduce scaler and the rank-1 update — which frees index 4, and the
|
||
* reader takes it for the private staging buffer its bulk-read-then-word-copy idiom needs.
|
||
* That buffer is never reserved, never pushed and never popped by ANY processor, which is
|
||
* exactly what makes writing through its write pointer safe: the pointer can never have
|
||
* moved off the buffer base. The round's spec named index 7 for it, which is CB_PROD — the
|
||
* compute thread pushes and pops that inside the same descriptor, so scratch there would
|
||
* land on live pages.
|
||
*
|
||
* INDEX 8 EXISTS BECAUSE A BUFFER MAY HAVE ONLY ONE PRODUCER FOR THE LIFE OF THE RESIDENCY,
|
||
* not merely one per descriptor. PHASE DELTA's compute-private working state used to share
|
||
* index 0 with PHASE CONV's qkv stream, and the two phases push it from DIFFERENT
|
||
* PROCESSORS: the reader's `cb_push_back` INCREMENTS the `tiles_received` stream register,
|
||
* while the compute's `cb_push_back` keeps its own local counter and STORES that over the
|
||
* register. Every increment the reader had made was therefore discarded by the first DELTA
|
||
* push, the register jumped backwards, and the consumer's `received - acked` wrapped to a
|
||
* huge unsigned — so `cb_wait_front` on that buffer never blocked again for the rest of the
|
||
* run. Measured on silicon 2026-08-18: PHASE CONV's second pass copied whatever the eight
|
||
* CB_IN pages happened to hold, source tile `s` for output tile `t` satisfying
|
||
* `s ≡ t (mod 8)` at every one of 256 tiles. See kernels/resident_gdn_protocol.h. */
|
||
#define FT_TT_OP_GDN_CB_IN 0u /**< CONV ONLY: the qkv stream. The READER is its only
|
||
* producer, in either phase. */
|
||
#define FT_TT_OP_GDN_CB_STATE 1u /**< CONV: the rolling window. DELTA: one state tile-row */
|
||
#define FT_TT_OP_GDN_CB_SCALER 2u /**< DELTA: k then q, ROW-REPLICATED — scaler AND rank-1 */
|
||
#define FT_TT_OP_GDN_CB_VECTOR 3u /**< CONV: the weight taps. DELTA: v, decay, beta, scale */
|
||
#define FT_TT_OP_GDN_CB_STAGE 4u /**< the READER's private scratch — never pushed or popped */
|
||
#define FT_TT_OP_GDN_CB_PRED 5u /**< staging: the (b) and (e) reduce columns */
|
||
#define FT_TT_OP_GDN_CB_DELTA 6u /**< CONV: the running tap sum. DELTA: (v - pred) * beta */
|
||
#define FT_TT_OP_GDN_CB_PROD 7u /**< staging: k_replica * delta — there is no FMA */
|
||
#define FT_TT_OP_GDN_CB_WORK 8u /**< DELTA ONLY: the compute thread's private working
|
||
* state, which it both packs into and unpacks from */
|
||
#define FT_TT_OP_GDN_CB_OUT 16u
|
||
/** MULTI-ROW DELTA ONLY: the compute thread's carry of one head's whole state from one verify
|
||
* row to the next (kGdnCbCarry). Compute-produced AND compute-consumed, like CB_WORK. */
|
||
#define FT_TT_OP_GDN_CB_CARRY 9u
|
||
#define FT_TT_OP_GDN_CB_COUNT 11u
|
||
|
||
/** Where a v-head's constants sit in CB_VECTOR, after its `state_tiles` v columns. Mirrors
|
||
* `kGdnVector*` in kernels/resident_gdn_protocol.h; the reader REFUSES a descriptor whose
|
||
* CB_VECTOR cannot hold `state_tiles + FT_TT_OP_GDN_VECTOR_CONSTANTS` pages, so this is a
|
||
* number `ft_tt_op_gdn_place_cbs()` has to know rather than a device-side detail. */
|
||
#define FT_TT_OP_GDN_VECTOR_DECAY 0u
|
||
#define FT_TT_OP_GDN_VECTOR_BETA 1u
|
||
#define FT_TT_OP_GDN_VECTOR_SCALE 2u
|
||
#define FT_TT_OP_GDN_VECTOR_CONSTANTS 3u
|
||
|
||
/**
|
||
* Tile columns one CONV block covers — the THIRD two-sided compile-time contract in this
|
||
* family, after the CB indices and the tile size.
|
||
*
|
||
* The compute kernel cannot read it off the CB interface (`cb_interface` does not exist on
|
||
* the MATH thread and the reference fails at LINK time), and resident-protocol rule 3 forbids
|
||
* a trip count taken from runtime buffer state in any case. So the depth is baked into BOTH
|
||
* dataflow kernels as a compile-time arg — and the host needs the same number, because it is
|
||
* the host that sizes the buffers the reader then checks the depth against.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* TWO SINCE 2026-08-27, AND THE READER IS PHASE CONV'S WALL
|
||
* ---------------------------------------------------------------------------
|
||
* PHASE CONV is 20.1% of the decode token and MATH was ELIMINATED as its wall — deleting the
|
||
* SiLU from the image entirely moved it 0.09%. What was left was the reader, and at depth 1
|
||
* `read_one_conv()` issues 1,792 read barriers and 2,816 single-tile 4 KiB NoC reads a
|
||
* descriptor. Halving that count is worth, measured on silicon against a control re-taken in
|
||
* the same session at a 0.07% drift:
|
||
*
|
||
* depth 1 CONV 1162.0 / 1162.2 us a descriptor WALL 0.1378 / 0.1379 s/token
|
||
* depth 2 CONV 939.4 us WALL 0.1325 s/token
|
||
*
|
||
* **-19.2% on PHASE CONV and -3.85% on the token, 7.26 -> 7.55 tok/s**, `cmp` IDENTICAL at
|
||
* 189 B / 80fc6259 on every rung. CONV fell 5.4 ms/token and the wall fell 5.3 — the saving
|
||
* converts at ~98%, which is what a term that is not overlapped with anything looks like. It
|
||
* is the same shape PHASE DELTA had: reader 1.76x while its compute arm moved 0.33%.
|
||
*
|
||
* AND THE SAVING SATURATES AT TWO — DEPTH 4 IS REFUTED AS A RUNG, not merely blocked. On the
|
||
* one head-split width where a depth-4 build runs clean, PHASE CONV measures **945.6 / 945.7
|
||
* us** against depth 2's **939.5** on the same binary at the same one-await bracket: 0.7%
|
||
* WORSE. Halving the barrier count again buys nothing, so 2 is the end of this ladder whatever
|
||
* happens to the grid collision below.
|
||
*
|
||
* DEPTH 4 IS ALSO REFUSED ON SILICON AT THE SHIPPED HEAD SPLIT, and the refusal is correct. CB_VECTOR is
|
||
* 112 pages there and CONV's push chunk is `depth*taps` = 16; a CONV descriptor leaves the
|
||
* write pointer at page 16, and a DELTA descriptor on a FOUR-MEMBER head split then pushes
|
||
* `8 heads * (state_tiles + 3)` = 56 more, landing at 72 — which is not a multiple of 16, so
|
||
* `ft_tensix_cb_push_grid_is_safe()` refuses the next CONV. At depth 1 and 2 the arithmetic
|
||
* lands back on the grid (16 mod 4 and 16 mod 8), which is why the same code is clean there.
|
||
* The observed shape matches exactly: 1,175 of 1,176 CONV descriptors refused, each recovering
|
||
* on a re-arm that rebases the pointer, and one clean descriptor per re-arm. **The expression
|
||
* was then tested as a FOUR-POINT PREDICTION and held 4 of 4** — clean at N = 1 and 2 (0
|
||
* refusals, `cmp` IDENTICAL), refusing at N = 4 (1,175) and N = 8 (1,559), which is the
|
||
* arithmetic `16 + (32/N)*7 mod 112` landing on a multiple of 16 or not.
|
||
*
|
||
* THE DEPTH IS NOT A PLACEHOLDER AT ANY VALUE. The reader refuses any depth whose blocks do
|
||
* not fit the placed buffers, and CB_VECTOR must hold `2 * depth * taps` weight pages rounded
|
||
* onto `lcm(state_tiles + 3, depth * taps)`. Moving it is a measured change to this constant,
|
||
* `place_cbs` and both kernel build lines together.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* OVERRIDABLE AT BUILD TIME, AND NOW CHECKED ON THE CARD
|
||
* ---------------------------------------------------------------------------
|
||
* A depth SWEEP needs the host and both kernels moved together, so this is `#ifndef`-guarded
|
||
* exactly as `FT_TT_MATMUL_DST_SYNC` is: `-DFT_TT_OP_GDN_CONV_DEPTH=N` on the host build, and
|
||
* reader compile-time arg 7 / compute compile-time arg 9 set to the same N in a program
|
||
* directory built for that arm. Nothing derives one side from another.
|
||
*
|
||
* THE DIRECTION THAT USED TO BE SILENT IS NOW REFUSED ON THE CARD. Every device check this
|
||
* family had asks whether the PLACED BUFFERS CAN SERVE the image's depth, so a host that
|
||
* placed for a LARGER depth than the kernels were built at passed all of them: the arm ran at
|
||
* the kernels' depth inside the host's placement and produced a correct answer and THE
|
||
* CONTROL'S TIMING, reported as the arm's. `ft_tt_op_gdn_parameters()` now writes this value
|
||
* into byte 3 of the CONV-taps word and both kernels refuse a CONV descriptor that does not
|
||
* match the constant they were compiled with — see `kGdnByteConvDepth` in
|
||
* kernels/resident_gdn_protocol.h.
|
||
*
|
||
* IT MUST FIT A BYTE, because that is where it rides, and nothing reachable is near the
|
||
* limit. THE ARENA ENDS THIS LADDER, NOT THE READER — measured off hardware by calling
|
||
* `ft_tt_op_gdn_place_cbs()` at each rung, not modelled: at Ornith's 4 taps and `state_tiles`
|
||
* 4, CB_VECTOR is rounded onto `lcm(state_tiles + 3, depth * taps)` = `lcm(7, 4*depth)`, a 7
|
||
* that shares no factor with anything, so the buffer grows FOURFOLD a doubling. The family's
|
||
* ten buffers place **85 / 115 / 187 pages at depth 1 / 2 / 4** of the 330-page arena and
|
||
* **depth 8 returns ENOMEM**.
|
||
*
|
||
* AND THE RUNGS BETWEEN THE POWERS OF TWO ARE NOT RUNGS. Both kernels shrink the depth until
|
||
* it divides `conv_tile_cols` (256 at Ornith), so a build at 3 or 6 runs at 2 or 4 while the
|
||
* host placed onto `lcm(7, 12)` or `lcm(7, 24)` — a placement the shrunk depth's index grid
|
||
* does not divide, which the reader refuses. The reachable sweep is **1, 2, 4**.
|
||
*/
|
||
#ifndef FT_TT_OP_GDN_CONV_DEPTH
|
||
#define FT_TT_OP_GDN_CONV_DEPTH 2u
|
||
#endif
|
||
|
||
/** The knob that moves the HOST'S side of the contract without a host rebuild. See
|
||
* `ft_tt_op_gdn_conv_depth()`. */
|
||
#define FT_TT_OP_GDN_CONV_DEPTH_ENV "FT_TT_OP_GDN_CONV_DEPTH"
|
||
|
||
/**
|
||
* The CONV block depth this host places its circular buffers for and DECLARES on the wire.
|
||
*
|
||
* `FT_TT_OP_GDN_CONV_DEPTH` unless `FT_TT_OP_GDN_CONV_DEPTH` names another in 1..255, so a
|
||
* depth sweep runs several program directories against ONE host binary — which is what makes
|
||
* its per-descriptor figures comparable, since every `us each` in this backend is
|
||
* host-bracketed. Asking for a depth the armed program was not built at is REFUSED on the
|
||
* card, not silently ignored.
|
||
*
|
||
* @return the depth, or 0 for a knob set to something unusable — in which case
|
||
* `ft_tt_op_gdn_parameters()` and `ft_tt_op_gdn_place_cbs()` both refuse
|
||
*
|
||
* Thread-safe: after the first call Reentrant: no Signal-safe: no
|
||
*/
|
||
uint32_t ft_tt_op_gdn_conv_depth(void);
|
||
|
||
/**
|
||
* The knob's PARSER, split out of the cached accessor so it can be tested at more than one
|
||
* value in one process.
|
||
*
|
||
* @param value the environment string, or NULL/empty for "not set"
|
||
* @return the depth, `FT_TT_OP_GDN_CONV_DEPTH` when unset, or 0 for anything unusable —
|
||
* never a silent fallback to the default, which is how a sweep measures its control
|
||
* five times and reports a ladder
|
||
*
|
||
* Thread-safe: yes Reentrant: yes Signal-safe: yes
|
||
*/
|
||
uint32_t ft_tt_op_gdn_conv_depth_parse(const char *value);
|
||
|
||
/**
|
||
* PHASE DELTA's DST batch width — the FOURTH compile-time contract of this family, and the
|
||
* only one whose failure mode is slow rather than wrong.
|
||
*
|
||
* MEASURED ON SILICON 2026-08-18: the DELTA descriptor costs 3,368 µs and is bound by neither
|
||
* compute (95 GFLOP/s on a Tensix that can issue ~1,400) nor bandwidth (1.27 GB/s, 3–5% of one
|
||
* core's DRAM). It is bound by ~2,688 single-tile DST sections a descriptor at ~1,250 ns each,
|
||
* against ~64 cycles of tile arithmetic — a `ZEROACC` 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. `reduce_row_sum` in the compute kernel already batched this way; the
|
||
* three per-tile loops around it never used a DST index above 0.
|
||
*
|
||
* THE CEILING IS FOUR AND IT IS NOT A TUNING KNOB. DST is 16 tiles of 32x32, DST_SYNC half
|
||
* hands MATH one half at a time, and `dst_accum_mode 1` — MANDATORY for the one op in this
|
||
* backend that carries state ACROSS TOKENS — halves it again. Four is also exactly Ornith's
|
||
* `state_tiles`, so every DELTA loop batches whole. `ftg_dst_batch()` in
|
||
* kernels/resident_gdn_protocol.h is the single implementation of the clamp, shared by the
|
||
* reader, the compute kernel and `resolve_cb_pages()` here.
|
||
*
|
||
* 1 IS THE IDENTITY. Every batched loop collapses to the per-tile sequence that shipped, which
|
||
* is what makes a `dst_batch_max = 1` build the exact control arm — and it is also what a short
|
||
* build line silently produces, since an out-of-range `ft_tensix_compile_time_arg` folds to a
|
||
* literal 0 under SFPI GCC 15.1.0 and `ftg_dst_batch()` maps 0 to 1. Unlike the CONV depth,
|
||
* whose zero-fold makes every CONV descriptor REFUSE, this one cannot be caught by watching for
|
||
* an error: the arg COUNT beside each build line, pinned by test_gdn_ops.c, is the only guard.
|
||
*/
|
||
#define FT_TT_OP_GDN_DST_BATCH 4u
|
||
|
||
/**
|
||
* The highest PHASE DELTA WRITER RUNG this placement supports — the SEVENTH compile-time
|
||
* contract of this family, and the one that sizes CB_OUT.
|
||
*
|
||
* A NoC write barrier is a per-core DRAIN. The DELTA writer took one PER TILE —
|
||
* `v_heads * state_tiles * (state_tiles + 1)` = 640 a descriptor at Ornith's shape — against
|
||
* this backend's own rule of one barrier per stream group, the rule that makes the block
|
||
* reader's barrier worth 5.9x. `ftg_writer_block()` in kernels/resident_gdn_protocol.h is the
|
||
* one implementation of the group, shared by the writer and by `resolve_cb_pages()` here.
|
||
*
|
||
* THIS CONSTANT SIZES THE BUFFER, IT DOES NOT SELECT THE BEHAVIOUR. The writer's rung is
|
||
* compile-time arg 3 of `WRITER_CTARGS` and the writer clamps it to half of what is placed
|
||
* here, so the two sides cannot disagree into a hang — the failure mode `reserve_back(cb_out,
|
||
* 4)` produced against a two-page CB_OUT on silicon 2026-08-19. Sizing for the ceiling is
|
||
* what `FT_TT_OP_GDN_DST_BATCH` already does for CB_PROD, and for the same reason: it keeps a
|
||
* rung-0 build an exact control arm for a rung-1 one, on one host binary.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* 1, AND THE MEASUREMENT SAYS SO ON BOTH SIDES. Silicon 2026-08-19, `cmp` IDENTICAL and
|
||
* `company=0` on every arm.
|
||
*
|
||
* THE BARRIER COUNT IS A NULL. Rungs 0/1/2 leave 640/128/32 barriers in a DELTA descriptor
|
||
* and it costs 1836.0 / 1835.3 / 1845.1 us at N=1 and 680.8 / 680.4 / 684.8 at N=4 — a 20x
|
||
* reduction in a per-core drain buys 0.0%, and the largest group is reproducibly ~0.5% WORSE
|
||
* because the writer waits longer before it starts draining. The ~413 us the scoping put on
|
||
* these barriers was writer-THREAD time that was never on the critical path.
|
||
*
|
||
* WHAT THE BUFFER BUYS IS CAPACITY, AND IT SATURATES AT TWO TILE-ROW GROUPS. One kernel image
|
||
* (the per-tile sequence) over three placements — CB_OUT at 2, 10 and 40 pages — costs
|
||
* 1908.3 / 1835.2 / 1835.1 us at N=1 and 697.3 / 679.4 / 679.1 at N=4. So 2 -> 10 pages is
|
||
* worth 3.8% / 2.6% and 10 -> 40 is worth NOTHING: with only two pages the compute thread's
|
||
* `reserve_back(cb_out, 1)` hard-handshakes the writer on every tile, and ten pages is all it
|
||
* takes to decouple them. PHASE CONV is flat at 1222.3-1223.2 across all of it, which is the
|
||
* null that says this is the DELTA cadence and not a general effect.
|
||
*
|
||
* 1 = one barrier per TILE-ROW, `state_tiles + 1` = 5 pages at Ornith, so CB_OUT is 10 pages
|
||
* of 4 KiB = 40 KiB. Rung 2 would place 40 pages for 160 KiB of a 1,320 KiB arena and buy
|
||
* nothing measurable, so the 120 KiB stays in the arena. The rung-2 code path is kept because
|
||
* the clamp makes it free and it is the third point of the ladder above.
|
||
*/
|
||
#define FT_TT_OP_GDN_WRITER_RUNG 1u
|
||
|
||
/**
|
||
* The two PHASE DELTA restructuring bitmasks the shipped kernels are built with — the fifth
|
||
* and sixth compile-time contracts of this family, and the two that carry the measurement.
|
||
*
|
||
* MEASURED ON SILICON 2026-08-19, all arms `cmp` IDENTICAL against the 48-token oracle, on a
|
||
* card with no other process on it, baseline reproduced to 0.2 µs (3351.2 / 3351.0):
|
||
*
|
||
* READER bit 0, the column-major replica build 3351.2 → 1918.7 µs −42.7%
|
||
* COMPUTE bit 0, the fused state emit 3351.2 → 3340.2 µs −0.33%
|
||
* both 3351.2 → 1905.4 µs −43.1%
|
||
*
|
||
* THAT PAIR OF NUMBERS IS THE WHOLE DIAGNOSIS. The compute arm removes FOUR of the
|
||
* twenty-seven tile operations a tile-row — 15% of the MATH and UNPACK work — and moves the
|
||
* descriptor by a third of a percent. The reader arm removes 960 of the 1,024 LOADS in each
|
||
* of 256 replica tiles, touches `brisc` and nothing else, produces byte-identical circular
|
||
* buffer contents, and moves it by 43%. PHASE DELTA was never bound by its compute thread:
|
||
* it was bound by ~262,000 scalar L1 word copies on the READER, and the ~958 ns a
|
||
* "single-tile operation" was that, seen through a division.
|
||
*
|
||
* COMPUTE bit 1 is NOT in the shipped mask, and the reason is evidence rather than doubt. Its
|
||
* first build refused on silicon (it applied the batched push to CB_OUT, which holds two
|
||
* pages); with CB_OUT excluded it is `cmp` IDENTICAL and worth **0.56%** — 1,905.3 → 1,894.7 µs
|
||
* against its own baseline in the same session. That is a real gain on ONE run, of the same
|
||
* size as the DST batch's 1.2%, and both are what is left once the reader stops being the
|
||
* wall. It stays out of the default until it has a second run behind it.
|
||
*/
|
||
#define FT_TT_OP_GDN_READER_EXTRAS 1u
|
||
#define FT_TT_OP_GDN_DELTA_EXTRAS 1u
|
||
|
||
/** Every circular buffer of this family is Float32 (kernels/cb_tables/gdn_float32.cbtable)
|
||
* and every DRAM operand it touches is therefore an fp32 tile.
|
||
*
|
||
* THIS IS THE ONE THE HOST GOT WRONG. `place_cbs` used to size five of the nine buffers
|
||
* from `activation_in.tile_bytes` while the kernels are compiled with TILE_BYTES = 4096 and
|
||
* validate only the word `parameters()` happens to have taken from the STATE operand. A
|
||
* request with a bf16 activation — which is what the shipped strip allocator produces and
|
||
* what this suite's own fixture used — therefore placed 2048-byte pages under a reader that
|
||
* writes 4096-byte tiles into them, and the tile_bytes refusal did not fire because it was
|
||
* reading the other operand. `ft_tt_op_gdn_parameters()` now refuses any operand that is
|
||
* not this size, so the two cannot disagree. */
|
||
#define FT_TT_OP_GDN_FP32_TILE_BYTES FT_TT_OP_TILE_BYTES_FP32
|
||
|
||
/** Qwen3.5 (Ornith 9B), read off Pantheon's `pn_qwen35moe_config` — not assumed. */
|
||
#define FT_TT_OP_GDN_QWEN35_STATE 128u /**< head_k_dim == head_v_dim */
|
||
#define FT_TT_OP_GDN_QWEN35_K_HEADS 16u /**< ssm_n_group */
|
||
#define FT_TT_OP_GDN_QWEN35_V_HEADS 32u /**< ssm_dt_rank */
|
||
#define FT_TT_OP_GDN_QWEN35_CONV_TAPS 4u /**< ssm.conv_kernel */
|
||
#define FT_TT_OP_GDN_QWEN35_CONV_DIM 8192u /**< 2*key_dim + val_dim */
|
||
|
||
/** Mirrors kGdnMultiRowMax: rows one multi-row DELTA descriptor may carry. */
|
||
#define FT_TT_OP_GDN_MULTI_ROW_MAX 16u
|
||
/** The widest state (in tiles, S/32) whose multi-row buffers are placed: Ornith's S = 128. */
|
||
#define FT_TT_OP_GDN_MULTI_ROW_STATE_TILES_MAX 4u
|
||
|
||
/** One GDN call. The operands a phase does not read may be left zeroed. */
|
||
typedef struct ft_tt_op_gdn_request {
|
||
uint32_t phase; /**< FT_TT_OP_GDN_PHASE_* */
|
||
uint32_t state_size; /**< S; a multiple of 32 */
|
||
uint32_t v_heads;
|
||
uint32_t k_heads; /**< must divide v_heads — the GQA repeat is vh % k_heads */
|
||
uint32_t conv_dim; /**< a multiple of 32 */
|
||
uint32_t conv_taps;
|
||
/** PHASE DELTA head slice — the v-heads THIS member owns, for a head-split across a
|
||
* group of GDN residencies. `head_count` 0 means the whole descriptor and is what an
|
||
* unsliced caller leaves it at; a 0 count encodes as zero bytes, so an unsliced
|
||
* descriptor is BYTE-IDENTICAL to the one that shipped. `head_first + head_count` must
|
||
* not exceed `v_heads`, and a CONV descriptor may not carry a slice at all — the
|
||
* convolution divides by CHANNEL, not by head, and its operands are tap-major planes a
|
||
* column slice is not contiguous in. See kGdnByteHeadFirst in
|
||
* kernels/resident_gdn_protocol.h for why the slice rides in the CONV-taps word. */
|
||
uint32_t head_first;
|
||
uint32_t head_count;
|
||
/** PHASE CONV column slice — the TILE COLUMNS this member convolves, at the full plane
|
||
* stride. `col_count` 0 means the whole stream and encodes as zero bytes, so an unsliced
|
||
* CONV descriptor is BYTE-IDENTICAL to the one that shipped. Rides in the same two bytes
|
||
* of the CONV-taps word as the DELTA head slice; each phase reads its own meaning. */
|
||
uint32_t col_first;
|
||
uint32_t col_count;
|
||
ft_tt_op_operand activation_in;
|
||
ft_tt_op_operand activation_out;
|
||
ft_tt_op_operand conv_weight; /**< CONV */
|
||
ft_tt_op_operand gate; /**< DELTA: decay then beta, fp32 */
|
||
ft_tt_op_operand state_conv; /**< CONV; read and written */
|
||
ft_tt_op_operand state_recurrent; /**< DELTA; read and written */
|
||
/** S2: when set, the phase's state is READ from state_conv / state_recurrent and WRITTEN
|
||
* here (same NoC endpoint — one coordinate word) — FT_TT_OP_GDN_FLAG_STATE_OUT. */
|
||
ft_tt_op_operand state_out;
|
||
/** S2c: the row select (0-31; 0 = the shipped row-0 walk). CONV needs the strip flag. */
|
||
uint32_t row;
|
||
/** MULTI-ROW (DELTA PM 5, CONV PM 6 in strip mode): rows [row, row + row_count) in one
|
||
* descriptor; 0 or 1 is the one-row walk, byte for byte. Row r's carried state (DELTA:
|
||
* the recurrent state, CONV: the rolled window) goes to `state_rows_out[r]` (same bank as
|
||
* the state it read), carried by the EXTENSION block ft_tt_op_gdn_row_extension()
|
||
* encodes into the next parameter slot. DELTA's gate is row-major at ftg_gate_stride(). */
|
||
uint32_t row_count;
|
||
ft_tt_op_operand state_rows_out[FT_TT_OP_GDN_MULTI_ROW_MAX];
|
||
/** FT_TT_OP_GDN_FLAG_* bits, encoded into word 14. 0 = the shipped walk. */
|
||
uint32_t flags;
|
||
} ft_tt_op_gdn_request;
|
||
|
||
/** Mirrors kGdnFlagNocReplicate — change both or neither. */
|
||
#define FT_TT_OP_GDN_FLAG_NOC_REPLICATE 8u
|
||
#define FT_TT_OP_GDN_FLAG_RED_NO_COPY 16u
|
||
/** Mirrors kGdnFlagConvStrip / kGdnFlagRedStripHalf (M9). */
|
||
#define FT_TT_OP_GDN_FLAG_CONV_STRIP 32u
|
||
#define FT_TT_OP_GDN_FLAG_RED_STRIP_HALF 64u
|
||
/** Mirrors kGdnFlagStateOut; set by the encoder from `state_out`, never by a caller. */
|
||
#define FT_TT_OP_GDN_FLAG_STATE_OUT 128u
|
||
/** Mirrors kGdnRowShift: the row select rides in flags bits 16-20 (`row`). */
|
||
#define FT_TT_OP_GDN_ROW_SHIFT 16u
|
||
/** Mirrors kGdnFlagRedRowsRow0 (RED ONLY): a multi-row reader stages row `row` for every row. */
|
||
#define FT_TT_OP_GDN_FLAG_RED_ROWS_ROW0 (1u << 26)
|
||
/** Mirrors kGdnRowCountShift: the multi-row count rides in flags bits 21-25 (`row_count`). */
|
||
#define FT_TT_OP_GDN_ROW_COUNT_SHIFT 21u
|
||
|
||
/**
|
||
* Is this a shape the triad can execute? The same predicate the kernel applies, so both
|
||
* sides refuse the same descriptors independently.
|
||
*
|
||
* Thread-safe: yes Reentrant: yes Signal-safe: yes
|
||
*/
|
||
int ft_tt_op_gdn_shape_is_supported(const ft_tt_op_gdn_request *request);
|
||
|
||
/**
|
||
* Encode `request` into the sixteen parameter words. Pure — touches no device.
|
||
*
|
||
* @return FT_TT_OK, or FT_TT_EINVAL for a null argument or an unsupported shape.
|
||
*
|
||
* Thread-safe: yes Reentrant: yes Signal-safe: yes
|
||
*/
|
||
int ft_tt_op_gdn_parameters(const ft_tt_op_gdn_request *request,
|
||
uint32_t out_words[FT_TT_OP_GDN_WORDS]);
|
||
|
||
/**
|
||
* Encode a MULTI-ROW DELTA descriptor's extension block — the sixteen words written into the
|
||
* parameter slot AFTER the descriptor's own: word r = the DRAM address of row r's state output,
|
||
* r < row_count, the rest zero. Pure.
|
||
*
|
||
* @return FT_TT_OK, or FT_TT_EINVAL for a request that is not multi-row DELTA, an unset row
|
||
* operand, or one outside the recurrent state's bank (one coordinate word carries both)
|
||
*
|
||
* Thread-safe: yes Reentrant: yes Signal-safe: yes
|
||
*/
|
||
int ft_tt_op_gdn_row_extension(const ft_tt_op_gdn_request *request,
|
||
uint32_t out_words[FT_TT_OP_GDN_WORDS]);
|
||
|
||
/**
|
||
* Place this op's circular buffers in the L1 arena and encode the descriptor blob.
|
||
*
|
||
* @param out_mask receives the local CB mask the launch message wants
|
||
* @return FT_TT_OK, FT_TT_EINVAL, or FT_TT_ENOMEM when the arena cannot hold them
|
||
*
|
||
* Thread-safe: yes Reentrant: yes Signal-safe: no
|
||
*/
|
||
int ft_tt_op_gdn_place_cbs(const ft_tt_op_gdn_request *request,
|
||
ft_tt_launch_cb_entry out_entries[FT_TT_OP_GDN_CB_COUNT],
|
||
void *buffer, size_t buffer_bytes, uint64_t *out_mask);
|
||
|
||
/* ================================================================
|
||
* WHAT ONE LAYER COSTS IN CARD DRAM — every operand, one expression each
|
||
* ================================================================
|
||
*
|
||
* A HOST STAGER ALLOCATES ALL SIX OF THESE AND NOT TWO, which is why there are now six
|
||
* functions where there were two. The two that existed answered the CAPACITY question and
|
||
* were then reached for as buffer sizes, and for the conv window those are not the same
|
||
* number: `conv_dim * (taps-1)` fp32 ELEMENTS is 96 KiB a layer, and the window the reader
|
||
* addresses is `(taps-1)` TAP-MAJOR PLANES of `conv_dim/32` fp32 TILES — 3 MiB a layer, 32x
|
||
* larger, because a plane is a one-row tilized strip and the tile quantum is 32 rows.
|
||
* `ft_tt_op_gdn_conv_state_bytes()` now returns the DRAM figure, because a buffer that is
|
||
* 1/32 of what the kernel reads is not a capacity discrepancy, it is a NoC read landing in
|
||
* whatever the allocator put above it.
|
||
*
|
||
* The recurrent state is unaffected: `[S,S]` per head is a whole number of 32x32 tiles with
|
||
* no padding, so `v_heads*S*S*4` is both the element count and the tile count times 4096.
|
||
*
|
||
* Every one of these returns 0 for a request whose shape this family will not execute, so an
|
||
* unchecked caller allocates nothing rather than something wrong.
|
||
*/
|
||
|
||
/**
|
||
* Bytes of carried recurrent state one LAYER owns: `v_heads * S * S * 4`, which is also
|
||
* `v_heads * state_tiles^2` fp32 tiles.
|
||
*
|
||
* Exposed because the caller allocates it, and because the capacity number this op is
|
||
* charged against should be computed from the same expression that sizes the buffer rather
|
||
* than restated beside it.
|
||
*/
|
||
size_t ft_tt_op_gdn_state_bytes(const ft_tt_op_gdn_request *request);
|
||
|
||
/**
|
||
* Bytes of the rolling conv window one LAYER owns, AS THE READER ADDRESSES IT: `(taps-1)`
|
||
* tap-major planes of `conv_dim/32` fp32 tiles.
|
||
*
|
||
* NOT `conv_dim * (taps-1) * 4`. See the note above; that expression is 1/32 of this one.
|
||
*/
|
||
size_t ft_tt_op_gdn_conv_state_bytes(const ft_tt_op_gdn_request *request);
|
||
|
||
/** Bytes of the tap-major conv WEIGHT one LAYER owns: `taps` planes of `conv_dim/32` fp32
|
||
* tiles. A constant — staged once, transposed out of the oracle's row-major [conv_dim,taps]. */
|
||
size_t ft_tt_op_gdn_conv_weight_bytes(const ft_tt_op_gdn_request *request);
|
||
|
||
/** Bytes of ONE conv stream — the qkv input or the conv_out output of PHASE CONV. One row
|
||
* block of `conv_dim/32` fp32 tiles, so the two are the same size. */
|
||
size_t ft_tt_op_gdn_conv_stream_bytes(const ft_tt_op_gdn_request *request);
|
||
|
||
/** Bytes of the staged `q | k | v` input of PHASE DELTA. q and k are per-K-HEAD and v is
|
||
* per-V-head, so the three blocks are NOT the same size whenever they need different row
|
||
* block counts. */
|
||
size_t ft_tt_op_gdn_delta_input_bytes(const ft_tt_op_gdn_request *request);
|
||
|
||
/** Bytes of `y`, the output of PHASE DELTA: a tilized [v_heads, S] fp32 matrix. */
|
||
size_t ft_tt_op_gdn_delta_output_bytes(const ft_tt_op_gdn_request *request);
|
||
|
||
/**
|
||
* Bytes the DELTA gate block's allocation must be at least: `(2*v_heads*4 + 63) & ~63`.
|
||
*
|
||
* The block itself is `decay[v_heads]` then `beta[v_heads]` fp32 and nothing reads past it —
|
||
* but the reader pulls it with ONE NoC read whose length is rounded UP to the 64-byte DRAM
|
||
* granule, so a tight allocation is read off the end of.
|
||
*/
|
||
size_t ft_tt_op_gdn_gate_bytes(const ft_tt_op_gdn_request *request);
|
||
|
||
#endif /* FT_TT_OP_GDN_H */
|