622 lines
24 KiB
C
622 lines
24 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_dma_vs_pio.c
|
|
* @brief The Phase 0 decision-2 measurement, carried over to Phase 3: does the
|
|
* BAR2 H2D DMA engine beat 287 MB/s WC PIO on a weight upload?
|
|
*
|
|
* Phase 0 (`docs/tt_phase0_decisions.md` §2) parked this deliberately: the DMA
|
|
* path is real on this host because `ALLOCATE_DMA_BUF` calls
|
|
* `dma_alloc_coherent` — kernel memory, physically contiguous by construction,
|
|
* so **no hugepages and no IOMMU** are involved, and the engine lives behind
|
|
* BAR2, whose absence-of-BAR4 problem does not apply. The decision was "adopt
|
|
* only if it wins on a measurement". This is that measurement.
|
|
*
|
|
* WHAT IS BEING COMPARED
|
|
* PIO: 16 x 16 B stores per 256 B block through a WC-mapped TLB window,
|
|
* which is the shape tt-umd uses and the shape that produced 287 MB/s.
|
|
* DMA: memcpy host buffer -> the coherent bounce buffer, then program the
|
|
* Synopsys read channel behind BAR2 and poll XFERSIZE to zero.
|
|
*
|
|
* The DMA number INCLUDES the bounce memcpy, because a weight upload has to
|
|
* pay it: the artifact is mmap'd, the bounce buffer is 512 KiB, the data has
|
|
* to be copied in. A number that excludes it is not a number about uploading
|
|
* weights. Both are reported so the split is visible.
|
|
*
|
|
* **Host CPU is measured too, and it is the axis that decides this.** Both
|
|
* paths are bounded by the same x1 Gen2 link, so MB/s alone cannot separate
|
|
* them; what differs is what the host's two cores are doing meanwhile. PIO is
|
|
* a store loop and burns a core by construction. DMA can hand the copy to the
|
|
* engine — but only if it stops busy-polling XFERSIZE, so the sleep-polled
|
|
* variant is measured as well as the spin-polled one.
|
|
*
|
|
* THE ADDRESS MODEL, AND WHY IT IS NOT A PROBE
|
|
* The DMA destination is a **device AXI address**, and on Blackhole that is
|
|
* the BAR0-relative offset of a 2 MiB TLB window:
|
|
* `tlb_offset = DYNAMIC_TLB_2M_BASE(=0) + index * DYNAMIC_TLB_2M_SIZE`
|
|
* (the same TLB-window addressing as host stores; see
|
|
* BlackholeA0/PCIExpressTile/HostToDeviceTLBs.md). So DMA lands wherever the window is aimed,
|
|
* exactly like a host store into the same window — the window is the
|
|
* translator either way. Two facts make this safe to act on rather than a
|
|
* guess: it is transliterated from the vendor's working implementation, not
|
|
* discovered by trying addresses; and `get_dma_tlb_size(BLACKHOLE)` is
|
|
* **2 MiB**, i.e. the DMA path upstream uses on
|
|
* Blackhole is the 2 MiB window kind this board has, not a 4 GiB BAR4 window
|
|
* it does not have.
|
|
*
|
|
* Still, the first transfer is 4 bytes and is read back before anything
|
|
* bulk runs. If the address model were wrong, the smallest possible write is
|
|
* the one that finds out.
|
|
*
|
|
* SAFETY
|
|
* - DRAM bank 1, coordinates from ft_tt_coord against live ARC telemetry.
|
|
* Never probed. No Tensix core is touched and nothing is launched.
|
|
* - Every device wait is bounded (FT_TT_DMA_TIMEOUT_MS). A DMA that never
|
|
* retires returns an error instead of spinning.
|
|
* - MSI stop/abort addresses are programmed to the completion page exactly as
|
|
* tt-umd does. They must NOT be left at zero: the engine would then write
|
|
* its completion MSI to host physical address 0.
|
|
*
|
|
* WHY THE IOCTL STRUCTS ARE LOCAL TO THIS FILE
|
|
* `ft_tt_kmd_abi.h` carries payload structs only for the ioctls Foundry
|
|
* actually issues (its deviation V4), and ALLOCATE_DMA_BUF was deliberately
|
|
* dropped. This is a measurement, not an adoption: if DMA wins, the structs
|
|
* move into the vendored header as part of building the real upload path. If
|
|
* it loses, nothing was added to the backend. Layout follows the
|
|
* kernel driver's documented ALLOCATE_DMA_BUF ioctl.
|
|
*
|
|
* Build (on the card host — it needs the card):
|
|
* cc -std=c99 -O2 -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion \
|
|
* -Wstrict-prototypes -Wformat=2 -fstack-protector-strong \
|
|
* -DFT_TT_PHASE1B_NOC_ENABLED -I.. \
|
|
* ../ft_tt_kmd.c ../ft_tt_device.c ../ft_tt_tlb.c \
|
|
* ../ft_tt_arc_telemetry.c ../ft_tt_coord.c test_dma_vs_pio.c \
|
|
* -o build/test_dma_vs_pio
|
|
*
|
|
* @status Written for the Phase 3 follow-up. Results in
|
|
* docs/tt_bringup_log.md §13.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 199309L
|
|
|
|
#include "ft_tt_arc_telemetry.h"
|
|
#include "ft_tt_coord.h"
|
|
#include "ft_tt_device.h"
|
|
#include "ft_tt_kmd_abi.h"
|
|
#include "ft_tt_tlb.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* ALLOCATE_DMA_BUF payload — kernel driver's documented ioctl layout */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
struct ft_tt_allocate_dma_buf_in {
|
|
uint32_t requested_size;
|
|
uint8_t buf_index;
|
|
uint8_t flags;
|
|
uint8_t reserved0[2];
|
|
uint64_t reserved1[2];
|
|
};
|
|
|
|
struct ft_tt_allocate_dma_buf_out {
|
|
uint64_t physical_address;
|
|
uint64_t mapping_offset;
|
|
uint32_t size;
|
|
uint32_t reserved0;
|
|
uint64_t noc_address;
|
|
uint64_t reserved1;
|
|
};
|
|
|
|
struct ft_tt_allocate_dma_buf {
|
|
struct ft_tt_allocate_dma_buf_in in;
|
|
struct ft_tt_allocate_dma_buf_out out;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Synopsys read channel 0, BAR2 — vendor DMA engine layout */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
#define FT_TT_DMA_REG_EN 0x100u
|
|
#define FT_TT_DMA_REG_DOORBELL 0x104u
|
|
#define FT_TT_DMA_REG_XFERSIZE 0x11Cu
|
|
#define FT_TT_DMA_REG_SAR_LOW 0x120u
|
|
#define FT_TT_DMA_REG_SAR_HIGH 0x124u
|
|
#define FT_TT_DMA_REG_DAR_LOW 0x128u
|
|
#define FT_TT_DMA_REG_DAR_HIGH 0x12Cu
|
|
#define FT_TT_DMA_REG_INT_SETUP 0x188u
|
|
#define FT_TT_DMA_REG_MSI_STOP_LOW 0x190u
|
|
#define FT_TT_DMA_REG_MSI_STOP_HIGH 0x194u
|
|
#define FT_TT_DMA_REG_MSI_ABORT_LOW 0x1A0u
|
|
#define FT_TT_DMA_REG_MSI_ABORT_HIGH 0x1A4u
|
|
#define FT_TT_DMA_REG_MSI_MSGD 0x1A8u
|
|
|
|
/** Local interrupt (bit 3) + remote stop interrupt (bit 5). */
|
|
#define FT_TT_DMA_INT_SETUP_VALUE 0x28u
|
|
|
|
#define FT_TT_DMA_TIMEOUT_MS 2000.0
|
|
|
|
/** Sleep between XFERSIZE polls in the CPU-offload variant. A 512 KiB chunk at
|
|
* link rate takes ~1.9 ms, so 100 µs costs at most ~5% latency and is the
|
|
* difference between one core pinned and one core idle. */
|
|
#define DMA_POLL_SLEEP_US 100L
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Measurement parameters */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
#define WINDOW_BYTES (2u * 1024u * 1024u)
|
|
#define DRAM_AIM_BASE 0x200000u
|
|
#define DMA_BUF_BYTES (512u * 1024u) /* tt-umd's empirical sweet spot */
|
|
#define COMPLETION_BYTES 4096u
|
|
#define TRANSFER_BYTES (2u * 1024u * 1024u) /* one full window per pass */
|
|
#define TRANSFER_REPEATS 8u
|
|
#define DRAM_BANK_INDEX 1u /* bank 0 is what every other test uses */
|
|
|
|
/** One claimed coherent bounce buffer plus its completion page. */
|
|
typedef struct ft_tt_dma_bounce_buffer {
|
|
unsigned char *host; /**< mapped bounce buffer */
|
|
uint64_t physical; /**< its physical address, for SAR */
|
|
uint64_t completion_phys; /**< completion page physical address, for MSI */
|
|
size_t bytes; /**< usable bounce bytes (excludes completion) */
|
|
size_t mapping_bytes; /**< what to munmap */
|
|
} ft_tt_dma_bounce_buffer;
|
|
|
|
static int failures;
|
|
static int checks;
|
|
|
|
static void check(int condition, const char *what)
|
|
{
|
|
checks++;
|
|
if (!condition) {
|
|
failures++;
|
|
printf(" FAIL %s\n", what);
|
|
} else {
|
|
printf(" ok %s\n", what);
|
|
}
|
|
}
|
|
|
|
static double now_seconds(void)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
|
}
|
|
|
|
/** Host CPU consumed by this process so far, user + system. The whole point of
|
|
* a DMA engine is that this stops tracking wall time. */
|
|
static double cpu_seconds(void)
|
|
{
|
|
struct rusage usage;
|
|
getrusage(RUSAGE_SELF, &usage);
|
|
return (double)usage.ru_utime.tv_sec + (double)usage.ru_utime.tv_usec / 1e6 +
|
|
(double)usage.ru_stime.tv_sec + (double)usage.ru_stime.tv_usec / 1e6;
|
|
}
|
|
|
|
/** The PIO shape the 287 MB/s baseline was measured with: 16 B at a time, no
|
|
* SIMD, because this host has no AVX (spec §8.4). */
|
|
static void pio_block_write(volatile unsigned char *dst, const unsigned char *src, size_t bytes)
|
|
{
|
|
volatile uint64_t *d = (volatile uint64_t *)(void *)dst;
|
|
const uint64_t *s = (const uint64_t *)(const void *)src;
|
|
size_t words = bytes / 8u;
|
|
|
|
for (size_t i = 0; i < words; i++)
|
|
d[i] = s[i];
|
|
}
|
|
|
|
static void pio_block_read(unsigned char *dst, volatile const unsigned char *src, size_t bytes)
|
|
{
|
|
uint64_t *d = (uint64_t *)(void *)dst;
|
|
volatile const uint64_t *s = (volatile const uint64_t *)(const void *)src;
|
|
size_t words = bytes / 8u;
|
|
|
|
for (size_t i = 0; i < words; i++)
|
|
d[i] = s[i];
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* The DMA path */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Claim a coherent bounce buffer from the kernel and map it.
|
|
*
|
|
* The allocation is `bytes + one page`: the trailing page is the completion
|
|
* page whose physical address the engine's MSI stop/abort registers point at.
|
|
* tt-umd does the same.
|
|
*
|
|
* @param fd open /dev/tenstorrent/N
|
|
* @param bytes usable bounce bytes
|
|
* @param out receives the buffer; release with ft_tt_dma_bounce_free()
|
|
* @return 0 on success, -1 on failure (reason printed)
|
|
*/
|
|
static int ft_tt_dma_bounce_alloc(int fd, size_t bytes, ft_tt_dma_bounce_buffer *out)
|
|
{
|
|
struct ft_tt_allocate_dma_buf request;
|
|
size_t mapping_bytes = bytes + COMPLETION_BYTES;
|
|
void *mapping = NULL;
|
|
|
|
memset(&request, 0, sizeof(request));
|
|
request.in.requested_size = (uint32_t)mapping_bytes;
|
|
request.in.buf_index = 0;
|
|
|
|
if (ioctl(fd, (unsigned long)FT_TT_KMD_IOCTL_ALLOCATE_DMA_BUF, &request) != 0) {
|
|
perror("ALLOCATE_DMA_BUF");
|
|
return -1;
|
|
}
|
|
|
|
mapping = mmap(NULL, mapping_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
|
|
(off_t)request.out.mapping_offset);
|
|
if (mapping == MAP_FAILED) {
|
|
perror("mmap dma buffer");
|
|
return -1;
|
|
}
|
|
|
|
out->host = (unsigned char *)mapping;
|
|
out->physical = request.out.physical_address;
|
|
out->completion_phys = request.out.physical_address + bytes;
|
|
out->bytes = bytes;
|
|
out->mapping_bytes = mapping_bytes;
|
|
return 0;
|
|
}
|
|
|
|
static void ft_tt_dma_bounce_free(ft_tt_dma_bounce_buffer *buffer)
|
|
{
|
|
if (buffer->host != NULL) {
|
|
(void)munmap(buffer->host, buffer->mapping_bytes);
|
|
buffer->host = NULL;
|
|
}
|
|
}
|
|
|
|
static void write_reg(volatile unsigned char *bar2, uint32_t offset, uint32_t value)
|
|
{
|
|
*(volatile uint32_t *)(void *)(bar2 + offset) = value;
|
|
}
|
|
|
|
static uint32_t read_reg(volatile unsigned char *bar2, uint32_t offset)
|
|
{
|
|
return *(volatile uint32_t *)(void *)(bar2 + offset);
|
|
}
|
|
|
|
/**
|
|
* One host-to-device DMA: program read channel 0 and poll it to completion.
|
|
* Transliterated from `BlackholeDmaTransfer::h2d_transfer`.
|
|
*
|
|
* @param bar2 mapped BAR2 (uncached)
|
|
* @param buffer the coherent bounce buffer, already filled
|
|
* @param axi_dst device AXI address = tlb_window_index * 2 MiB + offset
|
|
* @param bytes multiple of 4, at most buffer->bytes
|
|
* @param poll_sleep_us 0 to spin on XFERSIZE; otherwise sleep this long
|
|
* between polls, which is what frees the core
|
|
* @return 0 on success, -1 on timeout
|
|
*/
|
|
static int ft_tt_dma_h2d(volatile unsigned char *bar2, const ft_tt_dma_bounce_buffer *buffer,
|
|
uint32_t axi_dst, size_t bytes, long poll_sleep_us)
|
|
{
|
|
double start;
|
|
|
|
write_reg(bar2, FT_TT_DMA_REG_INT_SETUP, FT_TT_DMA_INT_SETUP_VALUE);
|
|
write_reg(bar2, FT_TT_DMA_REG_MSI_STOP_LOW, (uint32_t)(buffer->completion_phys & 0xFFFFFFFFu));
|
|
write_reg(bar2, FT_TT_DMA_REG_MSI_STOP_HIGH, (uint32_t)(buffer->completion_phys >> 32));
|
|
write_reg(bar2, FT_TT_DMA_REG_MSI_ABORT_LOW,
|
|
(uint32_t)((buffer->completion_phys + 4u) & 0xFFFFFFFFu));
|
|
write_reg(bar2, FT_TT_DMA_REG_MSI_ABORT_HIGH,
|
|
(uint32_t)((buffer->completion_phys + 4u) >> 32));
|
|
write_reg(bar2, FT_TT_DMA_REG_MSI_MSGD, 0);
|
|
|
|
write_reg(bar2, FT_TT_DMA_REG_EN, 0x1u);
|
|
write_reg(bar2, FT_TT_DMA_REG_SAR_LOW, (uint32_t)(buffer->physical & 0xFFFFFFFFu));
|
|
write_reg(bar2, FT_TT_DMA_REG_SAR_HIGH, (uint32_t)(buffer->physical >> 32));
|
|
write_reg(bar2, FT_TT_DMA_REG_DAR_LOW, axi_dst);
|
|
write_reg(bar2, FT_TT_DMA_REG_DAR_HIGH, 0);
|
|
write_reg(bar2, FT_TT_DMA_REG_XFERSIZE, (uint32_t)bytes);
|
|
write_reg(bar2, FT_TT_DMA_REG_DOORBELL, 0x1u);
|
|
|
|
start = now_seconds();
|
|
while (read_reg(bar2, FT_TT_DMA_REG_XFERSIZE) != 0u) {
|
|
if (poll_sleep_us > 0) {
|
|
struct timespec nap;
|
|
nap.tv_sec = 0;
|
|
nap.tv_nsec = poll_sleep_us * 1000L;
|
|
(void)nanosleep(&nap, NULL);
|
|
}
|
|
if ((now_seconds() - start) * 1000.0 > FT_TT_DMA_TIMEOUT_MS) {
|
|
printf(" DMA timeout: XFERSIZE still %u after %.0f ms\n",
|
|
read_reg(bar2, FT_TT_DMA_REG_XFERSIZE), FT_TT_DMA_TIMEOUT_MS);
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Correctness — before any bandwidth number is believed */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Prove the DMA landed where we think it did, and that it tracks its source.
|
|
*
|
|
* A single matching pattern would not prove either: the destination could hold
|
|
* a stale value that happens to match, and a transfer that silently did nothing
|
|
* looks identical to one that wrote the same bytes twice. So the destination is
|
|
* **poisoned by PIO first** and three different payloads are sent, including an
|
|
* all-zero one, which is the payload a "did nothing" implementation cannot
|
|
* fake against a non-zero poison (dev notes §3).
|
|
*/
|
|
static void verify_dma_lands(volatile unsigned char *bar2, ft_tt_dma_bounce_buffer *buffer,
|
|
volatile unsigned char *aperture, uint32_t axi_base)
|
|
{
|
|
static const unsigned char patterns[3] = {0xA5u, 0x00u, 0x3Cu};
|
|
unsigned char *poison = malloc(4096u);
|
|
unsigned char *readback = malloc(4096u);
|
|
|
|
if (poison == NULL || readback == NULL) {
|
|
check(0, "host scratch allocation");
|
|
free(poison);
|
|
free(readback);
|
|
return;
|
|
}
|
|
|
|
/* Smallest legal transfer first: if the AXI address model is wrong, four
|
|
* bytes is the cheapest way to find out. */
|
|
memset(buffer->host, 0x5Au, 4u);
|
|
memset(poison, 0xFFu, 4096u);
|
|
pio_block_write(aperture, poison, 4096u);
|
|
check(ft_tt_dma_h2d(bar2, buffer, axi_base, 4u, 0) == 0, "4-byte DMA retires");
|
|
pio_block_read(readback, aperture, 4096u);
|
|
check(memcmp(readback, buffer->host, 4u) == 0, "4-byte DMA landed in DRAM bank 1");
|
|
check(readback[4] == 0xFFu, "4-byte DMA wrote exactly 4 bytes, not more");
|
|
|
|
for (size_t p = 0; p < sizeof(patterns); p++) {
|
|
memset(poison, (int)(0xE0u + p), 4096u);
|
|
pio_block_write(aperture, poison, 4096u);
|
|
pio_block_read(readback, aperture, 4096u);
|
|
if (readback[0] != poison[0]) {
|
|
check(0, "poison took");
|
|
break;
|
|
}
|
|
|
|
memset(buffer->host, patterns[p], 4096u);
|
|
if (ft_tt_dma_h2d(bar2, buffer, axi_base, 4096u, 0) != 0) {
|
|
check(0, "4 KiB DMA retires");
|
|
break;
|
|
}
|
|
pio_block_read(readback, aperture, 4096u);
|
|
check(memcmp(readback, buffer->host, 4096u) == 0,
|
|
"4 KiB DMA tracks its source over a poisoned destination");
|
|
}
|
|
|
|
/* A full bounce buffer, at a non-zero offset, with position-dependent
|
|
* content — a transfer that lands at the wrong offset or reorders cannot
|
|
* survive this. */
|
|
for (size_t i = 0; i < buffer->bytes; i++)
|
|
buffer->host[i] = (unsigned char)(i * 31u + 7u);
|
|
|
|
check(ft_tt_dma_h2d(bar2, buffer, axi_base + (uint32_t)buffer->bytes, buffer->bytes, 0) == 0,
|
|
"512 KiB DMA retires at a non-zero window offset");
|
|
{
|
|
unsigned char *bulk = malloc(buffer->bytes);
|
|
if (bulk == NULL) {
|
|
check(0, "bulk readback allocation");
|
|
} else {
|
|
pio_block_read(bulk, aperture + buffer->bytes, buffer->bytes);
|
|
check(memcmp(bulk, buffer->host, buffer->bytes) == 0,
|
|
"512 KiB DMA is byte-exact at its offset");
|
|
free(bulk);
|
|
}
|
|
}
|
|
|
|
free(poison);
|
|
free(readback);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Bandwidth */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** Total MiB moved by one bandwidth row. */
|
|
#define ROW_MEBIBYTES ((double)TRANSFER_BYTES * TRANSFER_REPEATS / (1024.0 * 1024.0))
|
|
|
|
/**
|
|
* @param out_cpu_fraction receives host CPU seconds per wall second — 1.0 is
|
|
* one core saturated for the whole transfer
|
|
* @return MB/s
|
|
*/
|
|
static double measure_pio(volatile unsigned char *aperture, const unsigned char *host_buf,
|
|
double *out_cpu_fraction)
|
|
{
|
|
double c0 = cpu_seconds();
|
|
double t0 = now_seconds();
|
|
for (uint32_t r = 0; r < TRANSFER_REPEATS; r++)
|
|
pio_block_write(aperture, host_buf, TRANSFER_BYTES);
|
|
double seconds = now_seconds() - t0;
|
|
|
|
*out_cpu_fraction = (cpu_seconds() - c0) / seconds;
|
|
return ROW_MEBIBYTES / seconds;
|
|
}
|
|
|
|
/**
|
|
* DMA the same total bytes in `chunk`-sized pieces, reporting the honest upload
|
|
* rate (bounce memcpy included), the engine-only rate, and what the host CPU
|
|
* paid for it.
|
|
*
|
|
* @param poll_sleep_us 0 to spin, >0 to sleep between XFERSIZE polls
|
|
* @param out_engine_mb_s receives the rate excluding the bounce memcpy
|
|
* @param out_cpu_fraction receives host CPU seconds per wall second
|
|
* @return the rate including the memcpy, or -1.0 if a transfer failed
|
|
*/
|
|
static double measure_dma(volatile unsigned char *bar2, ft_tt_dma_bounce_buffer *buffer,
|
|
uint32_t axi_base, const unsigned char *host_buf, size_t chunk,
|
|
long poll_sleep_us, double *out_engine_mb_s, double *out_cpu_fraction)
|
|
{
|
|
double engine_seconds = 0.0;
|
|
double c0 = cpu_seconds();
|
|
double t0 = now_seconds();
|
|
|
|
for (uint32_t r = 0; r < TRANSFER_REPEATS; r++) {
|
|
for (size_t offset = 0; offset < TRANSFER_BYTES; offset += chunk) {
|
|
double e0;
|
|
memcpy(buffer->host, host_buf + offset, chunk);
|
|
e0 = now_seconds();
|
|
if (ft_tt_dma_h2d(bar2, buffer, axi_base + (uint32_t)offset, chunk, poll_sleep_us) != 0)
|
|
return -1.0;
|
|
engine_seconds += now_seconds() - e0;
|
|
}
|
|
}
|
|
|
|
double total_seconds = now_seconds() - t0;
|
|
|
|
*out_engine_mb_s = ROW_MEBIBYTES / engine_seconds;
|
|
*out_cpu_fraction = (cpu_seconds() - c0) / total_seconds;
|
|
return ROW_MEBIBYTES / total_seconds;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
ft_tt_ctx *ctx = NULL;
|
|
ft_tt_grid_info grid;
|
|
ft_tt_noc_coord dram;
|
|
ft_tt_tlb_window *window = NULL;
|
|
ft_tt_dma_bounce_buffer bounce = {NULL, 0, 0, 0, 0};
|
|
ft_tt_kmd_resources resources;
|
|
volatile unsigned char *bar2 = NULL;
|
|
volatile unsigned char *aperture = NULL;
|
|
unsigned char *host_buf = NULL;
|
|
uint32_t window_index = 0;
|
|
uint32_t axi_base = 0;
|
|
int rc = 1;
|
|
|
|
ctx = ft_tt_init_device(0);
|
|
if (ctx == NULL) {
|
|
fprintf(stderr, "could not open device 0\n");
|
|
return 1;
|
|
}
|
|
|
|
if (ft_tt_arc_telemetry_read_grid(ctx, &grid) != FT_TT_OK ||
|
|
ft_tt_coord_check_grid(&grid) != FT_TT_OK) {
|
|
fprintf(stderr, "grid not sound; refusing to touch the card\n");
|
|
goto cleanup;
|
|
}
|
|
if (ft_tt_coord_dram_bank(&grid, DRAM_BANK_INDEX, FT_TT_COORD_TRANSLATED, &dram) != FT_TT_OK) {
|
|
fprintf(stderr, "could not derive DRAM bank %u\n", DRAM_BANK_INDEX);
|
|
goto cleanup;
|
|
}
|
|
if (ft_tt_device_get_resources(ctx, &resources) != FT_TT_OK || resources.bar2_len == 0) {
|
|
fprintf(stderr, "no BAR2 on this card; the DMA engine is unreachable\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
bar2 = mmap(NULL, (size_t)resources.bar2_len, PROT_READ | PROT_WRITE, MAP_SHARED,
|
|
ft_tt_ctx_fd(ctx), (off_t)resources.bar2_uc_offset);
|
|
if ((void *)bar2 == MAP_FAILED) {
|
|
perror("mmap BAR2");
|
|
bar2 = NULL;
|
|
goto cleanup;
|
|
}
|
|
|
|
if (ft_tt_tlb_window_alloc(ctx, WINDOW_BYTES, FT_TT_KMD_CACHE_WC, &window) != FT_TT_OK) {
|
|
fprintf(stderr, "TLB window alloc failed\n");
|
|
goto cleanup;
|
|
}
|
|
if (ft_tt_tlb_window_aim(ctx, window, (uint16_t)dram.x, (uint16_t)dram.y, DRAM_AIM_BASE) !=
|
|
FT_TT_OK) {
|
|
fprintf(stderr, "TLB window aim failed\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
aperture = ft_tt_tlb_window_aperture(window);
|
|
window_index = ft_tt_tlb_window_id(window);
|
|
if (window_index >= FT_TT_TLB_2M_WINDOW_COUNT) {
|
|
fprintf(stderr, "window index %u is outside the 2 MiB aperture region\n", window_index);
|
|
goto cleanup;
|
|
}
|
|
axi_base = window_index * WINDOW_BYTES;
|
|
|
|
if (ft_tt_dma_bounce_alloc(ft_tt_ctx_fd(ctx), DMA_BUF_BYTES, &bounce) != 0)
|
|
goto cleanup;
|
|
|
|
host_buf = malloc(TRANSFER_BYTES);
|
|
if (host_buf == NULL)
|
|
goto cleanup;
|
|
for (size_t i = 0; i < TRANSFER_BYTES; i++)
|
|
host_buf[i] = (unsigned char)(i * 17u + 3u);
|
|
|
|
printf("DRAM bank %u translated (%u, %u); TLB window %u -> AXI 0x%08X\n", DRAM_BANK_INDEX,
|
|
dram.x, dram.y, window_index, axi_base);
|
|
printf("bounce buffer %zu B at PA 0x%016llX, completion at 0x%016llX\n\n", bounce.bytes,
|
|
(unsigned long long)bounce.physical, (unsigned long long)bounce.completion_phys);
|
|
|
|
printf("correctness\n");
|
|
verify_dma_lands(bar2, &bounce, aperture, axi_base);
|
|
|
|
if (failures > 0) {
|
|
printf("\n%d/%d checks failed; refusing to report a bandwidth number\n", failures, checks);
|
|
goto cleanup;
|
|
}
|
|
|
|
printf("\nbandwidth (%u MiB x %u passes per row; host CPU is cores busy per wall second)\n",
|
|
TRANSFER_BYTES / (1024u * 1024u), TRANSFER_REPEATS);
|
|
{
|
|
double cpu = 0.0;
|
|
double mb_s = measure_pio(aperture, host_buf, &cpu);
|
|
printf(" PIO WC 16 B stores %8.1f MB/s host CPU %.2f\n",
|
|
mb_s, cpu);
|
|
}
|
|
|
|
for (size_t chunk = 64u * 1024u; chunk <= DMA_BUF_BYTES; chunk *= 2u) {
|
|
double engine = 0.0;
|
|
double cpu = 0.0;
|
|
double total = measure_dma(bar2, &bounce, axi_base, host_buf, chunk, 0, &engine, &cpu);
|
|
if (total < 0.0) {
|
|
printf(" DMA %4zu KiB spin-polled transfer failed\n", chunk / 1024u);
|
|
failures++;
|
|
break;
|
|
}
|
|
printf(" DMA %4zu KiB spin-polled %8.1f MB/s (engine only %8.1f) host CPU %.2f\n",
|
|
chunk / 1024u, total, engine, cpu);
|
|
}
|
|
|
|
/* The only axis on which DMA can still win: hand the copy to the engine and
|
|
* stop spinning on it. Same bytes, same chunk, sleeping between polls. */
|
|
{
|
|
double engine = 0.0;
|
|
double cpu = 0.0;
|
|
double total = measure_dma(bar2, &bounce, axi_base, host_buf, DMA_BUF_BYTES,
|
|
DMA_POLL_SLEEP_US, &engine, &cpu);
|
|
if (total < 0.0) {
|
|
printf(" DMA 512 KiB sleep-polled transfer failed\n");
|
|
failures++;
|
|
} else {
|
|
printf(" DMA 512 KiB sleep-polled %8.1f MB/s (engine only %8.1f) host CPU "
|
|
"%.2f\n",
|
|
total, engine, cpu);
|
|
}
|
|
}
|
|
|
|
printf("\n%d/%d checks passed\n", checks - failures, checks);
|
|
rc = (failures == 0) ? 0 : 1;
|
|
|
|
cleanup:
|
|
ft_tt_dma_bounce_free(&bounce);
|
|
free(host_buf);
|
|
if (window != NULL)
|
|
(void)ft_tt_tlb_window_free(ctx, window);
|
|
if (bar2 != NULL)
|
|
(void)munmap((void *)bar2, (size_t)resources.bar2_len);
|
|
ft_tt_ctx_free(ctx);
|
|
return rc;
|
|
}
|