/* 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 #include #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 */