288 lines
11 KiB
C
288 lines
11 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file ft_tt_fttw.c
|
|
* @brief Implementation of the FTTW artifact loader. See ft_tt_fttw.h.
|
|
*
|
|
* @status Reads the production MiniCPM5-1B artifact and uploads real tensors.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "ft_tt_fttw.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include "convert/ft_float_convert.h"
|
|
#include "ft_tt_device.h"
|
|
#include "ft_tt_op_l1_layout.h"
|
|
#include "ft_tt_tilize.h"
|
|
|
|
/** How much of a tensor to hand the accessor per call when the caller says "default". */
|
|
#define FTTW_DEFAULT_CHUNK_BYTES 0x10000u
|
|
|
|
int ft_tt_fttw_open(const char *path, ft_tt_fttw *out_artifact)
|
|
{
|
|
struct stat status;
|
|
const unsigned char *base;
|
|
uint64_t directory_bytes;
|
|
int fd;
|
|
|
|
if (!path || !out_artifact)
|
|
return FT_TT_EINVAL;
|
|
memset(out_artifact, 0, sizeof(*out_artifact));
|
|
|
|
fd = open(path, O_RDONLY);
|
|
if (fd < 0) {
|
|
ft_tt_log_error("fttw: cannot open %s", path);
|
|
return FT_TT_EIO;
|
|
}
|
|
if (fstat(fd, &status) != 0 || (size_t)status.st_size < FT_TT_ARTIFACT_HEADER_BYTES) {
|
|
ft_tt_log_error("fttw: %s is shorter than a header", path);
|
|
close(fd);
|
|
return FT_TT_EIO;
|
|
}
|
|
|
|
base = mmap(NULL, (size_t)status.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
if (base == MAP_FAILED) {
|
|
ft_tt_log_error("fttw: cannot map %s", path);
|
|
close(fd);
|
|
return FT_TT_EIO;
|
|
}
|
|
|
|
/* A RANGE of versions, because v1 and v2 differ only in fields this loader does not
|
|
* interpret: v2 uses the entry's spare word as `layout` and stores a matmul weight
|
|
* transposed. The loader is a byte copy either way; it is the CONSUMER that has to
|
|
* read `layout`, which is 0 on every v1 entry because that word was written as zero
|
|
* padding. A FUTURE version is still refused, which is the direction that could be
|
|
* silently wrong here. */
|
|
memcpy(&out_artifact->header, base, sizeof(out_artifact->header));
|
|
if (out_artifact->header.magic != FT_TT_ARTIFACT_MAGIC ||
|
|
out_artifact->header.version < FT_TT_ARTIFACT_VERSION_MIN ||
|
|
out_artifact->header.version > FT_TT_ARTIFACT_VERSION) {
|
|
ft_tt_log_error("fttw: %s magic 0x%08x version %u is not an FTTW v%u..v%u artifact", path,
|
|
out_artifact->header.magic, out_artifact->header.version,
|
|
FT_TT_ARTIFACT_VERSION_MIN, FT_TT_ARTIFACT_VERSION);
|
|
munmap((void *)base, (size_t)status.st_size);
|
|
close(fd);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
|
|
/* A flag bit changes what the PAYLOAD MEANS — the rotary one says `attn_q`/`attn_k`
|
|
* hold split-half rows where GGUF had interleaved ones — so an unknown bit is refused
|
|
* rather than ignored. Ignoring it is exactly the silent-wrong-answer shape the
|
|
* `layout` field exists to prevent. */
|
|
if (out_artifact->header.flags & ~FT_TT_ARTIFACT_FLAGS_KNOWN) {
|
|
ft_tt_log_error("fttw: %s header flags 0x%08x carry bits this build does not know", path,
|
|
out_artifact->header.flags);
|
|
munmap((void *)base, (size_t)status.st_size);
|
|
close(fd);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
|
|
/* The directory is written last, so a truncated file has a valid-looking header and
|
|
* no directory. Bound it before anything indexes it. */
|
|
directory_bytes = (uint64_t)out_artifact->header.tensor_count * FT_TT_ARTIFACT_ENTRY_BYTES;
|
|
if (out_artifact->header.directory_offset + directory_bytes > (uint64_t)status.st_size) {
|
|
ft_tt_log_error("fttw: %s directory of %u entries at %llu does not fit %lld bytes", path,
|
|
out_artifact->header.tensor_count,
|
|
(unsigned long long)out_artifact->header.directory_offset,
|
|
(long long)status.st_size);
|
|
munmap((void *)base, (size_t)status.st_size);
|
|
close(fd);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
|
|
out_artifact->base = base;
|
|
out_artifact->bytes = (size_t)status.st_size;
|
|
out_artifact->fd = fd;
|
|
out_artifact->directory =
|
|
(const ft_tt_artifact_entry *)(base + out_artifact->header.directory_offset);
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
void ft_tt_fttw_close(ft_tt_fttw *artifact)
|
|
{
|
|
if (!artifact)
|
|
return;
|
|
if (artifact->base)
|
|
munmap((void *)artifact->base, artifact->bytes);
|
|
if (artifact->fd > 0)
|
|
close(artifact->fd);
|
|
memset(artifact, 0, sizeof(*artifact));
|
|
}
|
|
|
|
const ft_tt_artifact_entry *ft_tt_fttw_find(const ft_tt_fttw *artifact, const char *name)
|
|
{
|
|
uint32_t index;
|
|
|
|
if (!artifact || !artifact->directory || !name)
|
|
return NULL;
|
|
for (index = 0; index < artifact->header.tensor_count; index++) {
|
|
const ft_tt_artifact_entry *entry = &artifact->directory[index];
|
|
|
|
if (strncmp(entry->name, name, sizeof(entry->name)) == 0)
|
|
return entry;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
uint32_t ft_tt_fttw_tile_bytes(uint32_t format)
|
|
{
|
|
switch (format) {
|
|
case FT_TT_FORMAT_FP32:
|
|
return FT_TT_OP_TILE_BYTES_FP32;
|
|
case FT_TT_FORMAT_BF16:
|
|
return FT_TT_OP_TILE_BYTES_BF16;
|
|
case FT_TT_FORMAT_BFP8_B:
|
|
return FT_TT_OP_TILE_BYTES_BFP8_B;
|
|
case FT_TT_FORMAT_BFP4_B:
|
|
return FT_TT_OP_TILE_BYTES_BFP4_B;
|
|
default:
|
|
return 0u;
|
|
}
|
|
}
|
|
|
|
int ft_tt_fttw_upload_range(const ft_tt_fttw *artifact, const ft_tt_artifact_entry *entry,
|
|
uint64_t source_offset, uint64_t source_bytes,
|
|
const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
|
|
uint32_t address, uint32_t chunk_bytes)
|
|
{
|
|
const unsigned char *source;
|
|
uint64_t remaining;
|
|
uint64_t offset = 0;
|
|
|
|
if (!artifact || !entry || !noc)
|
|
return FT_TT_EINVAL;
|
|
if (entry->data_offset + entry->byte_count > (uint64_t)artifact->bytes) {
|
|
ft_tt_log_error("fttw: %s payload runs past the artifact", entry->name);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
/* Bounded against the TENSOR, not merely against the mapping. A run that stayed inside
|
|
* the file but left the tensor would upload a neighbour's bytes, and the card would then
|
|
* compute a plausible wrong answer with no error anywhere. */
|
|
if (source_bytes == 0u || source_offset + source_bytes > entry->byte_count) {
|
|
ft_tt_log_error("fttw: %s range +%llu..%llu is outside its %llu payload bytes",
|
|
entry->name, (unsigned long long)source_offset,
|
|
(unsigned long long)(source_offset + source_bytes),
|
|
(unsigned long long)entry->byte_count);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (chunk_bytes == 0u)
|
|
chunk_bytes = FTTW_DEFAULT_CHUNK_BYTES;
|
|
|
|
source = artifact->base + entry->data_offset + source_offset;
|
|
remaining = source_bytes;
|
|
while (remaining > 0u) {
|
|
uint32_t bytes = (remaining < chunk_bytes) ? (uint32_t)remaining : chunk_bytes;
|
|
int rc = noc->write(noc->user, x, y, (uint64_t)address + offset, source + offset, bytes);
|
|
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_log_error("fttw: uploading %s at +%llu failed: %s", entry->name,
|
|
(unsigned long long)(source_offset + offset),
|
|
ft_tt_error_string(rc));
|
|
return rc;
|
|
}
|
|
offset += bytes;
|
|
remaining -= bytes;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_fttw_upload(const ft_tt_fttw *artifact, const ft_tt_artifact_entry *entry,
|
|
const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t address,
|
|
uint32_t chunk_bytes)
|
|
{
|
|
const unsigned char *source;
|
|
uint64_t remaining;
|
|
uint64_t offset = 0;
|
|
|
|
if (!artifact || !entry || !noc)
|
|
return FT_TT_EINVAL;
|
|
if (entry->data_offset + entry->byte_count > (uint64_t)artifact->bytes) {
|
|
ft_tt_log_error("fttw: %s payload runs past the artifact", entry->name);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (chunk_bytes == 0u)
|
|
chunk_bytes = FTTW_DEFAULT_CHUNK_BYTES;
|
|
|
|
source = artifact->base + entry->data_offset;
|
|
remaining = entry->byte_count;
|
|
while (remaining > 0u) {
|
|
uint32_t bytes = (remaining < chunk_bytes) ? (uint32_t)remaining : chunk_bytes;
|
|
int rc = noc->write(noc->user, x, y, (uint64_t)address + offset, source + offset, bytes);
|
|
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_log_error("fttw: uploading %s at +%llu failed: %s", entry->name,
|
|
(unsigned long long)offset, ft_tt_error_string(rc));
|
|
return rc;
|
|
}
|
|
offset += bytes;
|
|
remaining -= bytes;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_fttw_upload_broadcast_rows(const ft_tt_fttw *artifact,
|
|
const ft_tt_artifact_entry *entry,
|
|
const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
|
|
uint32_t address, void *scratch, size_t scratch_bytes)
|
|
{
|
|
uint16_t replicated[FT_TT_OP_TILE_DIM * FT_TT_OP_TILE_DIM];
|
|
const float *values;
|
|
uint32_t elements;
|
|
uint32_t tile_count;
|
|
uint32_t tile;
|
|
|
|
if (!artifact || !entry || !noc || !scratch)
|
|
return FT_TT_EINVAL;
|
|
if (entry->n_dims != 1u || entry->format != FT_TT_FORMAT_FP32) {
|
|
ft_tt_log_error("fttw: %s is not a 1-D fp32 tensor (dims %u format %u)", entry->name,
|
|
entry->n_dims, entry->format);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (scratch_bytes < FT_TT_OP_TILE_BYTES_BF16)
|
|
return FT_TT_EINVAL;
|
|
if (entry->data_offset + entry->byte_count > (uint64_t)artifact->bytes)
|
|
return FT_TT_EINVAL;
|
|
|
|
values = (const float *)(const void *)(artifact->base + entry->data_offset);
|
|
elements = (uint32_t)entry->dims[0];
|
|
tile_count = ft_tt_tile_count_for(elements);
|
|
|
|
for (tile = 0; tile < tile_count; tile++) {
|
|
uint32_t base_column = tile * FT_TT_OP_TILE_DIM;
|
|
uint32_t column;
|
|
uint32_t row;
|
|
int rc;
|
|
|
|
/* One tile of the broadcast: 32 identical rows of 32 bf16 values, zero-padded
|
|
* where the vector ends. Built row-major, then face-ordered by the same tilize
|
|
* the converter uses so there is one layout in the system, not two. */
|
|
for (column = 0; column < FT_TT_OP_TILE_DIM; column++) {
|
|
uint32_t source_index = base_column + column;
|
|
uint16_t value = 0u;
|
|
|
|
if (source_index < elements)
|
|
value = ft_bf16_from_float(values[source_index]);
|
|
for (row = 0; row < FT_TT_OP_TILE_DIM; row++)
|
|
replicated[row * FT_TT_OP_TILE_DIM + column] = value;
|
|
}
|
|
|
|
if (ft_tt_tilize_strip(replicated, sizeof(uint16_t), FT_TT_OP_TILE_DIM,
|
|
FT_TT_OP_TILE_DIM, FT_TT_OP_TILE_DIM * sizeof(uint16_t),
|
|
scratch) != 0)
|
|
return FT_TT_EINVAL;
|
|
|
|
rc = noc->write(noc->user, x, y, (uint64_t)address + (uint64_t)tile * FT_TT_OP_TILE_BYTES_BF16,
|
|
scratch, FT_TT_OP_TILE_BYTES_BF16);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|