318 lines
13 KiB
C
318 lines
13 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file ft_tt_noc_copy.h
|
|
* @brief The PIO copy shape and the window-walk arithmetic, with no dependency
|
|
* on how the window was aimed — so the tt-kmd path (`ft_tt_noc.c`) and the
|
|
* userspace-driver path (`ft_tt_udrv_noc.c`) move bytes through exactly the
|
|
* same code instead of two implementations that drift.
|
|
*
|
|
* Header-only and `static inline` on purpose. These are four short functions
|
|
* in the hot path, and a separate translation unit would have to be added to
|
|
* every Makefile that already links `ft_tt_noc.c` — sixteen of them — for no
|
|
* gain. Nothing here includes a project header, opens anything, or logs, so
|
|
* it can be compiled into a suite whose whole claim is that it cannot reach a
|
|
* device.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHY THE COPY IS NOT A memcpy
|
|
* ---------------------------------------------------------------------------
|
|
* The aperture is device MMIO. tt-umd moves 16 x 16 B per 256 B block because
|
|
* that is what keeps the PCIe transaction count where it wants it — but their
|
|
* device_memcpy.cpp is AVX2 and the card host here has NO AVX at all, so their
|
|
* binary SIGILLs on it exactly like tt_umd did. We keep the 16 B unit and the
|
|
* 256 B block and issue them as SSE2 `movdqu`, which is baseline x86-64. On any
|
|
* non-x86 host (the Mac, for compile checks) it degrades to a 64-bit store loop
|
|
* of the same shape.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* THE STORE FENCE IS A SEPARATE CALL, AND THAT IS DELIBERATE
|
|
* ---------------------------------------------------------------------------
|
|
* `ft_tt_noc_copy_slice()` ends in a COMPILER barrier, which stops the
|
|
* optimiser hoisting, merging or eliding stores to a region it can see no other
|
|
* reader of. It is NOT a CPU fence, and through a write-combined mapping those
|
|
* are different things: WC stores sit in the core's write-combining buffers and
|
|
* are flushed on their own schedule. `ft_tt_noc_copy_store_fence()` is the
|
|
* `sfence` that retires them, and it is a separate call so that adding it to a
|
|
* new path does not silently change the transaction shape of an old one.
|
|
*
|
|
* @status The copy shape is unchanged from ft_tt_noc.c, where it was verified
|
|
* on the card (docs/tt_bringup_log.md §9) and the disassembly checked on the
|
|
* bank for VEX instructions. The extraction is mechanical; the store fence is
|
|
* the only new code and it is called by the userspace-driver write path only.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#ifndef FT_TT_NOC_COPY_H
|
|
#define FT_TT_NOC_COPY_H
|
|
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
#if defined(__SSE2__)
|
|
#include <emmintrin.h>
|
|
#endif
|
|
|
|
/* ================================================================
|
|
* Copy shape
|
|
* ================================================================ */
|
|
|
|
/** One store unit: 16 B, an SSE2 `movdqu`. tt-umd uses 32 B (AVX2), which the
|
|
* card host cannot execute. */
|
|
#define FT_TT_NOC_COPY_UNIT_BYTES 16u
|
|
|
|
/** 16 units per block, i.e. tt-umd's 256 B blocking, preserved so the PCIe
|
|
* transaction shape matches theirs even though the instruction does not. */
|
|
#define FT_TT_NOC_COPY_BLOCK_BYTES 256u
|
|
|
|
/**
|
|
* The store width a transfer is issued in. Not a tuning knob — the two are for
|
|
* different KINDS of device access and are not interchangeable:
|
|
*
|
|
* - BLOCK16 is bulk data. 16 B units in 256 B blocks, the shape above.
|
|
* - WORD32 is one 32-bit store or load per word, which is the only legal way
|
|
* to touch a 4-byte device REGISTER (the per-Tensix soft-reset register at
|
|
* 0xFFB121B0) or a sub-16 B mailbox field. A 16 B store over a 4 B register
|
|
* writes three neighbours with it.
|
|
*/
|
|
typedef enum ft_tt_noc_transfer_width {
|
|
FT_TT_NOC_WIDTH_BLOCK16 = 0, /**< 16 B units, 256 B blocks — bulk data */
|
|
FT_TT_NOC_WIDTH_WORD32 = 1 /**< one 32-bit access per word — registers, mailbox */
|
|
} ft_tt_noc_transfer_width;
|
|
|
|
/**
|
|
* Bytes per access for a width: 16 for BLOCK16, 4 for WORD32, 0 for an unknown
|
|
* width. Also the alignment both the device address and the length must satisfy.
|
|
*
|
|
* Thread-safe: yes Reentrant: yes Signal-safe: yes
|
|
*/
|
|
static inline uint32_t ft_tt_noc_transfer_width_bytes(ft_tt_noc_transfer_width width)
|
|
{
|
|
switch (width) {
|
|
case FT_TT_NOC_WIDTH_BLOCK16:
|
|
return FT_TT_NOC_COPY_UNIT_BYTES;
|
|
case FT_TT_NOC_WIDTH_WORD32:
|
|
return 4u;
|
|
default:
|
|
return 0u;
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
* The window walk, as arithmetic
|
|
* ================================================================ */
|
|
|
|
/**
|
|
* How much of a transfer fits in one window placement, and where.
|
|
*
|
|
* The walk is the part of the transport that can silently corrupt a
|
|
* neighbouring address rather than fail, so it is separated out as pure
|
|
* arithmetic a unit test on the Mac can prove wrong — a wrong endpoint hangs
|
|
* the chip, and a wrong OFFSET quietly writes somewhere else on the right one.
|
|
*/
|
|
typedef struct ft_tt_noc_window_slice {
|
|
uint64_t window_base; /**< what the window must be aimed at for this slice */
|
|
size_t offset_in_window; /**< where the slice starts inside the aperture */
|
|
size_t bytes; /**< how much of the transfer this slice covers */
|
|
} ft_tt_noc_window_slice;
|
|
|
|
/**
|
|
* First slice of a transfer of `bytes` at `address` through a `window_bytes`
|
|
* aperture: the window base it needs, the offset inside that window, and how
|
|
* many bytes fit before the next window boundary. Walk a whole transfer by
|
|
* advancing address/host/remaining by `bytes` and calling again.
|
|
*
|
|
* The pure form: no logging, no project status codes, so it can live in a
|
|
* dependency-free header. `ft_tt_noc_window_slice_at()` is this plus a log line
|
|
* and an FT_TT_* return.
|
|
*
|
|
* @param window_bytes aperture size; must be a non-zero power of two
|
|
* @param out_slice written only on success
|
|
* @return true on success; false for a NULL out or a window size
|
|
* that is not a power of two
|
|
*
|
|
* Thread-safe: yes Reentrant: yes Signal-safe: yes
|
|
*/
|
|
static inline bool ft_tt_noc_window_slice_compute(uint64_t address, size_t bytes,
|
|
size_t window_bytes,
|
|
ft_tt_noc_window_slice *out_slice)
|
|
{
|
|
uint64_t window_base;
|
|
size_t offset_in_window;
|
|
size_t chunk;
|
|
|
|
if (!out_slice)
|
|
return false;
|
|
if (window_bytes == 0u || (window_bytes & (window_bytes - 1u)) != 0u)
|
|
return false;
|
|
|
|
window_base = address & ~((uint64_t)window_bytes - 1u);
|
|
offset_in_window = (size_t)(address - window_base);
|
|
chunk = window_bytes - offset_in_window;
|
|
if (chunk > bytes)
|
|
chunk = bytes;
|
|
|
|
out_slice->window_base = window_base;
|
|
out_slice->offset_in_window = offset_in_window;
|
|
out_slice->bytes = chunk;
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The PIO copy
|
|
* ================================================================ */
|
|
|
|
/*
|
|
* Both directions move FT_TT_NOC_COPY_UNIT_BYTES at a time, grouped into
|
|
* FT_TT_NOC_COPY_BLOCK_BYTES blocks, with a compiler barrier between blocks.
|
|
* The barrier is what stops the optimiser hoisting, merging or eliding stores
|
|
* to a region it cannot see any other reader of — the aperture is MMIO, and
|
|
* every store is a PCIe transaction whose ORDER and COUNT are the thing being
|
|
* engineered here.
|
|
*/
|
|
#define FT_TT_NOC_COPY_BARRIER() __asm__ __volatile__("" : : : "memory")
|
|
|
|
/**
|
|
* One 16 B unit. Identical in both directions: on x86 it is a `movdqu` load and
|
|
* a `movdqu` store, and which side is the aperture only decides which of the
|
|
* two is the PCIe transaction.
|
|
*/
|
|
static inline void ft_tt_noc_copy_unit(unsigned char *dst, const unsigned char *src)
|
|
{
|
|
#if defined(__SSE2__)
|
|
_mm_storeu_si128((__m128i *)(void *)dst, _mm_loadu_si128((const __m128i *)(const void *)src));
|
|
#else
|
|
/* Non-x86 (the Mac, for compile checks): same 16 B unit, two 64-bit moves.
|
|
* The card host is always x86 with SSE2, so this path never runs on it. */
|
|
uint64_t low;
|
|
uint64_t high;
|
|
|
|
memcpy(&low, src, sizeof(low));
|
|
memcpy(&high, src + sizeof(low), sizeof(high));
|
|
memcpy(dst, &low, sizeof(low));
|
|
memcpy(dst + sizeof(low), &high, sizeof(high));
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Copy `bytes` between the aperture and host memory in tt-umd's 16 x 16 B per
|
|
* 256 B shape. `bytes` must be a multiple of FT_TT_NOC_COPY_UNIT_BYTES.
|
|
*/
|
|
static inline void ft_tt_noc_copy_blocked(unsigned char *dst, const unsigned char *src,
|
|
size_t bytes)
|
|
{
|
|
const size_t units_per_block = FT_TT_NOC_COPY_BLOCK_BYTES / FT_TT_NOC_COPY_UNIT_BYTES;
|
|
size_t done = 0;
|
|
|
|
while (bytes - done >= FT_TT_NOC_COPY_BLOCK_BYTES) {
|
|
size_t unit;
|
|
|
|
for (unit = 0; unit < units_per_block; unit++) {
|
|
size_t at = done + unit * FT_TT_NOC_COPY_UNIT_BYTES;
|
|
|
|
ft_tt_noc_copy_unit(dst + at, src + at);
|
|
}
|
|
FT_TT_NOC_COPY_BARRIER();
|
|
done += FT_TT_NOC_COPY_BLOCK_BYTES;
|
|
}
|
|
|
|
while (done < bytes) {
|
|
ft_tt_noc_copy_unit(dst + done, src + done);
|
|
done += FT_TT_NOC_COPY_UNIT_BYTES;
|
|
}
|
|
FT_TT_NOC_COPY_BARRIER();
|
|
}
|
|
|
|
/**
|
|
* The WORD32 copy: exactly one 32-bit device access per word, which is what a
|
|
* 4-byte register and a 4-byte mailbox field require. `volatile` here is not
|
|
* belt and braces — it is what stops the compiler merging two adjacent word
|
|
* stores into one wider one, which over a register file is a different
|
|
* transaction entirely.
|
|
*
|
|
* The host side goes through memcpy per word because a caller's buffer (a
|
|
* packed go message, say) need not be 4-byte aligned, and a misaligned
|
|
* uint32_t load is undefined.
|
|
*/
|
|
static inline void ft_tt_noc_copy_words(volatile uint32_t *aperture_words,
|
|
unsigned char *host_bytes, size_t word_count,
|
|
bool to_device)
|
|
{
|
|
size_t index;
|
|
|
|
for (index = 0; index < word_count; index++) {
|
|
uint32_t word;
|
|
|
|
if (to_device) {
|
|
memcpy(&word, host_bytes + index * 4u, sizeof(word));
|
|
aperture_words[index] = word;
|
|
} else {
|
|
word = aperture_words[index];
|
|
memcpy(host_bytes + index * 4u, &word, sizeof(word));
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Move one slice between the aperture and host memory in the shape `width` names. */
|
|
static inline void ft_tt_noc_copy_slice(volatile unsigned char *aperture,
|
|
unsigned char *host_bytes, size_t bytes, bool is_read,
|
|
ft_tt_noc_transfer_width width)
|
|
{
|
|
if (width == FT_TT_NOC_WIDTH_WORD32) {
|
|
ft_tt_noc_copy_words((volatile uint32_t *)aperture, host_bytes, bytes / 4u, !is_read);
|
|
return;
|
|
}
|
|
|
|
/* The aperture is cast to a plain pointer for the blocked copy; ordering and
|
|
* non-elision come from the barrier in ft_tt_noc_copy_blocked(), not from
|
|
* `volatile`, because the 16 B unit is one instruction and volatile would
|
|
* force it back to scalar. */
|
|
{
|
|
unsigned char *window_bytes_ptr = (unsigned char *)(void *)aperture;
|
|
|
|
if (is_read)
|
|
ft_tt_noc_copy_blocked(host_bytes, window_bytes_ptr, bytes);
|
|
else
|
|
ft_tt_noc_copy_blocked(window_bytes_ptr, host_bytes, bytes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retire write-combining buffers, so a store issued through a WC mapping has
|
|
* left the CPU.
|
|
*
|
|
* A compiler barrier does not do this and neither does `volatile`: through a
|
|
* write-combined mapping the core is allowed to hold and merge stores in its WC
|
|
* buffers indefinitely from the program's point of view. `sfence` is the
|
|
* architectural retirement point on x86; elsewhere a full barrier stands in.
|
|
*
|
|
* Call it after the last store of a write and before anything that reads the
|
|
* result back — including a read through a different mapping, which will
|
|
* otherwise race the buffer rather than the card.
|
|
*
|
|
* MEASURED, AND SAID PLAINLY RATHER THAN OVERSOLD: on the card host a WC
|
|
* store to the card lands WITHOUT this call. The mutation that removed every
|
|
* fence from `test_udrv_card_noc.c` and the transport ran the whole on-card
|
|
* suite green (2026-08-17). So the fence is not observably load-bearing on
|
|
* THIS CPU with THESE access patterns. It is kept because it costs nothing and
|
|
* because "the buffer happened to drain in time" is a timing property of one
|
|
* host, not a guarantee — and the thing being built is meant to run on a
|
|
* machine nobody here has.
|
|
*
|
|
* Thread-safe: yes Reentrant: yes Signal-safe: yes
|
|
*/
|
|
static inline void ft_tt_noc_copy_store_fence(void)
|
|
{
|
|
#if defined(__SSE2__)
|
|
_mm_sfence();
|
|
#else
|
|
__sync_synchronize();
|
|
#endif
|
|
FT_TT_NOC_COPY_BARRIER();
|
|
}
|
|
|
|
#endif /* FT_TT_NOC_COPY_H */
|