540 lines
19 KiB
C
540 lines
19 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_metal_region.c
|
|
* @brief Tests no-copy mmap regions and offset binding: that wrapping a
|
|
* mapping really aliases it instead of copying, that a range resolves to
|
|
* the right (segment, offset), and that a real Q4_K matvec reading a
|
|
* weight at a nonzero offset produces exactly what it produces at zero.
|
|
*
|
|
* The offsets used are multiples of 32 but NOT of the 16 KiB page, which
|
|
* is the real GGUF case: tensor data starts at an arbitrary file offset
|
|
* and tensors are only 32-byte aligned.
|
|
*
|
|
* Exits 77 when no Metal device is present so GPU-less CI skips cleanly.
|
|
*
|
|
* @status All tests pass on M4 / macOS 26; each was proven able to fail.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "lib/test.h"
|
|
#include "metal/fm_metal.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
/* A byte offset that is a multiple of 32 (GGUF's tensor alignment) but
|
|
* deliberately not a multiple of the 16384-byte page. */
|
|
#define WEIGHT_OFFSET (1024u * 1024u + 32u)
|
|
|
|
#define Q4K_BLOCK_BYTES 144u
|
|
|
|
#define SCRATCH_DIR \
|
|
"metal/tests/" /* run from the repo root; test_* is gitignored */
|
|
|
|
static const char *kScratchPath = SCRATCH_DIR "test_region_map.bin";
|
|
static const char *kScratchPathB = SCRATCH_DIR "test_region_map_b.bin";
|
|
|
|
/* ================================================================
|
|
* Mapping helpers
|
|
* ================================================================ */
|
|
|
|
/* Creates a sparse file of `bytes` and maps it read/write, private.
|
|
* Returns the mapping base, or NULL. */
|
|
static void *map_file(const char *path, size_t bytes, int *fd_out)
|
|
{
|
|
int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
|
|
if (fd < 0) return NULL;
|
|
if (ftruncate(fd, (off_t)bytes) != 0) { close(fd); return NULL; }
|
|
|
|
void *base = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
|
|
if (base == MAP_FAILED) { close(fd); return NULL; }
|
|
|
|
*fd_out = fd;
|
|
return base;
|
|
}
|
|
|
|
static void unmap_file(const char *path, void *base, size_t bytes, int fd)
|
|
{
|
|
munmap(base, bytes);
|
|
close(fd);
|
|
unlink(path);
|
|
}
|
|
|
|
static void *map_scratch(size_t bytes, int *fd_out)
|
|
{
|
|
return map_file(kScratchPath, bytes, fd_out);
|
|
}
|
|
|
|
static void unmap_scratch(void *base, size_t bytes, int fd)
|
|
{
|
|
unmap_file(kScratchPath, base, bytes, fd);
|
|
}
|
|
|
|
/* ================================================================
|
|
* Q4_K test weight
|
|
* ================================================================ */
|
|
|
|
static uint16_t f32_to_f16(float f)
|
|
{
|
|
uint32_t bits;
|
|
memcpy(&bits, &f, 4);
|
|
uint32_t sign = (bits >> 16) & 0x8000u;
|
|
int32_t exp = (int32_t)((bits >> 23) & 0xFF) - 127 + 15;
|
|
uint32_t mant = bits & 0x7FFFFFu;
|
|
if (exp <= 0) return (uint16_t)sign;
|
|
if (exp >= 31) return (uint16_t)(sign | 0x7C00u);
|
|
return (uint16_t)(sign | ((uint32_t)exp << 10) | (mant >> 13));
|
|
}
|
|
|
|
/* Fills `rows` structurally valid Q4_K super-blocks (one per row, so
|
|
* cols = 256). Values are small and bounded so the matvec output is
|
|
* finite and clearly nonzero. `seed` selects a distinct weight — tests
|
|
* rely on two different seeds producing visibly different outputs, so
|
|
* that a binding which ignores its offset cannot pass. */
|
|
static void fill_q4k_rows(uint8_t *dst, int rows, uint32_t seed)
|
|
{
|
|
uint32_t rng = 0x9e3779b9u + seed * 0x85ebca6bu;
|
|
for (int r = 0; r < rows; r++) {
|
|
uint8_t *blk = dst + (size_t)r * Q4K_BLOCK_BYTES;
|
|
uint16_t d = f32_to_f16(0.01f + 0.001f * (float)((r + seed) % 7));
|
|
uint16_t dmin = f32_to_f16(0.005f);
|
|
memcpy(blk + 0, &d, 2);
|
|
memcpy(blk + 2, &dmin, 2);
|
|
for (int i = 0; i < 12; i++)
|
|
blk[4 + i] = (uint8_t)(8 + ((r * 3 + i * 5 + (int)seed * 11) % 40));
|
|
for (int i = 0; i < 128; i++) {
|
|
rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
|
|
blk[16 + i] = (uint8_t)(rng & 0xFFu);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Fraction-free "these are clearly different vectors" check. */
|
|
static int vectors_differ(const float *a, const float *b, int n)
|
|
{
|
|
for (int i = 0; i < n; i++)
|
|
if (fabsf(a[i] - b[i]) > 1e-3f) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* Runs the Q4_K matvec with the weight at `weight_offset` inside `region`
|
|
* and writes `rows` floats into out_host. Returns 0 on success. */
|
|
static int matvec_from_region(fm_region *region, size_t weight_offset,
|
|
int rows, int cols, fm_buffer *x, fm_buffer *out,
|
|
float *out_host)
|
|
{
|
|
size_t row_stride = (size_t)(cols / 256) * Q4K_BLOCK_BYTES;
|
|
|
|
fm_bufref w;
|
|
if (fm_region_bind(region, weight_offset, (size_t)rows * row_stride, &w) != 0)
|
|
return -1;
|
|
|
|
memset(fm_buffer_data(out), 0, (size_t)rows * sizeof(float));
|
|
fm_matvec_q4_k_at(w, fm_bufref_at(x, 0), fm_bufref_at(out, 0),
|
|
rows, cols, (int)row_stride);
|
|
fm_sync();
|
|
memcpy(out_host, fm_buffer_data(out), (size_t)rows * sizeof(float));
|
|
return 0;
|
|
}
|
|
|
|
/* ================================================================
|
|
* No-copy wrapping
|
|
* ================================================================ */
|
|
|
|
static void test_map_no_copy_aliases_the_mapping(void)
|
|
{
|
|
size_t bytes = 4u * 1024 * 1024;
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
fm_buffer *buf = fm_buffer_map_no_copy(base, bytes);
|
|
ASSERT_NOT_NULL(buf);
|
|
|
|
/* The whole point: the buffer must BE the mapping, not a duplicate. */
|
|
ASSERT_TRUE(fm_buffer_data(buf) == base);
|
|
|
|
/* And it must stay aliased — a host write is visible through it. */
|
|
((uint8_t *)base)[12345] = 0xA7;
|
|
ASSERT_INT_EQ(0xA7, ((uint8_t *)fm_buffer_data(buf))[12345]);
|
|
|
|
fm_buffer_free(buf);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
static void test_map_no_copy_refuses_unaligned_base(void)
|
|
{
|
|
size_t bytes = 1024u * 1024;
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
/* 32-byte aligned but not page-aligned: fm_buffer_from_ptr would
|
|
* silently copy here, which is the bug. This must refuse instead. */
|
|
fm_buffer *buf = fm_buffer_map_no_copy((char *)base + 32, bytes - 32);
|
|
ASSERT_NULL(buf);
|
|
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
static void test_map_no_copy_refuses_oversize_length(void)
|
|
{
|
|
size_t bytes = 1024u * 1024;
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
size_t max_length = fm_buffer_max_length();
|
|
ASSERT_TRUE(max_length > 0);
|
|
|
|
/* Rejected on the length check before Metal is asked, so this never
|
|
* trips the validation layer's hard assert. */
|
|
ASSERT_NULL(fm_buffer_map_no_copy(base, max_length + 1));
|
|
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
/* ================================================================
|
|
* Region geometry and binding
|
|
* ================================================================ */
|
|
|
|
static void test_small_mapping_is_one_segment(void)
|
|
{
|
|
size_t bytes = 8u * 1024 * 1024;
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
fm_region *region = fm_region_open(base, bytes, 0);
|
|
ASSERT_NOT_NULL(region);
|
|
ASSERT_INT_EQ(1, fm_region_segment_count(region));
|
|
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
static void test_bind_resolves_to_the_host_address(void)
|
|
{
|
|
size_t bytes = 8u * 1024 * 1024;
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
fm_region *region = fm_region_open(base, bytes, 0);
|
|
ASSERT_NOT_NULL(region);
|
|
|
|
size_t offsets[] = { 0, 32, 4096, WEIGHT_OFFSET, bytes - 1024 };
|
|
for (unsigned i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) {
|
|
fm_bufref ref;
|
|
ASSERT_INT_EQ(0, fm_region_bind(region, offsets[i], 512, &ref));
|
|
const char *bound = (const char *)fm_buffer_data(ref.buffer) + ref.offset;
|
|
ASSERT_TRUE(bound == (const char *)base + offsets[i]);
|
|
}
|
|
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
static void test_bind_rejects_bad_ranges(void)
|
|
{
|
|
size_t bytes = 8u * 1024 * 1024;
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
fm_region *region = fm_region_open(base, bytes, 64 * 1024);
|
|
ASSERT_NOT_NULL(region);
|
|
|
|
fm_bufref ref;
|
|
ASSERT_INT_EQ(-1, fm_region_bind(region, bytes - 16, 512, &ref)); /* past the end */
|
|
ASSERT_INT_EQ(-1, fm_region_bind(region, bytes + 4096, 16, &ref)); /* wholly outside */
|
|
ASSERT_INT_EQ(-1, fm_region_bind(region, 8, 512, &ref)); /* misaligned offset */
|
|
ASSERT_INT_EQ(-1, fm_region_bind(region, 4096, 128 * 1024, &ref)); /* over max span */
|
|
ASSERT_INT_EQ(-1, fm_region_bind(region, 4096, 0, &ref)); /* empty range */
|
|
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
/* ================================================================
|
|
* A real kernel reading at a nonzero offset
|
|
* ================================================================ */
|
|
|
|
/* Places weight `seed` at `offset` in a fresh second mapping and runs the
|
|
* matvec from its offset 0 — the independent reference an offset-bound
|
|
* run must reproduce. Returns 0 on success. */
|
|
static int reference_at_offset_zero(int rows, int cols, uint32_t seed,
|
|
fm_buffer *x, fm_buffer *out, float *result)
|
|
{
|
|
size_t weight_bytes = (size_t)rows * Q4K_BLOCK_BYTES;
|
|
size_t bytes = 1024u * 1024;
|
|
int fd = -1;
|
|
void *base = map_file(kScratchPathB, bytes, &fd);
|
|
if (!base) return -1;
|
|
|
|
fill_q4k_rows((uint8_t *)base, rows, seed);
|
|
|
|
fm_region *region = fm_region_open(base, bytes, 0);
|
|
int rc = region ? matvec_from_region(region, 0, rows, cols, x, out, result) : -1;
|
|
(void)weight_bytes;
|
|
|
|
fm_region_close(region);
|
|
unmap_file(kScratchPathB, base, bytes, fd);
|
|
return rc;
|
|
}
|
|
|
|
static void test_matvec_at_nonzero_offset_matches_offset_zero(void)
|
|
{
|
|
int rows = 64, cols = 256;
|
|
size_t bytes = 8u * 1024 * 1024;
|
|
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
/* DIFFERENT weights at offset 0 and at the 32-aligned, non-page-aligned
|
|
* offset. If the binding ignored its offset both runs would read the
|
|
* decoy at 0 and agree, so the weights must not match. */
|
|
fill_q4k_rows((uint8_t *)base, rows, 1); /* decoy at 0 */
|
|
fill_q4k_rows((uint8_t *)base + WEIGHT_OFFSET, rows, 2); /* real weight */
|
|
ASSERT_TRUE(WEIGHT_OFFSET % 32 == 0);
|
|
ASSERT_TRUE(WEIGHT_OFFSET % 16384 != 0);
|
|
|
|
fm_region *region = fm_region_open(base, bytes, 0);
|
|
ASSERT_NOT_NULL(region);
|
|
|
|
fm_buffer *x = fm_buffer_alloc((size_t)cols * sizeof(float));
|
|
fm_buffer *out = fm_buffer_alloc((size_t)rows * sizeof(float));
|
|
ASSERT_NOT_NULL(x);
|
|
ASSERT_NOT_NULL(out);
|
|
float *xh = (float *)fm_buffer_data(x);
|
|
for (int i = 0; i < cols; i++) xh[i] = 0.5f - (float)(i % 11) * 0.1f;
|
|
|
|
float decoy[64], at_offset[64], reference[64];
|
|
ASSERT_INT_EQ(0, matvec_from_region(region, 0, rows, cols, x, out, decoy));
|
|
ASSERT_INT_EQ(0, matvec_from_region(region, WEIGHT_OFFSET, rows, cols, x, out, at_offset));
|
|
|
|
/* The kernel actually ran, and it did not just re-read offset 0. */
|
|
float magnitude = 0.0f;
|
|
for (int r = 0; r < rows; r++) magnitude += fabsf(at_offset[r]);
|
|
ASSERT_TRUE(magnitude > 1e-3f);
|
|
for (int r = 0; r < rows; r++) ASSERT_TRUE(isfinite(at_offset[r]));
|
|
ASSERT_TRUE(vectors_differ(decoy, at_offset, rows));
|
|
|
|
/* And it read the right weight: same bytes at offset 0 of another
|
|
* region give exactly the same numbers. */
|
|
ASSERT_INT_EQ(0, reference_at_offset_zero(rows, cols, 2, x, out, reference));
|
|
for (int r = 0; r < rows; r++)
|
|
ASSERT_FLT_NEAR(reference[r], at_offset[r], 0.0f);
|
|
|
|
fm_buffer_free(x);
|
|
fm_buffer_free(out);
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
static void test_matvec_at_region_end_is_not_truncated(void)
|
|
{
|
|
int rows = 64, cols = 256;
|
|
size_t weight_bytes = (size_t)rows * Q4K_BLOCK_BYTES;
|
|
size_t bytes = 8u * 1024 * 1024;
|
|
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
/* Last weight-sized range in the region, kept 32-byte aligned, and a
|
|
* different decoy weight at 0. */
|
|
size_t tail_offset = (bytes - weight_bytes) & ~(size_t)31;
|
|
fill_q4k_rows((uint8_t *)base, rows, 3);
|
|
fill_q4k_rows((uint8_t *)base + tail_offset, rows, 4);
|
|
|
|
fm_region *region = fm_region_open(base, bytes, 0);
|
|
ASSERT_NOT_NULL(region);
|
|
|
|
fm_buffer *x = fm_buffer_alloc((size_t)cols * sizeof(float));
|
|
fm_buffer *out = fm_buffer_alloc((size_t)rows * sizeof(float));
|
|
ASSERT_NOT_NULL(x);
|
|
ASSERT_NOT_NULL(out);
|
|
float *xh = (float *)fm_buffer_data(x);
|
|
for (int i = 0; i < cols; i++) xh[i] = 0.25f + (float)(i % 5) * 0.05f;
|
|
|
|
float decoy[64], at_tail[64], reference[64];
|
|
ASSERT_INT_EQ(0, matvec_from_region(region, 0, rows, cols, x, out, decoy));
|
|
ASSERT_INT_EQ(0, matvec_from_region(region, tail_offset, rows, cols, x, out, at_tail));
|
|
|
|
ASSERT_TRUE(vectors_differ(decoy, at_tail, rows));
|
|
ASSERT_INT_EQ(0, reference_at_offset_zero(rows, cols, 4, x, out, reference));
|
|
|
|
/* The very last row must be right too — that is where a truncated
|
|
* buffer length would show up. */
|
|
for (int r = 0; r < rows; r++)
|
|
ASSERT_FLT_NEAR(reference[r], at_tail[r], 0.0f);
|
|
ASSERT_TRUE(fabsf(at_tail[rows - 1]) > 1e-6f);
|
|
|
|
fm_buffer_free(x);
|
|
fm_buffer_free(out);
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
/* ================================================================
|
|
* Multi-segment regions
|
|
* ================================================================ */
|
|
|
|
static void test_oversized_mapping_splits_into_segments(void)
|
|
{
|
|
size_t bytes = 8u * 1024 * 1024;
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
/* Pretend one buffer can only cover 2 MiB. */
|
|
fm_region_force_segment_span(2u * 1024 * 1024);
|
|
fm_region *region = fm_region_open(base, bytes, 256 * 1024);
|
|
fm_region_force_segment_span(0);
|
|
ASSERT_NOT_NULL(region);
|
|
ASSERT_TRUE(fm_region_segment_count(region) > 1);
|
|
|
|
/* Every 32-aligned range in the region must resolve to the right host
|
|
* bytes, including ranges that cross where a naive split would cut. */
|
|
for (size_t offset = 0; offset + 4096 <= bytes; offset += 64 * 1024 + 32) {
|
|
fm_bufref ref;
|
|
ASSERT_INT_EQ(0, fm_region_bind(region, offset, 4096, &ref));
|
|
const char *bound = (const char *)fm_buffer_data(ref.buffer) + ref.offset;
|
|
ASSERT_TRUE(bound == (const char *)base + offset);
|
|
ASSERT_TRUE(ref.offset + 4096 <= fm_buffer_size(ref.buffer));
|
|
}
|
|
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
static void test_matvec_through_a_later_segment(void)
|
|
{
|
|
int rows = 64, cols = 256;
|
|
size_t weight_bytes = (size_t)rows * Q4K_BLOCK_BYTES;
|
|
size_t bytes = 8u * 1024 * 1024;
|
|
/* Well past the first 2 MiB segment, 32-aligned, not page-aligned. */
|
|
size_t far_offset = 6u * 1024 * 1024 + 32u;
|
|
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base);
|
|
|
|
/* Different weight at 0 so a binding that ignores its offset, or one
|
|
* that always picks segment 0, reads visibly wrong numbers. */
|
|
fill_q4k_rows((uint8_t *)base, rows, 5);
|
|
fill_q4k_rows((uint8_t *)base + far_offset, rows, 6);
|
|
(void)weight_bytes;
|
|
|
|
fm_region_force_segment_span(2u * 1024 * 1024);
|
|
fm_region *region = fm_region_open(base, bytes, 256 * 1024);
|
|
fm_region_force_segment_span(0);
|
|
ASSERT_NOT_NULL(region);
|
|
ASSERT_TRUE(fm_region_segment_count(region) > 1);
|
|
|
|
fm_buffer *x = fm_buffer_alloc((size_t)cols * sizeof(float));
|
|
fm_buffer *out = fm_buffer_alloc((size_t)rows * sizeof(float));
|
|
ASSERT_NOT_NULL(x);
|
|
ASSERT_NOT_NULL(out);
|
|
float *xh = (float *)fm_buffer_data(x);
|
|
for (int i = 0; i < cols; i++) xh[i] = 0.3f - (float)(i % 9) * 0.07f;
|
|
|
|
float decoy[64], at_far[64], reference[64];
|
|
ASSERT_INT_EQ(0, matvec_from_region(region, 0, rows, cols, x, out, decoy));
|
|
ASSERT_INT_EQ(0, matvec_from_region(region, far_offset, rows, cols, x, out, at_far));
|
|
|
|
float magnitude = 0.0f;
|
|
for (int r = 0; r < rows; r++) magnitude += fabsf(at_far[r]);
|
|
ASSERT_TRUE(magnitude > 1e-3f);
|
|
ASSERT_TRUE(vectors_differ(decoy, at_far, rows));
|
|
|
|
ASSERT_INT_EQ(0, reference_at_offset_zero(rows, cols, 6, x, out, reference));
|
|
for (int r = 0; r < rows; r++)
|
|
ASSERT_FLT_NEAR(reference[r], at_far[r], 0.0f);
|
|
|
|
fm_buffer_free(x);
|
|
fm_buffer_free(out);
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
/* The real thing: a mapping larger than the device's maxBufferLength,
|
|
* which is exactly the 14.89 GiB weight case that started this. */
|
|
static void test_mapping_larger_than_one_buffer(void)
|
|
{
|
|
size_t max_length = fm_buffer_max_length();
|
|
ASSERT_TRUE(max_length > 0);
|
|
|
|
size_t bytes = max_length + 1024u * 1024 * 1024; /* ~1 GiB over the limit */
|
|
int fd = -1;
|
|
void *base = map_scratch(bytes, &fd);
|
|
ASSERT_NOT_NULL(base); /* sparse file: costs address space, not disk */
|
|
|
|
fm_region *region = fm_region_open(base, bytes, 512u * 1024 * 1024);
|
|
ASSERT_NOT_NULL(region);
|
|
ASSERT_TRUE(fm_region_segment_count(region) > 1);
|
|
|
|
/* A weight near the very end must still bind, and the resolved
|
|
* address must be exact — this is where a 32-bit offset or a
|
|
* truncated segment length would show. */
|
|
size_t tail = (bytes - 64 * 1024) & ~(size_t)31;
|
|
fm_bufref ref;
|
|
ASSERT_INT_EQ(0, fm_region_bind(region, tail, 64 * 1024, &ref));
|
|
ASSERT_TRUE((const char *)fm_buffer_data(ref.buffer) + ref.offset
|
|
== (const char *)base + tail);
|
|
|
|
/* And it must genuinely be the mapping, not a copy. */
|
|
((uint8_t *)base)[tail] = 0x5C;
|
|
ASSERT_INT_EQ(0x5C, ((const uint8_t *)fm_buffer_data(ref.buffer))[ref.offset]);
|
|
|
|
fm_region_close(region);
|
|
unmap_scratch(base, bytes, fd);
|
|
}
|
|
|
|
/* ================================================================
|
|
* main
|
|
* ================================================================ */
|
|
|
|
int main(void)
|
|
{
|
|
printf("\n Metal No-Copy Region + Offset Binding Tests\n");
|
|
printf(" ==========================================\n\n");
|
|
|
|
if (fm_init() != 0 || !fm_available()) {
|
|
fprintf(stderr, "Metal unavailable\n");
|
|
return 77;
|
|
}
|
|
|
|
printf(" device maxBufferLength = %zu bytes (%.2f GiB)\n",
|
|
fm_buffer_max_length(), fm_buffer_max_length() / 1073741824.0);
|
|
printf(" bind offset alignment = %zu bytes\n\n",
|
|
fm_buffer_offset_alignment());
|
|
|
|
RUN_TEST(map_no_copy_aliases_the_mapping);
|
|
RUN_TEST(map_no_copy_refuses_unaligned_base);
|
|
RUN_TEST(map_no_copy_refuses_oversize_length);
|
|
|
|
RUN_TEST(small_mapping_is_one_segment);
|
|
RUN_TEST(bind_resolves_to_the_host_address);
|
|
RUN_TEST(bind_rejects_bad_ranges);
|
|
|
|
RUN_TEST(matvec_at_nonzero_offset_matches_offset_zero);
|
|
RUN_TEST(matvec_at_region_end_is_not_truncated);
|
|
|
|
RUN_TEST(oversized_mapping_splits_into_segments);
|
|
RUN_TEST(matvec_through_a_later_segment);
|
|
RUN_TEST(mapping_larger_than_one_buffer);
|
|
|
|
fm_free();
|
|
|
|
TEST_REPORT();
|
|
return __ft_failed > 0 ? 1 : 0;
|
|
}
|