187 lines
8.7 KiB
C
187 lines
8.7 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file ft_tt_artifact_cache.h
|
|
* @brief GGUF in, a card-ready FTTW artifact out — produced once, cached beside the
|
|
* checkpoint, and never asked about again.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* THE REQUIREMENT THIS EXISTS FOR
|
|
* ---------------------------------------------------------------------------
|
|
* A user downloads a GGUF and our software makes it run. They never see the words
|
|
* "Bfp8_b" or "FTTW", never pick a rung, and are never told their file is the wrong
|
|
* format.
|
|
*
|
|
* What cannot be removed: the card cannot consume GGUF layout. Tensix eats 32x32 tiles in
|
|
* four 16x16 faces and the matmul kernels read a block-float the unpacker decodes
|
|
* natively, so SOME conversion always happens. This makes it invisible and cached — a
|
|
* shader cache, not a decision. The Tensix program blob is the precedent: produce once,
|
|
* cache, use thereafter.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* THE THREE ANSWERS IT CAN GIVE, AND WHY NONE OF THEM IS A SILENT FALLBACK
|
|
* ---------------------------------------------------------------------------
|
|
* - **hit** — a cached artifact whose own trailer matches this checkpoint and these
|
|
* settings. Nothing is converted and nothing is uploaded twice.
|
|
* - **produced** — no artifact, or one whose trailer disagreed. The disagreement is
|
|
* NAMED in `note` before the reconversion, because "reconverting" with no reason is how
|
|
* a cache that never hits looks exactly like a cache that works.
|
|
* - **refused** — the model does not fit at any rung the card can load, or the converter
|
|
* cannot classify it. Both carry the arithmetic. A refusal here means the caller stays
|
|
* on the CPU or stops; it must never mean "convert it anyway and hope".
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WRITES ARE ATOMIC, AND THAT IS LOAD-BEARING
|
|
* ---------------------------------------------------------------------------
|
|
* Conversion writes `<artifact>.tmp.<pid>` and rename()s it into place. A converter killed
|
|
* halfway leaves a temp file, never a short artifact at the cache path — so the next
|
|
* launch finds "no artifact" and converts, rather than finding a plausible one and serving
|
|
* a truncated model.
|
|
*
|
|
* @status None
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#ifndef FT_TT_ARTIFACT_CACHE_H
|
|
#define FT_TT_ARTIFACT_CACHE_H
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#include "ft_tt_artifact_provenance.h"
|
|
#include "ft_tt_capacity_plan.h"
|
|
#include "ft_tt_weight_convert.h"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/** Longest artifact path this library will build. */
|
|
#define FT_TT_ARTIFACT_CACHE_PATH_BYTES 1024u
|
|
|
|
/** Room for one explanatory sentence. */
|
|
#define FT_TT_ARTIFACT_CACHE_NOTE_BYTES 512u
|
|
|
|
/** Status codes. Each one means a different thing for the caller to do. */
|
|
enum {
|
|
FT_TT_ARTIFACT_CACHE_OK = 0,
|
|
FT_TT_ARTIFACT_CACHE_ERROR = -1,
|
|
FT_TT_ARTIFACT_CACHE_EINVAL = -3,
|
|
FT_TT_ARTIFACT_CACHE_EIO = -4,
|
|
/** The checkpoint could not be opened or read as GGUF. */
|
|
FT_TT_ARTIFACT_CACHE_ESOURCE = -5,
|
|
/** No rung the card can load leaves room for this model. `reason` has the arithmetic. */
|
|
FT_TT_ARTIFACT_CACHE_ENOFIT = -11,
|
|
/** The converter would refuse this checkpoint: unclassified 2-D tensors. */
|
|
FT_TT_ARTIFACT_CACHE_EUNCLASSIFIED = -9,
|
|
/** A conversion was needed, was attempted, and failed. */
|
|
FT_TT_ARTIFACT_CACHE_ECONVERT = -14
|
|
};
|
|
|
|
/** How the conversion should report itself while it runs. */
|
|
typedef enum {
|
|
FT_TT_CACHE_PROGRESS_SILENT = 0,
|
|
/** A percent/rate/ETA line on stderr: a live bar on a terminal, a decile line in a log. */
|
|
FT_TT_CACHE_PROGRESS_STDERR = 1
|
|
} ft_tt_cache_progress_style;
|
|
|
|
/** What to convert, where to keep it, and what it has to fit inside. */
|
|
typedef struct {
|
|
const char *gguf_path; /**< the checkpoint; any part of a split set */
|
|
const char *cache_dir; /**< NULL or "" keeps the artifact beside the checkpoint */
|
|
ft_tt_capacity_budget budget;
|
|
/** An ft_tt_rope_pairing to force, or a negative value to resolve it from the
|
|
* checkpoint. The product path leaves it negative: which pairing is CORRECT is a
|
|
* property of the checkpoint, not a preference, and asking the user would be asking
|
|
* them a question about a defect they have never heard of. */
|
|
int rope_pairing;
|
|
/** An ft_tt_ledger_mode to force, or a negative value to select automatically. Forcing
|
|
* is an instrument (and how a bfp4 arm is measured on a model that fits at bfp8); the
|
|
* product path leaves it negative and never asks the user. */
|
|
int forced_mode;
|
|
ft_tt_cache_progress_style progress_style;
|
|
ft_tt_convert_progress_fn progress; /**< overrides `progress_style` when set */
|
|
void *progress_user;
|
|
} ft_tt_artifact_cache_request;
|
|
|
|
/** What happened, in enough detail to print one honest line about it. */
|
|
typedef struct {
|
|
char artifact_path[FT_TT_ARTIFACT_CACHE_PATH_BYTES];
|
|
ft_tt_capacity_selection selection;
|
|
ft_tt_artifact_provenance provenance;
|
|
uint64_t source_bytes;
|
|
ft_tt_rope_pairing rope_pairing; /**< the pairing that was resolved and used */
|
|
const char *rope_reason; /**< a static string: why that pairing */
|
|
int produced; /**< 1 = converted during this call, 0 = the cache answered */
|
|
int had_artifact; /**< 1 = a file was already at the cache path when this call started */
|
|
char note[FT_TT_ARTIFACT_CACHE_NOTE_BYTES]; /**< why the cached artifact was rejected;
|
|
* meaningful only when `had_artifact` */
|
|
char reason[FT_TT_ARTIFACT_CACHE_NOTE_BYTES]; /**< the refusal, on a non-OK return */
|
|
} ft_tt_artifact_cache_result;
|
|
|
|
/**
|
|
* The path a checkpoint's artifact is cached at: the checkpoint's name with `.gguf`
|
|
* replaced by `.<rung>.fttw`.
|
|
*
|
|
* The rung is IN THE NAME so two rungs of one checkpoint can coexist — which is what makes
|
|
* a bfp4-vs-bfp8 comparison one command instead of a file shuffle — and so a human reading
|
|
* a directory can see what each artifact is without a tool.
|
|
*
|
|
* @param gguf_path the checkpoint
|
|
* @param cache_dir NULL/"" for beside the checkpoint, else a directory
|
|
* @param mode the rung
|
|
* @param out destination
|
|
* @param out_bytes its size
|
|
* @return FT_TT_ARTIFACT_CACHE_OK, or EINVAL when the path does not fit
|
|
*
|
|
* Thread-safe: yes Reentrant: yes Signal-safe: no
|
|
*/
|
|
int ft_tt_artifact_cache_path(const char *gguf_path, const char *cache_dir,
|
|
ft_tt_ledger_mode mode, char *out, size_t out_bytes);
|
|
|
|
/**
|
|
* The rotary pairing a checkpoint needs, decided from the checkpoint.
|
|
*
|
|
* Phase 7 established that every GGUF `llama`-family checkpoint stores q/k INTERLEAVED and
|
|
* that converting it that way costs +22.1% perplexity on chat-templated prompts, which is
|
|
* every request a serve receives. Split-half is the fix — but it is WRONG for the `qwen`
|
|
* family, which already stores split-half, and it is UNDEFINED for a checkpoint that does
|
|
* not declare its head geometry. So the answer is a property of the file.
|
|
*
|
|
* @param map an open GGUF map
|
|
* @param family from ft_tt_ledger_family_from_architecture()
|
|
* @param architecture the `general.architecture` string, for the metadata key prefix
|
|
* @param out_reason receives a static string naming why; may be NULL
|
|
* @return the pairing to convert with
|
|
*
|
|
* Thread-safe: yes Reentrant: yes Signal-safe: no
|
|
*/
|
|
ft_tt_rope_pairing ft_tt_artifact_cache_rope_pairing(const ft_gguf_map *map,
|
|
ft_tt_model_family family,
|
|
const char *architecture,
|
|
const char **out_reason);
|
|
|
|
/**
|
|
* Make sure a card-ready artifact for this checkpoint exists, and say which one it is.
|
|
*
|
|
* Opens the checkpoint, chooses the rung, checks the cache by the cached artifact's own
|
|
* bytes, converts if it must, and returns the path to serve. The caller never chooses a
|
|
* format and never learns one exists unless it prints `result.selection`.
|
|
*
|
|
* @param request what to convert and what it must fit inside
|
|
* @param result zeroed then filled; `reason` is set on every non-OK return
|
|
* @return FT_TT_ARTIFACT_CACHE_OK and a usable `result.artifact_path`, or one of
|
|
* the status codes above
|
|
*
|
|
* Thread-safe: no (writes files) Reentrant: no Signal-safe: no
|
|
*/
|
|
int ft_tt_artifact_cache_ensure(const ft_tt_artifact_cache_request *request,
|
|
ft_tt_artifact_cache_result *result);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* FT_TT_ARTIFACT_CACHE_H */
|