Files
llama.cpp-xrip/tools/server/server-models-sched.h
85cc432e2c server: add router device memory margin parameter for dynamic unloading (#21231)
* server: add --models-memory-max parameter to allow dynamically unloading models when they exceed a memory size threshold

estimate with to-be-loaded model size included

use no_alloc to get memory requirements for model load

only set model memory_mb if not previously calculated

use memory margin instead of total size limit, apply to each device separately

add server memory debug logging

move llama_context_device_memory function to llama-ext.h

fix model count exceeded check

improve memory_per_device map naming

improve variable naming, fix style

also strip models memory margin from child processes

cont : clean-up

replace device memory map with buft memory map. Use llama_get_memory_breakdown

extract duplicated check into helper function

move model memory estimation to subprocess

precompute name->buft map, map GPU host types to CPU buft

cleanup unused variable

remove duplicated init calls

* adapt to upstream changes

* wip

* common_memory_reports internface

* SERVER_ROLE_MEASURE

* server-models-sched

* fix build

* server : fix memory accounting of measure children and claim spin

A measure child runs no_alloc, so the free memory it reports does not
include its estimate: subtracting self there erased the memory held by
other processes, down to zero with --fit on, and the next load could
overshoot the margin. Subtract self only for instance reports.

A queued load only checked --models-max before claiming, so when the
memory margin blocked it behind a busy model, load() threw and the
waiter retried in a tight loop. Refuse the claim while the limits are
exceeded and no idle model can be unloaded, so the waiter sleeps on
the condition variable until one goes idle.

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Co-authored-by: Pascal <admin@serveurperso.com>
2026-10-27 12:14:42 +01:00

182 lines
7.6 KiB
C++

#pragma once
#include "server-common.h"
#include <deque>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
// when to load and unload model instances: request queue (--models-max) and memory accounting (--models-memory-margin)
// see "Memory-based unloading" in the README
struct server_models;
// memory of a model instance per device, in bytes; from a measure child or from the running instance
struct server_model_memory {
struct device_t {
std::string kind; // "device", "host" or "buffer_type"
std::string name; // ggml device name, "Host" for host memory
size_t total = 0; // device memory, 0 unless kind == "device"
size_t free = 0; // free device memory when this was measured, 0 unless kind == "device"
size_t model = 0; // held by the instance, by kind of buffer
size_t context = 0;
size_t compute = 0;
bool is_device() const { return kind == "device"; }
size_t self() const { return model + context + compute; }
};
uint32_t n_ctx = 0; // context size this was measured at
std::vector<device_t> devices;
server_model_memory() = default;
server_model_memory(uint32_t n_ctx, const std::vector<struct common_memory_breakdown_row> & rows);
json to_json() const;
static server_model_memory from_json(const json & j); // throws on a malformed input
};
// memory measurement state of a model
enum server_model_memory_status {
SERVER_MODEL_MEMORY_UNMEASURED, // queued for a measure child
SERVER_MODEL_MEMORY_MEASURING, // its measure child is running
SERVER_MODEL_MEMORY_MEASURED,
SERVER_MODEL_MEMORY_FAILED, // measure failed, no memory limit for this model
};
static std::string server_model_memory_status_to_string(server_model_memory_status status) {
switch (status) {
case SERVER_MODEL_MEMORY_UNMEASURED: return "unmeasured";
case SERVER_MODEL_MEMORY_MEASURING: return "measuring";
case SERVER_MODEL_MEMORY_MEASURED: return "measured";
case SERVER_MODEL_MEMORY_FAILED: return "failed";
default: return "unknown";
}
}
struct server_model_memory_info {
server_model_memory_status status = SERVER_MODEL_MEMORY_UNMEASURED;
int n_attempts = 0; // to limit the retries
std::optional<server_model_memory> min = std::nullopt; // at the smallest context --fit may pick
std::optional<server_model_memory> max = std::nullopt; // at the largest context --fit picks, a snapshot
std::optional<server_model_memory> effective = std::nullopt; // what the running instance allocated, reset when it exits
bool is_settled() const {
return status == SERVER_MODEL_MEMORY_MEASURED || status == SERVER_MODEL_MEMORY_FAILED;
}
json to_json() const;
};
// functions taking lk need models.mutex held by lk
struct server_models_sched {
server_models_sched(server_models & models) : models(models) {}
//
// request queue (--models-max)
//
bool has_capacity(std::unique_lock<std::mutex> & lk);
// returns "" if no model can be given up
std::string pick_victim(std::unique_lock<std::mutex> & lk);
// requests wanting the same model share one entry, so they all need only one slot
// and all get unblocked by the single load that entry performs
void join(std::unique_lock<std::mutex> & lk, const std::string & model_id);
void leave(std::unique_lock<std::mutex> & lk, const std::string & model_id);
bool queue_empty(std::unique_lock<std::mutex> & lk);
// true if it is this model's turn to load, and nobody is loading it yet
bool try_claim(std::unique_lock<std::mutex> & lk, const std::string & model_id);
// on failure the entry is back in line; on success it stays until its waiters leave,
// so the model coming up is never picked as a victim before they use it
void claim_done(std::unique_lock<std::mutex> & lk, const std::string & model_id, bool ok);
// evict idle models while queued requests outnumber the slots that are free or being freed
// never blocks, so it is safe from any thread
void tick(std::unique_lock<std::mutex> & lk);
//
// loading a model
//
// true if loading name now exceeds models_max or the memory margin
bool limits_exceeded(std::unique_lock<std::mutex> & lk, const std::string & name, bool verbose = true);
// wait for the measure of name, then unload idle LRU models until it fits; call without models.mutex
void make_room(const std::string & name);
//
// memory accounting (--models-memory-margin), the router never touches a GPU
// track what each instance holds, child reports only calibrate the rest: works for per-process (Metal) and system-wide (CUDA) free memory
//
bool memory_enabled() const;
// queue a measure child for every model not measured yet
void measure_unmeasured(std::unique_lock<std::mutex> & lk);
// count a new instance at its min until it reports
void on_spawned(std::unique_lock<std::mutex> & lk, const std::string & name);
// memory: what the instance allocated, nullopt on wake up from sleep
void on_ready(std::unique_lock<std::mutex> & lk, const std::string & name, const std::optional<server_model_memory> & memory);
// a sleeping instance frees its model and context
void on_sleeping(std::unique_lock<std::mutex> & lk, const std::string & name);
void on_unloaded(std::unique_lock<std::mutex> & lk, const std::string & name);
// stop the measure child and drop the queue, for shutdown
void cancel_measurements(std::unique_lock<std::mutex> & lk);
bool is_measuring(std::unique_lock<std::mutex> & lk);
// called from the monitor thread, take models.mutex themselves
void handle_measured(const std::string & name, const json & payload);
void on_measure_exit(const std::string & name, const std::shared_ptr<server_subproc> & proc, int exit_code);
private:
struct entry_t {
std::string model_id;
int n_waiters; // requests waiting for this model
bool loading; // one of the waiters is doing the load right now
};
struct device_memory_t {
int64_t total = 0; // bytes
int64_t external = 0; // bytes not tracked by the router: other processes, backend contexts
};
server_models & models;
// requests waiting for a slot
std::deque<entry_t> queue;
// one measure child runs at a time
std::deque<std::string> measure_queue;
std::string measuring; // model whose measure child is running, empty when idle
std::shared_ptr<server_subproc> measure_proc;
std::map<std::string, device_memory_t> device_memory; // by device name, empty until the first child reports
std::map<std::string, std::map<std::string, size_t>> held; // model -> device -> bytes counted for its instance
entry_t * find(const std::string & model_id);
void check_lock(std::unique_lock<std::mutex> & lk);
size_t count_running();
// all below need models.mutex
void enqueue_measure(const std::string & name, bool front = false);
void measure_next();
// update device_memory from a child report; reporter: the instance that sent it, empty for a measure child
void calibrate(const server_model_memory & mem, const std::string & reporter);
void reserve(const std::string & name); // count the instance's memory (effective, else min)
void release(const std::string & name);
int64_t available(const std::string & dev) const; // total - external - what the instances hold
// bytes missing per device to load name at its min, empty if it fits
std::map<std::string, size_t> deficit(const std::string & name) const;
};