Spaces:
Running
Running
Dmitry Beresnev commited on
Commit ·
27c1c3c
1
Parent(s): 30c6ca2
Add inference metrics + bench script, tune threads/batch for 2-vCPU HF Space, backoff on readiness polling
Browse files- Dockerfile +10 -3
- config.toml.example +6 -3
- cpp/model_manager.cpp +6 -1
- cpp/runtime_components.cpp +51 -0
- cpp/runtime_components.h +10 -0
Dockerfile
CHANGED
|
@@ -140,6 +140,13 @@ EXPOSE 7860
|
|
| 140 |
# - loads default model at startup
|
| 141 |
# - supports /switch-model runtime model change
|
| 142 |
# - proxies /v1/chat/completions to active worker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
ENV DEFAULT_MODEL=QuantFactory/Qwen2.5-7B-Instruct-GGUF:q4_k_m \
|
| 144 |
MANAGER_HOST=0.0.0.0 \
|
| 145 |
MANAGER_PORT=7860 \
|
|
@@ -149,10 +156,10 @@ ENV DEFAULT_MODEL=QuantFactory/Qwen2.5-7B-Instruct-GGUF:q4_k_m \
|
|
| 149 |
DEFAULT_MAX_TOKENS=2048 \
|
| 150 |
MAX_TOKENS_PER_REQUEST=4096 \
|
| 151 |
MODEL_N_CTX=8192 \
|
| 152 |
-
MODEL_THREADS=
|
| 153 |
MODEL_NGL=0 \
|
| 154 |
-
MODEL_BATCH=
|
| 155 |
-
MODEL_UBATCH=
|
| 156 |
|
| 157 |
CMD ["llm-manager"]
|
| 158 |
#
|
|
|
|
| 140 |
# - loads default model at startup
|
| 141 |
# - supports /switch-model runtime model change
|
| 142 |
# - proxies /v1/chat/completions to active worker
|
| 143 |
+
# Tuned for the free HF Space (~2 vCPU, CPU-only, OpenBLAS build):
|
| 144 |
+
# - MODEL_THREADS matches the vCPU quota; more threads thrash a compute-bound
|
| 145 |
+
# generation loop (was 4).
|
| 146 |
+
# - MODEL_BATCH/UBATCH sized so the OpenBLAS prompt-eval path actually gets
|
| 147 |
+
# batches worth multiplying (was 64/32). RAM headroom (~16 GB vs ~5 GB used)
|
| 148 |
+
# covers the larger compute buffers.
|
| 149 |
+
# Validate any change with tests/bench.py (see PERFORMANCE_REFACTORING_PLAN.md).
|
| 150 |
ENV DEFAULT_MODEL=QuantFactory/Qwen2.5-7B-Instruct-GGUF:q4_k_m \
|
| 151 |
MANAGER_HOST=0.0.0.0 \
|
| 152 |
MANAGER_PORT=7860 \
|
|
|
|
| 156 |
DEFAULT_MAX_TOKENS=2048 \
|
| 157 |
MAX_TOKENS_PER_REQUEST=4096 \
|
| 158 |
MODEL_N_CTX=8192 \
|
| 159 |
+
MODEL_THREADS=2 \
|
| 160 |
MODEL_NGL=0 \
|
| 161 |
+
MODEL_BATCH=256 \
|
| 162 |
+
MODEL_UBATCH=128
|
| 163 |
|
| 164 |
CMD ["llm-manager"]
|
| 165 |
#
|
config.toml.example
CHANGED
|
@@ -14,10 +14,13 @@ switch_timeout_sec = 300
|
|
| 14 |
|
| 15 |
[llama]
|
| 16 |
n_ctx = 8192
|
| 17 |
-
threads
|
|
|
|
|
|
|
|
|
|
| 18 |
ngl = 0
|
| 19 |
-
batch =
|
| 20 |
-
ubatch =
|
| 21 |
|
| 22 |
[auth]
|
| 23 |
header = "Authorization"
|
|
|
|
| 14 |
|
| 15 |
[llama]
|
| 16 |
n_ctx = 8192
|
| 17 |
+
# Set threads to the number of vCPUs actually available; oversubscribing
|
| 18 |
+
# slows compute-bound generation. Batch sizes feed the BLAS prompt-eval
|
| 19 |
+
# path — validate changes with tests/bench.py.
|
| 20 |
+
threads = 2
|
| 21 |
ngl = 0
|
| 22 |
+
batch = 256
|
| 23 |
+
ubatch = 128
|
| 24 |
|
| 25 |
[auth]
|
| 26 |
header = "Authorization"
|
cpp/model_manager.cpp
CHANGED
|
@@ -239,6 +239,10 @@ pid_t ModelManager::spawn_worker(const std::string &model, int port) {
|
|
| 239 |
|
| 240 |
bool ModelManager::wait_until_ready(pid_t pid, int port, int timeout_sec) {
|
| 241 |
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
while (std::chrono::steady_clock::now() < deadline) {
|
| 243 |
if (!is_alive(pid)) return false;
|
| 244 |
try {
|
|
@@ -248,7 +252,8 @@ bool ModelManager::wait_until_ready(pid_t pid, int port, int timeout_sec) {
|
|
| 248 |
if (status == 200) return true;
|
| 249 |
} catch (...) {
|
| 250 |
}
|
| 251 |
-
std::this_thread::sleep_for(
|
|
|
|
| 252 |
}
|
| 253 |
return false;
|
| 254 |
}
|
|
|
|
| 239 |
|
| 240 |
bool ModelManager::wait_until_ready(pid_t pid, int port, int timeout_sec) {
|
| 241 |
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec);
|
| 242 |
+
// Exponential backoff: fast detection when the worker comes up (or dies)
|
| 243 |
+
// quickly, without hammering it during a minutes-long model load.
|
| 244 |
+
auto poll_interval = std::chrono::milliseconds(100);
|
| 245 |
+
const auto max_interval = std::chrono::milliseconds(800);
|
| 246 |
while (std::chrono::steady_clock::now() < deadline) {
|
| 247 |
if (!is_alive(pid)) return false;
|
| 248 |
try {
|
|
|
|
| 252 |
if (status == 200) return true;
|
| 253 |
} catch (...) {
|
| 254 |
}
|
| 255 |
+
std::this_thread::sleep_for(poll_interval);
|
| 256 |
+
poll_interval = std::min(poll_interval * 2, max_interval);
|
| 257 |
}
|
| 258 |
return false;
|
| 259 |
}
|
cpp/runtime_components.cpp
CHANGED
|
@@ -170,6 +170,49 @@ void MetricsRegistry::observe_queue_wait_ms(int64_t value) {
|
|
| 170 |
queue_wait_samples_.fetch_add(1);
|
| 171 |
}
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
std::string MetricsRegistry::render_prometheus(const QueueSnapshot &queue, ModelManager &manager) const {
|
| 174 |
std::ostringstream oss;
|
| 175 |
oss << "llm_manager_requests_total " << requests_total_.load() << '\n';
|
|
@@ -187,6 +230,11 @@ std::string MetricsRegistry::render_prometheus(const QueueSnapshot &queue, Model
|
|
| 187 |
oss << "llm_manager_cancellations_total " << cancellations_total_.load() << '\n';
|
| 188 |
oss << "llm_manager_switch_total " << switch_total_.load() << '\n';
|
| 189 |
oss << "llm_manager_worker_restarts_total " << worker_restarts_total_.load() << '\n';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
const auto active = manager.active_worker();
|
| 191 |
oss << "llm_manager_active_worker " << (active ? 1 : 0) << '\n';
|
| 192 |
return oss.str();
|
|
@@ -315,6 +363,9 @@ void Scheduler::worker_loop() {
|
|
| 315 |
registry_.complete(ctx, RequestState::CANCELLED, {499, R"({"error":"Request cancelled"})"});
|
| 316 |
continue;
|
| 317 |
}
|
|
|
|
|
|
|
|
|
|
| 318 |
registry_.complete(ctx, RequestState::DONE, {status, body});
|
| 319 |
} catch (const std::exception &e) {
|
| 320 |
log_line("request_id=" + ctx->request_id + " scheduler_exception=" + std::string(e.what()));
|
|
|
|
| 170 |
queue_wait_samples_.fetch_add(1);
|
| 171 |
}
|
| 172 |
|
| 173 |
+
void MetricsRegistry::observe_inference(
|
| 174 |
+
uint64_t prompt_ms,
|
| 175 |
+
uint64_t predicted_ms,
|
| 176 |
+
uint64_t prompt_tokens,
|
| 177 |
+
uint64_t predicted_tokens) {
|
| 178 |
+
inference_prompt_ms_total_.fetch_add(prompt_ms);
|
| 179 |
+
inference_predicted_ms_total_.fetch_add(predicted_ms);
|
| 180 |
+
inference_prompt_tokens_total_.fetch_add(prompt_tokens);
|
| 181 |
+
inference_predicted_tokens_total_.fetch_add(predicted_tokens);
|
| 182 |
+
inference_samples_.fetch_add(1);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Pull llama-server's timings/usage out of a completion body so /queue/metrics
|
| 186 |
+
// can report real prompt-eval and generation speed (tokens/sec falls out of
|
| 187 |
+
// predicted_tokens_total / predicted_ms_total).
|
| 188 |
+
static void record_inference_metrics(MetricsRegistry &metrics, const std::string &body) {
|
| 189 |
+
json completion = json::parse(body, nullptr, false);
|
| 190 |
+
if (completion.is_discarded() || !completion.is_object()) return;
|
| 191 |
+
|
| 192 |
+
auto num_or_zero = [](const json &obj, const char *key) -> uint64_t {
|
| 193 |
+
if (!obj.contains(key) || !obj[key].is_number()) return 0;
|
| 194 |
+
const double v = obj[key].get<double>();
|
| 195 |
+
return v > 0 ? static_cast<uint64_t>(v) : 0;
|
| 196 |
+
};
|
| 197 |
+
|
| 198 |
+
uint64_t prompt_ms = 0, predicted_ms = 0, prompt_tokens = 0, predicted_tokens = 0;
|
| 199 |
+
if (completion.contains("timings") && completion["timings"].is_object()) {
|
| 200 |
+
const auto &t = completion["timings"];
|
| 201 |
+
prompt_ms = num_or_zero(t, "prompt_ms");
|
| 202 |
+
predicted_ms = num_or_zero(t, "predicted_ms");
|
| 203 |
+
prompt_tokens = num_or_zero(t, "prompt_n");
|
| 204 |
+
predicted_tokens = num_or_zero(t, "predicted_n");
|
| 205 |
+
}
|
| 206 |
+
if (completion.contains("usage") && completion["usage"].is_object()) {
|
| 207 |
+
const auto &u = completion["usage"];
|
| 208 |
+
if (prompt_tokens == 0) prompt_tokens = num_or_zero(u, "prompt_tokens");
|
| 209 |
+
if (predicted_tokens == 0) predicted_tokens = num_or_zero(u, "completion_tokens");
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
if (prompt_ms == 0 && predicted_ms == 0 && prompt_tokens == 0 && predicted_tokens == 0) return;
|
| 213 |
+
metrics.observe_inference(prompt_ms, predicted_ms, prompt_tokens, predicted_tokens);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
std::string MetricsRegistry::render_prometheus(const QueueSnapshot &queue, ModelManager &manager) const {
|
| 217 |
std::ostringstream oss;
|
| 218 |
oss << "llm_manager_requests_total " << requests_total_.load() << '\n';
|
|
|
|
| 230 |
oss << "llm_manager_cancellations_total " << cancellations_total_.load() << '\n';
|
| 231 |
oss << "llm_manager_switch_total " << switch_total_.load() << '\n';
|
| 232 |
oss << "llm_manager_worker_restarts_total " << worker_restarts_total_.load() << '\n';
|
| 233 |
+
oss << "llm_manager_inference_prompt_ms_total " << inference_prompt_ms_total_.load() << '\n';
|
| 234 |
+
oss << "llm_manager_inference_predicted_ms_total " << inference_predicted_ms_total_.load() << '\n';
|
| 235 |
+
oss << "llm_manager_inference_prompt_tokens_total " << inference_prompt_tokens_total_.load() << '\n';
|
| 236 |
+
oss << "llm_manager_inference_predicted_tokens_total " << inference_predicted_tokens_total_.load() << '\n';
|
| 237 |
+
oss << "llm_manager_inference_samples " << inference_samples_.load() << '\n';
|
| 238 |
const auto active = manager.active_worker();
|
| 239 |
oss << "llm_manager_active_worker " << (active ? 1 : 0) << '\n';
|
| 240 |
return oss.str();
|
|
|
|
| 363 |
registry_.complete(ctx, RequestState::CANCELLED, {499, R"({"error":"Request cancelled"})"});
|
| 364 |
continue;
|
| 365 |
}
|
| 366 |
+
if (status >= 200 && status < 300) {
|
| 367 |
+
record_inference_metrics(metrics_, body);
|
| 368 |
+
}
|
| 369 |
registry_.complete(ctx, RequestState::DONE, {status, body});
|
| 370 |
} catch (const std::exception &e) {
|
| 371 |
log_line("request_id=" + ctx->request_id + " scheduler_exception=" + std::string(e.what()));
|
cpp/runtime_components.h
CHANGED
|
@@ -73,6 +73,11 @@ public:
|
|
| 73 |
void inc_worker_restarts_total();
|
| 74 |
void observe_request_latency_ms(int64_t value);
|
| 75 |
void observe_queue_wait_ms(int64_t value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
std::string render_prometheus(const QueueSnapshot &queue, ModelManager &manager) const;
|
| 77 |
|
| 78 |
private:
|
|
@@ -87,6 +92,11 @@ private:
|
|
| 87 |
std::atomic<uint64_t> cancellations_total_{0};
|
| 88 |
std::atomic<uint64_t> switch_total_{0};
|
| 89 |
std::atomic<uint64_t> worker_restarts_total_{0};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
};
|
| 91 |
|
| 92 |
class PrioritySchedulerQueue {
|
|
|
|
| 73 |
void inc_worker_restarts_total();
|
| 74 |
void observe_request_latency_ms(int64_t value);
|
| 75 |
void observe_queue_wait_ms(int64_t value);
|
| 76 |
+
void observe_inference(
|
| 77 |
+
uint64_t prompt_ms,
|
| 78 |
+
uint64_t predicted_ms,
|
| 79 |
+
uint64_t prompt_tokens,
|
| 80 |
+
uint64_t predicted_tokens);
|
| 81 |
std::string render_prometheus(const QueueSnapshot &queue, ModelManager &manager) const;
|
| 82 |
|
| 83 |
private:
|
|
|
|
| 92 |
std::atomic<uint64_t> cancellations_total_{0};
|
| 93 |
std::atomic<uint64_t> switch_total_{0};
|
| 94 |
std::atomic<uint64_t> worker_restarts_total_{0};
|
| 95 |
+
std::atomic<uint64_t> inference_prompt_ms_total_{0};
|
| 96 |
+
std::atomic<uint64_t> inference_predicted_ms_total_{0};
|
| 97 |
+
std::atomic<uint64_t> inference_prompt_tokens_total_{0};
|
| 98 |
+
std::atomic<uint64_t> inference_predicted_tokens_total_{0};
|
| 99 |
+
std::atomic<uint64_t> inference_samples_{0};
|
| 100 |
};
|
| 101 |
|
| 102 |
class PrioritySchedulerQueue {
|