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 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=4 \
153
  MODEL_NGL=0 \
154
- MODEL_BATCH=64 \
155
- MODEL_UBATCH=32
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 = 4
 
 
 
18
  ngl = 0
19
- batch = 128
20
- ubatch = 64
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(std::chrono::milliseconds(800));
 
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 {