Foundry/tt/kernels/resident_topk_select.c

624 lines
28 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/**
* @file resident_topk_select.c
* @brief The working third of the RESIDENT TOPK/SAMPLE program, and the processor that
* PUBLISHES COMPLETION. It is the writer's slot in the triad and it does the selection
* itself, which is the whole shape of this op: see resident_topk_protocol.h §2 for why
* top-k is a scalar streaming selection and not a tile map.
*
* TWO OPCODES, ONE PROGRAM, AND THAT IS THE GROUPING RULE NOT A CONVENIENCE. A resident
* binary is bound to a fixed per-processor slab, so only one program is resident on a
* core at a time and swapping costs five kernel images -- ~0.19 ms over the x1 link
* (docs/tt_bringup_log.md §20.4). A decode step must do TOPK and then SAMPLE on the SAME
* core in sequence, so splitting them would put an image reload inside every token.
*
* WHAT LEAVES THE CARD: four bytes. TOPK leaves N pairs in L1 for SAMPLE to read; SAMPLE
* writes ONE token id to the address the descriptor names -- normally the next step's
* embedding input, so the loop closes here rather than at the host. That hand-off is a
* 16-byte ALIGNED NoC write and not `noc_inline_dw_write`; the note in
* resident_topk_protocol.h says what the inline call cost and why the API excludes the
* destination this op actually uses.
*
*
* C99 on Foundry's own `ft_tensix_dataflow.h` — no tt-metal include (plan Phase 6.6).
* The wait mode, which used to be a `template <uint32_t kMode>` argument, is now an
* ordinary parameter carrying the same compile-time value; `-Os` folds it identically.
* Compile-time args:
* 0: CB_LOGITS index 1: tile size in bytes
* 2: wait mode -- 0 upstream, 1 stoppable (ships), 2 bound-only
* Runtime args: the ten of resident_queue_protocol.h, identical on all five processors
*
* Build (NCRISC, NoC 1):
* CTARGS=0,2048,1 NOC=1 KERNEL_TEXT_BASE=0x18000 bash \
* foundry/tt/build_tensix_dataflow_kernel.sh \
* foundry/tt/kernels/resident_topk_select.c ncrisc /tmp/ttk_topk
*
* @status GREEN ON SILICON (Tensix 51, 57, 59). text 9816 B against a 12 KiB slab,
* .data+.bss 128 B. Full MiniCPM5-1B row (130,560 logits) selected EXACTLY,
* 2.590 ms/token. The token hand-off used `noc_inline_dw_write` until 2026-08-13
* and left the core unbootable every run while reporting 0 failures; it is now an
* aligned 16-byte `noc_async_write` and the suite's last stage re-boots the core
* it ran on so the same defect cannot pass again.
* @issues The sampler's arithmetic is soft float (no F extension on this core, MEASURED).
* That is a cost on a set of at most 64 survivors, never on the 130,560-element
* scan, which is integer throughout.
* @todo None
*/
#include <stdbool.h>
#include <stdint.h>
#include "ft_tensix_dataflow.h"
#include "resident_queue_protocol.h"
#include "resident_topk_protocol.h"
#define CB_LOGITS ft_tensix_compile_time_arg(0)
#define TILE_BYTES ft_tensix_compile_time_arg(1)
#define WAIT_MODE ft_tensix_compile_time_arg(2)
/** Elements of a tile row that live in one face (docs/tt_kernel_model.md §9). */
enum { kFaceWidth = 16 };
enum { kTileWidth = 32 };
/** Byte stride from face (r, 0) to face (r, 1) inside a bf16 tile: one 16x16 face. */
enum { kFaceBytes = 512 };
static uint32_t block_depth(uint32_t run, uint32_t requested)
{
return (requested > 0u && run > 0u && run % requested == 0u) ? requested : 1u;
}
/**
* Refuse a descriptor this program cannot execute, INSTEAD of blocking on it forever.
*
* The same four conditions the reader refuses (see its own note — the two circular-buffer
* ones are not the documented "depth divides the run", and the difference OVERWROTE L1 on
* this card), plus the one that is this processor's alone: **a zero select count would index
* `entries[capacity - 1]` at capacity 0**, i.e. one word before the array. The host refuses
* it too, and that is not a reason to leave it out here — a kernel that trusts the host to
* have validated is a kernel whose safety lives in a different file from its unsafe
* operation.
*
* Both processors compute the SAME predicate from the SAME words, so they refuse together
* and stay in lockstep; a guard on one side only would leave the other pushing into a buffer
* nobody drains.
*
* AND THE RESULT ADDRESS IS A WIRE-SUPPLIED POINTER THIS PROCESSOR WRITES THROUGH. A magic
* word in the block is not a bounds check — it is read AFTER the dereference on the SAMPLE
* side and it is not checked at all on the TOPK side, which WRITES the block. Upstream
* refuses a NoC write below `MEM_MAP_READ_ONLY_END` for exactly this
* (its NoC-address sanitizer): below that line sit the firmware
* mailboxes, and overwriting them is how a core comes back unbootable at its NEXT boot while
* the run that did it reports 0 failures. This is the file where that happened.
*/
static bool descriptor_is_executable(ftq_context *ctx, uint32_t tile_count, uint32_t depth,
uint32_t select_count, uint32_t row, uint32_t result_address)
{
if (tile_count == 0u || depth == 0u)
ctx->error = kErrBadParameters;
else if (!ft_tensix_cb_pop_grid_is_safe(CB_LOGITS, depth))
ctx->error = kErrBadParameters;
else if (select_count == 0u || select_count > kTopkMaxSelect || row >= kTileWidth)
ctx->error = kErrBadParameters;
else if (!ft_tensix_l1_block_is_writable(result_address, kTopkResultBytes))
ctx->error = kErrBadParameters;
else
return true;
return false;
}
/**
* Offer the 16 logits of ONE face row to the selection.
*
* The reject path is one subtract and one unsigned compare per logit and never converts a
* float (resident_topk_protocol.h §3); `ftk_select_offer` -- which is where the whole
* comparator lives -- runs only for a candidate that beat the N-th best.
*
* Reads through a NON-volatile pointer on purpose: the CB's page counters already
* established that these bytes arrived, so forbidding the compiler to schedule the loads
* would cost a stall per word for no ordering anyone needs.
*/
static void offer_face_row(ftk_select *selection, const uint32_t* words, uint32_t first_index)
{
for (uint32_t pair = 0u; pair < kFaceWidth / 2u; pair++) {
const uint32_t packed = words[pair];
const uint32_t low = packed & 0xFFFFu;
const uint32_t high = packed >> 16;
if (ftk_select_admits(selection, low))
ftk_select_offer(selection, low, first_index + pair * 2u);
if (ftk_select_admits(selection, high))
ftk_select_offer(selection, high, first_index + pair * 2u + 1u);
}
}
/**
* Offer one tile's 32 logits for the decoded token's row.
*
* A tile row is TWO runs of 16 contiguous elements, not one run of 32: the tile is four
* 16x16 faces in face order, so columns 0-15 live in face (row/16, 0) and columns 16-31 in
* face (row/16, 1), 512 bytes apart (docs/tt_kernel_model.md §9). Treating it as 32
* contiguous elements is the classic face bug -- and it would be INVISIBLE to a value
* check, because it selects real logits, just the wrong half of two different tiles.
*/
static void offer_tile_row(ftk_select *selection, uint32_t tile_address, uint32_t row,
uint32_t first_index)
{
const uint32_t face_row = row / kFaceWidth;
const uint32_t within_face = row % kFaceWidth;
const uint32_t low_offset = face_row * 2u * kFaceBytes + within_face * kFaceWidth * 2u;
const uint32_t* low_words = (const uint32_t *)(uintptr_t)(tile_address + low_offset);
const uint32_t* high_words =
(const uint32_t *)(uintptr_t)(tile_address + low_offset + kFaceBytes);
offer_face_row(selection, low_words, first_index);
offer_face_row(selection, high_words, first_index + kFaceWidth);
}
/** Publish the selection into the L1 result block the parameter block names. */
static void write_result_block(const ftk_select *selection, volatile FTQ_L1_PTR uint32_t *result)
{
volatile FTQ_L1_PTR uint32_t *pairs = result + kTopkResultHeaderWords;
for (uint32_t entry = 0u; entry < selection->count; entry++) {
pairs[entry * kTopkPairWords + kTopkPairIndex] = selection->entries[entry].index;
pairs[entry * kTopkPairWords + kTopkPairValueBits] =
ftk_fp32_bits_from_bf16(selection->entries[entry].bf16_bits);
}
result[kTopkResultTokenValid] = 0u;
result[kTopkResultCount] = selection->count;
// Magic LAST, so a host or a SAMPLE descriptor that sees the magic is entitled to the
// count and the pairs beside it. One in-order RISC-V, the same claim the queue's
// token-before-count makes.
result[kTopkResultSequence] = result[kTopkResultSequence] + 1u;
result[kTopkResultMagic] = kTopkResultMagicValue;
}
/**
* The selection half of one TOPK descriptor.
*
* Every trip count comes from the parameter block and none from a wait, which is what makes
* the drain safe (resident_queue_protocol.h rule 3): an abandoned wait still runs the loop
* to completion and still pops every page, so the reader's push/pop counters stay balanced.
*/
static bool select_one_logit_row(ftq_context *ctx, volatile FTQ_L1_PTR uint32_t *parameters)
{
const uint32_t tile_count = parameters[kTopkTileCount];
const uint32_t row = parameters[kTopkRow];
const uint32_t depth = block_depth(tile_count, parameters[kTopkDepth]);
const bool drain_only = (parameters[kTopkFlags] & kTopkFlagDrainOnly) != 0u;
volatile FTQ_L1_PTR uint32_t *result =
(volatile FTQ_L1_PTR uint32_t *)(uintptr_t)(parameters[kTopkResultAddress]);
ftk_select selection;
if (!descriptor_is_executable(ctx, tile_count, depth, parameters[kTopkSelectCount], row,
parameters[kTopkResultAddress]))
return false;
ftk_select_init(&selection, parameters[kTopkSelectCount]);
for (uint32_t tile = 0u; tile < tile_count; tile += depth) {
const bool have = ftq_cb_wait_front_stoppable(ctx, (int32_t)CB_LOGITS,
(int32_t)depth, WAIT_MODE);
if (have && !drain_only) {
const uint32_t page = ft_tensix_cb_read_pointer(CB_LOGITS);
for (uint32_t within = 0u; within < depth; within++)
offer_tile_row(&selection, page + within * TILE_BYTES, row,
(tile + within) * kTileWidth);
}
ft_tensix_cb_pop_front(CB_LOGITS, depth);
}
if (drain_only)
selection.count = 0u;
write_result_block(&selection, result);
return !ctx->aborting;
}
/* ================================================================
* The sampler
* ================================================================
* Transliterated from Pantheon's `sparse_sample()` / `sparse_argmax()` (src/core/engine.c),
* which is the path the K80 ALREADY takes whenever `fc_topk_logits` reduced the row -- the
* host scatters k pairs into an all -INFINITY vocabulary and samples that. So this is not a
* new approximation: it is the deployed semantics, and the reference the gate uses is that
* same function.
*
* THE ORDER IS THE POINT, AND ONE ORDERING HERE WAS A REAL BUG ON THE K80. Greedy is a PURE
* ARGMAX ON THE RAW VALUES, taken BEFORE any repetition penalty. Applying the penalty first
* can demote the true argmax below a near-tied runner-up -- on Bonsai-27B that flipped ':'
* (25.41) to ' given' (25.08) and lost the answer, with a forward pass that was faithful
* throughout (engine.c's own note). The penalty belongs to the SAMPLING path only, and
* `sample_greedy()` below is deliberately a separate function so the two orders cannot be
* merged by accident.
*/
/** At most kTopkMaxSelect survivors, held on the stack. NCRISC's local memory budget is
* ~4 KiB (dev notes §5); three float arrays of 64 is 768 bytes. */
typedef struct ftk_survivors {
uint32_t ids[kTopkMaxSelect];
float values[kTopkMaxSelect];
float probabilities[kTopkMaxSelect];
float work[kTopkMaxSelect];
uint32_t count;
} ftk_survivors;
/**
* Gather the result block into ASCENDING INDEX order.
*
* Ascending is not cosmetic. The reference's cumulative draw walks the vocabulary in index
* order and returns the first token whose running sum passes `r`; a different order gives a
* different token for the same draw. The result block is stored descending by VALUE because
* that is what `fc_topk_logits` returns, so the sort happens exactly once, here.
*/
static bool gather_survivors(volatile FTQ_L1_PTR uint32_t *result, ftk_survivors *survivors)
{
volatile FTQ_L1_PTR uint32_t *pairs = result + kTopkResultHeaderWords;
uint32_t count;
if (result[kTopkResultMagic] != kTopkResultMagicValue)
return false;
count = result[kTopkResultCount];
if (count == 0u || count > kTopkMaxSelect)
return false;
for (uint32_t entry = 0u; entry < count; entry++) {
const uint32_t index = pairs[entry * kTopkPairWords + kTopkPairIndex];
const uint32_t bits = pairs[entry * kTopkPairWords + kTopkPairValueBits];
uint32_t position = entry;
float value;
__builtin_memcpy(&value, &bits, sizeof(value));
while (position > 0u && survivors->ids[position - 1u] > index) {
survivors->ids[position] = survivors->ids[position - 1u];
survivors->values[position] = survivors->values[position - 1u];
position--;
}
survivors->ids[position] = index;
survivors->values[position] = value;
}
survivors->count = count;
return true;
}
/** Greedy: strict `>` scanned in ascending index order, so the LOWEST id wins a tie --
* identical to the full path's `for (i=1..n_vocab) if (logits[i] > logits[best])`. */
static uint32_t sample_greedy(const ftk_survivors *survivors)
{
uint32_t best = 0u;
for (uint32_t entry = 1u; entry < survivors->count; entry++)
if (survivors->values[entry] > survivors->values[best])
best = entry;
return survivors->ids[best];
}
/** The repetition penalty, over the recent ring, oldest entry first. Positive logits are
* divided and non-positive ones multiplied, per occurrence -- the reference applies it once
* per appearance in the window, not once per distinct token. */
static void apply_repeat_penalty(ftk_survivors *survivors, volatile FTQ_L1_PTR uint32_t *state,
float penalty)
{
volatile FTQ_L1_PTR uint32_t *recent = state + kSampleRecentOffsetWords;
const uint32_t stored = state[kSampleStateRecentCount];
const uint32_t window = state[kSampleStateRepeatWindow];
const uint32_t cursor = state[kSampleStateRecentCursor];
uint32_t take = (window < stored) ? window : stored;
if (penalty == 1.0f || take == 0u)
return;
for (uint32_t back = take; back > 0u; back--) {
const uint32_t slot = (cursor + kSampleRecentCapacity - back) % kSampleRecentCapacity;
const uint32_t token = recent[slot];
for (uint32_t entry = 0u; entry < survivors->count; entry++) {
if (survivors->ids[entry] != token)
continue;
if (survivors->values[entry] > 0.0f)
survivors->values[entry] = survivors->values[entry] / penalty;
else
survivors->values[entry] = survivors->values[entry] * penalty;
}
}
}
/** Descending insertion sort of `work[0..count)`. At most 64 elements, so the O(n^2) is
* 4096 compares in the worst case and it keeps the tie order the reference's qsort has no
* opinion about (equal values are interchangeable in a threshold). */
static void sort_descending(float* work, uint32_t count)
{
for (uint32_t entry = 1u; entry < count; entry++) {
const float value = work[entry];
uint32_t position = entry;
while (position > 0u && work[position - 1u] < value) {
work[position] = work[position - 1u];
position--;
}
work[position] = value;
}
}
/** Softmax over the survivors, then the two nucleus filters, then the draw. Statement for
* statement `sparse_sample()`; the deviations are named where they occur. */
static uint32_t sample_stochastic(ftk_survivors *survivors, volatile FTQ_L1_PTR uint32_t *state,
float temperature, float top_p, uint32_t top_k)
{
const float inverse_temperature = 1.0f / temperature;
float maximum;
float sum = 0.0f;
float inverse_sum;
uint32_t rng_state = state[kSampleStateRngState];
float draw;
float cumulative = 0.0f;
for (uint32_t entry = 0u; entry < survivors->count; entry++)
survivors->values[entry] = survivors->values[entry] * inverse_temperature;
maximum = survivors->values[0];
for (uint32_t entry = 1u; entry < survivors->count; entry++)
if (survivors->values[entry] > maximum)
maximum = survivors->values[entry];
for (uint32_t entry = 0u; entry < survivors->count; entry++) {
survivors->probabilities[entry] = ftk_exp_nonpositive(survivors->values[entry] - maximum);
sum = sum + survivors->probabilities[entry];
}
inverse_sum = 1.0f / sum;
for (uint32_t entry = 0u; entry < survivors->count; entry++)
survivors->probabilities[entry] = survivors->probabilities[entry] * inverse_sum;
if (top_k > 0u && top_k < survivors->count) {
float threshold;
for (uint32_t entry = 0u; entry < survivors->count; entry++)
survivors->work[entry] = survivors->probabilities[entry];
sort_descending(survivors->work, survivors->count);
threshold = survivors->work[top_k - 1u];
for (uint32_t entry = 0u; entry < survivors->count; entry++)
if (survivors->probabilities[entry] < threshold)
survivors->probabilities[entry] = 0.0f;
}
if (top_p < 1.0f) {
uint32_t candidates = 0u;
float mass = 0.0f;
float threshold = 0.0f;
for (uint32_t entry = 0u; entry < survivors->count; entry++)
if (survivors->probabilities[entry] > 0.0f)
survivors->work[candidates++] = survivors->probabilities[entry];
sort_descending(survivors->work, candidates);
for (uint32_t entry = 0u; entry < candidates; entry++) {
if (!(mass < top_p))
break;
if (survivors->work[entry] <= 0.0f)
break;
mass = mass + survivors->work[entry];
threshold = survivors->work[entry];
}
for (uint32_t entry = 0u; entry < survivors->count; entry++)
if (survivors->probabilities[entry] < threshold)
survivors->probabilities[entry] = 0.0f;
}
sum = 0.0f;
for (uint32_t entry = 0u; entry < survivors->count; entry++)
sum = sum + survivors->probabilities[entry];
if (sum > 0.0f) {
inverse_sum = 1.0f / sum;
for (uint32_t entry = 0u; entry < survivors->count; entry++)
survivors->probabilities[entry] = survivors->probabilities[entry] * inverse_sum;
}
draw = ftk_rng_next_unit_float(&rng_state);
state[kSampleStateRngState] = rng_state;
for (uint32_t entry = 0u; entry < survivors->count; entry++) {
cumulative = cumulative + survivors->probabilities[entry];
if (draw < cumulative)
return survivors->ids[entry];
}
// Every probability underflowed. The reference's full-vocabulary scan falls off the end
// and returns token 0; matching that is deliberate, and it is NOT "the lowest survivor".
for (uint32_t entry = survivors->count; entry > 0u; entry--)
if (survivors->probabilities[entry - 1u] > 0.0f)
return survivors->ids[entry - 1u];
return 0u;
}
/** Append the token to the recent ring and the emitted ring, and evaluate the stop
* condition -- the state a closed decode loop carries so the host is not in the path
* (resident_topk_protocol.h §5). */
static void record_token(volatile FTQ_L1_PTR uint32_t *state, uint32_t token)
{
volatile FTQ_L1_PTR uint32_t *recent = state + kSampleRecentOffsetWords;
volatile FTQ_L1_PTR uint32_t *emitted = state + kSampleEmittedOffsetWords;
const uint32_t recent_cursor = state[kSampleStateRecentCursor];
const uint32_t emitted_count = state[kSampleStateEmittedCount];
const uint32_t eos_count = state[kSampleStateEosCount];
const uint32_t budget = state[kSampleStateTokenBudget];
uint32_t stopped = 0u;
recent[recent_cursor % kSampleRecentCapacity] = token;
state[kSampleStateRecentCursor] = (recent_cursor + 1u) % kSampleRecentCapacity;
if (state[kSampleStateRecentCount] < kSampleRecentCapacity)
state[kSampleStateRecentCount] = state[kSampleStateRecentCount] + 1u;
emitted[emitted_count % kSampleEmittedCapacity] = token;
state[kSampleStateEmittedCount] = emitted_count + 1u;
state[kSampleStateLastToken] = token;
for (uint32_t entry = 0u; entry < eos_count && entry < kSampleEosCapacity; entry++)
if (state[kSampleEosOffsetWords + entry] == token)
stopped = 1u;
if (budget != 0u && emitted_count + 1u >= budget)
stopped = 1u;
if (stopped)
state[kSampleStateStopped] = 1u;
}
/**
* Refuse a token hand-off this processor cannot issue LEGALLY, instead of issuing it anyway.
*
* The transfer is 16 bytes and both of its ends must be 16-byte aligned — `ft_tt_noc_alignment()`
* gives a DRAM write the L1 alignment, in address AND length, and the source end is inside
* the result block. The check is repeated here rather than left to the host for the same
* reason the reader's is: `ft_tt_noc_check_transfer()` runs on the host, and the transfer is
* issued HERE. See resident_topk_protocol.h's hand-off note for what a sub-alignment write
* cost.
*/
static bool token_handoff_is_executable(ftq_context *ctx, uint32_t result_address,
uint32_t token_out_base)
{
if (token_out_base == 0u)
return true;
if ((token_out_base % kTopkTokenOutAlignment) != 0u ||
(result_address % kTopkTokenOutAlignment) != 0u) {
ctx->error = kErrBadParameters;
return false;
}
return true;
}
/** The sampler half of one SAMPLE descriptor. */
static bool sample_one_token(ftq_context *ctx, volatile FTQ_L1_PTR uint32_t *parameters)
{
const uint32_t result_address = parameters[kTopkResultAddress];
const uint32_t token_out_base = parameters[kTopkTokenOutBase];
volatile FTQ_L1_PTR uint32_t *result =
(volatile FTQ_L1_PTR uint32_t *)(uintptr_t)(result_address);
volatile FTQ_L1_PTR uint32_t *state =
(volatile FTQ_L1_PTR uint32_t *)(uintptr_t)(parameters[kTopkStateAddress]);
ftk_survivors survivors;
float temperature;
float top_p;
float penalty;
uint32_t token;
// Before anything mutates the sampler state: a descriptor that cannot be finished must
// not be half-executed, and the hand-off is the last thing this function does.
if (!token_handoff_is_executable(ctx, result_address, token_out_base))
return false;
// BOTH wire-supplied pointers are dereferenced AND written below — `result` by the token
// slot and the valid flag, `state` by record_token(). The magic check underneath is read
// THROUGH the pointer it is meant to validate, so it cannot be the bounds check; see
// descriptor_is_executable() above for why the second comparison (the read-only end) is
// the one that keeps the core bootable.
if (!ft_tensix_l1_block_is_writable(result_address, kTopkResultBytes) ||
!ft_tensix_l1_block_is_writable(parameters[kTopkStateAddress], kSampleStateBytes)) {
ctx->error = kErrBadParameters;
return false;
}
if (state[kSampleStateMagic] != kSampleStateMagicValue) {
ctx->error = kErrBadParameters;
return false;
}
if (!gather_survivors(result, &survivors)) {
ctx->error = kErrBadParameters;
return false;
}
__builtin_memcpy(&temperature, (const void*)&state[kSampleStateTemperatureBits],
sizeof(temperature));
__builtin_memcpy(&top_p, (const void*)&state[kSampleStateTopPBits], sizeof(top_p));
__builtin_memcpy(&penalty, (const void*)&state[kSampleStateRepeatPenaltyBits], sizeof(penalty));
if (temperature <= 0.0f) {
// Greedy: NO repetition penalty, argmax on the raw values. The ordering is the
// whole point -- see the block comment above.
token = sample_greedy(&survivors);
} else {
apply_repeat_penalty(&survivors, state, penalty);
token = sample_stochastic(&survivors, state, temperature, top_p, state[kSampleStateTopK]);
}
record_token(state, token);
result[kTopkResultToken] = token;
result[kTopkTokenSlotWords + 1u] = 0u;
result[kTopkTokenSlotWords + 2u] = 0u;
result[kTopkTokenSlotWords + 3u] = 0u;
result[kTopkResultTokenValid] = 1u;
if (token_out_base != 0u) {
// A LEGAL TRANSFER, not an inline write: `noc_inline_dw_write` excludes DRAM
// destinations by its own contract, and on Blackhole it degrades into a 4-byte write
// that is below this backend's own 16-byte write alignment. Four bytes of it landed
// correctly and left the core unbootable — resident_topk_protocol.h's hand-off note
// has the measurement. The slot the id sits in IS the source, so there is no second
// copy of the value to disagree with the one the host reads.
ft_tensix_noc_write(result_address + kTopkTokenSlotWords * 4u,
ft_tensix_noc_address(parameters[kTopkTokenOutX],
parameters[kTopkTokenOutY],
token_out_base),
kTopkTokenSlotBytes);
ft_tensix_noc_write_barrier();
}
return true;
}
void kernel_main(void)
{
ftq_context ctx;
uint32_t completed;
bool running = true;
ftq_context_init(&ctx);
ftq_status_set(&ctx, kStateBoot);
if (!ftq_check_control(&ctx)) {
ftq_status_set(&ctx, kStateExit);
return;
}
completed = ctx.control[kFieldCompletedCount];
ctx.cursor = completed;
while (running) {
// FETCH on `head`, so this processor drains the logit CB while the reader is still
// filling it. Publishing waits on `tail`, below.
if (!ftq_wait_for_work(&ctx, kFieldHead))
break;
ctx.aborting = false;
ftq_status_set(&ctx, kStateExec);
volatile FTQ_L1_PTR uint32_t *descriptor = ftq_descriptor(&ctx);
const uint32_t opcode = descriptor[kDescOpcode];
const uint32_t token = descriptor[kDescToken];
const uint32_t group_sequence = descriptor[kDescA5];
bool done = true;
ftq_status_opcode(&ctx, opcode);
if (opcode == kOpTopk) {
ftq_status_set(&ctx, kStateCbWaitFront);
done = select_one_logit_row(&ctx, ftq_parameters(descriptor));
ftq_status_set(&ctx, kStateExec);
} else if (opcode == kOpSample) {
done = sample_one_token(&ctx, ftq_parameters(descriptor));
} else if (opcode == kOpStop) {
running = false;
}
if (!done) {
running = false;
continue;
}
// THE PRECONDITION, and the only place it belongs: the reader must have finished
// this descriptor before its completion is published.
if (!ftq_wait_until_reaches(&ctx, kFieldTail, ctx.cursor + 1u)) {
running = false;
continue;
}
ctx.cursor++;
// Token before count: a host that sees the count is entitled to trust the token it
// reads next. Both are plain stores from ONE in-order RISC-V.
ctx.control[kFieldCompletedToken] = token;
ctx.control[kFieldCompletedCount] = ++completed;
ftq_group_post(&ctx, group_sequence, token);
}
// This processor does NOT touch kFieldError or kFieldState: the reader owns those, and
// one writer per word is the whole ordering argument.
ftq_status_set(&ctx, kStateExit);
}