Foundry/tt/ft_tt_buffer.h

227 lines
10 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_buffer.h
* @brief Device memory allocation for the Foundry Blackhole backend: hand out
* L1 and DRAM addresses that are legal to write, correctly aligned, and
* inside the space that is actually free.
*
* Module prefix: ft_tt_. Pure C99 + libc. Touches no device and issues no NoC
* transaction — it is arithmetic over ft_tt_grid_info and ft_tt_memmap.h, so it
* is fully testable with no card in the machine.
*
* ---------------------------------------------------------------------------
* IT IS A BUMP ALLOCATOR, AND IT SAYS SO
* ---------------------------------------------------------------------------
* One cursor per arena, allocation walks it up, and ft_tt_buffer_free() only
* accepts the MOST RECENT live allocation in that arena — anything else is
* refused with FT_TT_EINVAL rather than silently leaking or corrupting the
* cursor. Bulk teardown is ft_tt_allocator_reset(). The plan (Phase 1) calls a
* bump allocator sufficient; a real free-list belongs with Phase 5's
* core-group partitioning, where lifetimes stop being LIFO.
*
* ---------------------------------------------------------------------------
* THE BOUNDS ARE REAL, NOT NOMINAL
* ---------------------------------------------------------------------------
* - L1 is 1536 KiB, and the bottom 40,832 B of it is firmware text, the
* mailbox region and the fabric/routing tables (ft_tt_memmap.h). The floor
* is FT_TT_L1_USABLE_BASE, leaving 1,532,032 B.
* - **FT_TT_L1_USABLE_BASE IS NOT ENOUGH ONCE A CORE IS RUNNING.** 0x9F80 is
* exactly where the per-processor firmware `.data` staging regions START
* (docs/tt_launch_notes.md §2), so a default allocator will hand out the
* `.data` image of the firmware currently executing on the core. Any caller
* that has loaded images must pass `l1_floor` =
* ft_tt_image_placement_l1_floor() over the placements
* ft_tt_launch_load_image() reported.
* - **FT_TT_MEM_MAX_KERNEL_SIZE IS NOT A BOUND.** Upstream rounds it up to a
* whole 1497 KiB, which OVERSHOOTS the L1 that exists above MEM_MAP_END by
* 896 bytes. Using it here would hand out 896 bytes of address space that is
* not there. FT_TT_L1_USABLE_SIZE is the bound.
* - DRAM is one arena per enabled bank, sized from the selected architecture's
* `dram_bytes_per_bank` (0xff000000 = 4,278,190,080 B on Blackhole). The bank
* COUNT comes from ARC telemetry and nowhere else — 8 on this card, and never
* assumed.
*
* ---------------------------------------------------------------------------
* THE ARENA ARRAY IS A CAPACITY, AND THE BOUND IT CHECKS IS THE LIVE ONE
* ---------------------------------------------------------------------------
* `dram[]` is sized by FT_TT_MAX_DRAM_BANKS (ft_tt_arch.h), which is this
* BACKEND's static capacity — not `FT_TT_NUM_DRAM_BANKS`, which is Blackhole's
* bank count and a fact about one part. They are both 8 today and that is a
* coincidence worth keeping separate: the allocator's array cannot be sized from
* a table selected at runtime on a 7.7 GB host, so it needs a ceiling, and a
* ceiling read off one architecture's constant is how a number quietly acquires
* two meanings.
*
* Three checks, in the order they can catch something:
* 1. `ft_tt_arch_check()` refuses an arch table declaring more banks than the
* capacity — a part we cannot hold is a named refusal at device open.
* 2. `ft_tt_allocator_init()` refuses a live bank count above the capacity.
* 3. ...and above the SELECTED ARCH's own bank count, because telemetry
* reporting more banks than the part has means the mask was misread, and
* handing out addresses in a bank that is not there is a NoC transaction
* to nowhere.
* Every runtime bank index is then checked against the LIVE count, never the
* constant.
*
* Addresses are aligned to the strictest rule the NoC helpers impose on that
* space (ft_tt_noc_alignment): 16 B in L1, 64 B in DRAM. So any buffer this
* hands out is legal for both ft_tt_write_* and ft_tt_read_*.
*
* @status Unit-tested off-hardware, and exercised on the card as the source of
* every address test_noc.c writes to (docs/tt_bringup_log.md §9). The
* `l1_floor` override now has a caller: ft_tt_image_placement_l1_floor()
* (ft_tt_launch.h) turns the placements of the loaded images into it, and
* tests/test_launch.c proves an allocation that would have landed on a
* loaded image is refused.
* @issues None
* @todo None
*/
#ifndef FT_TT_BUFFER_H
#define FT_TT_BUFFER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "ft_tt_arch.h"
#include "ft_tt_device.h"
#include "ft_tt_memmap.h"
#include "ft_tt_noc.h"
/* ================================================================
* Types
* ================================================================ */
/**
* One allocation. Plain data, copied by value.
*
* An L1 buffer is a CORE-RELATIVE address: the same offset exists on every
* Tensix, and this allocator does not track which cores a caller actually used.
* That mirrors how tt-metal places buffers across a core grid, and it is why
* `logical_bank` is meaningless for L1.
*/
typedef struct ft_tt_buffer {
ft_tt_memory_space space;
uint32_t logical_bank; /**< DRAM only: which enabled bank. 0 for L1 */
uint64_t address; /**< device address within that space */
size_t bytes; /**< what the caller asked for */
size_t reserved_bytes; /**< what the arena consumed, i.e. bytes rounded up */
} ft_tt_buffer;
/** One bump arena. Fields are public so the struct can be caller-owned; treat
* them as read-only and use the accessors. */
typedef struct ft_tt_arena {
uint64_t floor;
uint64_t ceiling; /**< exclusive */
uint64_t cursor;
uint32_t live_count;
} ft_tt_arena;
/**
* L1 plus one arena per DRAM bank. Caller-owned storage — declaring it rather
* than allocating it keeps the backend allocation-free on the host, which
* matters on a 2-core / 7.7 GB box.
*/
typedef struct ft_tt_allocator {
ft_tt_ctx *ctx; /**< optional; only for ft_tt_mem_info accounting */
bool initialized;
uint32_t dram_bank_count;
ft_tt_arena l1;
ft_tt_arena dram[FT_TT_MAX_DRAM_BANKS]; /**< capacity, not a bank count */
} ft_tt_allocator;
/* ================================================================
* Lifecycle
* ================================================================ */
/**
* Set up the arenas from a grid read off ARC telemetry.
*
* @param allocator caller-owned storage, zeroed and filled on success
* @param ctx may be NULL. When given, DRAM allocations are reported
* through ft_tt_ctx_note_device_alloc() so ft_tt_mem_info()
* answers a real free figure instead of the full 31.88 GiB
* @param grid topology; `valid` must be true, `arch` must be set, and
* `dram_bank_count` must be 1..min(FT_TT_MAX_DRAM_BANKS,
* grid->arch->dram.bank_count). The count is NEVER assumed
* @param l1_floor lowest L1 address to hand out, or 0 for
* FT_TT_L1_USABLE_BASE. Rounded up to 16 B. Must be below
* the top of L1 and at or above FT_TT_L1_USABLE_BASE —
* allocating over the mailbox or firmware is a wedged core,
* not a failed test
* @return FT_TT_OK; FT_TT_EINVAL on a bad argument or floor;
* FT_TT_EUNAVAIL if the grid was never read
*
* Thread-safe: yes Reentrant: yes Signal-safe: no (logs on failure)
*/
int ft_tt_allocator_init(ft_tt_allocator *allocator, ft_tt_ctx *ctx, const ft_tt_grid_info *grid,
uint64_t l1_floor);
/**
* Return every arena to its floor, dropping all outstanding buffers. Any
* ft_tt_buffer a caller still holds is dangling afterwards.
* @return FT_TT_OK, or FT_TT_EINVAL.
*/
int ft_tt_allocator_reset(ft_tt_allocator *allocator);
/* ================================================================
* Allocation
* ================================================================ */
/**
* Carve `bytes` off the top of an arena.
*
* The returned address is aligned to ft_tt_noc_alignment() for the space (16 B
* L1, 64 B DRAM), and `reserved_bytes` is `bytes` rounded up to the same, so a
* caller may safely read back everything it wrote.
*
* @param allocator an initialised allocator
* @param space FT_TT_MEM_SPACE_L1 or FT_TT_MEM_SPACE_DRAM
* @param logical_bank DRAM: 0..dram_bank_count-1. L1: must be 0
* @param bytes > 0
* @param out_buffer written only on success
* @return FT_TT_OK; FT_TT_EINVAL on a bad argument;
* FT_TT_ENOMEM when the arena cannot fit it (the shortfall
* is logged)
*
* Thread-safe: no (mutates the allocator) Reentrant: no Signal-safe: no
*/
int ft_tt_buffer_alloc(ft_tt_allocator *allocator, ft_tt_memory_space space, uint32_t logical_bank,
size_t bytes, ft_tt_buffer *out_buffer);
/**
* Give a buffer back.
*
* **LIFO ONLY.** A bump allocator can only retract its last step, so this
* succeeds exactly when `buffer` is the most recent live allocation in its
* arena, and returns FT_TT_EINVAL otherwise with what it expected logged. It
* never silently ignores a bad free — a leaked DRAM arena on a card with 201
* TLB windows and no MMU is not something to discover later.
*
* @return FT_TT_OK; FT_TT_EINVAL on a bad argument, an empty arena, or an
* out-of-order free
*
* Thread-safe: no Reentrant: no Signal-safe: no
*/
int ft_tt_buffer_free(ft_tt_allocator *allocator, const ft_tt_buffer *buffer);
/* ================================================================
* Queries
* ================================================================ */
/** Bytes still allocatable in one arena, or 0 for a bad argument. */
size_t ft_tt_allocator_free_bytes(const ft_tt_allocator *allocator, ft_tt_memory_space space,
uint32_t logical_bank);
/** Bytes handed out and not yet freed in one arena, or 0 for a bad argument. */
size_t ft_tt_allocator_used_bytes(const ft_tt_allocator *allocator, ft_tt_memory_space space,
uint32_t logical_bank);
/** Live allocations in one arena, or 0 for a bad argument. */
uint32_t ft_tt_allocator_live_count(const ft_tt_allocator *allocator, ft_tt_memory_space space,
uint32_t logical_bank);
#endif /* FT_TT_BUFFER_H */