/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * test_noc.c — the named NoC read/write helpers (ft_tt_noc.{h,c}) and the * device-memory bump allocator (ft_tt_buffer.{h,c}). * * Two halves, deliberately separated: * * unit Pure arithmetic: the alignment rule table, the transfer-bounds * check, and every allocator behaviour. NO DEVICE IS OPENED, so this * runs on the Mac and is where the rules get proven wrong — a wrong * NoC coordinate hangs the chip and costs a physical power cycle * (spec §7.3), so as little as possible is discovered on silicon. * * l1 Write and read back one Tensix's L1 through ft_tt_write_l1 / * ft_tt_read_l1, on a core DERIVED from live telemetry. * dram Round-trip every enabled DRAM bank, and prove the banks are * distinct endpoints rather than aliases of one. * cross A transfer that genuinely spans several 2 MiB windows. This is the * re-windowing path, and ft_tt_noc_path_aim_count() is what tells a * real multi-window run from a lucky in-window one. * align Alignment refusals against a live path: the helper must reject * without issuing anything. * bw MB/s through the helpers, against the 287 MB/s WC baseline the raw * window layer measured (spec §9.8). * * SAFETY: every coordinate comes from ft_tt_coord against the live grid, and * the Tensix used is index 1 or higher — index 0 is reserved for the launch * work happening in parallel. * * Build (unit half needs no fence; the device halves do): * make -f Makefile.noc check unit only * make -f Makefile.noc PHASE1B=1 all then ./build-noc/test_noc dram */ /* clock_gettime/CLOCK_MONOTONIC are POSIX and -std=c99 turns off glibc's implicit * feature-test macros. 200809L, not 199309L: the older level also hides snprintf. */ #define _POSIX_C_SOURCE 200809L #include "ft_tt_arc_telemetry.h" #include "ft_tt_buffer.h" #include "ft_tt_arch.h" #include "ft_tt_coord.h" #include "ft_tt_device.h" #include "ft_tt_noc.h" #include #include #include #include /* The Tensix this test is allowed to touch. Index 0 belongs to the launch * bring-up running concurrently; sharing a core across two agents is how a * half-loaded firmware image gets blamed on the wrong change. */ #define TEST_TENSIX_INDEX 1u /* The DRAM bank the multi-MiB steps (cross, align, bw) write to. Bank 0 belongs to * the kernel-toolchain work running in parallel; the all-bank sweep in step_dram * still has to touch it, because "the banks are distinct endpoints" is exactly what * it proves, but nothing else here needs to. */ #define TEST_DRAM_BANK 1u #define KIB 1024u #define MIB (1024u * 1024u) static int failures; static int checks; static void check(const char *what, bool ok, const char *detail) { checks++; if (!ok) failures++; printf(" %-4s %-58s %s\n", ok ? "ok" : "FAIL", what, detail ? detail : ""); } static void check_rc(const char *what, int actual, int expected) { char detail[96]; snprintf(detail, sizeof(detail), "rc=%d (%s), expected %d (%s)", actual, ft_tt_error_string(actual), expected, ft_tt_error_string(expected)); check(what, actual == expected, detail); } static double now_seconds(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; } /** Position-dependent so a shifted, truncated or partially-landed transfer is * visible instead of looking like a clean success. */ static void fill_pattern(unsigned char *buffer, size_t bytes, uint32_t seed) { size_t index; for (index = 0; index + 4u <= bytes; index += 4u) { uint32_t word = seed ^ (uint32_t)(index / 4u) * 0x9E3779B9u; memcpy(buffer + index, &word, sizeof(word)); } } /** memcmp with a first-difference report, because "mismatch" alone does not * distinguish a dropped tail from a wrong window. */ static bool compare_pattern(const unsigned char *expected, const unsigned char *got, size_t bytes, char *detail, size_t detail_bytes) { size_t index; for (index = 0; index < bytes; index++) { if (expected[index] != got[index]) { snprintf(detail, detail_bytes, "first difference at byte %zu of %zu (wrote 0x%02x, read 0x%02x)", index, bytes, expected[index], got[index]); return false; } } snprintf(detail, detail_bytes, "%zu bytes byte-exact", bytes); return true; } /* ================================================================ * unit: alignment and bounds * ================================================================ */ static void unit_alignment_rules(void) { char detail[96]; printf("\n-- alignment rules (ft_tt_noc_alignment) --\n"); snprintf(detail, sizeof(detail), "L1 write %u, L1 read %u", ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, false), ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, true)); check("L1 is 16 B in both directions", ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, false) == 16u && ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, true) == 16u, detail); snprintf(detail, sizeof(detail), "DRAM write %u, DRAM read %u", ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, false), ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, true)); check("DRAM writes take 16 B, DRAM reads take 64 B", ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, false) == 16u && ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, true) == 64u, detail); check("an unknown space has no alignment", ft_tt_noc_alignment((ft_tt_memory_space)7, false) == 0u, NULL); snprintf(detail, sizeof(detail), "L1 %llu, DRAM %llu", (unsigned long long)ft_tt_noc_space_size(FT_TT_MEM_SPACE_L1), (unsigned long long)ft_tt_noc_space_size(FT_TT_MEM_SPACE_DRAM)); check("space sizes are 1536 KiB and 0xff000000", ft_tt_noc_space_size(FT_TT_MEM_SPACE_L1) == (uint64_t)FT_TT_MEM_L1_SIZE && ft_tt_noc_space_size(FT_TT_MEM_SPACE_DRAM) == (uint64_t)FT_TT_MEM_DRAM_SIZE, detail); } static void unit_transfer_checks(void) { uint64_t l1_top = (uint64_t)FT_TT_MEM_L1_SIZE; uint64_t dram_top = (uint64_t)FT_TT_MEM_DRAM_SIZE; printf("\n-- transfer legality (ft_tt_noc_check_transfer) --\n"); check_rc("L1 write, 16 B aligned both ways", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F80u, 4096u), FT_TT_OK); check_rc("L1 write with a misaligned address is refused", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F88u, 4096u), FT_TT_EINVAL); check_rc("L1 write with a misaligned length is refused", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F80u, 4100u), FT_TT_EINVAL); check_rc("a zero-length transfer is legal", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F80u, 0u), FT_TT_OK); check_rc("a 16 B-aligned DRAM WRITE is legal", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, false, 16u, 16u), FT_TT_OK); check_rc("the same address is refused for a DRAM READ (64 B rule)", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 16u, 64u), FT_TT_EINVAL); check_rc("a 64 B-aligned DRAM read is legal", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 64u, 64u), FT_TT_OK); check_rc("a DRAM read of 16 B is refused — the LENGTH needs 64 too", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 64u, 16u), FT_TT_EINVAL); check_rc("a transfer ending exactly at the top of L1 is legal", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, l1_top - 16u, 16u), FT_TT_OK); check_rc("one 16 B unit past the top of L1 is refused", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, l1_top - 16u, 32u), FT_TT_EINVAL); check_rc("a transfer starting at the top of L1 is refused", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, l1_top, 16u), FT_TT_EINVAL); check_rc("a transfer ending exactly at the top of a DRAM bank is legal", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, dram_top - 64u, 64u), FT_TT_OK); check_rc("past the top of a DRAM bank is refused", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, dram_top, 64u), FT_TT_EINVAL); check_rc("an unknown space is refused", ft_tt_noc_check_transfer((ft_tt_memory_space)7, false, 0u, 16u), FT_TT_EINVAL); /* --------------------------------------------------------------------- * THE PAIRING. Everything above scores ONE end against ONE space, which is what the * checker could do until now — and it is why a transfer whose two ends are each legal * on their own could still be illegal. Upstream tests the CONGRUENCE and nothing * weaker. * * The first case below is the whole point: an L1 address at 0x9F80 and a DRAM address * at 0x40 BOTH pass their own rule — 0x9F80 is 16-aligned, 0x40 is 64-aligned — and * they sit at different offsets inside the 64-byte window the stricter end imposes, so * the transfer is not one the NoC will do. `ft_tt_noc_check_transfer()` calls it legal * from either side. That is the hole. * ------------------------------------------------------------------- */ printf("\n-- transfer legality, BOTH ends (ft_tt_noc_check_transfer_pair) --\n"); check_rc("each end of a 16-vs-64 mismatch passes its OWN rule: the L1 end", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F90u, 64u), FT_TT_OK); check_rc("...and the DRAM end", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 0x40u, 64u), FT_TT_OK); check_rc("but the PAIR is refused — 0x9F90 is 16 into the 64 B window and 0x40 is 0", ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9F90u, FT_TT_MEM_SPACE_DRAM, 0x40u, true, 64u), FT_TT_EINVAL); check_rc("move the L1 end onto the same offset and the pair is legal", ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9FC0u, FT_TT_MEM_SPACE_DRAM, 0x40u, true, 64u), FT_TT_OK); /* THE HISTORICAL CASE, replayed. 0x137A0 was the Phase 2 test map's first derived L1 * base: 16- and 32-byte aligned, and 32 bytes into the 64-byte window. As the LOCAL end * of a DRAM read it produced tiles that were byte-exact up to element 992 of 1024 and * wrong in their last 64 bytes, with no refusal, no error, and three of four stages * still passing (DEV_NOTES.md §3). Nothing in the checker could see it, because each * end passed its own rule. */ check_rc("0x137A0 is a legal L1 address on its own", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x137A0u, 2048u), FT_TT_OK); check_rc("and DRAM tile 0 is a legal DRAM read on its own", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 0u, 2048u), FT_TT_OK); check_rc("the PAIR is refused — this is the 992-of-1024 bug, caught before it is issued", ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x137A0u, FT_TT_MEM_SPACE_DRAM, 0u, true, 2048u), FT_TT_EINVAL); /* An L1-to-L1 pair is judged in a 16 B window, and both ends already have to be * 16-aligned to pass their own test — so the congruence is automatic there and adds * nothing. It is the DRAM READ direction, and only that direction, where the window is * wider than either end's own rule. Stated as a check so a future alignment change has * to face it. */ check_rc("an L1-to-L1 pair is judged in a 16 B window, so congruence is automatic", ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x10000u, FT_TT_MEM_SPACE_L1, 0x20010u, false, 64u), FT_TT_OK); check_rc("the pair still refuses what a single end would: a misaligned local address", ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9F88u, FT_TT_MEM_SPACE_L1, 0x9F88u, false, 64u), FT_TT_EINVAL); check_rc("and a span past the top of DRAM", ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x10000u, FT_TT_MEM_SPACE_DRAM, dram_top, true, 64u), FT_TT_EINVAL); /* The DRAM WRITE direction takes the 16-byte rule on the DRAM end, so the window the * pair is judged in is 16 and not 64 — a distinction that only exists because the * alignment is direction-dependent, and the reason the window is computed rather than * fixed. */ check_rc("a DRAM WRITE judges the pair in a 16 B window, not a 64 B one", ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9F90u, FT_TT_MEM_SPACE_DRAM, 0x10u, false, 16u), FT_TT_OK); } /* ================================================================ * unit: the shared window walk * ================================================================ */ /** * ft_tt_noc_window_slice_at() is the arithmetic BOTH device paths run on — the * named L1/DRAM helpers and ft_tt_launch's run control, which since the Phase 2 * reconciliation goes through ft_tt_noc_transfer_raw() instead of its own copy * of this loop. It is also the piece that fails QUIETLY: a wrong window base is * still a valid transaction, just to the wrong address on the right core. So it * is a pure function, and it gets proven here rather than on silicon. */ static void unit_window_slices(void) { const size_t window = 2u * MIB; ft_tt_noc_window_slice slice; char detail[160]; printf("\n-- window slicing (ft_tt_noc_window_slice_at) --\n"); check_rc("a transfer wholly inside one window slices whole", ft_tt_noc_window_slice_at(0x9F80u, 4096u, window, &slice), FT_TT_OK); snprintf(detail, sizeof(detail), "base 0x%llx, offset %zu, %zu bytes", (unsigned long long)slice.window_base, slice.offset_in_window, slice.bytes); check("it aims at 0 and takes all 4096 bytes", slice.window_base == 0u && slice.offset_in_window == 0x9F80u && slice.bytes == 4096u, detail); check_rc("a transfer that straddles a boundary is cut at it", ft_tt_noc_window_slice_at(window - 64u * KIB, 5u * MIB, window, &slice), FT_TT_OK); snprintf(detail, sizeof(detail), "base 0x%llx, offset %zu, %zu bytes", (unsigned long long)slice.window_base, slice.offset_in_window, slice.bytes); check("the first slice stops exactly at the 2 MiB line", slice.window_base == 0u && slice.offset_in_window == window - 64u * KIB && slice.bytes == 64u * KIB, detail); check_rc("an address exactly on a boundary starts a fresh window", ft_tt_noc_window_slice_at(window, 32u, window, &slice), FT_TT_OK); snprintf(detail, sizeof(detail), "base 0x%llx, offset %zu", (unsigned long long)slice.window_base, slice.offset_in_window); check("base is the boundary and the offset is 0", slice.window_base == (uint64_t)window && slice.offset_in_window == 0u && slice.bytes == 32u, detail); /* The soft-reset register is why this function cannot assume a memory space: * 0xFFB121B0 is in neither L1 nor a DRAM bank (docs/tt_driver_notes.md §5). */ check_rc("the soft-reset register slices like anything else", ft_tt_noc_window_slice_at(0xFFB121B0u, 4u, window, &slice), FT_TT_OK); snprintf(detail, sizeof(detail), "base 0x%llx, offset 0x%zx", (unsigned long long)slice.window_base, slice.offset_in_window); /* 0xFFB00000 is NOT 2 MiB aligned, so the window lands 1 MiB lower and the * register sits 0x1121B0 into the aperture — still inside it. */ check("base 0xFFA00000, offset 0x1121B0, 4 bytes", slice.window_base == 0xFFA00000u && slice.offset_in_window == 0x1121B0u && slice.bytes == 4u && slice.offset_in_window < window, detail); check_rc("a zero-length transfer slices to nothing", ft_tt_noc_window_slice_at(0x9F80u, 0u, window, &slice), FT_TT_OK); check("and it asks for no bytes", slice.bytes == 0u, NULL); check_rc("a window size that is not a power of two is refused", ft_tt_noc_window_slice_at(0u, 16u, 3u * MIB, &slice), FT_TT_EINVAL); check_rc("a zero window size is refused", ft_tt_noc_window_slice_at(0u, 16u, 0u, &slice), FT_TT_EINVAL); check_rc("a NULL slice is refused", ft_tt_noc_window_slice_at(0u, 16u, window, NULL), FT_TT_EINVAL); /* Walk a whole 5 MiB transfer the way ft_tt_noc_transfer_raw() does. This is * the off-hardware twin of the `cross` step: same span, same expected four * window moves, no card. */ { uint64_t address = (uint64_t)window - 64u * KIB; size_t remaining = 5u * MIB; size_t moved = 0; unsigned windows = 0; uint64_t previous_base = UINT64_MAX; bool bases_ascend = true; while (remaining > 0u && windows < 64u) { if (ft_tt_noc_window_slice_at(address, remaining, window, &slice) != FT_TT_OK) break; if (previous_base != UINT64_MAX && slice.window_base != previous_base + window) bases_ascend = false; previous_base = slice.window_base; address += slice.bytes; moved += slice.bytes; remaining -= slice.bytes; windows++; } snprintf(detail, sizeof(detail), "%u windows, %zu of %u bytes moved", windows, moved, 5u * MIB); check("a 5 MiB span 64 KiB below a boundary walks four windows and loses nothing", windows == 4u && moved == (size_t)5u * MIB && remaining == 0u && bases_ascend, detail); } } /** * The two store widths. WORD32 exists because a 4-byte device register cannot be * written 16 bytes at a time; if this table ever said otherwise, the launch path * would write the soft-reset register's three neighbours with it. */ static void unit_transfer_widths(void) { char detail[128]; printf("\n-- store widths (ft_tt_noc_transfer_width_bytes) --\n"); snprintf(detail, sizeof(detail), "BLOCK16 %u, WORD32 %u", ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_BLOCK16), ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_WORD32)); check("BLOCK16 is 16 B and WORD32 is 4 B", ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_BLOCK16) == 16u && ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_WORD32) == 4u, detail); check("an unknown width has no size", ft_tt_noc_transfer_width_bytes((ft_tt_noc_transfer_width)9) == 0u, NULL); check_rc("a 4 B register access is legal at WORD32", ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0xFFB121B0u, 4u), FT_TT_OK); check_rc("the same 4 B access is REFUSED at BLOCK16 — a 16 B store over a register", ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_BLOCK16, 0xFFB121B0u, 4u), FT_TT_EINVAL); check_rc("a 2-byte-misaligned address is refused at WORD32", ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0xFFB121B2u, 4u), FT_TT_EINVAL); check_rc("a length that is not a whole number of words is refused", ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0x1000u, 6u), FT_TT_EINVAL); check_rc("a 16 B-aligned bulk transfer is legal at BLOCK16", ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_BLOCK16, 0x1000u, 4096u), FT_TT_OK); check_rc("an unknown width is refused", ft_tt_noc_check_raw_transfer((ft_tt_noc_transfer_width)9, 0u, 16u), FT_TT_EINVAL); check_rc("a zero-length raw transfer is legal", ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0x1000u, 0u), FT_TT_OK); } /* ================================================================ * unit: the allocator * ================================================================ */ /** The topology this card reports, built by hand so the allocator can be tested * with no device present. Matches what ARC telemetry returns here. */ static ft_tt_grid_info fake_grid(uint32_t dram_bank_count) { ft_tt_grid_info grid; memset(&grid, 0, sizeof(grid)); grid.valid = true; /* Off-hardware nothing selects an architecture, so the fixture names one. */ grid.arch = ft_tt_arch_blackhole(); grid.tensix_enabled_col = 16383u; grid.tensix_column_count = 14u; grid.tensix_core_count = 140u; grid.enabled_gddr = 255u; grid.dram_bank_count = dram_bank_count; return grid; } static void unit_allocator_init(void) { ft_tt_allocator allocator; ft_tt_grid_info grid = fake_grid(8u); ft_tt_grid_info unread; char detail[128]; printf("\n-- allocator setup --\n"); memset(&unread, 0, sizeof(unread)); check_rc("a grid that was never read from telemetry is refused", ft_tt_allocator_init(&allocator, NULL, &unread, 0u), FT_TT_EUNAVAIL); { ft_tt_grid_info zero_banks = fake_grid(0u); ft_tt_grid_info too_many = fake_grid(FT_TT_MAX_DRAM_BANKS + 1u); ft_tt_grid_info no_arch = fake_grid(8u); ft_tt_grid_info more_than_the_part; ft_tt_arch six_bank_part; check_rc("zero DRAM banks is refused", ft_tt_allocator_init(&allocator, NULL, &zero_banks, 0u), FT_TT_EINVAL); check_rc("more banks than the arena array can hold is refused", ft_tt_allocator_init(&allocator, NULL, &too_many, 0u), FT_TT_EINVAL); /* The bank count is meaningless without knowing the part it came from, * and guessing one is how a wrong bank becomes a NoC write to nowhere. */ no_arch.arch = NULL; check_rc("a grid with no architecture is refused", ft_tt_allocator_init(&allocator, NULL, &no_arch, 0u), FT_TT_EINVAL); /* The bound that is NOT the compiled-in constant: telemetry claiming more * banks than the SELECTED part has means the mask was misread. A part with * fewer banks than the capacity is the case a 6-bank arch would hit, and * the only way to reach it from here is to name one. */ six_bank_part = *ft_tt_arch_blackhole(); six_bank_part.dram.bank_count = 6u; more_than_the_part = fake_grid(8u); more_than_the_part.arch = &six_bank_part; check_rc("8 live banks on a 6-bank part is refused", ft_tt_allocator_init(&allocator, NULL, &more_than_the_part, 0u), FT_TT_EINVAL); more_than_the_part.dram_bank_count = 6u; check_rc("...and 6 on the same part initialises", ft_tt_allocator_init(&allocator, NULL, &more_than_the_part, 0u), FT_TT_OK); /* The bank bound at USE time must be the live count, not the array * capacity. Asked as an ALLOCATION and against EINVAL specifically: * querying free bytes on an out-of-range arena answers 0 either way, * because the untouched arenas are zeroed — so the obvious version of * this check passes on a bound that is wrong. */ { ft_tt_buffer past_the_end; check_rc("bank 6 on a 6-bank part names no arena, whatever the capacity is", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 6u, 64u, &past_the_end), FT_TT_EINVAL); check_rc("...and bank 5 does", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 5u, 64u, &past_the_end), FT_TT_OK); } } check_rc("an L1 floor below the usable base is refused", ft_tt_allocator_init(&allocator, NULL, &grid, FT_TT_L1_USABLE_BASE - 16u), FT_TT_EINVAL); check_rc("an L1 floor at the top of L1 is refused", ft_tt_allocator_init(&allocator, NULL, &grid, FT_TT_MEM_L1_SIZE), FT_TT_EINVAL); check_rc("a default allocator initialises", ft_tt_allocator_init(&allocator, NULL, &grid, 0u), FT_TT_OK); snprintf(detail, sizeof(detail), "%zu free, FT_TT_L1_USABLE_SIZE is %u", ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u), FT_TT_L1_USABLE_SIZE); check("the L1 arena is exactly the L1 above the reserved map", ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u) == (size_t)FT_TT_L1_USABLE_SIZE, detail); snprintf(detail, sizeof(detail), "MEM_MAX_KERNEL_SIZE %u overshoots by %u bytes", FT_TT_MEM_MAX_KERNEL_SIZE, FT_TT_MEM_MAX_KERNEL_SIZE - FT_TT_L1_USABLE_SIZE); check("and it is SMALLER than MEM_MAX_KERNEL_SIZE, which is not a bound", (size_t)FT_TT_L1_USABLE_SIZE < (size_t)FT_TT_MEM_MAX_KERNEL_SIZE, detail); snprintf(detail, sizeof(detail), "%zu free per bank", ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 0u)); check("each DRAM arena is one whole bank", ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 0u) == (size_t)FT_TT_MEM_DRAM_SIZE, detail); check("a bank index past the enabled count names no arena", ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 8u) == 0u, NULL); } static void unit_allocator_alloc(void) { ft_tt_allocator allocator; ft_tt_grid_info grid = fake_grid(8u); ft_tt_buffer first; ft_tt_buffer second; ft_tt_buffer dram; char detail[160]; printf("\n-- allocation --\n"); (void)ft_tt_allocator_init(&allocator, NULL, &grid, 0u); check_rc("a zero-byte allocation is refused", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 0u, &first), FT_TT_EINVAL); check_rc("an L1 allocation with a non-zero bank is refused", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 1u, 64u, &first), FT_TT_EINVAL); check_rc("a 1-byte L1 allocation succeeds", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 1u, &first), FT_TT_OK); snprintf(detail, sizeof(detail), "address 0x%llx, %zu asked, %zu reserved", (unsigned long long)first.address, first.bytes, first.reserved_bytes); check("it starts at the usable L1 floor and reserves a whole 16 B unit", first.address == (uint64_t)FT_TT_L1_USABLE_BASE && first.reserved_bytes == 16u, detail); check_rc("a second allocation follows it", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 4096u, &second), FT_TT_OK); snprintf(detail, sizeof(detail), "0x%llx then 0x%llx", (unsigned long long)first.address, (unsigned long long)second.address); check("the second address is the first plus its reservation", second.address == first.address + (uint64_t)first.reserved_bytes, detail); check_rc("a DRAM allocation on bank 3 succeeds", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 3u, 100u, &dram), FT_TT_OK); snprintf(detail, sizeof(detail), "address 0x%llx, %zu reserved", (unsigned long long)dram.address, dram.reserved_bytes); check("DRAM rounds up to the 64 B read alignment", dram.reserved_bytes == 128u, detail); check_rc("and the result is legal for a DRAM READ, unaided", ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, dram.address, dram.reserved_bytes), FT_TT_OK); check("allocating on bank 3 did not move bank 4", ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 4u) == 0u, NULL); check_rc("a bank past the enabled count is refused", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 8u, 64u, &dram), FT_TT_EINVAL); printf("\n-- exhaustion --\n"); (void)ft_tt_allocator_reset(&allocator); check_rc("the whole usable L1 can be taken in one go", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, FT_TT_L1_USABLE_SIZE, &first), FT_TT_OK); check_rc("one more 16 B unit does not fit", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 16u, &second), FT_TT_ENOMEM); (void)ft_tt_allocator_reset(&allocator); check_rc("MEM_MAX_KERNEL_SIZE bytes of L1 CANNOT be allocated — it overshoots real L1", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, FT_TT_MEM_MAX_KERNEL_SIZE, &first), FT_TT_ENOMEM); (void)ft_tt_allocator_reset(&allocator); check_rc("a whole DRAM bank can be taken in one go", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 0u, FT_TT_MEM_DRAM_SIZE, &dram), FT_TT_OK); check_rc("and then a single 64 B unit does not", ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 0u, 64u, &second), FT_TT_ENOMEM); } static void unit_allocator_free(void) { ft_tt_allocator allocator; ft_tt_grid_info grid = fake_grid(8u); ft_tt_buffer first; ft_tt_buffer second; ft_tt_buffer other_bank; char detail[128]; printf("\n-- LIFO free --\n"); (void)ft_tt_allocator_init(&allocator, NULL, &grid, 0u); (void)ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 256u, &first); (void)ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 256u, &second); check_rc("freeing the OLDER buffer first is refused, not silently ignored", ft_tt_buffer_free(&allocator, &first), FT_TT_EINVAL); snprintf(detail, sizeof(detail), "%u live", ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_L1, 0u)); check("and the refusal left the arena untouched", ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 2u && ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 512u, detail); check_rc("freeing the newest buffer works", ft_tt_buffer_free(&allocator, &second), FT_TT_OK); check_rc("then the older one works too", ft_tt_buffer_free(&allocator, &first), FT_TT_OK); check("the arena is back to empty", ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 0u && ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 0u, NULL); check_rc("a third free of an empty arena is refused", ft_tt_buffer_free(&allocator, &first), FT_TT_EINVAL); (void)ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 1u, 4096u, &other_bank); other_bank.logical_bank = 2u; check_rc("a buffer freed against the wrong bank is refused", ft_tt_buffer_free(&allocator, &other_bank), FT_TT_EINVAL); check_rc("reset drops everything", ft_tt_allocator_reset(&allocator), FT_TT_OK); check("every arena is back at its floor", ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 1u) == 0u && ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_DRAM, 1u) == 0u, NULL); } static void run_unit(void) { unit_alignment_rules(); unit_transfer_checks(); unit_window_slices(); unit_transfer_widths(); unit_allocator_init(); unit_allocator_alloc(); unit_allocator_free(); } /* ================================================================ * Hardware steps * ================================================================ */ typedef struct device_session { ft_tt_ctx *ctx; ft_tt_grid_info grid; ft_tt_allocator allocator; ft_tt_noc_path path; } device_session; /** Open the card, re-read the grid, and stand up an allocator and a WC path. * Refuses to go further if the grid is not one the coordinate map can express * — every device step below derives its endpoint from it. */ static int session_open(device_session *session) { int rc; memset(session, 0, sizeof(*session)); session->ctx = ft_tt_init_device(0); if (!session->ctx) { fprintf(stderr, "could not open /dev/tenstorrent/0\n"); return FT_TT_ENODEV; } rc = ft_tt_arc_telemetry_read_grid(session->ctx, &session->grid); check_rc("ARC telemetry read", rc, FT_TT_OK); if (rc != FT_TT_OK) return rc; rc = ft_tt_coord_check_grid(&session->grid); check_rc("the grid is expressible by the coordinate map", rc, FT_TT_OK); if (rc != FT_TT_OK) return rc; rc = ft_tt_allocator_init(&session->allocator, session->ctx, &session->grid, 0u); check_rc("allocator initialised from the live grid", rc, FT_TT_OK); if (rc != FT_TT_OK) return rc; rc = ft_tt_noc_path_open(session->ctx, FT_TT_KMD_CACHE_WC, &session->path); check_rc("WC data path opened", rc, FT_TT_OK); return rc; } static void session_close(device_session *session) { if (session->path.window) (void)ft_tt_noc_path_close(&session->path); if (session->ctx) ft_tt_ctx_free(session->ctx); memset(session, 0, sizeof(*session)); } /** L1 round-trip on a Tensix that is NOT index 0. */ static void step_l1(device_session *session) { const size_t bytes = 4u * KIB; ft_tt_noc_coord core; ft_tt_buffer buffer; unsigned char *expected = NULL; unsigned char *got = NULL; char detail[192]; uint64_t aims_after_write; int rc; printf("\n-- L1 round-trip on Tensix %u --\n", TEST_TENSIX_INDEX); rc = ft_tt_coord_tensix_by_index(&session->grid, TEST_TENSIX_INDEX, FT_TT_COORD_TRANSLATED, &core); snprintf(detail, sizeof(detail), "translated (%u, %u)", core.x, core.y); check("Tensix coordinate derived from telemetry", rc == FT_TT_OK, detail); if (rc != FT_TT_OK) return; rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_L1, 0u, bytes, &buffer); check_rc("L1 buffer allocated", rc, FT_TT_OK); if (rc != FT_TT_OK) return; expected = malloc(bytes); got = malloc(bytes); if (!expected || !got) { check("host buffers", false, "out of memory"); free(expected); free(got); return; } fill_pattern(expected, bytes, 0xA5A5A5A5u); memset(got, 0, bytes); rc = ft_tt_write_l1(&session->path, core, buffer.address, expected, bytes); check_rc("ft_tt_write_l1", rc, FT_TT_OK); aims_after_write = ft_tt_noc_path_aim_count(&session->path); rc = ft_tt_read_l1(&session->path, core, buffer.address, got, bytes); check_rc("ft_tt_read_l1", rc, FT_TT_OK); check("L1 round-trip is byte-exact", compare_pattern(expected, got, bytes, detail, sizeof(detail)), detail); snprintf(detail, sizeof(detail), "%llu aims for the write, %llu total", (unsigned long long)aims_after_write, (unsigned long long)ft_tt_noc_path_aim_count(&session->path)); check("4 KiB inside one window costs ONE aim, and the read costs none", aims_after_write == 1u && ft_tt_noc_path_aim_count(&session->path) == 1u, detail); (void)ft_tt_buffer_free(&session->allocator, &buffer); free(expected); free(got); } /** Round-trip every enabled DRAM bank, then prove they are not aliases. */ static void step_dram(device_session *session) { const size_t bytes = 64u * KIB; unsigned char *expected = malloc(bytes); unsigned char *got = malloc(bytes); ft_tt_buffer buffers[FT_TT_NUM_DRAM_BANKS]; ft_tt_noc_coord coords[FT_TT_NUM_DRAM_BANKS]; uint32_t banks = session->grid.dram_bank_count; uint32_t bank; char detail[192]; memset(buffers, 0, sizeof(buffers)); memset(coords, 0, sizeof(coords)); printf("\n-- DRAM round-trip, all %u banks --\n", banks); if (!expected || !got) { check("host buffers", false, "out of memory"); free(expected); free(got); return; } for (bank = 0; bank < banks; bank++) { char label[96]; int rc = ft_tt_coord_dram_bank(&session->grid, bank, FT_TT_COORD_TRANSLATED, &coords[bank]); snprintf(label, sizeof(label), "bank %u coordinate derived", bank); snprintf(detail, sizeof(detail), "translated (%u, %u)", coords[bank].x, coords[bank].y); check(label, rc == FT_TT_OK, detail); if (rc != FT_TT_OK) { free(expected); free(got); return; } rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_DRAM, bank, bytes, &buffers[bank]); if (rc != FT_TT_OK) { snprintf(label, sizeof(label), "bank %u buffer allocated", bank); check_rc(label, rc, FT_TT_OK); free(expected); free(got); return; } /* Seed by bank so bank n's data can never be mistaken for bank m's. */ fill_pattern(expected, bytes, 0x5A5A0000u + bank); memset(got, 0, bytes); rc = ft_tt_write_dram(&session->path, coords[bank], buffers[bank].address, expected, bytes); snprintf(label, sizeof(label), "bank %u ft_tt_write_dram", bank); check_rc(label, rc, FT_TT_OK); rc = ft_tt_read_dram(&session->path, coords[bank], buffers[bank].address, got, bytes); snprintf(label, sizeof(label), "bank %u ft_tt_read_dram", bank); check_rc(label, rc, FT_TT_OK); snprintf(label, sizeof(label), "bank %u round-trip is byte-exact", bank); check(label, compare_pattern(expected, got, bytes, detail, sizeof(detail)), detail); } /* Every bank was written a different pattern and every bank was read back * correctly above; re-reading bank 0 now proves the later writes did not * land on it, i.e. these are eight endpoints and not one aliased eight * times. */ if (banks > 1u) { int rc; fill_pattern(expected, bytes, 0x5A5A0000u); memset(got, 0, bytes); rc = ft_tt_read_dram(&session->path, coords[0], buffers[0].address, got, bytes); check_rc("re-read bank 0 after writing all the others", rc, FT_TT_OK); check("bank 0 still holds ITS pattern — the banks are distinct endpoints", compare_pattern(expected, got, bytes, detail, sizeof(detail)), detail); } for (bank = banks; bank > 0u; bank--) (void)ft_tt_buffer_free(&session->allocator, &buffers[bank - 1u]); free(expected); free(got); } /** * A transfer that starts 64 KiB below a 2 MiB boundary and runs for 5 MiB, so * it covers four windows. This is the re-windowing path; nothing else in the * tree exercises it. */ static void step_cross(device_session *session) { const size_t bytes = 5u * MIB; const uint64_t offset = (uint64_t)2u * MIB - 64u * KIB; unsigned char *expected = malloc(bytes); unsigned char *got = malloc(bytes); ft_tt_buffer region; ft_tt_noc_coord bank_port; uint64_t start; char detail[192]; uint64_t aims_before; uint64_t aims_after_write; int rc; printf("\n-- window-crossing transfer (%u MiB starting 64 KiB below a 2 MiB boundary) --\n", (unsigned)(bytes / MIB)); if (!expected || !got) { check("host buffers", false, "out of memory"); free(expected); free(got); return; } rc = ft_tt_coord_dram_bank(&session->grid, TEST_DRAM_BANK, FT_TT_COORD_TRANSLATED, &bank_port); check_rc("test DRAM bank coordinate derived", rc, FT_TT_OK); if (rc != FT_TT_OK) { free(expected); free(got); return; } /* Reserve the whole span through the allocator, so the two Phase 1 pieces * are exercised together rather than each against a hand-picked address. */ rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_DRAM, TEST_DRAM_BANK, (size_t)offset + bytes, ®ion); check_rc("DRAM region allocated for the crossing", rc, FT_TT_OK); if (rc != FT_TT_OK) { free(expected); free(got); return; } start = region.address + offset; fill_pattern(expected, bytes, 0xC0FFEE00u); memset(got, 0, bytes); /* Park the window on the LAST span this transfer will touch, so the walk * cannot start out already in position and quietly need one aim fewer. */ (void)ft_tt_write_dram(&session->path, bank_port, start + 4u * MIB, expected, 64u); aims_before = ft_tt_noc_path_aim_count(&session->path); rc = ft_tt_write_dram(&session->path, bank_port, start, expected, bytes); check_rc("ft_tt_write_dram across four windows", rc, FT_TT_OK); aims_after_write = ft_tt_noc_path_aim_count(&session->path); snprintf(detail, sizeof(detail), "%llu window moves", (unsigned long long)(aims_after_write - aims_before)); check("the write really did move the window four times", aims_after_write - aims_before == 4u, detail); rc = ft_tt_read_dram(&session->path, bank_port, start, got, bytes); check_rc("ft_tt_read_dram across four windows", rc, FT_TT_OK); snprintf(detail, sizeof(detail), "%llu window moves", (unsigned long long)(ft_tt_noc_path_aim_count(&session->path) - aims_after_write)); check("and so did the read", ft_tt_noc_path_aim_count(&session->path) - aims_after_write == 4u, detail); check("the whole span survived re-windowing", compare_pattern(expected, got, bytes, detail, sizeof(detail)), detail); (void)ft_tt_buffer_free(&session->allocator, ®ion); free(expected); free(got); } /** Alignment refusals against a live path: nothing may reach the device. */ static void step_align(device_session *session) { unsigned char scratch[128]; ft_tt_noc_coord bank_port; ft_tt_noc_coord core; ft_tt_noc_coord bad_coord; uint64_t aims_before; char detail[128]; int rc; printf("\n-- alignment refusals on a live path --\n"); memset(scratch, 0x11, sizeof(scratch)); rc = ft_tt_coord_dram_bank(&session->grid, TEST_DRAM_BANK, FT_TT_COORD_TRANSLATED, &bank_port); check_rc("test DRAM bank coordinate derived", rc, FT_TT_OK); if (rc != FT_TT_OK) return; rc = ft_tt_coord_tensix_by_index(&session->grid, TEST_TENSIX_INDEX, FT_TT_COORD_TRANSLATED, &core); check_rc("Tensix coordinate derived", rc, FT_TT_OK); if (rc != FT_TT_OK) return; aims_before = ft_tt_noc_path_aim_count(&session->path); check_rc("a misaligned DRAM write is refused", ft_tt_write_dram(&session->path, bank_port, 8u, scratch, 64u), FT_TT_EINVAL); check_rc("a 16 B-aligned DRAM read is refused (64 B rule)", ft_tt_read_dram(&session->path, bank_port, 16u, scratch, 64u), FT_TT_EINVAL); check_rc("an L1 write running past the top of L1 is refused", ft_tt_write_l1(&session->path, core, FT_TT_MEM_L1_SIZE - 16u, scratch, 32u), FT_TT_EINVAL); check_rc("a NULL host buffer is refused", ft_tt_write_dram(&session->path, bank_port, 0u, NULL, 64u), FT_TT_EINVAL); /* A coordinate too large for the 6-bit x_end/y_end fields would be * truncated into a DIFFERENT, live endpoint. That is the failure mode that * costs a power cycle, so it must be refused rather than masked. */ bad_coord.x = 64u; bad_coord.y = 0u; check_rc("a coordinate that does not fit the 6-bit TLB fields is refused", ft_tt_write_dram(&session->path, bad_coord, 0u, scratch, 64u), FT_TT_EINVAL); snprintf(detail, sizeof(detail), "%llu aims before, %llu after", (unsigned long long)aims_before, (unsigned long long)ft_tt_noc_path_aim_count(&session->path)); check("not one of those refusals aimed the window", ft_tt_noc_path_aim_count(&session->path) == aims_before, detail); } /** MB/s through the helpers, against the raw-window 287 MB/s WC baseline. */ static void step_bandwidth(device_session *session) { const size_t bytes = 8u * MIB; unsigned char *host = malloc(bytes); ft_tt_buffer region; ft_tt_noc_coord bank_port; double seconds; double megabytes = (double)bytes / (1024.0 * 1024.0); double t0; int rc; printf("\n-- bandwidth through the helpers (WC, %u MiB, four windows) --\n", (unsigned)(bytes / MIB)); if (!host) { check("host buffer", false, "out of memory"); return; } memset(host, 0x5A, bytes); rc = ft_tt_coord_dram_bank(&session->grid, TEST_DRAM_BANK, FT_TT_COORD_TRANSLATED, &bank_port); check_rc("test DRAM bank coordinate derived", rc, FT_TT_OK); if (rc == FT_TT_OK) rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_DRAM, TEST_DRAM_BANK, bytes, ®ion); check_rc("DRAM region allocated", rc, FT_TT_OK); if (rc != FT_TT_OK) { free(host); return; } t0 = now_seconds(); rc = ft_tt_write_dram(&session->path, bank_port, region.address, host, bytes); seconds = now_seconds() - t0; check_rc("bulk write", rc, FT_TT_OK); printf(" host -> card %8.1f MB/s (raw window layer measured 287 MB/s WC)\n", megabytes / seconds); t0 = now_seconds(); rc = ft_tt_read_dram(&session->path, bank_port, region.address, host, bytes); seconds = now_seconds() - t0; check_rc("bulk read", rc, FT_TT_OK); printf(" card -> host %8.1f MB/s (raw window layer measured 2.3 MB/s)\n", megabytes / seconds); (void)ft_tt_buffer_free(&session->allocator, ®ion); free(host); } /* ================================================================ * main * ================================================================ */ static void usage(void) { fprintf(stderr, "usage: test_noc [unit|l1|dram|cross|align|bw|hw]\n" " unit no device is opened (default)\n" " hw l1 + dram + cross + align, in that order\n"); } int main(int argc, char **argv) { const char *step = (argc > 1) ? argv[1] : "unit"; device_session session; bool want_l1; bool want_dram; bool want_cross; bool want_align; bool want_bw; printf("=== test_noc: %s ===\n", step); if (strcmp(step, "unit") == 0) { run_unit(); printf("\n%d checks, %d failed\n", checks, failures); return failures == 0 ? 0 : 1; } want_l1 = strcmp(step, "l1") == 0 || strcmp(step, "hw") == 0; want_dram = strcmp(step, "dram") == 0 || strcmp(step, "hw") == 0; want_cross = strcmp(step, "cross") == 0 || strcmp(step, "hw") == 0; want_align = strcmp(step, "align") == 0 || strcmp(step, "hw") == 0; want_bw = strcmp(step, "bw") == 0; if (!want_l1 && !want_dram && !want_cross && !want_align && !want_bw) { usage(); return 2; } if (session_open(&session) != FT_TT_OK) { session_close(&session); printf("\n%d checks, %d failed\n", checks, failures); return 1; } if (failures == 0 && want_l1) step_l1(&session); if (failures == 0 && want_dram) step_dram(&session); if (failures == 0 && want_cross) step_cross(&session); if (failures == 0 && want_align) step_align(&session); if (failures == 0 && want_bw) step_bandwidth(&session); session_close(&session); printf("\n%d checks, %d failed\n", checks, failures); return failures == 0 ? 0 : 1; }