489 lines
16 KiB
C
489 lines
16 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file ft_tt_device.c
|
|
* @brief Implementation of the Foundry Blackhole device surface: open,
|
|
* enumerate, map the TLB register file, close.
|
|
*
|
|
* @status Phase 1a, run against the p150a at 12:00.0 on the card host. Read-only
|
|
* against the device: the only ioctls issued are GET_DEVICE_INFO,
|
|
* GET_DRIVER_INFO and QUERY_MAPPINGS, and the only mapping made is the 4 KiB
|
|
* TLB register file, mapped PROT_READ.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
/* POSIX.1-2008 is needed for O_CLOEXEC and mmap under -std=c99, which turns off
|
|
* the implicit feature-test macros glibc would otherwise define. */
|
|
#ifndef _POSIX_C_SOURCE
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#endif
|
|
#ifndef _FILE_OFFSET_BITS
|
|
#define _FILE_OFFSET_BITS 64
|
|
#endif
|
|
|
|
#include "ft_tt_device.h"
|
|
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
#include "ft_tt_memmap.h"
|
|
|
|
#define FT_TT_CHARDEV_PATH_FORMAT "/dev/tenstorrent/%d"
|
|
#define FT_TT_CHARDEV_PATH_MAX 64
|
|
|
|
struct ft_tt_ctx {
|
|
int device_index;
|
|
const ft_tt_arch *arch;
|
|
ft_tt_kmd_device *kmd;
|
|
ft_tt_kmd_device_attrs attrs;
|
|
ft_tt_kmd_resources resources;
|
|
ft_tt_device_info info;
|
|
ft_tt_tlb_counts tlb_counts;
|
|
ft_tt_grid_info grid;
|
|
volatile uint32_t *tlb_registers;
|
|
size_t tlb_registers_size;
|
|
size_t device_alloc_bytes;
|
|
uint32_t live_window_count;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Diagnostics */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
const char *ft_tt_error_string(int code)
|
|
{
|
|
switch (code) {
|
|
case FT_TT_OK: return "ok";
|
|
case FT_TT_ERROR: return "error";
|
|
case FT_TT_ENOMEM: return "out of host memory";
|
|
case FT_TT_EINVAL: return "invalid argument";
|
|
case FT_TT_EIO: return "device I/O failed";
|
|
case FT_TT_ENODEV: return "no such device";
|
|
case FT_TT_EPERM: return "blocked by the phase-1a NoC fence";
|
|
case FT_TT_EUNAVAIL: return "not available in this phase";
|
|
case FT_TT_ENOTFOUND: return "not found in this binary";
|
|
default: return "unknown status";
|
|
}
|
|
}
|
|
|
|
void ft_tt_log_error(const char *fmt, ...)
|
|
{
|
|
va_list args;
|
|
|
|
fputs("[ft_tt] ", stderr);
|
|
va_start(args, fmt);
|
|
vfprintf(stderr, fmt, args);
|
|
va_end(args);
|
|
fputc('\n', stderr);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Open */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* Cache the identity the two info ioctls report, so later queries cost nothing
|
|
* and every caller sees the same snapshot. */
|
|
static int collect_device_info(ft_tt_ctx *ctx)
|
|
{
|
|
uint64_t value = 0;
|
|
int rc;
|
|
|
|
rc = ft_tt_kmd_get_attrs(ctx->kmd, &ctx->attrs);
|
|
if (rc != 0) {
|
|
ft_tt_log_error("GET_DEVICE_INFO failed on device %d: %s", ctx->device_index,
|
|
strerror(-rc));
|
|
return FT_TT_EIO;
|
|
}
|
|
|
|
rc = ft_tt_kmd_get_resources(ctx->kmd, &ctx->resources);
|
|
if (rc != 0)
|
|
return FT_TT_EINVAL;
|
|
|
|
ctx->info.chip_arch = ctx->attrs.chip_arch;
|
|
ctx->info.pci_vendor_id = (uint16_t)ctx->attrs.pci_vendor_id;
|
|
ctx->info.pci_device_id = (uint16_t)ctx->attrs.pci_device_id;
|
|
ctx->info.pci_subsystem_vendor_id = (uint16_t)ctx->attrs.pci_subsystem_vendor_id;
|
|
ctx->info.pci_subsystem_id = (uint16_t)ctx->attrs.pci_subsystem_id;
|
|
ctx->info.pci_domain = (uint16_t)ctx->attrs.pci_domain;
|
|
ctx->info.pci_bus = (uint8_t)ctx->attrs.pci_bus;
|
|
ctx->info.pci_device = (uint8_t)ctx->attrs.pci_device;
|
|
ctx->info.pci_function = (uint8_t)ctx->attrs.pci_function;
|
|
|
|
rc = ft_tt_kmd_driver_attr(ctx->kmd, FT_TT_KMD_DRIVER_API_VERSION, &value);
|
|
if (rc != 0) {
|
|
ft_tt_log_error("GET_DRIVER_INFO failed on device %d: %s", ctx->device_index,
|
|
strerror(-rc));
|
|
return FT_TT_EIO;
|
|
}
|
|
ctx->info.driver_api_version = (uint32_t)value;
|
|
|
|
if (ft_tt_kmd_driver_attr(ctx->kmd, FT_TT_KMD_DRIVER_VERSION_MAJOR, &value) == 0)
|
|
ctx->info.driver_version_major = (uint8_t)value;
|
|
if (ft_tt_kmd_driver_attr(ctx->kmd, FT_TT_KMD_DRIVER_VERSION_MINOR, &value) == 0)
|
|
ctx->info.driver_version_minor = (uint8_t)value;
|
|
if (ft_tt_kmd_driver_attr(ctx->kmd, FT_TT_KMD_DRIVER_VERSION_PATCH, &value) == 0)
|
|
ctx->info.driver_version_patch = (uint8_t)value;
|
|
|
|
ctx->tlb_counts.window_2m_total = (uint32_t)ctx->attrs.window_count_2m;
|
|
ctx->tlb_counts.window_4g_total = (uint32_t)ctx->attrs.window_count_4g;
|
|
ctx->tlb_counts.window_2m_available =
|
|
ctx->tlb_counts.window_2m_total ? ctx->tlb_counts.window_2m_total - 1u : 0u;
|
|
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/*
|
|
* Map the 4 KiB TLB configuration register file out of BAR0 UC, and nothing
|
|
* else. See ft_tt_tlb_registers() for why the rest of BAR0 stays unmapped.
|
|
* tt-kmd accepts a partial mapping of a BAR at any page-aligned offset
|
|
* (the driver's mmap range check).
|
|
*/
|
|
static int map_tlb_register_file(ft_tt_ctx *ctx)
|
|
{
|
|
int prot = PROT_READ;
|
|
off_t offset;
|
|
void *mapping;
|
|
|
|
#ifdef FT_TT_PHASE1B_NOC_ENABLED
|
|
prot = PROT_READ | PROT_WRITE;
|
|
#endif
|
|
|
|
if (ctx->resources.bar0_len < (uint64_t)FT_TT_TLB_REGS_OFFSET + FT_TT_TLB_REGS_LEN) {
|
|
ft_tt_log_error("BAR0 is %llu bytes, too small to contain the TLB register file at 0x%x",
|
|
(unsigned long long)ctx->resources.bar0_len, FT_TT_TLB_REGS_OFFSET);
|
|
return FT_TT_ENODEV;
|
|
}
|
|
|
|
offset = (off_t)(ctx->resources.bar0_uc_offset + FT_TT_TLB_REGS_OFFSET);
|
|
mapping = mmap(NULL, FT_TT_TLB_REGS_LEN, prot, MAP_SHARED, ft_tt_kmd_fd(ctx->kmd), offset);
|
|
if (mapping == MAP_FAILED) {
|
|
ft_tt_log_error("mmap of the TLB register file failed: %s", strerror(errno));
|
|
return FT_TT_EIO;
|
|
}
|
|
|
|
ctx->tlb_registers = (volatile uint32_t *)mapping;
|
|
ctx->tlb_registers_size = FT_TT_TLB_REGS_LEN;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/**
|
|
* Ask the firmware for the MAXIMUM AI clock, unless `FT_TT_AI_CLK=min` says otherwise.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* THE DEFAULT WAS THE MINIMUM, AND NOBODY CHOSE IT
|
|
* ---------------------------------------------------------------------------
|
|
* tt-kmd reads an open WITHOUT `O_APPEND` as a legacy client and initialises that fd to
|
|
* `FT_TT_KMD_POWER_FLAGS_ALL & ~FT_TT_KMD_POWER_FLAG_MAX_AI_CLOCK` — every power flag
|
|
* on except bit 0, which the firmware documents as `1=Max AI Clock, 0=Min AI Clock`. So
|
|
* every Foundry process has been asking for the minimum on every open since the first one,
|
|
* and `DEV_NOTES.md`'s "Busy AICLK 800 MHz, unchanged under load — not a boost, not a
|
|
* throttle" was the firmware honouring that request exactly.
|
|
*
|
|
* MEASURED 2026-08-15 on this p150a, one bit of one ioctl the only variable:
|
|
*
|
|
* | | min (the old default) | max |
|
|
* |---|---|---|
|
|
* | AICLK | 800 MHz | **1350 MHz** |
|
|
* | Vcore | 740 mV | 861 mV |
|
|
* | board power, 300-token decode | 37 W | 78 W (of 150) |
|
|
* | ASIC temperature | 59 C | 69 C (of 90) |
|
|
* | MiniCPM5-1B token, on-card attention | 0.093 s | **0.077 s** |
|
|
* | Bfp8 matmul reader floor | 101 ns/tile-pair | **60** |
|
|
*
|
|
* The aggregation is a bitwise OR across every open fd, so this request can only ever ADD
|
|
* a flag and can never take one away from another client.
|
|
*
|
|
* **Set `FT_TT_AI_CLK=min` to get the old behaviour back.** It is a deliberate escape, in
|
|
* the shape `PN_TT_DISPATCH=oneshot` already established: if you did not ask for the minimum
|
|
* clock, you do not get it. The cost of the default is ~38 W of chip-wide clock tree paid
|
|
* whether or not any core is busy — measured, and it dwarfs the 1-7 W four busy Tensix add.
|
|
*
|
|
* A refusal is logged and not fatal: an older driver without the ioctl answers EINVAL, and a
|
|
* backend at 800 MHz is exactly what we had yesterday.
|
|
*/
|
|
bool ft_tt_ai_clock_wants_maximum(void)
|
|
{
|
|
const char *choice = getenv("FT_TT_AI_CLK");
|
|
|
|
return !(choice && strcmp(choice, "min") == 0);
|
|
}
|
|
|
|
static void request_ai_clock(ft_tt_ctx *ctx)
|
|
{
|
|
int wants_min = !ft_tt_ai_clock_wants_maximum();
|
|
int rc = ft_tt_kmd_request_max_ai_clock(ctx->kmd, !wants_min);
|
|
|
|
if (rc != 0)
|
|
ft_tt_log_error("SET_POWER_STATE refused (%s) — the AI clock stays where the driver's "
|
|
"default put it",
|
|
strerror(-rc));
|
|
else if (wants_min)
|
|
ft_tt_log_error("FT_TT_AI_CLK=min — asking for the MINIMUM AI clock (800 MHz on this part)");
|
|
}
|
|
|
|
ft_tt_ctx *ft_tt_init_device(int device_index)
|
|
{
|
|
char path[FT_TT_CHARDEV_PATH_MAX];
|
|
ft_tt_ctx *ctx = NULL;
|
|
int rc;
|
|
int n;
|
|
|
|
if (device_index < 0) {
|
|
ft_tt_log_error("invalid device index %d", device_index);
|
|
return NULL;
|
|
}
|
|
|
|
n = snprintf(path, sizeof(path), FT_TT_CHARDEV_PATH_FORMAT, device_index);
|
|
if (n < 0 || (size_t)n >= sizeof(path)) {
|
|
ft_tt_log_error("device index %d does not fit a chardev path", device_index);
|
|
return NULL;
|
|
}
|
|
|
|
ctx = calloc(1, sizeof(*ctx));
|
|
if (!ctx) {
|
|
ft_tt_log_error("out of memory opening %s", path);
|
|
return NULL;
|
|
}
|
|
ctx->device_index = device_index;
|
|
|
|
rc = ft_tt_kmd_open(path, &ctx->kmd, 0);
|
|
if (rc != 0) {
|
|
ft_tt_log_error("cannot open %s: %s", path, strerror(-rc));
|
|
goto cleanup;
|
|
}
|
|
|
|
request_ai_clock(ctx);
|
|
|
|
rc = collect_device_info(ctx);
|
|
if (rc != FT_TT_OK)
|
|
goto cleanup;
|
|
|
|
/* THE ARCHITECTURE GATE. Everything below this line — the memory map, the
|
|
* coordinate tables, the mailbox layout, the reset bits — describes one
|
|
* part, and this is the only place that checks which part is in the slot.
|
|
* ft_tt_arch_select() logs what it saw and returns NULL for anything it has
|
|
* no verified tables for; it never falls back to a default (spec §7.3: a
|
|
* wrong coordinate hangs the chip). */
|
|
ctx->arch = ft_tt_arch_select(ctx->info.pci_vendor_id, ctx->info.pci_device_id,
|
|
ctx->info.chip_arch);
|
|
if (!ctx->arch) {
|
|
ft_tt_log_error("%s: refusing to open — see the architecture error above", path);
|
|
goto cleanup;
|
|
}
|
|
|
|
rc = map_tlb_register_file(ctx);
|
|
if (rc != FT_TT_OK)
|
|
goto cleanup;
|
|
|
|
return ctx;
|
|
|
|
cleanup:
|
|
ft_tt_ctx_free(ctx);
|
|
return NULL;
|
|
}
|
|
|
|
void ft_tt_ctx_free(ft_tt_ctx *ctx)
|
|
{
|
|
if (!ctx)
|
|
return;
|
|
|
|
if (ctx->live_window_count != 0)
|
|
ft_tt_log_error("closing device %d with %u TLB window(s) still held", ctx->device_index,
|
|
ctx->live_window_count);
|
|
|
|
if (ctx->tlb_registers) {
|
|
if (munmap((void *)(uintptr_t)ctx->tlb_registers, ctx->tlb_registers_size) != 0)
|
|
ft_tt_log_error("munmap of the TLB register file failed: %s", strerror(errno));
|
|
ctx->tlb_registers = NULL;
|
|
}
|
|
|
|
if (ctx->kmd) {
|
|
int rc = ft_tt_kmd_close(ctx->kmd);
|
|
if (rc != 0)
|
|
ft_tt_log_error("close of device %d failed: %s", ctx->device_index, strerror(-rc));
|
|
ctx->kmd = NULL;
|
|
}
|
|
|
|
free(ctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Accessors */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
bool ft_tt_ctx_available(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx != NULL && ctx->kmd != NULL && ctx->tlb_registers != NULL;
|
|
}
|
|
|
|
int ft_tt_ctx_device_index(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ctx->device_index : -1;
|
|
}
|
|
|
|
int ft_tt_ctx_fd(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ft_tt_kmd_fd(ctx->kmd) : -1;
|
|
}
|
|
|
|
ft_tt_kmd_device *ft_tt_ctx_kmd_device(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ctx->kmd : NULL;
|
|
}
|
|
|
|
const ft_tt_arch *ft_tt_ctx_arch(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ctx->arch : NULL;
|
|
}
|
|
|
|
int ft_tt_device_get_info(const ft_tt_ctx *ctx, ft_tt_device_info *out_info)
|
|
{
|
|
if (!ctx || !out_info)
|
|
return FT_TT_EINVAL;
|
|
|
|
*out_info = ctx->info;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_device_get_resources(const ft_tt_ctx *ctx, ft_tt_kmd_resources *out_resources)
|
|
{
|
|
if (!ctx || !out_resources)
|
|
return FT_TT_EINVAL;
|
|
|
|
*out_resources = ctx->resources;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_tlb_counts_get(const ft_tt_ctx *ctx, ft_tt_tlb_counts *out_counts)
|
|
{
|
|
if (!ctx || !out_counts)
|
|
return FT_TT_EINVAL;
|
|
|
|
*out_counts = ctx->tlb_counts;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
const volatile uint32_t *ft_tt_tlb_registers(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ctx->tlb_registers : NULL;
|
|
}
|
|
|
|
size_t ft_tt_tlb_registers_size(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ctx->tlb_registers_size : 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Capacity */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
int ft_tt_grid_get(const ft_tt_ctx *ctx, ft_tt_grid_info *out_grid)
|
|
{
|
|
if (!ctx || !out_grid)
|
|
return FT_TT_EINVAL;
|
|
|
|
*out_grid = ctx->grid;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_grid_set(ft_tt_ctx *ctx, const ft_tt_grid_info *grid)
|
|
{
|
|
if (!ctx || !grid)
|
|
return FT_TT_EINVAL;
|
|
|
|
/* A grid's telemetry masks only mean something under an architecture, so
|
|
* the context's own table is stamped on unconditionally. A caller that
|
|
* supplied a DIFFERENT one has mixed two cards' topologies, which is worth
|
|
* refusing rather than silently correcting. */
|
|
if (grid->arch && grid->arch != ctx->arch) {
|
|
ft_tt_log_error("device %d: refusing a grid built for %s on a %s card", ctx->device_index,
|
|
grid->arch->name, ctx->arch ? ctx->arch->name : "(none)");
|
|
return FT_TT_EINVAL;
|
|
}
|
|
|
|
ctx->grid = *grid;
|
|
ctx->grid.arch = ctx->arch;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_mem_info(const ft_tt_ctx *ctx, size_t *free_bytes, size_t *total_bytes)
|
|
{
|
|
size_t total;
|
|
|
|
if (!ctx)
|
|
return FT_TT_EINVAL;
|
|
|
|
/* Bank count comes from ARC telemetry and nowhere else. Refuse rather than
|
|
* report the nominal 8 banks as a measurement. */
|
|
if (!ctx->grid.valid || !ctx->arch)
|
|
return FT_TT_EUNAVAIL;
|
|
|
|
/* Both factors are live: the bank count from telemetry, the bank size from
|
|
* the selected architecture. Neither is a compiled-in constant. */
|
|
total = (size_t)ctx->grid.dram_bank_count * (size_t)ctx->arch->core.dram_bytes_per_bank;
|
|
|
|
if (total_bytes)
|
|
*total_bytes = total;
|
|
if (free_bytes)
|
|
*free_bytes = total > ctx->device_alloc_bytes ? total - ctx->device_alloc_bytes : 0;
|
|
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
size_t ft_tt_ctx_alloc_bytes(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ctx->device_alloc_bytes : 0;
|
|
}
|
|
|
|
void ft_tt_ctx_note_device_alloc(ft_tt_ctx *ctx, int64_t delta_bytes)
|
|
{
|
|
if (!ctx || delta_bytes == 0)
|
|
return;
|
|
|
|
if (delta_bytes > 0) {
|
|
ctx->device_alloc_bytes += (size_t)delta_bytes;
|
|
} else {
|
|
size_t release = (size_t)(-delta_bytes);
|
|
|
|
if (release > ctx->device_alloc_bytes) {
|
|
ft_tt_log_error("device memory tally underflow on device %d (%zu held, %zu released)",
|
|
ctx->device_index, ctx->device_alloc_bytes, release);
|
|
ctx->device_alloc_bytes = 0;
|
|
} else {
|
|
ctx->device_alloc_bytes -= release;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32_t ft_tt_ctx_live_window_count(const ft_tt_ctx *ctx)
|
|
{
|
|
return ctx ? ctx->live_window_count : 0;
|
|
}
|
|
|
|
void ft_tt_ctx_note_window(ft_tt_ctx *ctx, int delta)
|
|
{
|
|
if (!ctx)
|
|
return;
|
|
|
|
if (delta > 0) {
|
|
ctx->live_window_count += (uint32_t)delta;
|
|
} else if (delta < 0) {
|
|
uint32_t drop = (uint32_t)(-delta);
|
|
|
|
if (drop > ctx->live_window_count) {
|
|
ft_tt_log_error("window tally underflow on device %d (%u held, %u released)",
|
|
ctx->device_index, ctx->live_window_count, drop);
|
|
ctx->live_window_count = 0;
|
|
} else {
|
|
ctx->live_window_count -= drop;
|
|
}
|
|
}
|
|
}
|