Foundry/tt/tools/ft_tt_core.c

1565 lines
71 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_core.c
* @brief `ft_tt_core` — target one Tensix, a list, a range or all of them: report what
* state they are in, prove whether they still compute, and climb a recovery ladder on
* the ones that do not.
*
* ---------------------------------------------------------------------------
* READ-ONLY BY DEFAULT
* ---------------------------------------------------------------------------
* `status` and `topology` write nothing. `probe` and `recover` write to the card and are
* refused without `--write`. Rung 5 resets a PCIe bus and needs its own second flag on
* top. A tool that can wedge a core by accident is worse than no tool.
*
* ---------------------------------------------------------------------------
* WHY A READ IS NOT A HEALTH CHECK
* ---------------------------------------------------------------------------
* A wedged Tensix answers NoC reads exactly like a healthy one. `status` therefore
* reports what the registers SAY and names its verdicts so that none of them can be read
* as "this core works" — a core with all five RISC-Vs held comes back "held in reset",
* which is what a cold core and a wedged core both look like. `probe` is the
* discriminator: it boots firmware and runs a real vector-add triad through the
* unpacker, FPU and packer, and compares the result byte-for-byte.
*
* ---------------------------------------------------------------------------
* USAGE
* ---------------------------------------------------------------------------
* ft_tt_core status <targets> [--detail]
* ft_tt_core probe <targets> --write [--program DIR] [--repeats N] [--force]
* ft_tt_core recover <targets> --rung N --write [--then-probe] [--program DIR] [--force]
* ft_tt_core topology [PCI-ADDRESS]
*
* <targets> 60 | 60,62,65 | 60-69 | 60-62,65 | all
*
* Rungs: 0 deassert the RISC-Vs (one core, one register write)
* 1 reload firmware and boot (one core)
* 2 poke what the core waits on (one core)
* 3 ASIC reset (whole card)
* 4 RESET_DEVICE ASIC_DMC_RESET (whole card)
* 5 PCIe secondary bus reset (whole card; needs
* --allow-pcie-secondary-bus-reset as well as --write)
*
* ---------------------------------------------------------------------------
* THE RUNG IS THE BLAST RADIUS; `--driver` IS THE MECHANISM
* ---------------------------------------------------------------------------
* `--driver kmd` (the default) reaches rung 3 through tt-kmd's `RESET_DEVICE` ioctl and
* completes it with a PCI remove + bridge rescan. `--driver udrv` does the SAME RUNG with no
* vendor driver at all: three PCI config-space writes, our own config snapshot restored in
* place, and a link retrain (`ft_tt_udrv_reset.h`).
*
* The ladder is NOT extended with new numbers for this. A rung number says what the operation
* can destroy — that is what a caller has to reason about, and it is identical either way.
* Numbering the userspace ASIC reset "6" would put it above the SBR, implying it is more
* drastic than a bus reset, which is false. So the vocabulary fits, and `--driver` is
* orthogonal to it. `--driver udrv` is only meaningful at rung 3: rung 4 needs two ARC
* messages and there is no userspace ARC transport yet, and rung 5 is already pure sysfs.
*
* ---------------------------------------------------------------------------
* WHY THE CARD RESET IS THE FLOOR — settled 2026-08-12, do not re-open
* ---------------------------------------------------------------------------
* Recovering ONE wedged core costs the whole card, and that is a property of the silicon,
* not a missing feature here. Four things were checked, so nobody has to check them again:
*
* 1. Every wedge seen has been in the Tensix BACKEND — MATH stalled in the instruction
* FIFO, dvalid never satisfied, a boot handshake that never finishes. None of it is
* executing anything that could poll a flag, so rungs 0-2 are not incomplete, they are
* structurally out of reach (`docs/tt_bringup_log.md` §21.2).
* 2. **The per-tile reset register is a QUASAR feature.** `SMN_SLV_TILE_RESET`
* (`TENSIX_RESET_N`, `NOC_RESET_N`, `OVERLAY_RESET_N`, `D2D_RESET_N`) exists only under
* `hw/inc/internal/tt-2xx/quasar/`. Blackhole is tt-1xx and has no `TILE_RESET` at all.
* 3. The Tensix backend soft-reset bits are not a substitute. Firmware boot never programs
* the backend datapath — it sets reset PCs, and the stock sync init zeroes only
* CB tile counters — so the 14 cores corrupted that day already got a full boot AND a
* complete kernel LLK re-init, and still returned garbage until the machine was reset.
* 4. PERST# alone does not do it: §21.4 measured a PCIe secondary bus reset leaving wedged
* cores byte-identical. Only `RESET_DEVICE ASIC_RESET` has ever cleared one, and the ARC
* exposes no per-tile granularity.
*
* So: check `status --detail` first, in case a wedge is ever NOT backend-resident and a
* per-core rung can reach it. Otherwise rung 3 is the answer, and the advisory lock
* (`ft_tt_card_lock.h`) is what makes it safe — it does not make the reset less
* destructive, it makes it something other users see coming and refuse. Revisit on Quasar.
*
* @status Runs on the p150a test card. `probe all` is 140/140 on this card
* (2026-08-17). Replaces tests/tt_resident_recover.c, which was written for the core-60
* wedge, never ran, and did not build against the current ft_tt_launch.h.
* `--driver udrv` is on silicon as of 2026-08-17: it recovered core 101 from a deliberate
* 0x00047000 wedge in place, no PCI remove and no rescan, and retrained the link from the
* 2.5 GT/s the reset left behind.
*
* **AND THE WHOLE TOOL NOW RUNS WITH NO VENDOR MODULE, which is the board it was built
* for (2026-08-17).** `--driver` selects the TRANSPORT for every command — status, probe
* and all six rungs — and not just the mechanism for a whole-card rung, and `udrv` means
* udrv even when a module happens to be loaded, so it can be compared against the oracle
* on the card host that has one. With `tenstorrent` unloaded: `status all` read 140 cores,
* `probe all --write` was **140 passed / 0 failed / 0 refused**, and
* `recover 71 --rung 3 --write` did the ASIC reset and read both states. The same probe
* over `--driver kmd` with no module exits 1, which is what makes the green mean
* something.
*
* Three things were in the way and none of them was the driver. `--driver udrv` opened
* the chardev to print the before-state; it checked the root-only PCI remove/rescan
* permission that the in-place config restore never uses; and `probe` and rungs 0-2 took
* their per-core claim through the tt-kmd session's wrapper, when a claim is a flock
* under /tmp that has never needed a device handle.
* @issues
* - `topology` and `--driver udrv` read and write PCI config space through sysfs, which is
* root-only, so both need sudo. The card lock and the per-core claim namespace live under
* /tmp and a sudo run leaves them ROOT-OWNED, which then refuses every non-root card job
* with FT_TT_EIO ("device I/O failed") — a message that reads like a card fault and is
* not one. Fix the ownership, do not diagnose the card (DEV_NOTES §3).
* @todo None
*/
#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "ft_tt_card_lock.h"
#include "ft_tt_core_probe.h"
#include "ft_tt_core_readiness.h"
#include "ft_tt_tensix_claim.h"
#include "ft_tt_udrv.h"
#include "ft_tt_udrv_claim.h"
#include "ft_tt_udrv_reset.h"
#include "ft_tt_core_recover.h"
#include "ft_tt_core_status.h"
#include "ft_tt_core_status_print.h"
#include "ft_tt_device_reset.h"
#include "ft_tt_hardware_session.h"
#include "ft_tt_runtime.h"
/** More than any p150 has cores; the real bound is the LIVE core count, checked below. */
#define TOOL_MAX_TARGETS 256u
#define TOOL_CHARDEV_PATH "/dev/tenstorrent/0"
/** Where sysfs publishes PCI devices. Both the card and its bridge are read from here. */
#define TOOL_PCI_DEVICES_DIR "/sys/bus/pci/devices"
/** Which mechanism a whole-card rung uses. The rung says WHAT is destroyed; this says HOW. */
typedef enum tool_reset_driver {
TOOL_RESET_DRIVER_KMD = 0, /**< tt-kmd's RESET_DEVICE ioctl, then remove + rescan */
TOOL_RESET_DRIVER_UDRV = 1 /**< three config writes, our own snapshot restored in place */
} tool_reset_driver;
typedef enum tool_command {
TOOL_COMMAND_STATUS,
TOOL_COMMAND_READINESS,
TOOL_COMMAND_PROBE,
TOOL_COMMAND_RECOVER,
TOOL_COMMAND_TOPOLOGY,
TOOL_COMMAND_CLAIM
} tool_command;
typedef struct tool_options {
tool_command command;
const char *target_text;
bool allow_write;
bool allow_secondary_bus_reset;
bool detail;
bool then_probe;
bool force;
unsigned rung;
bool rung_given;
tool_reset_driver reset_driver;
bool udrv_reenumerate; /**< --driver udrv only: use remove + rescan, not in-place restore */
unsigned repeats;
const char *program_directory;
const char *save_directory;
/* Card-lock coordination (ft_tt_card_lock.h) -- only rungs 3-5 and `claim` use these. */
const char *card_lock_reason;
bool override_card_lock;
/* Per-core coordination (ft_tt_tensix_claim.h) -- probe and rungs 0-2 use this. */
bool override_core_claims;
unsigned hold_seconds; /**< `claim` only; 0 = hold until Ctrl-C */
} tool_options;
typedef struct tool_targets {
uint32_t index[TOOL_MAX_TARGETS];
unsigned count;
bool all;
} tool_targets;
static void print_usage(const char *program_name)
{
fprintf(stderr,
"usage:\n"
" %s status <targets> [--detail]\n"
" %s readiness <targets> may firmware be booted onto these cores RIGHT\n"
" NOW? Read-only. The same reading Pantheon's\n"
" arming path refuses on\n"
" %s probe <targets> --write [--program DIR] [--repeats N] [--force]\n"
" %s recover <targets> --rung N --write [--then-probe] [--program DIR]\n"
" [--driver kmd|udrv] [--udrv-reenumerate]\n"
" [--allow-pcie-secondary-bus-reset] [--save-dir DIR]\n"
" [--reason TEXT] [--override-card-lock]\n"
" %s topology [PCI-ADDRESS] (no address = this card; an address makes the\n"
" rung-5 refusal demonstrable on another device)\n"
" %s claim --reason TEXT [--hold-seconds N]\n"
" register that this card is in use; holds a shared lock until\n"
" Ctrl-C (or N seconds), so `recover --rung 3/4/5` refuses and\n"
" names you instead of resetting the card out from under you\n"
"\n"
" <targets> 60 | 60,62,65 | 60-69 | 60-62,65 | all\n"
" --force probe or reload a core whose kernel is still 'running' — only\n"
" for a core already known wedged\n"
" --driver which TRANSPORT reaches the card, and how a whole-card rung is\n"
" performed. It applies to EVERY command: status, probe, and all\n"
" six rungs. kmd (default) = tt-kmd's\n"
" RESET_DEVICE ioctl then a PCI remove + rescan. udrv = NO VENDOR\n"
" DRIVER: three PCI config writes, our own config snapshot restored\n"
" in place, and a link retrain. Rung 3 only. The rung number is the\n"
" blast radius and does not change with the mechanism. udrv works\n"
" with NO tt-kmd loaded: the before/after state is then read over\n"
" foundry's own driver, and the root-only PCI remove/rescan check\n"
" is skipped because the in-place restore does not use it.\n"
" --udrv-reenumerate with --driver udrv, finish with remove + rescan instead\n"
" of the in-place config restore — the fallback, for comparison\n"
" rungs 0 deassert RISC-Vs · 1 reload firmware · 2 poke the wait\n"
" 3 ASIC reset · 4 RESET_DEVICE ASIC_DMC_RESET\n"
" 5 PCIe secondary bus reset (also needs "
"--allow-pcie-secondary-bus-reset)\n"
" — rungs 3-5 reset the WHOLE CARD and take the card lock first;\n"
" --override-card-lock proceeds anyway and says loudly who it\n"
" overrode. Rungs 0-2 and probe touch ONE CORE EACH: they take a\n"
" per-core claim instead (ft_tt_tensix_claim.h) and refuse, naming\n"
" the holder, rather than driving someone else's core.\n"
" --override-core-claims probe/rungs 0-2 proceed on a core another process\n"
" holds. It is a claim, not a fence — say why out loud first.\n",
program_name, program_name, program_name, program_name, program_name,
program_name);
}
/** Add one index, refusing a list longer than the array. */
static int add_target(tool_targets *targets, unsigned long value)
{
if (targets->count >= TOOL_MAX_TARGETS) {
fprintf(stderr, "too many targets (max %u)\n", TOOL_MAX_TARGETS);
return FT_TT_EINVAL;
}
targets->index[targets->count++] = (uint32_t)value;
return FT_TT_OK;
}
/** Parse "all", "60", "60,62", "60-69" or any comma-separated mix of those. */
static int parse_targets(const char *text, tool_targets *out)
{
char buffer[512];
char *saved = NULL;
char *token;
memset(out, 0, sizeof(*out));
if (!text)
return FT_TT_EINVAL;
if (strcmp(text, "all") == 0) {
out->all = true;
return FT_TT_OK;
}
if (strlen(text) >= sizeof(buffer))
return FT_TT_EINVAL;
snprintf(buffer, sizeof(buffer), "%s", text);
for (token = strtok_r(buffer, ",", &saved); token; token = strtok_r(NULL, ",", &saved)) {
char *dash = strchr(token, '-');
unsigned long first;
unsigned long last;
unsigned long value;
if (dash) {
*dash = '\0';
first = strtoul(token, NULL, 10);
last = strtoul(dash + 1, NULL, 10);
if (last < first)
return FT_TT_EINVAL;
for (value = first; value <= last; value++)
if (add_target(out, value) != FT_TT_OK)
return FT_TT_EINVAL;
} else {
if (token[0] == '\0')
return FT_TT_EINVAL;
if (add_target(out, strtoul(token, NULL, 10)) != FT_TT_OK)
return FT_TT_EINVAL;
}
}
return out->count == 0u ? FT_TT_EINVAL : FT_TT_OK;
}
/** Expand "all" and check every index against the LIVE core count, never a constant. */
static int resolve_targets(const ft_tt_grid_info *grid, tool_targets *targets)
{
uint32_t live = ft_tt_coord_tensix_core_count(grid);
unsigned entry;
if (live == 0u) {
fprintf(stderr, "the grid reports no enabled Tensix cores\n");
return FT_TT_EIO;
}
if (targets->all) {
uint32_t index;
targets->count = 0;
for (index = 0; index < live && targets->count < TOOL_MAX_TARGETS; index++)
targets->index[targets->count++] = index;
return FT_TT_OK;
}
for (entry = 0; entry < targets->count; entry++) {
if (targets->index[entry] >= live) {
fprintf(stderr, "Tensix %u is out of range: this card has %u enabled cores\n",
targets->index[entry], live);
return FT_TT_EINVAL;
}
}
return FT_TT_OK;
}
static int parse_options(int argc, char **argv, tool_options *out)
{
int argument;
/* Pre-scanned, not tracked through the loop, so the acknowledgement works whichever
* side of --program it is written on. Order-dependent safety is not safety. */
bool foreign_program_acknowledged = false;
memset(out, 0, sizeof(*out));
out->repeats = FT_TT_CORE_PROBE_DEFAULT_REPEATS;
out->program_directory = ft_tt_firmware_directory();
if (argc < 2)
return FT_TT_EINVAL;
for (argument = 2; argument < argc; argument++) {
if (strcmp(argv[argument], "--i-know-this-must-be-a-vector-add-triad") == 0)
foreign_program_acknowledged = true;
}
/* THE ENVIRONMENT REACHES THE SAME CODE PATH AS --program, AND GATING ONLY THE FLAG
* WAS AN INCOMPLETE FIX. ft_tt_firmware_directory() returns
* FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT when it is set, so an exported
* FT_TT_FIRMWARE_DIR silently pointed probe at another op's program directory --
* exactly the mixing the flag was gated to prevent. On 2026-08-13 a bisect script
* exported it for its own builds, the shell then ran probe, and it wedged core 26 and
* reported a false "boots but computes garbage". A safety check that covers one of two
* routes to the same call is not a safety check. Other suites legitimately use this
* variable, so the refusal belongs here in the tool rather than in the shared helper. */
if (getenv(FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT) != NULL &&
getenv(FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT)[0] != '\0' &&
!foreign_program_acknowledged) {
fprintf(stderr,
"refused: %s is set in the environment (%s).\n"
" probe drives its OWN vector_add triad; pointing it at another op's\n"
" directory mixes those kernels with vector_add's runtime args and WEDGES\n"
" THE CORE -- and a wedged core needs a whole-card reset to recover.\n"
" Unset it, or pass --i-know-this-must-be-a-vector-add-triad if that\n"
" directory really is another build of the same triad.\n",
FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT,
getenv(FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT));
return FT_TT_EINVAL;
}
for (argument = 2; argument < argc; argument++) {
if (strcmp(argv[argument], "--i-know-this-must-be-a-vector-add-triad") == 0)
foreign_program_acknowledged = true;
}
if (strcmp(argv[1], "status") == 0)
out->command = TOOL_COMMAND_STATUS;
else if (strcmp(argv[1], "readiness") == 0)
out->command = TOOL_COMMAND_READINESS;
else if (strcmp(argv[1], "probe") == 0)
out->command = TOOL_COMMAND_PROBE;
else if (strcmp(argv[1], "recover") == 0)
out->command = TOOL_COMMAND_RECOVER;
else if (strcmp(argv[1], "topology") == 0)
out->command = TOOL_COMMAND_TOPOLOGY;
else if (strcmp(argv[1], "claim") == 0)
out->command = TOOL_COMMAND_CLAIM;
else
return FT_TT_EINVAL;
for (argument = 2; argument < argc; argument++) {
const char *option = argv[argument];
bool has_value = argument + 1 < argc;
if (option[0] != '-') {
if (out->target_text)
return FT_TT_EINVAL;
out->target_text = option;
} else if (strcmp(option, "--write") == 0) {
out->allow_write = true;
} else if (strcmp(option, "--allow-pcie-secondary-bus-reset") == 0) {
out->allow_secondary_bus_reset = true;
} else if (strcmp(option, "--detail") == 0) {
out->detail = true;
} else if (strcmp(option, "--force") == 0) {
out->force = true;
} else if (strcmp(option, "--then-probe") == 0) {
out->then_probe = true;
} else if (strcmp(option, "--rung") == 0 && has_value) {
out->rung = (unsigned)strtoul(argv[++argument], NULL, 10);
out->rung_given = true;
} else if (strcmp(option, "--repeats") == 0 && has_value) {
out->repeats = (unsigned)strtoul(argv[++argument], NULL, 10);
} else if (strcmp(option, "--program") == 0 && has_value) {
/* WEDGES CORES IF MISUSED, so it is gated. probe drives ITS OWN vector_add
* triad — its runtime args, CB configuration and processor mask (0x1f, all
* five). Pointing it at some other op's program directory boots that
* directory's firmware and loads that directory's kernels, then drives them
* with vector_add's expectations. The boot handshake never finishes and the
* core is wedged until a card reset. It cost Tensix 30 and 31 on 2026-08-12.
*
* A file-existence check cannot catch this: every program directory holds the
* same file names. The only valid --program is a DIFFERENT BUILD OF THE SAME
* vector_add triad, which the tool cannot verify, so the caller asserts it. */
if (!foreign_program_acknowledged) {
fprintf(stderr,
"--program needs --i-know-this-must-be-a-vector-add-triad.\n"
" probe drives its own vector_add triad; pointing it at another\n"
" op's directory mixes those kernels with vector_add's runtime\n"
" args and WEDGES THE CORE (cost Tensix 30 and 31, 2026-08-12).\n"
" The only valid use is another build of the SAME triad.\n"
" To health-check a core, run probe with no --program.\n");
return FT_TT_EINVAL;
}
out->program_directory = argv[++argument];
} else if (strcmp(option, "--i-know-this-must-be-a-vector-add-triad") == 0) {
foreign_program_acknowledged = true;
} else if (strcmp(option, "--save-dir") == 0 && has_value) {
out->save_directory = argv[++argument];
} else if (strcmp(option, "--reason") == 0 && has_value) {
out->card_lock_reason = argv[++argument];
} else if (strcmp(option, "--override-card-lock") == 0) {
out->override_card_lock = true;
} else if (strcmp(option, "--override-core-claims") == 0) {
out->override_core_claims = true;
} else if (strcmp(option, "--driver") == 0 && has_value) {
const char *name = argv[++argument];
if (strcmp(name, "kmd") == 0) {
out->reset_driver = TOOL_RESET_DRIVER_KMD;
} else if (strcmp(name, "udrv") == 0) {
out->reset_driver = TOOL_RESET_DRIVER_UDRV;
} else {
fprintf(stderr, "--driver must be kmd or udrv\n");
return FT_TT_EINVAL;
}
} else if (strcmp(option, "--udrv-reenumerate") == 0) {
out->udrv_reenumerate = true;
} else if (strcmp(option, "--hold-seconds") == 0 && has_value) {
out->hold_seconds = (unsigned)strtoul(argv[++argument], NULL, 10);
} else {
fprintf(stderr, "unknown option: %s\n", option);
return FT_TT_EINVAL;
}
}
if (out->command != TOOL_COMMAND_TOPOLOGY && out->command != TOOL_COMMAND_CLAIM &&
!out->target_text)
return FT_TT_EINVAL;
if (out->repeats == 0u)
return FT_TT_EINVAL;
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* readiness */
/* ------------------------------------------------------------------------- */
/**
* May firmware be booted onto these cores RIGHT NOW?
*
* READ-ONLY, like `status`, and for the same reason: a NoC read of a wedged core is
* indistinguishable from a read of a healthy one and changes neither. So it goes through the
* transport seam rather than the tt-kmd session and needs no claim.
*
* This is the SAME reading Pantheon's arming path refuses on (`pn_tt_core_boot_guard.h`), so
* running it here answers "why did that serve refuse core 90" without loading anything, and
* — the reason it was added — it can be pointed at a core carrying a REAL leftover to see
* whether the detector sees it, with nothing written to the card either way.
*/
static int run_readiness(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
const tool_targets *targets)
{
static const char *const processor_name[FT_TT_PROC_COUNT] = {"BRISC", "NCRISC", "TRISC0",
"TRISC1", "TRISC2"};
unsigned not_ready = 0;
unsigned entry;
for (entry = 0; entry < targets->count; entry++) {
ft_tt_core_readiness readiness;
unsigned processor;
if (ft_tt_core_readiness_read(noc, grid, targets->index[entry], &readiness) !=
FT_TT_OK) {
fprintf(stderr, "Tensix %u does not resolve to a coordinate\n",
targets->index[entry]);
continue;
}
printf("Tensix %u — translated NoC (%u, %u)\n", readiness.status.index,
readiness.status.x, readiness.status.y);
printf(" %-10s %s\n", readiness.ready ? "READY" : "NOT READY",
ft_tt_core_readiness_string(readiness.state));
printf(" verdict '%s' reset 0x%08x instrn_buf 0x%08x go 0x%02x\n",
ft_tt_core_verdict_string(readiness.status.verdict),
readiness.status.state.reset_register,
readiness.status.state.instrn_buf_status,
readiness.status.state.go_messages[0].signal);
/* The CONTROL heartbeat and the PER-PROCESSOR ones printed side by side, because
* the whole reason this command exists rather than `status` alone is that they
* disagree in exactly the failure being guarded against: the reader has exited, so
* the control block is frozen while a TRISC is still spinning. */
printf(" resident queue %s, control heartbeat %s\n",
readiness.status.queue_present ? "present" : "absent",
readiness.status.queue_heartbeat_ticking ? "TICKING" : "frozen");
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
if (!readiness.processor[processor].slot_read)
continue;
printf(" %-7s state %s heartbeat %u %s\n", processor_name[processor],
ft_tt_queue_state_string(readiness.processor[processor].state),
readiness.processor[processor].heartbeat,
readiness.processor[processor].ticking ? "TICKING" : "frozen");
}
if (!readiness.ready)
printf(" waiting %s help\n", readiness.waiting_can_help ? "CAN" : "cannot");
if (!readiness.ready)
not_ready++;
}
printf("\n%u core%s, %u not ready\n", targets->count, targets->count == 1u ? "" : "s",
not_ready);
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* status */
/* ------------------------------------------------------------------------- */
/** Every target's state, over whichever transport opened the card.
*
* Accessor + grid, not a session: `status` is READ-ONLY — a NoC read of a wedged core is
* indistinguishable from a read of a healthy one and changes neither — so it is the one
* command that can run on a board with no vendor module and no claim, and
* `instrn_buf_status` (0x66 wedged / 0x77 healthy) read this way is the WHOLE health
* instrument there. */
static int run_status(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
const tool_targets *targets, bool detail)
{
unsigned verdict_total[FT_TT_CORE_VERDICT_LAST + 1u];
unsigned entry;
unsigned verdict;
memset(verdict_total, 0, sizeof(verdict_total));
if (!detail)
ft_tt_core_status_print_header();
for (entry = 0; entry < targets->count; entry++) {
ft_tt_core_status status;
if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) != FT_TT_OK) {
fprintf(stderr, "Tensix %u does not resolve to a coordinate\n", targets->index[entry]);
continue;
}
if (detail)
ft_tt_core_status_print_detail(&status);
else
ft_tt_core_status_print_line(&status);
verdict_total[status.verdict]++;
}
printf("\n%u core%s:", targets->count, targets->count == 1u ? "" : "s");
for (verdict = 0; verdict <= (unsigned)FT_TT_CORE_VERDICT_LAST; verdict++)
if (verdict_total[verdict] != 0u)
printf(" %u %s", verdict_total[verdict],
ft_tt_core_verdict_string((ft_tt_core_verdict)verdict));
printf("\n");
printf("(a read cannot tell a healthy parked core from a wedged one — that is what "
"`probe` is for)\n");
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* claim */
/* ------------------------------------------------------------------------- */
/** set by the signal handler, read by the wait loop; nothing else touches it, so a plain
* sig_atomic_t is enough without needing signal-safe cleanup inside the handler itself. */
static volatile sig_atomic_t g_claim_stop_requested = 0;
static void handle_claim_stop_signal(int signal_number)
{
(void)signal_number;
g_claim_stop_requested = 1;
}
/** Register a shared claim and hold it until Ctrl-C (or --hold-seconds runs out). Touches
* no card state -- this only ever writes to /tmp -- so it needs no --write and cannot be
* refused by the read-only-by-default rule. */
static int run_claim(const tool_options *options)
{
struct sigaction action;
const char *reason = options->card_lock_reason ? options->card_lock_reason
: "(no reason given)";
int fd = -1;
unsigned waited_seconds = 0;
int rc = ft_tt_card_lock_claim_acquire(options->card_lock_reason, &fd);
if (rc != FT_TT_OK) {
fprintf(stderr, "claim: could not register (rc=%d)\n", rc);
return rc;
}
printf("claim registered: pid %d, reason \"%s\"\n", (int)getpid(), reason);
if (options->hold_seconds != 0u)
printf(" held for up to %u second%s, or Ctrl-C to release early\n",
options->hold_seconds, options->hold_seconds == 1u ? "" : "s");
else
printf(" held until Ctrl-C -- a card-level reset (rung 3/4/5) will refuse and "
"name this claim while it runs\n");
fflush(stdout);
memset(&action, 0, sizeof(action));
action.sa_handler = handle_claim_stop_signal;
sigemptyset(&action.sa_mask);
sigaction(SIGINT, &action, NULL);
sigaction(SIGTERM, &action, NULL);
while (!g_claim_stop_requested) {
if (options->hold_seconds != 0u && waited_seconds >= options->hold_seconds)
break;
sleep(1u);
waited_seconds++;
}
ft_tt_card_lock_release(fd);
printf("claim released\n");
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* probe */
/* ------------------------------------------------------------------------- */
static int run_probe(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
const tool_targets *targets,
const tool_options *options)
{
ft_tt_core_probe_context context;
unsigned passed = 0;
unsigned failed = 0;
unsigned refused = 0;
unsigned entry;
int rc;
rc = ft_tt_core_probe_context_open(options->program_directory, &context);
if (rc != FT_TT_OK) {
fprintf(stderr, "cannot read firmware and kernels from %s (rc=%d)\n",
options->program_directory, rc);
return rc;
}
printf("probe: firmware and vector_add triad from %s, %u repeat%s per core\n",
options->program_directory, options->repeats, options->repeats == 1u ? "" : "s");
for (entry = 0; entry < targets->count; entry++) {
ft_tt_core_probe_result result;
ft_tt_core_status status;
if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) != FT_TT_OK) {
fprintf(stderr, "Tensix %u does not resolve to a coordinate\n", targets->index[entry]);
continue;
}
(void)ft_tt_core_probe_run(noc, grid, &context, &status,
options->repeats, options->force, &result);
ft_tt_core_probe_print_line(&result);
fflush(stdout);
if (result.refused)
refused++;
else if (ft_tt_core_probe_passed(&result))
passed++;
else
failed++;
}
ft_tt_core_probe_context_close(&context);
printf("\nprobe: %u passed, %u failed, %u refused, of %u core%s\n", passed, failed, refused,
targets->count, targets->count == 1u ? "" : "s");
return failed == 0u ? FT_TT_OK : FT_TT_ERROR;
}
/* ------------------------------------------------------------------------- */
/* recover */
/* ------------------------------------------------------------------------- */
/** One core through one of the core-level rungs, with a before and after status. */
static void recover_one_core(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
uint32_t index,
const tool_options *options, const ft_tt_firmware_set *firmware)
{
ft_tt_core_recovery_report report;
ft_tt_core_status before;
ft_tt_core_status after;
if (ft_tt_core_status_read(noc, grid, index, &before) != FT_TT_OK) {
fprintf(stderr, "Tensix %u does not resolve to a coordinate\n", index);
return;
}
printf("--- before ---\n");
ft_tt_core_status_print_detail(&before);
switch ((ft_tt_core_recovery_rung)options->rung) {
case FT_TT_RECOVERY_RUNG_DEASSERT_RISCS:
(void)ft_tt_core_recover_deassert_riscs(noc, &before, &report);
break;
case FT_TT_RECOVERY_RUNG_RELOAD_FIRMWARE:
(void)ft_tt_core_recover_reload_firmware(noc, &before, firmware,
options->force, &report);
break;
case FT_TT_RECOVERY_RUNG_POKE_WAIT:
default:
(void)ft_tt_core_recover_poke_wait(noc, &before, &report);
break;
}
printf("--- %s ---\n", ft_tt_core_recovery_rung_string((ft_tt_core_recovery_rung)options->rung));
ft_tt_core_recovery_print(&report);
printf("--- after ---\n");
if (ft_tt_core_status_read(noc, grid, index, &after) == FT_TT_OK)
ft_tt_core_status_print_detail(&after);
fflush(stdout);
}
/** Probe every target and print the result, used by `--then-probe`.
*
* Takes the ACCESSOR and the GRID rather than a session, because a card-level
* rung may be driven on a machine where the vendor chardev does not exist and
* the accessor came from foundry's own driver instead. Nothing in here cares
* which — that is the whole point of the seam. */
static void probe_targets(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
const tool_targets *targets, const tool_options *options)
{
ft_tt_core_probe_context context;
unsigned entry;
if (ft_tt_core_probe_context_open(options->program_directory, &context) != FT_TT_OK) {
fprintf(stderr, "--then-probe: cannot read images from %s\n", options->program_directory);
return;
}
printf("\n--- probe after the rung ---\n");
for (entry = 0; entry < targets->count; entry++) {
ft_tt_core_probe_result result;
ft_tt_core_status status;
if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) != FT_TT_OK)
continue;
(void)ft_tt_core_probe_run(noc, grid, &context, &status, options->repeats,
options->force, &result);
ft_tt_core_probe_print_line(&result);
fflush(stdout);
}
ft_tt_core_probe_context_close(&context);
}
/** Rungs 0-2: one core at a time, over the NoC. */
static int run_core_recovery(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
const tool_targets *targets, const tool_options *options)
{
ft_tt_firmware_set firmware;
bool firmware_loaded = false;
unsigned entry;
if (options->rung == FT_TT_RECOVERY_RUNG_RELOAD_FIRMWARE) {
if (ft_tt_firmware_set_load(options->program_directory, &firmware) != FT_TT_OK) {
fprintf(stderr, "rung 1 needs firmware images; %s did not provide them\n",
options->program_directory);
return FT_TT_EIO;
}
firmware_loaded = true;
}
for (entry = 0; entry < targets->count; entry++)
recover_one_core(noc, grid, targets->index[entry], options,
firmware_loaded ? &firmware : NULL);
if (firmware_loaded)
ft_tt_firmware_set_release(&firmware);
if (options->then_probe)
probe_targets(noc, grid, targets, options);
return FT_TT_OK;
}
/** The state of every target, printed before a whole-card rung erases it.
*
* Accessor + grid, not a session, for the same reason probe_targets() is: with
* tt-kmd unloaded `instrn_buf_status` read over a udrv NoC path is the WHOLE
* health instrument (0x66 wedged / 0x77 healthy), and it is carried in exactly
* this struct. */
static void print_targets_status(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
const tool_targets *targets, const char *title)
{
unsigned entry;
printf("--- %s ---\n", title);
for (entry = 0; entry < targets->count; entry++) {
ft_tt_core_status status;
if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) == FT_TT_OK)
ft_tt_core_status_print_detail(&status);
}
fflush(stdout);
}
/* ------------------------------------------------------------------------- */
/* main */
/* ------------------------------------------------------------------------- */
/**
* The card as a whole-card rung sees it: EITHER a tt-kmd hardware session OR,
* when there is no vendor module, an ft_tt_runtime over foundry's own driver.
*
* ---------------------------------------------------------------------------
* WHY THIS EXISTS, AND WHY IT IS NOT A FORK
* ---------------------------------------------------------------------------
* `--driver udrv` was built for a board with NO tt-kmd, and until 2026-08-17 it
* opened the chardev anyway — to print the before-state — so it could not run on
* the one board it exists for. The reset library itself was always fine; the
* tool around it was not.
*
* Everything a card-level rung does with the card is `ft_tt_core_status_read()`
* and `ft_tt_core_probe_run()`, and both take an `ft_tt_noc_access` and a
* `ft_tt_grid_info`. So this struct holds whichever thing produced those two
* and hands out pointers to them; NOTHING below it is duplicated or branched.
* With the module gone `instrn_buf_status` (0x66 wedged / 0x77 healthy) read
* over a udrv path is the whole health instrument, and it arrives through
* exactly this accessor.
*/
typedef struct tool_card {
ft_tt_hardware_session kmd;
bool kmd_open;
ft_tt_runtime udrv;
bool udrv_open;
const ft_tt_noc_access *noc; /**< NULL when nothing is open */
const ft_tt_grid_info *grid;
char address[FT_TT_PCI_ADDRESS_MAX]; /**< the claim namespace's card identity */
/** Per-core claims this invocation holds. Taken through ft_tt_tensix_claim
* DIRECTLY rather than through the tt-kmd session's wrapper: a claim is a
* flock under /tmp and has never needed a device handle, and routing it
* through the session is what tied `probe` and rungs 0-2 to a chardev that
* the target board will not have. */
ft_tt_tensix_claim_set *cores;
} tool_card;
/** Open, resolve targets, and hand back the card's PCI address for the device rungs. */
static int open_session(ft_tt_hardware_session *session, tool_targets *targets,
char *out_card_address, size_t card_address_bytes)
{
int rc = ft_tt_hardware_session_open(session);
if (rc != FT_TT_OK)
return rc;
rc = resolve_targets(&session->grid, targets);
if (rc != FT_TT_OK) {
ft_tt_hardware_session_close(session);
return rc;
}
/* Through the runtime, which answers on either transport — the session no
* longer necessarily holds a tt-kmd context, and the address is what the
* per-core claim namespace is named after. */
if (out_card_address)
(void)ft_tt_runtime_pci_address(&session->runtime, out_card_address, card_address_bytes);
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* per-core claims (ft_tt_tensix_claim.h) */
/* ------------------------------------------------------------------------- */
/**
* Claim every target before touching any of them.
*
* `probe` and rungs 0-2 write to ONE CORE EACH -- that core's L1, mailbox and
* soft-reset register -- so they belong on a per-core claim, not the whole-card
* lock. The card lock stays where it was, on the rungs that reset the card.
*
* Straight to `ft_tt_tensix_claim`, NOT through the tt-kmd session's wrapper. A
* claim has always been a flock under /tmp and has never needed a device handle;
* going through the session is the only reason `probe` and rungs 0-2 required a
* chardev, which is precisely what the board this driver exists for will not
* have.
*
* @return FT_TT_OK (also under an override, where nothing was claimed), or the
* refusal code with nothing written to the card
*/
static int claim_targets_or_explain(tool_card *card, const tool_targets *targets,
const tool_options *options, const char *what)
{
const char *reason = options->card_lock_reason ? options->card_lock_reason : what;
ft_tt_tensix_claim_request request;
ft_tt_tensix_claim_refusal refusal;
int rc;
memset(&request, 0, sizeof(request));
/* NULL card reads the environment; ours is the address the open resolved, so
* two invocations on one box cannot land in different namespaces. */
request.card = card->address[0] ? card->address : NULL;
request.indices = targets->index;
request.count = targets->count;
request.reason = reason;
rc = ft_tt_tensix_claim_acquire(&request, &card->cores, &refusal);
if (rc == FT_TT_UDRV_OK)
return FT_TT_OK;
ft_tt_tensix_claim_refusal_print(&refusal);
card->cores = NULL;
/* FT_TT_UDRV_EAGAIN and FT_TT_EPERM are the same number by design
* (ft_tt_udrv.h), but the caller's table is ft_tt_device.h's, so name it. */
rc = (rc == FT_TT_UDRV_EAGAIN) ? FT_TT_EPERM : rc;
/* The refusal itself was already printed, naming the holder's pid and reason
* (ft_tt_tensix_claim_refusal_print). All this adds is what to do next. */
if (!options->override_core_claims) {
fprintf(stderr,
" %s writes to each core it targets, so it will not drive a core somebody\n"
" else owns. Nothing has been written. Re-run with --override-core-claims\n"
" only if you know that process is finished with it.\n",
what);
return rc;
}
fprintf(stderr,
"--override-core-claims: PROCEEDING on a core another process holds. If it is "
"still driving that core, both of you will get wrong answers.\n");
return FT_TT_OK;
}
/**
* Name every core-claim holder a whole-card reset is about to destroy.
*
* The card lock only ever saw processes that registered a SHARED CLAIM through
* `ft_tt_core claim`. Now that ordinary work holds per-core claims instead, a
* reset that consulted only the card lock would report "nobody is using the
* card" while wiping eight live suites. The census closes that: the whole-card
* operation reads the per-core namespace too.
*
* @return how many cores are held, or 0 when the namespace cannot be read
*/
static unsigned report_core_claim_holders(const char *card_address)
{
ft_tt_udrv_claim_namespace ns;
ft_tt_udrv_claim_info *info;
uint32_t held = 0u;
uint32_t index;
/* Resolved through the claim module, not with NULL and a literal, so this
* census cannot end up reading a different directory than the claimants
* write to -- that mismatch would report "nobody is using the card" and be
* believed. */
if (ft_tt_udrv_claim_namespace_init(&ns, ft_tt_tensix_claim_resolve_root(NULL),
ft_tt_tensix_claim_resolve_card(card_address),
FT_TT_TENSIX_CLAIM_NAMESPACE,
FT_TT_TENSIX_CLAIM_INDEX_COUNT) != FT_TT_UDRV_OK)
return 0u;
info = calloc((size_t)ns.count, sizeof(*info));
if (!info)
return 0u;
if (ft_tt_udrv_claim_scan(&ns, info, (size_t)ns.count, &held) != FT_TT_UDRV_OK || held == 0u) {
free(info);
return 0u;
}
fprintf(stderr, " %u Tensix core%s are claimed by live processes:\n", held,
held == 1u ? "" : "s");
for (index = 0u; index < ns.count; index++) {
if (!info[index].held)
continue;
fprintf(stderr, " core %-3u pid %-7ld %s\n", index, (long)info[index].pid,
info[index].reason[0] != '\0' ? info[index].reason : "(no reason recorded)");
}
free(info);
return (unsigned)held;
}
/** Take the exclusive card lock, or explain the refusal / print the override banner.
* @return FT_TT_OK with `*out_fd` holding the lock (may be -1 under a granted override --
* nothing was acquired, so there is nothing to release), or a refusal code with
* `*out_fd` at -1 and nothing written to the card. */
static int acquire_card_lock_or_explain(const tool_options *options, int *out_fd)
{
ft_tt_card_lock_status lock_status;
int rc = ft_tt_card_lock_exclusive_try(options->card_lock_reason, out_fd, &lock_status);
if (rc == FT_TT_OK)
return FT_TT_OK;
/* CONTENDED and BROKEN are different failures and must not share a message. EPERM
* means somebody holds it and lock_status names them; anything else means the lock
* could not be consulted AT ALL, and claiming contention there sends the reader
* hunting a holder that does not exist. MEASURED on the card host: fs.protected_regular=2
* stopped root open()ing the lock, and this branch reported a phantom holder. */
if (rc != FT_TT_EPERM) {
fprintf(stderr,
"refused: rung %u resets the whole card, and the card lock could NOT BE "
"CONSULTED (%s).\n"
" This is NOT contention. The lock file could not be opened, so whether\n"
" anyone else is using the card is UNKNOWN -- refusing rather than\n"
" guessing. Nothing has been reset. Re-run with --override-card-lock only\n"
" if you are certain the card is yours alone.\n",
options->rung, ft_tt_error_string(rc));
if (!options->override_card_lock)
return rc;
} else if (!options->override_card_lock) {
fprintf(stderr,
"refused: rung %u resets the whole card, and another process already "
"holds the card lock:\n",
options->rung);
ft_tt_card_lock_status_print(&lock_status);
fprintf(stderr,
" Wait for it to finish or coordinate with whoever it is (`ft_tt_core "
"claim` names them here). If you are certain it is safe to proceed "
"anyway, re-run with --override-card-lock. Nothing has been reset.\n");
return FT_TT_EPERM;
}
fprintf(stderr,
"--override-card-lock: PROCEEDING WITHOUT THE LOCK. Rung %u resets the whole "
"card and will destroy the in-flight work of:\n",
options->rung);
ft_tt_card_lock_status_print(&lock_status);
*out_fd = -1; /* nothing was acquired under an override; nothing to release later */
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* rung 3 with NO VENDOR DRIVER (ft_tt_udrv_reset.h) */
/* ------------------------------------------------------------------------- */
/** "/sys/bus/pci/devices/0000:12:00.0" from "0000:12:00.0". */
static int pci_device_dir(const char *bdf, char *out, size_t out_bytes)
{
int written = snprintf(out, out_bytes, "%s/%s", TOOL_PCI_DEVICES_DIR, bdf);
return (written > 0 && (size_t)written < out_bytes) ? FT_TT_OK : FT_TT_EINVAL;
}
/** One line naming a device's negotiated link, or why it could not be read. */
static void print_link_status(const char *label, const char *device_dir)
{
ft_tt_udrv_link_status status;
if (ft_tt_udrv_link_status_read(device_dir, &status) != FT_TT_UDRV_OK) {
printf(" %-8s link: could not be read\n", label);
return;
}
printf(" %-8s link: %s x%u%s\n", label, ft_tt_udrv_link_speed_string(status.speed_code),
status.width, status.training ? " (TRAINING)" : "");
}
/**
* The link comes back at Gen1 after every card-level reset, and every host-involved timing
* then silently halves (DEV_NOTES §1). Retrain it on the BRIDGE, and only when the topology
* check says the bridge has exactly one child and that child is this card — retraining a
* bridge with K80s under it is not ours to do.
*/
static void retrain_link_if_downgraded(const char *card_dir, const char *card_bdf)
{
ft_tt_udrv_link_retrain_report retrain;
ft_tt_udrv_link_status status;
ft_tt_pcie_topology topology;
char bridge_dir[FT_TT_UDRV_RESET_PATH_MAX];
if (ft_tt_udrv_link_status_read(card_dir, &status) != FT_TT_UDRV_OK) {
printf("link: the card's LNKSTA could not be read; NOT retraining\n");
return;
}
if (status.speed_code >= FT_TT_UDRV_LINK_SPEED_5GT) {
printf("link: already %s x%u — no retrain needed\n",
ft_tt_udrv_link_speed_string(status.speed_code), status.width);
return;
}
if (ft_tt_pcie_topology_resolve(card_bdf, &topology) != FT_TT_OK || !topology.safe) {
printf("link: at %s but the bridge topology is not exclusively this card (%s) — "
"NOT retraining\n",
ft_tt_udrv_link_speed_string(status.speed_code),
topology.refusal ? topology.refusal : "unresolved");
return;
}
if (pci_device_dir(topology.bridge, bridge_dir, sizeof(bridge_dir)) != FT_TT_OK)
return;
printf("link: %s — retraining on bridge %s\n",
ft_tt_udrv_link_speed_string(status.speed_code), topology.bridge);
(void)ft_tt_udrv_link_retrain(bridge_dir, 2000u, &retrain);
ft_tt_udrv_link_retrain_print(&retrain);
print_link_status("card", card_dir);
print_link_status("bridge", bridge_dir);
}
/**
* Rung 3 through the userspace driver: snapshot config, three config writes, wait for the
* card to clear its own reset marker, write config back, retrain the link.
*
* The caller has already taken the card lock, censused the per-core claims, printed the
* before state and CLOSED the session — this function must not be reached with an aperture
* mapped, which is the discipline that replaces `tenstorrent_vma_zap`.
*
* @param card_bdf "0000:12:00.0"
* @param options `udrv_reenumerate` selects the remove + rescan fallback instead
* @return FT_TT_OK once the card is back and holds its config again
*/
static int run_udrv_asic_reset(const char *card_bdf, const tool_options *options)
{
ft_tt_udrv_config_restore_report restore;
ft_tt_udrv_config_snapshot snapshot;
ft_tt_udrv_reset_wait_report wait;
ft_tt_udrv_reset_report reset;
char card_dir[FT_TT_UDRV_RESET_PATH_MAX];
int rc = pci_device_dir(card_bdf, card_dir, sizeof(card_dir));
if (rc != FT_TT_OK)
return rc;
printf("\n--- rung 3, NO VENDOR DRIVER: %s ---\n", card_dir);
print_link_status("card", card_dir);
/* The snapshot is taken BEFORE the marker is set, so the saved command register has the
* marker clear and restoring it cannot re-arm what the card just finished clearing. */
rc = ft_tt_udrv_config_snapshot_take(card_dir, &snapshot);
if (rc != FT_TT_UDRV_OK) {
fprintf(stderr,
"refused: could not snapshot PCI config space (%d). Without it an in-place\n"
" ASIC reset leaves the BARs and the command register at zero and the card\n"
" undecodable. Nothing has been reset. Re-run under sudo if %s/config is\n"
" not writable.\n",
rc, card_dir);
return FT_TT_EPERM;
}
printf("config snapshot: vendor 0x%04x device 0x%04x, PCI Express capability at 0x%02x "
"version %u, BAR0 0x%08x, command 0x%04x\n",
snapshot.vendor_id, snapshot.device_id, snapshot.pcie_capability_offset,
snapshot.pcie_capability_version, snapshot.header[4],
(unsigned)(snapshot.header[1] & 0xFFFFu));
rc = ft_tt_udrv_reset_asic(card_dir, &reset);
if (rc != FT_TT_UDRV_OK) {
ft_tt_udrv_reset_print(&reset, NULL);
fprintf(stderr, "the ASIC reset did not complete its three writes\n");
return FT_TT_EIO;
}
/* WAIT FOR THE MARKER, then restore. Restoring while the marker is still set is the bug
* that made tt-kmd's in-place path look impossible (ft_tt_udrv_reset.h @status). */
rc = ft_tt_udrv_reset_wait_ready(card_dir, snapshot.vendor_id, 15000u, &wait);
ft_tt_udrv_reset_print(&reset, &wait);
if (rc != FT_TT_UDRV_OK) {
fprintf(stderr,
"the card did not come back within 15 s. It is reset and NOT restored;\n"
" `echo 1 > %s/remove` then `echo 1 > /sys/bus/pci/rescan` is the recovery.\n",
card_dir);
return FT_TT_EIO;
}
if (options->udrv_reenumerate) {
ft_tt_pcie_reenumeration_report reenumeration;
printf("--udrv-reenumerate: finishing with remove + rescan instead of the in-place "
"restore\n");
rc = ft_tt_pcie_reenumerate(card_bdf, &reenumeration);
ft_tt_pcie_reenumeration_print(&reenumeration);
if (rc != FT_TT_OK)
return rc;
} else {
rc = ft_tt_udrv_config_snapshot_restore(card_dir, &snapshot, &restore);
ft_tt_udrv_config_restore_print(&restore);
if (rc != FT_TT_UDRV_OK) {
fprintf(stderr, "the config restore did not land; the card is reset and "
"undecodable. Recover with remove + rescan.\n");
return FT_TT_EIO;
}
}
retrain_link_if_downgraded(card_dir, card_bdf);
return FT_TT_OK;
}
static void close_card(tool_card *card);
/**
* Open the card over the transport `--driver` names — and ONLY that one.
*
* `--driver kmd` (the default) is the vendor chardev, exactly as before.
* `--driver udrv` is foundry's own driver and NEVER the chardev, whether or not
* a module happens to be loaded. Two reasons, and the second is the important
* one:
*
* - a flag that silently uses the other transport when the better one is
* available is untestable on the machine that HAS the better one, and this
* bank is exactly that machine — tt-kmd is the oracle and is deliberately
* kept installed. `--driver udrv` has to mean udrv here or it can never be
* compared against anything;
* - a preference is a fallback wearing a different word, and a fallback that
* produces a plausible answer from the wrong path is the defect this project
* has shipped twice.
*
* What `--driver udrv` therefore does NOT get is `probe --write`, which needs
* the tt-kmd backend. That is a named limit, not a surprise: `probe` keeps the
* session (see main()).
*
* @param out_card zeroed and filled; `noc`/`grid` point into it, so it must
* not be copied by value afterwards
*/
static int open_card(tool_card *card, tool_targets *targets, const tool_options *options,
char *out_address, size_t address_bytes)
{
int rc;
memset(card, 0, sizeof(*card));
if (options->reset_driver != TOOL_RESET_DRIVER_UDRV) {
rc = open_session(&card->kmd, targets, card->address, sizeof(card->address));
if (rc != FT_TT_OK) {
fprintf(stderr, "the vendor chardev did not open (rc=%d). --driver udrv reads "
"the card without one.\n", rc);
return rc;
}
card->kmd_open = true;
card->noc = &card->kmd.noc;
card->grid = &card->kmd.grid;
if (out_address)
(void)snprintf(out_address, address_bytes, "%s", card->address);
return FT_TT_OK;
}
rc = ft_tt_runtime_open_with_driver(0, FT_TT_RUNTIME_DRIVER_UDRV, &card->udrv);
if (rc != FT_TT_OK) {
fprintf(stderr, "--driver udrv: the userspace driver could not open the card "
"(rc=%d). REFUSING — it will not quietly use the chardev instead. "
"Nothing has been done to the card.\n", rc);
return rc;
}
card->udrv_open = true;
card->noc = &card->udrv.noc;
card->grid = &card->udrv.grid;
rc = resolve_targets(card->grid, targets);
if (rc != FT_TT_OK) {
close_card(card);
return rc;
}
(void)ft_tt_runtime_pci_address(&card->udrv, card->address, sizeof(card->address));
if (out_address)
(void)snprintf(out_address, address_bytes, "%s", card->address);
printf("card read over foundry's own driver (no vendor ioctl in this path)\n");
return FT_TT_OK;
}
/** Close whatever is open and blank the pointers.
*
* Called BEFORE every reset, not only at the end: a reset invalidates every
* mapping the card had, and on the udrv path nothing zaps our VMAs for us
* (ft_tt_udrv.h §5(b) — the replacement is this discipline, not a mechanism). */
static void close_card(tool_card *card)
{
if (!card)
return;
ft_tt_tensix_claim_release(card->cores);
card->cores = NULL;
if (card->kmd_open)
ft_tt_hardware_session_close(&card->kmd);
if (card->udrv_open)
ft_tt_runtime_close(&card->udrv);
card->kmd_open = false;
card->udrv_open = false;
card->noc = NULL;
card->grid = NULL;
}
/** Rungs 3-5, which reset the whole card: capture, close, reset, reopen, report.
*
* Takes the exclusive card lock FIRST, before anything else -- the same property as the
* PCIe reenumeration pre-flight below: acquire and check before anything destructive, and
* release on every exit path via `cleanup`, including every failure path. A lock leaked by
* a failed reset would be worse than no lock at all. */
static int run_device_recovery(tool_targets *targets, const tool_options *options)
{
char card_address[FT_TT_PCI_ADDRESS_MAX] = {0};
/* The userspace ASIC reset restores config space IN PLACE and needs no PCI
* remove and no bus-wide rescan — that was measured, and `remove` + `rescan`
* is kept only as the explicit `--udrv-reenumerate` fallback. So the
* root-only reenumeration pre-flight applies to the kmd path and to that
* fallback, and NOT to the default udrv path. Demanding it there would
* refuse the one machine this driver exists for, for a permission it never
* uses. */
bool needs_reenumeration = options->reset_driver != TOOL_RESET_DRIVER_UDRV ||
options->udrv_reenumerate;
tool_card card;
int lock_fd = -1;
int rc = acquire_card_lock_or_explain(options, &lock_fd);
memset(&card, 0, sizeof(card));
if (rc != FT_TT_OK)
return rc;
rc = open_card(&card, targets, options, card_address, sizeof(card_address));
if (rc != FT_TT_OK)
goto cleanup;
printf("card is at PCI %s\n", card_address);
/* SECOND PRE-FLIGHT: the card lock above only sees processes that registered
* a shared claim. Ordinary work now holds PER-CORE claims, and a reset wipes
* those too, so consult that namespace before anything destructive. */
{
unsigned core_holders = report_core_claim_holders(card_address);
if (core_holders != 0u && !options->override_card_lock) {
fprintf(stderr,
"refused: rung %u resets the whole card, and %u core%s are claimed by "
"live processes (listed above).\n"
" A card reset returns every Tensix to power-on default, so it destroys "
"their work as surely as it would a shared claim's.\n"
" Wait for them, or re-run with --override-card-lock. Nothing has been "
"reset.\n",
options->rung, core_holders, core_holders == 1u ? " is" : "s are");
rc = FT_TT_EPERM;
goto cleanup;
}
if (core_holders != 0u)
fprintf(stderr, "--override-card-lock: those %u core claim%s will be destroyed.\n",
core_holders, core_holders == 1u ? "" : "s");
}
/* PRE-FLIGHT, BEFORE ANYTHING DESTRUCTIVE. Every card-level rung leaves the card
* reset-but-not-re-enumerated (§21.3): memory decode off, BARs disabled, tt-kmd
* unbound, /dev/tenstorrent gone. Only the PCI remove + rescan finishes the job, and
* those sysfs nodes are root-only. Discovering that AFTER the reset leaves the card
* dead until someone runs the rescan by hand -- which is exactly what happened on
* 2026-08-12 (errno 13 at "removing the card from the PCI bus", card down until it was
* re-enumerated manually). So refuse up front rather than half-finish. */
rc = needs_reenumeration ? ft_tt_pcie_reenumerate_is_permitted(card_address) : FT_TT_OK;
if (rc != FT_TT_OK) {
close_card(&card);
fprintf(stderr,
"refused: rung %u resets the whole card, and completing it needs write\n"
" access to /sys/bus/pci/devices/%s/remove and /sys/bus/pci/rescan,\n"
" which this process does not have. Without them the card would be left\n"
" reset-but-not-re-enumerated: BARs disabled, tt-kmd unbound, no chardev.\n"
" Re-run under sudo. Nothing has been reset.\n",
options->rung, card_address);
rc = FT_TT_EPERM;
goto cleanup;
}
print_targets_status(card.noc, card.grid, targets,
"before (captured now: a successful reset erases it)");
/* Every handle must be closed: the reset paths zap VMAs and reclaim TLB windows, so a
* live session's mapped windows are invalid the moment they return. On the udrv path
* nothing zaps them for us, which makes closing here the mechanism rather than a tidy-up. */
close_card(&card);
if (options->reset_driver == TOOL_RESET_DRIVER_UDRV) {
rc = run_udrv_asic_reset(card_address, options);
if (rc != FT_TT_OK)
goto cleanup;
} else if (options->rung == 5u) {
ft_tt_pcie_sbr_report report;
rc = ft_tt_pcie_secondary_bus_reset(card_address, options->save_directory, &report);
ft_tt_pcie_sbr_print(&report);
if (rc == FT_TT_EPERM) {
fprintf(stderr, "refused: %s\n",
report.topology.refusal ? report.topology.refusal : "topology check");
goto cleanup;
}
} else {
ft_tt_device_reset_report report;
ft_tt_pcie_reenumeration_report reenumeration;
ft_tt_device_reset_flag flag = options->rung == 3u ? FT_TT_RESET_DEVICE_ASIC_RESET
: FT_TT_RESET_DEVICE_ASIC_DMC_RESET;
rc = ft_tt_device_reset_issue(TOOL_CHARDEV_PATH, flag, &report);
ft_tt_device_reset_print(&report);
if (rc != FT_TT_OK)
goto cleanup;
/* The driver's in-place completion does not finish the job on this host: the card
* comes back with memory decode disabled and an ARC that answers 0xffffffff. See
* ft_tt_pcie_reenumerate()'s header — this is measured, not defensive. */
rc = ft_tt_pcie_reenumerate(card_address, &reenumeration);
ft_tt_pcie_reenumeration_print(&reenumeration);
if (rc != FT_TT_OK)
goto cleanup;
}
rc = open_card(&card, targets, options, NULL, 0);
if (rc != FT_TT_OK) {
fprintf(stderr, "the card did not reopen after the reset (rc=%d)\n", rc);
goto cleanup;
}
print_targets_status(card.noc, card.grid, targets, "after");
if (options->then_probe)
probe_targets(card.noc, card.grid, targets, options);
rc = FT_TT_OK;
cleanup:
close_card(&card);
ft_tt_card_lock_release(lock_fd);
return rc;
}
/** The refusals that keep a read-only tool read-only. */
static int check_permissions(const tool_options *options)
{
if (options->command == TOOL_COMMAND_PROBE && !options->allow_write) {
fprintf(stderr, "probe writes firmware, kernels and tiles to the card: pass --write\n");
return FT_TT_EPERM;
}
if (options->command != TOOL_COMMAND_RECOVER)
return FT_TT_OK;
if (!options->rung_given || options->rung > 5u) {
fprintf(stderr, "recover needs --rung 0..5\n");
return FT_TT_EINVAL;
}
if (!options->allow_write) {
fprintf(stderr, "recover writes to the card: pass --write\n");
return FT_TT_EPERM;
}
if (options->rung == 5u && !options->allow_secondary_bus_reset) {
fprintf(stderr, "rung 5 resets a PCIe bus: pass --allow-pcie-secondary-bus-reset "
"as well as --write\n");
return FT_TT_EPERM;
}
/* `--driver` selects the TRANSPORT for every command, and additionally the MECHANISM
* for a whole-card rung. Only rung 3 has a userspace mechanism, so those are the only
* two rungs it can be refused for — and the refusal names why rather than silently
* doing the kmd thing. Rung 4 is two ARC messages and there is no userspace ARC
* transport wired into the reset path yet; rung 5 is already pure sysfs and has no
* vendor-driver dependency to remove.
*
* `status`, `probe` and rungs 0-2 are NOT restricted: they reach the card only through
* the ft_tt_noc_access vtable and a per-core flock, and both exist on either transport.
* That is what makes the whole tool usable on a board with no vendor module. */
if (options->command == TOOL_COMMAND_RECOVER &&
options->reset_driver == TOOL_RESET_DRIVER_UDRV && options->rung >= 4u) {
fprintf(stderr,
"--driver udrv has no mechanism for rung %u. It is %s.\n", options->rung,
options->rung == 4u
? "ASIC+DMC, which needs two ARC messages and there is no userspace ARC "
"transport wired into the reset path yet"
: "already pure sysfs and uses no vendor driver");
return FT_TT_EINVAL;
}
if (options->udrv_reenumerate && options->reset_driver != TOOL_RESET_DRIVER_UDRV) {
fprintf(stderr, "--udrv-reenumerate only means something with --driver udrv\n");
return FT_TT_EINVAL;
}
return FT_TT_OK;
}
int main(int argc, char **argv)
{
tool_card card;
tool_options options;
tool_targets targets;
char card_address[FT_TT_PCI_ADDRESS_MAX] = {0};
int rc;
setvbuf(stdout, NULL, _IOLBF, 0);
if (parse_options(argc, argv, &options) != FT_TT_OK) {
print_usage(argv[0]);
return 2;
}
if (check_permissions(&options) != FT_TT_OK)
return 2;
if (options.command == TOOL_COMMAND_CLAIM)
return run_claim(&options) == FT_TT_OK ? 0 : 1;
if (options.command == TOOL_COMMAND_TOPOLOGY) {
ft_tt_pcie_topology topology;
/* An optional PCI address makes the REFUSAL demonstrable: point it at a device
* that is not the card — a K80 behind a bridge with two children — and watch it
* refuse. Read-only, and it does not change what rung 5 targets: that address is
* always derived from the open device, never taken from a command line. */
if (options.target_text) {
snprintf(card_address, sizeof(card_address), "%s", options.target_text);
} else {
memset(&targets, 0, sizeof(targets));
targets.all = true;
rc = open_card(&card, &targets, &options, card_address, sizeof(card_address));
if (rc != FT_TT_OK)
return 1;
close_card(&card);
}
if (ft_tt_pcie_topology_resolve(card_address, &topology) != FT_TT_OK)
fprintf(stderr, "could not read the PCIe topology for %s\n", card_address);
ft_tt_pcie_topology_print(&topology);
/* The negotiated link, at BOTH ends. A reset leaves it at Gen1 and every
* host-involved number then silently halves, so it belongs in the read-only
* command that gets run at the start and end of a round (DEV_NOTES §1). */
{
char device_dir[FT_TT_UDRV_RESET_PATH_MAX];
if (pci_device_dir(topology.card[0] ? topology.card : card_address, device_dir,
sizeof(device_dir)) == FT_TT_OK)
print_link_status("card", device_dir);
if (topology.bridge[0] &&
pci_device_dir(topology.bridge, device_dir, sizeof(device_dir)) == FT_TT_OK)
print_link_status("bridge", device_dir);
}
return topology.safe ? 0 : 1;
}
if (parse_targets(options.target_text, &targets) != FT_TT_OK) {
fprintf(stderr, "cannot parse targets: %s\n", options.target_text);
return 2;
}
if (options.command == TOOL_COMMAND_RECOVER && options.rung >= 3u)
return run_device_recovery(&targets, &options) == FT_TT_OK ? 0 : 1;
/* STATUS goes through the transport seam and NOT through the tt-kmd session, because it
* is the one command that has to work on a board with no vendor module: it reads only,
* so it needs no claim, and `instrn_buf_status` read this way is the whole health
* instrument once the module is gone. PROBE and rungs 0-2 WRITE to each core they
* target, so they keep the session that carries the per-core claim. */
if (options.command == TOOL_COMMAND_STATUS) {
rc = open_card(&card, &targets, &options, NULL, 0);
if (rc != FT_TT_OK)
return 1;
rc = run_status(card.noc, card.grid, &targets, options.detail);
close_card(&card);
return rc == FT_TT_OK ? 0 : 1;
}
/* READINESS travels with STATUS for the same reason: reads only, so no claim and no
* session, and it must work on a board with no vendor module. */
if (options.command == TOOL_COMMAND_READINESS) {
rc = open_card(&card, &targets, &options, NULL, 0);
if (rc != FT_TT_OK)
return 1;
rc = run_readiness(card.noc, card.grid, &targets);
close_card(&card);
return rc == FT_TT_OK ? 0 : 1;
}
rc = open_card(&card, &targets, &options, NULL, 0);
if (rc != FT_TT_OK)
return 1;
switch (options.command) {
case TOOL_COMMAND_PROBE:
rc = claim_targets_or_explain(&card, &targets, &options, "probe");
if (rc == FT_TT_OK)
rc = run_probe(card.noc, card.grid, &targets, &options);
break;
case TOOL_COMMAND_STATUS:
case TOOL_COMMAND_READINESS:
case TOOL_COMMAND_RECOVER:
default:
rc = claim_targets_or_explain(&card, &targets, &options, "recover");
if (rc == FT_TT_OK)
rc = run_core_recovery(card.noc, card.grid, &targets, &options);
break;
}
close_card(&card);
return rc == FT_TT_OK ? 0 : 1;
}