Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
auto_placement.h
Go to the documentation of this file.
1// SPDX-License-Identifier: LGPL-3.0-or-later
31#pragma once
32
33#include "gguf_metadata.h"
34#include "vram_footprint.h"
35
36#include <cstdint>
37
38namespace entropic {
39
45 bool known = false;
46
57 int gpu_layers = 0;
60 bool fully_resident = false;
61
69 int n_ubatch = 0;
70
81 const char* reason = "";
82};
83
86constexpr uint64_t kComputeBytesAtUbatch512 = 1222ull * 1024ull * 1024ull;
87
89constexpr int kComputeReferenceUbatch = 512;
90
92constexpr int kDefaultUbatch = 512;
93
99constexpr int kUbatchLadder[] = {512, 256, 128};
100
129 int n_ubatch) {
130 const int ubatch = n_ubatch > 0 ? n_ubatch : kDefaultUbatch;
131 const uint64_t contexts = base.draft_bytes > 0 ? 2ull : 1ull;
132 const uint64_t scaled =
134 / static_cast<uint64_t>(kComputeReferenceUbatch) * contexts;
135 // Only the EXCESS over `vram_reserve_mb`. That reserve is already inside
136 // the estimate and vram_footprint.h names covering compute buffers as
137 // its whole purpose, so holding back the full figure on top of it would
138 // charge the same bytes twice — the exact defect this change exists to
139 // remove, reintroduced one layer up. At the default 512 MB reserve and a
140 // 128-token ubatch the two nearly cancel, which is why the 26B-A4B at
141 // IQ2 still comes out fully resident, as measured.
142 const uint64_t reserve =
143 static_cast<uint64_t>(base.vram_reserve_mb) * 1024ull * 1024ull;
144 return scaled > reserve ? scaled - reserve : 0ull;
145}
146
159inline bool placement_fits(const FootprintInputs& base,
160 const GgufShape& shape,
161 int gpu_layers,
162 int cpu_moe_layers,
164 uint64_t* used) {
165 FootprintInputs in = base;
166 // A candidate naming every layer IS full residency; hand the estimator
167 // the sentinel so it takes the full branch and counts the embeddings
168 // and output head, which the partial branch deliberately excludes.
169 in.gpu_layers =
170 (gpu_layers < 0 || gpu_layers >= shape.block_count) ? -1 : gpu_layers;
171 in.block_count = shape.block_count;
172 in.block_bytes = shape.block_bytes;
173 in.non_block_bytes = shape.non_block_bytes;
174 in.host_expert_bytes =
175 shape.expert_bytes_per_block() * static_cast<uint64_t>(cpu_moe_layers);
177 if (used != nullptr) { *used = est.bytes; }
178 return est.known && est.bytes <= budget_bytes;
179}
180
192 const FootprintInputs& base,
194 uint64_t used = 0;
195 if (!placement_fits(base, shape, shape.block_count, 0, budget, &used)) {
196 return false;
197 }
198 out->known = true;
199 out->gpu_layers = -1; // the sentinel, not a count — see AutoPlacement.
200 out->fully_resident = true;
201 out->bytes = used;
202 out->reason = "fully resident";
203 return true;
204}
205
222 const FootprintInputs& base,
224 if (shape.expert_count <= 0 || shape.expert_bytes == 0) { return false; }
225 uint64_t used = 0;
226 for (int moe = 1; moe <= shape.block_count; ++moe) {
228 budget, &used)) {
229 continue;
230 }
231 out->known = true;
232 out->gpu_layers = -1; // all layers; experts are what moved.
233 out->fully_resident = true;
234 out->cpu_moe_layers = moe;
235 out->bytes = used;
236 out->reason = "all layers resident, experts host-side";
237 return true;
238 }
239 return false;
240}
241
253 const FootprintInputs& base,
255 uint64_t used = 0;
256 for (int layers = shape.block_count - 1; layers >= 0; --layers) {
257 if (!placement_fits(base, shape, layers, 0, budget, &used)) {
258 continue;
259 }
260 out->known = true;
261 out->gpu_layers = layers;
262 out->bytes = used;
263 out->reason = "partial offload";
264 return true;
265 }
266 return false;
267}
268
287 const FootprintInputs& base,
288 uint64_t free_vram_bytes, int configured,
289 AutoPlacement* out) {
290 const int current = configured > 0 ? configured : kDefaultUbatch;
291 for (const int ub : kUbatchLadder) {
292 if (ub >= current) { continue; }
294 const uint64_t budget =
296 if (!try_fully_resident(shape, base, budget, out)) { continue; }
297 out->n_ubatch = ub;
298 out->reason = "fully resident at a reduced n_ubatch";
299 return true;
300 }
301 return false;
302}
303
332 const FootprintInputs& base,
334 int n_ubatch = 0) {
335 AutoPlacement out;
336 if (!shape.known || free_vram_bytes == 0) {
337 out.reason = "GGUF shape or free VRAM unknown";
338 return out;
339 }
340 // The headroom the operator asked for is already inside the estimate
341 // (vram_reserve_mb), so it is not subtracted twice — that double-count
342 // is what v2.13.0 shipped.
343 //
344 // What IS subtracted is a compute-buffer allowance, because the
345 // estimator explicitly does not count graph/activation scratch and says
346 // so: llama.cpp sizes it by ubatch and model internals, and
347 // vram_footprint.h measured 1222 MiB for one gemma-4 E4B MTP context at
348 // a 512-token ubatch — more than twice the 512 MB default reserve, and
349 // paid TWICE by a speculative configuration holding two contexts.
350 //
351 // Auto is choosing on the operator's behalf, so it must leave room for
352 // the thing it cannot price rather than hand back a placement that fits
353 // the estimate and fails the load. This is an allowance, not a
354 // derivation; it is deliberately generous, and the cost of being wrong
355 // is a layer left on the host rather than a failed load.
356 const uint64_t allowance = compute_allowance_bytes(base, n_ubatch);
357 const uint64_t budget =
359 // Order is the measurement: ubatch costs ONE axis, experts and layers
360 // cost two. Spend the cheap currency first.
361 if (!try_fully_resident(shape, base, budget, &out)
362 && !try_smaller_ubatch(shape, base, free_vram_bytes, n_ubatch, &out)
363 && !try_expert_offload(shape, base, budget, &out)
364 && !try_fewer_layers(shape, base, budget, &out)) {
365 out.reason = "nothing fits, even with no layers offloaded";
366 return out;
367 }
368 // The budget already has the compute allowance netted out, so this is
369 // the real physical remainder, not an accounting one. Recomputed rather
370 // than threaded back out of four call sites: `try_smaller_ubatch` moves
371 // the budget, so the rung it chose is the one to price against.
373 base, out.n_ubatch > 0 ? out.n_ubatch : n_ubatch);
374 const uint64_t effective =
376 out.margin_bytes = effective > out.bytes ? effective - out.bytes : 0;
377 return out;
378}
379
380} // namespace entropic
long budget
Max content bytes; <= 0 = unbounded.
Read a GGUF's shape without loading its tensors (v2.13.1).
Activate model on GPU (WARM → ACTIVE).
constexpr uint64_t kComputeBytesAtUbatch512
Measured compute scratch for ONE context at a 512-token ubatch (vram_footprint.h: 1222 MiB,...
uint64_t compute_allowance_bytes(const FootprintInputs &base, int n_ubatch)
VRAM to hold back for graph/activation scratch.
FootprintEstimate estimate_vram_footprint(const FootprintInputs &in)
Estimate the VRAM a tier will occupy, or report that it cannot.
@ optional
optional: every escape asks the host's path approver
constexpr int kComputeReferenceUbatch
The ubatch that figure was measured at.
bool try_smaller_ubatch(const GgufShape &shape, const FootprintInputs &base, uint64_t free_vram_bytes, int configured, AutoPlacement *out)
Try full residency at each ubatch below the configured one.
bool try_fully_resident(const GgufShape &shape, const FootprintInputs &base, uint64_t budget, AutoPlacement *out)
Try the best case: every layer and every expert on the card.
constexpr int kUbatchLadder[]
Values auto will step down through, descending.
bool try_expert_offload(const GgufShape &shape, const FootprintInputs &base, uint64_t budget, AutoPlacement *out)
Try keeping every layer resident by moving experts host-side.
bool try_fewer_layers(const GgufShape &shape, const FootprintInputs &base, uint64_t budget, AutoPlacement *out)
Last resort: take the largest layer count that fits.
AutoPlacement derive_auto_placement(const GgufShape &shape, const FootprintInputs &base, uint64_t free_vram_bytes, int n_ubatch=0)
Choose the placement that uses the card best without overcommitting.
constexpr int kDefaultUbatch
llama.cpp's default ubatch, assumed when the tier does not set one.
bool placement_fits(const FootprintInputs &base, const GgufShape &shape, int gpu_layers, int cpu_moe_layers, uint64_t budget_bytes, uint64_t *used)
Does a candidate placement fit the budget?
A placement decision, with the reason it was reached.
bool known
False = could not decide; leave config alone.
const char * reason
Why this placement, for the operator's log.
uint64_t margin_bytes
Free VRAM left over once this placement is loaded, in bytes.
int cpu_moe_layers
Layers whose experts stay host-side.
bool fully_resident
True when gpu_layers is the -1 sentinel.
int gpu_layers
Layers to offload, or -1 meaning ALL.
uint64_t bytes
Estimated resident footprint of the choice.
int n_ubatch
n_ubatch auto chose, or 0 to leave the operator's value alone.
An estimate, or an explicit admission that there isn't one.
Everything the estimate needs, with no orchestrator or filesystem.
int vram_reserve_mb
Configured headroom to leave free.
uint64_t draft_bytes
Draft / MTP head bytes, resident alongside the target.
int block_count
Real layer count from <arch>.block_count.
int gpu_layers
Tier's requested offload (-1 = all).
A GGUF's shape, as far as its metadata states it.
gh#142: a VRAM footprint estimate honest enough to refuse a load on.