mirror of
https://github.com/Heretek-AI/Strata.git
synced 2026-11-07 10:05:58 -10:00
Inference engine, OpenAI/Anthropic API on localhost, optional image input, one-file setup (START-HERE.bat / setup.sh). Model files are downloaded from Hugging Face by the setup.
355 lines
22 KiB
C++
355 lines
22 KiB
C++
// include/strata/core/expert_source.hpp - P2.S3/P2.S5: the expert pool's adapter to the host loop.
|
|
//
|
|
// `session_loop` publishes, per layer, the NORMED activation `x_f`, the ten routed expert ids and their router
|
|
// weights, and expects `k x n_embd` floats back. This is the piece that turns those four things into an
|
|
// `ExpertPool::run` call. Nothing here is clever, and that is the point: the pool, the kernel and the loop are
|
|
// each already verified, so this file has exactly one job - get the CONTRACT between them right.
|
|
//
|
|
// THE CONTRACT, and each clause is a way to be wrong:
|
|
//
|
|
// 1. **`x_f` IS THE SAME PINNED BUFFER EVERY LAYER.** Its ADDRESS never changes, so the `ActQ` cannot be
|
|
// cached by pointer - a cache keyed on `x_f` would quantize layer 0 and reuse it for all 47 remaining
|
|
// layers, which is a finite, plausible, completely wrong token. There is no cache here at all: the
|
|
// conversion is one 2560-element pass against a 0.3 ms/layer budget, and a correct answer is worth more
|
|
// than the microseconds.
|
|
// 2. **THE POOL DOES NOT APPLY THE ROUTER WEIGHT.** `moe_combine` (`src/core/layer.cpp:479`) sums
|
|
// `w[i] * parts[i]` on the device. `ExpertJob::weight` is a diagnostic field; setting it here would
|
|
// apply the weight twice, which is invisible in a single layer and compounds over 48.
|
|
// 3. **THE KERNEL'S GEOMETRY IS FIXED BY THE ARTIFACT** (`H = 2560`, `FF = 640`, `BLOB = 1,382,400`). A
|
|
// different `n_embd` or a different expert width is REFUSED rather than mis-indexed, because the blob's
|
|
// internal offsets are compile-time constants and reading a 640-wide expert as a 2560-wide one walks off
|
|
// the end into the next expert's bytes without faulting.
|
|
#pragma once
|
|
|
|
#include "strata/core/expert_cache.hpp"
|
|
#include "strata/core/hit_hook.hpp"
|
|
#include "strata/kernels/cpu/pool.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace strata::core {
|
|
|
|
/// Where one routed expert's bytes come from.
|
|
///
|
|
/// Phase 2 has NO cache (`phase-2-correct-engine.md`: hit rate `h = 0`), so the only implementation is a
|
|
/// file-backed reader. The interface exists anyway because Phase 3 replaces exactly this object with the VRAM
|
|
/// cache, and because a test can supply an in-memory source without a 34 GB artifact.
|
|
class ExpertSource {
|
|
public:
|
|
virtual ~ExpertSource() = default;
|
|
|
|
/// The 1,382,400-byte blob for `(layer, expert)`, or nullptr if it cannot be produced.
|
|
///
|
|
/// The pointer only has to stay valid until the next `blob()` call: with `h = 0` every expert is computed
|
|
/// immediately and nothing is retained. A CACHING source must return pointers into the cache, not into a
|
|
/// reused staging buffer - otherwise the pool would read the next expert's bytes while computing this one.
|
|
virtual const uint8_t* blob(int64_t layer, int64_t expert) = 0;
|
|
|
|
/// Blobs touched, for the driver to report. A source that does not count returns 0.
|
|
virtual int64_t reads() const { return 0; }
|
|
|
|
/// Plan v0.3 P5: whether `blob(layer, expert)` lies in page-locked, CUDA-registered memory, so an
|
|
/// asynchronous host-to-device copy can DMA it directly (no staging copy on the CPU).
|
|
virtual bool pinned(int64_t layer, int64_t expert) const { (void) layer; (void) expert; return false; }
|
|
|
|
/// Called once before the first expert of a layer. A source that reads from disk wants to start the read
|
|
/// here so it overlaps the quantisation, and a prefetching source in Phase 3 wants the ids.
|
|
virtual void begin_layer(int64_t layer, const int32_t* ids, int64_t k) { (void) layer; (void) ids; (void) k; }
|
|
/// Plan v0.3 P6: the DEVICE address of a pinned, mapped blob (the GPU can read it over PCIe), or null.
|
|
virtual const uint8_t* device_alias(int64_t layer, int64_t expert) const { (void) layer; (void) expert; return nullptr; }
|
|
};
|
|
|
|
/// Plan v0.3 P6: what the GPU computes in a verify window's layer, written by the pool (mapped host memory) right
|
|
/// after the ring and published before the CPU starts its own share. Groups of entries that share a blob; the
|
|
/// blob is a VRAM slot or a pinned host blob read over PCIe.
|
|
struct GpuPlanSink {
|
|
int32_t* counts = nullptr; ///< [0] groups, [1] entries
|
|
int32_t* start = nullptr; ///< cap + 1
|
|
int32_t* dst = nullptr; ///< cap: the entry's row of parts (token * k + j)
|
|
int32_t* tok = nullptr; ///< cap: the entry's token
|
|
unsigned long long* ptr = nullptr; ///< cap: the group's blob, device address
|
|
/// Plan v0.3 P6 (DMA): the PCIe share is a second list of groups - `ptr2[q]` is staging slot q, `start2` indexes
|
|
/// the same dst/tok entries - computed by the GPU after `fetch` has copied their blobs into staging with the
|
|
/// copy engine. counts[0] = VRAM groups, counts[1] = all entries, counts[2] = PCIe groups.
|
|
unsigned long long* ptr2 = nullptr;
|
|
int32_t* start2 = nullptr;
|
|
unsigned long long staging = 0;
|
|
int64_t staging_cap = 0;
|
|
int64_t cap = 0;
|
|
void (*publish)(void* ctx) = nullptr;
|
|
/// Starts the DMA copies of `n` host blobs (pinned) into staging slots 0..n-1 and signals the GPU when they land.
|
|
void (*fetch)(void* ctx, const uint8_t* const* src, int n, size_t bytes) = nullptr;
|
|
void* ctx = nullptr;
|
|
/// Plan v0.3 P6: how a PCIe group reaches the GPU. 0 = the copy engine stages it (DMA, `fetch`); 1 = the grouped
|
|
/// kernel reads the mapped arena directly; 2 = a copy kernel stages it inside the graph. For 1 and 2 `ptr2`
|
|
/// holds the arena's device alias.
|
|
int pcie_mode = 0;
|
|
};
|
|
|
|
/// The adapter's own state. One per session, reused every layer so the token path allocates nothing (P2.T10).
|
|
struct ExpertDispatch {
|
|
strata::kernels::cpu::ExpertPool* pool = nullptr;
|
|
ExpertSource* src = nullptr;
|
|
int64_t n_expert = strata::kernels::cpu::NE;
|
|
|
|
/// Counters, for the driver to report rather than for control flow.
|
|
int64_t layers = 0;
|
|
int64_t experts = 0;
|
|
int64_t missing = 0;
|
|
|
|
// ================================ R4: THE VRAM TIER, MEASURED BEFORE IT IS USED ================================
|
|
//
|
|
// **THE CACHE IS CONSULTED AND FILLED HERE, AND NOTHING IS COMPUTED FROM IT YET.** That is deliberate and
|
|
// it is `R4-design-note.md` §7 step 2: the *dispatch* is measured on its own before any kernel is written,
|
|
// because a hit rate measured after the kernel exists cannot say whether a disappointing result was the
|
|
// policy, the split or the kernel.
|
|
//
|
|
// So every expert is still computed by the CPU, exactly as before, and the run is numerically identical
|
|
// with the cache on or off. What changes is that the engine now reports **h on its own routing, on a real
|
|
// workload** - which is the number `R4.1` asks for and which until now existed only from offline traces.
|
|
//
|
|
// The policy is compulsory-miss: the first time `(layer, expert)` is routed, if a slot is free it is
|
|
// admitted and filled from the arena. No eviction, because eviction policy is the measured question
|
|
// (`R4.1`'s LFU-decay vs LRU sweep) and a placeholder would set the hit rate everything is sized against.
|
|
ExpertCache* cache = nullptr;
|
|
int64_t cache_hits = 0; ///< lookups already resident
|
|
int64_t cache_admitted = 0; ///< lookups that took a slot
|
|
int64_t cache_refused = 0; ///< lookups with no slot free (the cache is full)
|
|
void* cache_stream = nullptr;
|
|
const char* cache_fail = nullptr;
|
|
|
|
// ================================ R4.2c: THE HITS GO TO THE GPU ================================
|
|
//
|
|
// **THE SPLIT IS BY ROUTER INDEX, AND THAT IS THE WHOLE TRICK.** A layer routes ten experts; the resident
|
|
// ones are computed on the GPU and the rest on the CPU, and both answers have to end up in `parts` at the
|
|
// index the ROUTER gave them, because that is the order `moe_combine` weights against. So the CPU's `out`
|
|
// rows are zeroed for the hits, and each hit carries its routed index to the kernel as `dst`.
|
|
//
|
|
// Everything below is per-session state rather than per-call, so the token path allocates nothing (P2.T10).
|
|
const uint8_t* cache_base = nullptr; ///< the slot arena on the DEVICE
|
|
int64_t cache_blob = 0; ///< bytes per slot
|
|
const uint64_t* cache_slot_off = nullptr; ///< plan v0.3 P6: per-slot offsets when the slots differ in size
|
|
void* hit_scratch = nullptr; ///< `moe_hit_grouped_scratch_bytes(K, ...)`
|
|
float* parts_out = nullptr; ///< the graph's `parts` buffer, on the device
|
|
/// Where the GPU's hits land, `K x n_embd`, DEVICE and separate from `parts_out` on purpose: see
|
|
/// `HitPhase`. Zeroed by the hit path each layer before the kernel writes it.
|
|
float* hit_out = nullptr;
|
|
int64_t parts_elems = 0; ///< `K * n_embd`, the length of both buffers
|
|
const float* mixed = nullptr; ///< the layer's normed activation, for the hit kernel's Q8_0
|
|
uint8_t* x_q8_0_hit = nullptr; ///< `(n_embd/32) * 34` bytes, its own buffer
|
|
/// **R4.2h: THE CPU's fp32 ACTIVATION SCALES, `(n_embd/32)` FLOATS.** Without them the GPU's hits are
|
|
/// computed with the `block_q8_0`'s fp16 `d` while the CPU's misses use the fp32 `ActQ::scale`
|
|
/// (`cpu/expert.cpp:92`) - **4.761e-04 relative on 80 of 80 chunks**, measured with both real
|
|
/// implementations linked in `bench/micro/act_quant_parity.cu`. That disagreement is why turning the
|
|
/// cache on changed the generated tokens. Required whenever the hit path runs.
|
|
float* x_q8_0_hit_scale = nullptr;
|
|
int32_t* d_slot = nullptr; ///< device, K entries
|
|
int32_t* d_dst = nullptr; ///< device, K entries
|
|
std::vector<int32_t> h_slot, h_dst; ///< host staging, sized at session setup
|
|
/// **PER ROUTER INDEX, DECIDED IN `Launch` AND CONSUMED BY THE POOL.** The two callbacks share it
|
|
/// so the decision is made exactly once, on this layer's ids, and neither side can re-decide it.
|
|
std::vector<uint8_t> is_hit;
|
|
bool decided = false;
|
|
/// Plan v0.3 P4 token graph: the STATIC residency table (`n_layers x n_expert`, slot or -1), the host's copy
|
|
/// of what the device hit path reads. When set, the pool leaves a resident expert's row at zero (the GPU
|
|
/// computes it) without any `Launch` callback.
|
|
const int32_t* host_res = nullptr;
|
|
/// Plan v0.3 P4: split every expert by rows across the pool's threads (default on; A/B `--no-split-rows`).
|
|
bool split_rows = true;
|
|
|
|
// ================================ R4.2d: DID THE GPU ACTUALLY START? ================================
|
|
//
|
|
// **THE HIT PATH IS 0.91 ms SLOWER AND THE ONLY EXPLANATION LEFT IS THAT IT NEVER OVERLAPS.** The kernel
|
|
// is 159 GB/s against the CPU's 35, the CPU's half of the drain falls 6 ms, and none of it reaches the
|
|
// token - which is what it would look like if the enqueued hit kernels did not BEGIN until the host next
|
|
// entered the driver, i.e. after `pool()` returned. That is the third sighting of this driver behaving
|
|
// that way (rounds 195/287 on the doorbell, round 36 on the head and sampler) and it decides whether R4 can
|
|
// pay at all, so it gets measured rather than argued.
|
|
//
|
|
// `hit_done` is recorded on the stream straight after the hit kernel. `Combine` - which runs after the
|
|
// pool - queries it: SUCCESS means the GPU finished while the CPU was working, NOT-READY means it had not.
|
|
// Same shape as the doorbell's `rings_mid_graph`, and for the same reason.
|
|
void* hit_done = nullptr; ///< cudaEvent_t, created at session setup
|
|
int64_t hit_ready = 0; ///< layers where the hit work was DONE by the time the pool returned
|
|
int64_t hit_late = 0; ///< layers where it was not
|
|
/// The candidate fix, as an A/B arm: enter the driver once right after the hit launch. If submission is
|
|
/// lazy, this starts the GPU work before the pool instead of after it.
|
|
bool hit_poke = false;
|
|
bool hit_cpu_order = false; ///< experimental CPU-order GPU expert arithmetic; opt-in only
|
|
int64_t n_hits = 0; ///< this layer's hits
|
|
/// Set by `Launch` and consumed by `Combine`, so a `Combine` with no `Launch` in front of it cannot
|
|
/// add a stale buffer into `parts`.
|
|
bool hit_pending = false;
|
|
const char* hit_fail = nullptr;
|
|
|
|
/// Whether the hit path is wired up. All of it or none of it: a half-configured hit path would compute
|
|
/// some experts twice and others not at all, which is a wrong token rather than an error.
|
|
bool hits_ready() const {
|
|
return cache != nullptr && cache_base != nullptr && parts_out != nullptr && hit_out != nullptr &&
|
|
mixed != nullptr &&
|
|
hit_scratch != nullptr && x_q8_0_hit != nullptr && x_q8_0_hit_scale != nullptr && d_slot != nullptr && d_dst != nullptr;
|
|
}
|
|
|
|
std::vector<strata::kernels::cpu::ExpertJob> jobs;
|
|
strata::kernels::cpu::ActQ act;
|
|
/// Plan v0.3 P6 verify window: one quantized activation per token, the multi-token jobs, and the expert ->
|
|
/// job map (reset after every layer).
|
|
std::vector<strata::kernels::cpu::ActQ> act_multi;
|
|
/// Plan v0.3 P6: a native pack's per-token activations (MAXT x kNativeActBytes).
|
|
std::vector<uint8_t> nact_multi;
|
|
std::vector<strata::kernels::cpu::ExpertJobMulti> jobs_multi;
|
|
std::vector<int16_t> job_of;
|
|
/// Plan v0.3 P6: the verify window's GPU plan (VRAM hits + the PCIe share of the misses); `pcie_num`/256 of
|
|
/// each layer's distinct missed experts (the last ones in routing order) are read by the GPU over PCIe.
|
|
GpuPlanSink* plan = nullptr;
|
|
int pcie_num = 0;
|
|
int64_t pcie_experts = 0; ///< distinct experts the GPU read over PCIe in verify windows
|
|
double ms_plan = 0, ms_actq = 0, ms_jobs = 0, ms_run = 0; ///< verify-window dispatch sections
|
|
/// Plan v0.3 P6: decayed routing counts per (layer, expert) during decode (sized by the caller; empty = off),
|
|
/// which the driver uses to swap the most-routed missing experts into the VRAM tier between rounds.
|
|
std::vector<float> usage;
|
|
int64_t multi_misses = 0; ///< distinct (layer, expert) pairs the CPU computed in verify windows
|
|
int64_t multi_entries = 0; ///< routed (token, expert) entries the CPU served in verify windows
|
|
/// Set when `dispatch` could not produce an answer. The loop itself has no error channel, so this is
|
|
/// where a source failure surfaces: the driver checks it after `session_loop` returns rather than the
|
|
/// engine computing from a half-filled `parts`.
|
|
///
|
|
/// **`failed` LATCHES AND `fail` IS ONLY THE MESSAGE.** Clearing the message must not re-arm the adapter:
|
|
/// a session that failed at layer 5 has already fed `moe_combine` whatever `parts` held, so every layer
|
|
/// after it is built on a hole - and resuming into a plausible-looking token is the exact outcome this
|
|
/// whole mechanism exists to prevent. `failed` is what the short-circuit reads.
|
|
bool failed = false;
|
|
const char* fail = nullptr;
|
|
int64_t fail_layer = -1;
|
|
int64_t fail_expert = -1;
|
|
};
|
|
|
|
/// `strata::core::PoolFn`, exactly. Silent on failure BY SIGNATURE - see `ExpertDispatch::fail`.
|
|
void expert_pool_dispatch(void* user, const float* x_f, const int32_t* ids, const float* weights, int64_t n_embd,
|
|
int64_t k, float* out);
|
|
|
|
/// Plan v0.3 P6: the pool for a verify window of `n_tok` tokens. `x_f` is (n_tok, n_embd), `ids` (n_tok, k) and
|
|
/// `out` (n_tok * k, n_embd). Each distinct missed expert is computed once for all the tokens routed to it;
|
|
/// resident experts' rows are zeroed (the GPU adds them). Requires `host_res` (the token-graph residency).
|
|
void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32_t* ids, int64_t n_tok, int64_t k,
|
|
float* out);
|
|
|
|
/// **THE HITS, LAUNCHED AFTER THE MISSES ARE STAGED AND BEFORE `post[l]`.** Same shape as `PoolFn` and for the
|
|
/// same reason: `session_loop` owns the ORDER and this owns the work, so the loop needs to know nothing about
|
|
/// expert caches. Returns immediately when there are no hits, which is every layer until the cache is warm.
|
|
///
|
|
/// It must run AFTER the host has copied the misses into `parts_dev` (it writes into the same buffer, on rows
|
|
/// the CPU zeroed) and BEFORE `post[l]` (which reads it). Both are stream-ordered on the loop's own stream.
|
|
void expert_hit_run(void* user, void* stream, HitPhase phase, const int32_t* ids, int64_t k);
|
|
/// The pool half of the same decision; see `ExpertDispatch::is_hit`.
|
|
|
|
/// **PHASE 2'S ONLY SOURCE: `experts.bin`, memory-mapped, no cache.**
|
|
///
|
|
/// `experts.bin` is 33,973,862,400 B and `BLOB` is 1,382,400, so it holds exactly `48 x 512 = 24,576` blobs and
|
|
/// the index is `layer * 512 + expert` with NO padding. A mapping is therefore the whole implementation: the
|
|
/// blob pointer is base plus a multiply, and the page fault that follows is the read.
|
|
///
|
|
/// **AND THAT IS NOT AS SLOW AS IT SOUNDS, WHICH IS THE POINT.** The expert set is 34 GB and this machine has
|
|
/// 64 GB of DDR5, so a warm OS page cache holds ALL of it - the second token onward is a DRAM read at the
|
|
/// measured 44.14 GB/s, not a disk read. The first token's 663.6 MB comes off the disk and is the cold-path
|
|
/// number; `benches` should report the two separately rather than blending them.
|
|
///
|
|
/// IT IS NOT AN LRU, AN LFU OR A PREFETCHER. Phase 3 replaces this object with those. Phase 2 is hit rate
|
|
/// `h = 0` on purpose, so that the CPU path and the host round trip are exercised on every layer of every
|
|
/// token - which is the only way they get debugged before speed matters.
|
|
class FileExpertSource : public ExpertSource {
|
|
public:
|
|
FileExpertSource() = default;
|
|
~FileExpertSource() override;
|
|
FileExpertSource(const FileExpertSource&) = delete;
|
|
FileExpertSource& operator=(const FileExpertSource&) = delete;
|
|
|
|
/// Maps `<pack_dir>/experts.bin` and checks its size against `n_layers * n_expert * BLOB`.
|
|
///
|
|
/// The size check is not a formality: a short file would fault at the END of a long sequence, and an
|
|
/// over-long one means the pack is not the one the geometry came from. Refuses with the two numbers.
|
|
bool open(const std::string& pack_dir, int64_t n_layers, int64_t n_expert, std::string& err);
|
|
void close();
|
|
|
|
bool mapped() const { return base_ != nullptr; }
|
|
int64_t blobs() const { return blobs_; }
|
|
|
|
const uint8_t* blob(int64_t layer, int64_t expert) override;
|
|
|
|
/// Blobs touched, for the driver to report. With `h = 0` this is `48 * k` per token and the number is only
|
|
/// interesting once Phase 3 makes it not so.
|
|
int64_t reads() const override { return reads_; }
|
|
|
|
private:
|
|
const uint8_t* base_ = nullptr;
|
|
int64_t blobs_ = 0;
|
|
int64_t n_expert_ = 0;
|
|
int64_t reads_ = 0;
|
|
#if defined(_WIN32)
|
|
void* file_ = nullptr;
|
|
void* mapping_ = nullptr;
|
|
#else
|
|
int fd_ = -1;
|
|
#endif
|
|
};
|
|
|
|
// ================================ THE RESIDENT ARENA (R2.1) ================================
|
|
//
|
|
// **THE MMAP ABOVE IS THE REVIEW'S FINDING C1 AND IT IS WORTH 2.2x.** Read the comment on `FileExpertSource`
|
|
// about the page cache holding all 34 GB: that is true of the expert file ALONE, and it is not what this engine
|
|
// does. The PLE/n-gram shard is a 26.8 GB file that the engine also maps, so 34 GB of experts plus 26.8 GB of
|
|
// n-gram is 60.8 GB of mapped, file-backed pages on a 63 GB machine - and file-backed pages are exactly the ones
|
|
// the OS drops from the standby list when it wants memory. The expert stream then re-faults from disk.
|
|
//
|
|
// Measured, this round: the pool runs at **~19 GB/s in the engine against 42.8 GB/s in `bench/micro/cpu_s2.cpp`
|
|
// on the same machine, reading the same 34 GB**. The micro does `std::fread` into a heap arena and reads
|
|
// ordinary (anonymous, resident) memory; the engine reads `MapViewOfFile`. That is the whole difference, and it
|
|
// is why this class exists.
|
|
//
|
|
// It reads `experts.bin` into a `PinnedArena` once at startup, so the expert stream comes from anonymous memory
|
|
// the OS has no cheaper reason to evict. `PinnedArena` also tries `cudaHostRegister`, which the GPU needs for
|
|
// Phase 3's cache fills and the CPU/PCIe miss split - but registration is best-effort and reported, not assumed.
|
|
class ArenaExpertSource : public ExpertSource {
|
|
public:
|
|
ArenaExpertSource() = default;
|
|
~ArenaExpertSource() override;
|
|
ArenaExpertSource(const ArenaExpertSource&) = delete;
|
|
ArenaExpertSource& operator=(const ArenaExpertSource&) = delete;
|
|
|
|
/// Allocates and loads `<pack_dir>/experts.bin`. Prints nothing; the caller reports `note()` and the load
|
|
/// rate, because those are the two numbers that say whether the arena is the one that was asked for.
|
|
bool open(const std::string& pack_dir, int64_t n_layers, int64_t n_expert, int threads, std::string& err);
|
|
/// Plan v0.3 P6: a native pack without experts.bin takes its experts from the model's shard 1.
|
|
void set_gguf(const std::string& shard1) { gguf_ = shard1; }
|
|
void close();
|
|
|
|
bool mapped() const { return base_ != nullptr; }
|
|
int64_t blobs() const { return blobs_; }
|
|
const uint8_t* blob(int64_t layer, int64_t expert) override;
|
|
int64_t reads() const { return reads_; }
|
|
bool pinned(int64_t layer, int64_t expert) const override;
|
|
const uint8_t* device_alias(int64_t layer, int64_t expert) const override;
|
|
|
|
/// What backing was obtained and why, for the startup print. "The engine adapts to the machine it is on" is
|
|
/// only true if the engine says what it got.
|
|
const std::string& note() const { return note_; }
|
|
double load_gib_per_second() const { return gib_per_s_; }
|
|
|
|
private:
|
|
void* arena_ = nullptr; ///< the PinnedArena, owned
|
|
std::vector<const uint8_t*> dev_slice_; ///< device alias of each registered slice (or of the whole range)
|
|
uint64_t slice_bytes_ = 0;
|
|
const uint8_t* base_ = nullptr;
|
|
int64_t blobs_ = 0;
|
|
int64_t n_expert_ = 0;
|
|
int64_t reads_ = 0;
|
|
std::string note_;
|
|
double gib_per_s_ = 0.0;
|
|
uint64_t pinned_bytes_ = 0;
|
|
std::string gguf_;
|
|
};
|
|
|
|
} // namespace strata::core
|