306 lines
17 KiB
C
306 lines
17 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/**
|
|
* @file rms_norm_compute.c
|
|
* @brief Compute third of Foundry's RMS-norm triad -- the `fc_rms_norm` equivalent
|
|
* (spec 5.1). For each row r of a [rows x W] activation:
|
|
*
|
|
* y[r][c] = x[r][c] * gamma[c] * rsqrt( mean_c(x[r][c]^2) + eps )
|
|
*
|
|
* at 32x32 tile granularity, so one "row block" is 32 rows wide by Wt tiles.
|
|
*
|
|
* THREE THINGS HERE ARE WORTH READING BEFORE EDITING, and they are the original C++
|
|
* kernel's, unchanged by the port:
|
|
*
|
|
* 1. eps COSTS NO CODE. The row-sum is produced by a row-wise SUM reduce against a scaler
|
|
* tile holding 1/W, and the reduce ACCUMULATES into DST across calls. So one extra
|
|
* reduce against a constant CB whose every element is eps*W/32 adds exactly eps to each
|
|
* row's mean square -- no broadcast add, no extra pack, no extra CB round trip. (Each
|
|
* row of that tile has 32 columns; 32 * (eps*W/32) * (1/W) = eps.) The host builds that
|
|
* tile; see ft_tt_op_rms_norm.c.
|
|
*
|
|
* 2. gamma ARRIVES AS FULL 32x32 TILES, not a broadcast row. The norm weight is a 1-D
|
|
* tensor and the FTTW artifact stores 1-D tensors untiled (bringup log 12.7). The
|
|
* LOADER expands it to Wt tiles with the row replicated 32 times, which turns the gamma
|
|
* multiply into a plain tile multiply instead of a broadcast op and costs 96 KiB of L1
|
|
* for a 1536-wide model.
|
|
*
|
|
* 3. THE SCALER AND EPS CBs ARE PUSHED ONCE AND NEVER POPPED. A CB wait is idempotent as
|
|
* long as nothing pops, so they stay resident for every row block.
|
|
*
|
|
* WHAT THE PORT ADDS, AND THE ONE HAZARD IT MAKES VISIBLE. Three things in this kernel are
|
|
* only correct because of statement ORDER, and none of them is enforced by a type:
|
|
*
|
|
* * `reduce_uninit` IS THE ONLY THING THAT CLEARS THE PACKER EDGE MASK. Between
|
|
* `ft_tensix_pack_reduce_row_mask_config()` and `ft_tensix_pack_reduce_mask_clear()`
|
|
* EVERY packed tile carries one datum per row and zeros elsewhere. MEASURED 2026-08-13:
|
|
* delete the clear at the bottom of the reduce block below and this kernel goes from
|
|
* PCC 0.999978735 to **PCC 0.195355069**, because `x*gamma` and the final product are
|
|
* then packed through the reduce mask. It is one line and nothing else moves -- the other
|
|
* four images stay byte-identical.
|
|
* The state IS per-core and DOES outlive the program, but the "so a kernel that exits in
|
|
* between poisons the next image on that core" this used to say is RETRACTED: the next
|
|
* image's `ft_tensix_pack_hw_configure()` rewrites `PCK_EDGE_OFFSET_SEC0 = 0xffff` with
|
|
* its four row-set selectors zeroed and `TILE_ROW_SET_MAPPING_0 = 0`, which makes the
|
|
* stale cfg 25 / cfg 21 unreachable. Proven both ways on cores 60-63 -- a matmul run on
|
|
* the same core in the same process after this kernel leaked scored 0.999998638, and only
|
|
* went to 0.25 once those two lines were deleted from its own startup. The live hazard is
|
|
* the rest of THIS kernel, and a RESIDENT triad, which runs startup once and then serves
|
|
* descriptors with nothing between them. `tools/check_tensix_reduce_exit_state.py`.
|
|
* * After the reduce, MATH still holds reduce's ADDR_MOD_0..3, its MOP template and its
|
|
* eight MVMULs in replay slots 16-23, and UNPACK still has the haloize bit SET. The
|
|
* multiply init that follows is what reprograms them, and nothing may unpack in between.
|
|
* * `ft_tensix_math_wait_for_dest_available()` (`tile_regs_acquire`) does NOT zero DST.
|
|
* The reduce is an accumulation into DST, so what makes each row block start from zero
|
|
* is PACK's per-tile `TT_ZEROACC` -- cross-thread, and invisible in this file.
|
|
*
|
|
* AND THE COLUMN-0 CONTRACT IS NOT "GARBAGE ELSEWHERE". the reduce kernel API states that while
|
|
* the reduce packer mask is programmed, every non-column-0 datum is packed as ZERO. So the
|
|
* `cb_scale` tile is a genuine column vector, and the column broadcast that consumes it
|
|
* reads column 0 only. Two comments in this tree used to claim the other lanes held
|
|
* leftovers; they were wrong and are corrected here.
|
|
*
|
|
* Compile-time args (positional):
|
|
* 0: cb_in activations, Wt pages
|
|
* 1: cb_gamma expanded norm weight, Wt pages, pushed once
|
|
* 2: cb_scaler one tile of 1/W, pushed once
|
|
* 3: cb_eps one tile of eps*W/32, pushed once
|
|
* 4: cb_xx intermediate x*x, Wt pages, produced and consumed here
|
|
* 5: cb_scale intermediate rsqrt result, 1 page, produced and consumed here
|
|
* 6: cb_xg intermediate x*gamma, 1 page, produced and consumed here
|
|
* 7: cb_out output, >= 2 pages
|
|
* Runtime args:
|
|
* 0: number of row blocks (rows / 32)
|
|
* 1: Wt (W / 32)
|
|
*
|
|
* Build:
|
|
* CTARGS=0,1,2,3,4,5,6,16 bash foundry/tt/build_tensix_kernel.sh \
|
|
* foundry/tt/kernels/rms_norm_compute.c /tmp/ttk_rms_norm
|
|
*
|
|
* IT BUILDS `--uniform-default`, i.e. DST_ACCUM_MODE 0 -- no `CB_TABLE` on that command line.
|
|
* That is not a detail: it selects rsqrt's bf16-ROUNDING arm (a `sfpstochrnd` before every
|
|
* DST store) rather than the fp32-dest arm. Both are compiled into
|
|
* `ft_tensix_sfpu_math.h`; changing the build to a table with `dst_accum_mode 1` changes
|
|
* which one is emitted and nothing in this file.
|
|
*
|
|
* @status CONVERTED, 2026-08-13 (plan Phase 6.6), first build and first diff. `rms_norm_
|
|
* compute.cpp` is retained only as the diff baseline. Measured against it with
|
|
* tools/diff_tensix_kernel_instructions.sh, per image:
|
|
* trisc2 (PACK) Tensix stream BYTE-IDENTICAL, 91 instructions, and the SAME 2548 B
|
|
* `.text`. 32 RISC-V lines differ, all of them one register swapped
|
|
* (a0<->a1) and two stack slots swapped (104<->108) -- the same
|
|
* spill-slot pattern vector_add's PACK and the MAX probe pair showed.
|
|
* trisc1 (MATH) identical opcode histogram on all 25 mnemonics EXCEPT `ttmop`, 8 vs
|
|
* 14; 2504 vs 2500 B. The six extra are C UNROLLING the two `mul_tiles`
|
|
* face loops (num_faces = 4, a compile-time constant in both arms -- the
|
|
* C++ image proves it with `li a5,4`) where C++ keeps a four-trip loop,
|
|
* and the loop bookkeeping C drops pays for them almost exactly. Every
|
|
* one of the 232 SFPU instructions, all 8 MVMULs, the replay record and
|
|
* all 59 SETC16 are in the same order. After normalising registers and
|
|
* branch targets the ONLY remaining delta is that unrolling: 6 added
|
|
* `ttmop`, 2 removed `li 4`, 3 removed `addi -1`.
|
|
* trisc0 (UNPACK) identical opcode histogram except STALLWAIT/MOP/SEMGET at 10 vs 5 --
|
|
* C++ TAIL-DUPLICATES the `unp_cfg_context` branch and carries a second
|
|
* static copy of those three per unpack. There are exactly 5 unpack call
|
|
* sites in this kernel and C emits exactly 5 of each; the C image is
|
|
* 60 B SMALLER (2448 -> 2388), which is 5 x 3 instructions to the byte.
|
|
* Same systemic C-vs-C++ layout gap the plan already records for
|
|
* `vector_add_compute` and the MAX probe pair.
|
|
* This kernel is also the FIRST CALLER of `ft_tensix_compute_reduce.h`'s SUM arm, which
|
|
* until now had never been diffed against a C++ build; it PASSES, per image and per
|
|
* function (isolation unit 6).
|
|
* @issues rsqrt runs over the whole DST tile, but only column 0 of the reduce result is
|
|
* meaningful; the other columns are ZERO (see the packer contract above), and rsqrt(0) is
|
|
* +inf. The column broadcast reads column 0 only, so those lanes are never consumed. Do
|
|
* not "fix" this by zeroing them -- it would cost a pass.
|
|
* @todo None
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
|
|
#include "ft_tensix_cb_formats.h"
|
|
#include "ft_tensix_compute.h"
|
|
#include "ft_tensix_compute_broadcast.h"
|
|
#include "ft_tensix_compute_cb.h"
|
|
#include "ft_tensix_compute_reduce.h"
|
|
#include "ft_tensix_kernel_args.h"
|
|
#ifdef FT_TRISC_MATH
|
|
#include "ft_tensix_compute_math.h"
|
|
#include "ft_tensix_compute_sfpu.h"
|
|
#include "ft_tensix_math_eltwise_sfpu.h"
|
|
#endif
|
|
#ifdef FT_TRISC_UNPACK
|
|
#include "ft_tensix_compute_sfpu.h"
|
|
#include "ft_tensix_compute_unpack.h"
|
|
#endif
|
|
#ifdef FT_TRISC_PACK
|
|
#include "ft_tensix_compute_pack.h"
|
|
#endif
|
|
|
|
/* One tile at a time, so only destination register 0 is ever live. */
|
|
#define DST_REGISTER 0u
|
|
|
|
void kernel_main(void)
|
|
{
|
|
const uint32_t cb_in = ft_tensix_compile_time_arg(0);
|
|
const uint32_t cb_gamma = ft_tensix_compile_time_arg(1);
|
|
const uint32_t cb_scaler = ft_tensix_compile_time_arg(2);
|
|
const uint32_t cb_eps = ft_tensix_compile_time_arg(3);
|
|
const uint32_t cb_xx = ft_tensix_compile_time_arg(4);
|
|
const uint32_t cb_scale = ft_tensix_compile_time_arg(5);
|
|
const uint32_t cb_xg = ft_tensix_compile_time_arg(6);
|
|
const uint32_t cb_out = ft_tensix_compile_time_arg(7);
|
|
|
|
const uint32_t row_blocks = ft_tensix_runtime_arg(0);
|
|
const uint32_t tile_cols = ft_tensix_runtime_arg(1);
|
|
|
|
uint32_t block;
|
|
uint32_t wt;
|
|
|
|
/* compute_kernel_hw_startup(cb_in, cb_in, cb_xx) -- both source registers take cb_in. */
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_hw_configure_ab(
|
|
ft_cb_unpack_src_format[cb_in], ft_cb_unpack_src_format[cb_in], ft_cb_unpack_dst_format[cb_in],
|
|
ft_cb_unpack_dst_format[cb_in], ft_cb_unpack_tile_face_rows[cb_in], ft_cb_unpack_tile_face_rows[cb_in],
|
|
ft_cb_unpack_tile_num_faces[cb_in], ft_cb_unpack_tile_num_faces[cb_in],
|
|
ft_tensix_cb(cb_in)->fifo_page_size, ft_tensix_cb(cb_in)->fifo_page_size));
|
|
|
|
FT_TENSIX_MATH(ft_tensix_math_pack_sync_init());
|
|
FT_TENSIX_MATH(ft_tensix_math_hw_configure(ft_cb_unpack_dst_format[cb_in],
|
|
ft_cb_unpack_dst_format[cb_in], FT_CB_DST_ACCUM_FP32));
|
|
|
|
FT_TENSIX_PACK(ft_tensix_pack_hw_configure(
|
|
ft_cb_pack_src_format[cb_xx], ft_cb_pack_dst_format[cb_xx], ft_tensix_cb(cb_xx)->fifo_page_size,
|
|
ft_cb_pack_tile_num_faces[cb_xx], ft_cb_pack_partial_face[cb_xx], 0 /*relu_config*/,
|
|
FT_CB_DST_ACCUM_FP32));
|
|
FT_TENSIX_PACK(ft_tensix_pack_init(ft_cb_pack_src_format[cb_xx], ft_cb_pack_tile_face_rows[cb_xx],
|
|
ft_cb_pack_tile_num_faces[cb_xx], 1 /*num_tiles*/));
|
|
FT_TENSIX_PACK(ft_tensix_pack_dest_init());
|
|
|
|
/* The constants the reader pushes once. Waiting for them here rather than inside the
|
|
* block loop is what makes "pushed once, never popped" work. */
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front(cb_scaler, 1));
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front(cb_eps, 1));
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front(cb_gamma, tile_cols));
|
|
|
|
for (block = 0; block < row_blocks; block++) {
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front(cb_in, tile_cols));
|
|
|
|
/* --- x*x into cb_xx ------------------------------------------------- */
|
|
FT_TENSIX_MATH(ft_tensix_math_eltwise_binary_mul_init(
|
|
FT_CB_MATH_FIDELITY, (uint32_t)ft_cb_unpack_tile_face_rows[cb_in]));
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab_init(ft_cb_unpack_tile_face_rows[cb_in],
|
|
ft_cb_unpack_num_face_rows[cb_in],
|
|
ft_cb_unpack_num_face_cols[cb_in]));
|
|
|
|
for (wt = 0; wt < tile_cols; wt++) {
|
|
FT_TENSIX_MATH(ft_tensix_math_wait_for_dest_available());
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab(cb_in, cb_in, wt, wt));
|
|
FT_TENSIX_MATH(ft_tensix_math_eltwise_binary_mul(
|
|
DST_REGISTER, (uint32_t)ft_cb_unpack_tile_num_faces[cb_in],
|
|
(uint32_t)ft_cb_unpack_tile_face_rows[cb_in]));
|
|
FT_TENSIX_MATH(ft_tensix_math_dest_section_done());
|
|
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_reserve_back(cb_xx, 1));
|
|
FT_TENSIX_PACK(ft_tensix_packer_wait_for_math_done());
|
|
FT_TENSIX_PACK(ft_tensix_pack_tile(DST_REGISTER, cb_xx));
|
|
FT_TENSIX_PACK(ft_tensix_pack_dest_section_done(FT_CB_DST_ACCUM_FP32));
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_push_back(cb_xx, 1));
|
|
}
|
|
|
|
/* --- mean square + eps, then rsqrt, into cb_scale -------------------- */
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front(cb_xx, tile_cols));
|
|
|
|
/* reduce_init<SUM, REDUCE_ROW>(cb_xx, cb_scaler, cb_scale). The SUM arm SWAPS the
|
|
* operands -- the scaler goes to SrcA and the data to SrcB -- which is what makes the
|
|
* row reduce a matrix multiply. */
|
|
FT_TENSIX_UNPACK(
|
|
ft_tensix_unpack_ab_reduce_row_init((uint32_t)ft_cb_unpack_tile_num_faces[cb_xx]));
|
|
FT_TENSIX_MATH(ft_tensix_math_reduce_row_init(
|
|
FT_CB_MATH_FIDELITY != 0 ? 1u : 0u, FT_CB_MATH_FIDELITY));
|
|
FT_TENSIX_PACK(
|
|
ft_tensix_pack_reduce_row_mask_config((uint32_t)ft_cb_pack_tile_face_rows[cb_scale]));
|
|
|
|
FT_TENSIX_MATH(ft_tensix_math_wait_for_dest_available());
|
|
|
|
for (wt = 0; wt < tile_cols; wt++) {
|
|
FT_TENSIX_MATH(ft_tensix_math_reduce_row(DST_REGISTER));
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab_reduce_row(cb_xx, cb_scaler, wt, 0));
|
|
}
|
|
|
|
/* eps, as one more accumulated row-sum. See note 1 in the file header. */
|
|
FT_TENSIX_MATH(ft_tensix_math_reduce_row(DST_REGISTER));
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab_reduce_row(cb_eps, cb_scaler, 0, 0));
|
|
|
|
FT_TENSIX_MATH(ft_tensix_math_rsqrt_tile_init());
|
|
FT_TENSIX_MATH(ft_tensix_math_rsqrt_tile(DST_REGISTER));
|
|
|
|
FT_TENSIX_MATH(ft_tensix_math_dest_section_done());
|
|
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_reserve_back(cb_scale, 1));
|
|
FT_TENSIX_PACK(ft_tensix_packer_wait_for_math_done());
|
|
FT_TENSIX_PACK(ft_tensix_pack_tile(DST_REGISTER, cb_scale));
|
|
FT_TENSIX_PACK(ft_tensix_pack_dest_section_done(FT_CB_DST_ACCUM_FP32));
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_push_back(cb_scale, 1));
|
|
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_pop_front(cb_xx, tile_cols));
|
|
|
|
/* reduce_uninit(). MATH restores NOTHING (the body is empty upstream); PACK's edge
|
|
* mask restore is the whole of it, and it is not optional -- see the file header. */
|
|
FT_TENSIX_MATH(ft_tensix_math_reduce_uninit());
|
|
FT_TENSIX_PACK(ft_tensix_pack_reduce_mask_clear());
|
|
|
|
/* --- y = (x * gamma) * scale ---------------------------------------- */
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front(cb_scale, 1));
|
|
|
|
for (wt = 0; wt < tile_cols; wt++) {
|
|
FT_TENSIX_MATH(ft_tensix_math_eltwise_binary_mul_init(
|
|
FT_CB_MATH_FIDELITY, (uint32_t)ft_cb_unpack_tile_face_rows[cb_in]));
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab_init(ft_cb_unpack_tile_face_rows[cb_in],
|
|
ft_cb_unpack_num_face_rows[cb_in],
|
|
ft_cb_unpack_num_face_cols[cb_in]));
|
|
|
|
FT_TENSIX_MATH(ft_tensix_math_wait_for_dest_available());
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab(cb_in, cb_gamma, wt, wt));
|
|
FT_TENSIX_MATH(ft_tensix_math_eltwise_binary_mul(
|
|
DST_REGISTER, (uint32_t)ft_cb_unpack_tile_num_faces[cb_in],
|
|
(uint32_t)ft_cb_unpack_tile_face_rows[cb_in]));
|
|
FT_TENSIX_MATH(ft_tensix_math_dest_section_done());
|
|
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_reserve_back(cb_xg, 1));
|
|
FT_TENSIX_PACK(ft_tensix_packer_wait_for_math_done());
|
|
FT_TENSIX_PACK(ft_tensix_pack_tile(DST_REGISTER, cb_xg));
|
|
FT_TENSIX_PACK(ft_tensix_pack_dest_section_done(FT_CB_DST_ACCUM_FP32));
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_push_back(cb_xg, 1));
|
|
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_wait_front(cb_xg, 1));
|
|
|
|
/* mul_bcast_cols_init_short(cb_xg, cb_scale) -- "short" means it reprograms the
|
|
* per-op ADDR_MOD and MOP state only; the hardware configure and the packer init
|
|
* from startup still stand. */
|
|
FT_TENSIX_MATH(ft_tensix_math_eltwise_binary_mul_broadcast_col_init(
|
|
FT_CB_MATH_FIDELITY, (uint32_t)ft_cb_unpack_tile_face_rows[cb_xg]));
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab_broadcast_col_init(
|
|
ft_cb_unpack_tile_face_rows[cb_xg], ft_cb_unpack_num_face_rows[cb_xg],
|
|
ft_cb_unpack_num_face_cols[cb_xg]));
|
|
|
|
FT_TENSIX_MATH(ft_tensix_math_wait_for_dest_available());
|
|
FT_TENSIX_UNPACK(ft_tensix_unpack_ab(cb_xg, cb_scale, 0, 0));
|
|
FT_TENSIX_MATH(ft_tensix_math_eltwise_binary_mul_broadcast_col(
|
|
DST_REGISTER, FT_CB_MATH_FIDELITY, (uint32_t)ft_cb_unpack_num_face_rows[cb_xg],
|
|
(uint32_t)ft_cb_unpack_num_face_cols[cb_xg],
|
|
(uint32_t)ft_cb_unpack_tile_face_rows[cb_xg]));
|
|
FT_TENSIX_MATH(ft_tensix_math_dest_section_done());
|
|
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_reserve_back(cb_out, 1));
|
|
FT_TENSIX_PACK(ft_tensix_packer_wait_for_math_done());
|
|
FT_TENSIX_PACK(ft_tensix_pack_tile(DST_REGISTER, cb_out));
|
|
FT_TENSIX_PACK(ft_tensix_pack_dest_section_done(FT_CB_DST_ACCUM_FP32));
|
|
FT_TENSIX_PACK(ft_tensix_compute_cb_push_back(cb_out, 1));
|
|
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_pop_front(cb_xg, 1));
|
|
}
|
|
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_pop_front(cb_scale, 1));
|
|
FT_TENSIX_UNPACK(ft_tensix_compute_cb_pop_front(cb_in, tile_cols));
|
|
}
|
|
}
|