server: refactor + correctness fixes for metrics (#26920)
* server: refactor metrics * move most fields to server_slot_stats * cont * rm result_timings * tie stats to batch * cont * nits: move place in code * exclude first generated token * more accurate batch metrics tracking * n_predict --> n_gen * metrics_on_prediction * metrics_flush_idle * metrics: seperate cache/processed prompt tokens * refactor server_task_result_metrics * add test * nits * fix flush before reset() * cont * rm dead code * nits
This commit is contained in:
@@ -334,6 +334,160 @@ json format_response_rerank(
|
||||
std::vector<std::string> & texts,
|
||||
int top_n);
|
||||
|
||||
//
|
||||
// stats and metrics
|
||||
//
|
||||
|
||||
// shared between server_slot and server_task_result_*
|
||||
struct server_slot_stats {
|
||||
uint64_t n_prompt_cached = 0;
|
||||
uint64_t n_prompt_processed = 0;
|
||||
uint64_t n_gen = 0;
|
||||
|
||||
// speculative decoding stats
|
||||
// note: the per-position breakdown lives in server_slot, it is not needed in a task result
|
||||
uint64_t n_draft_tokens = 0;
|
||||
uint64_t n_draft_accepted = 0;
|
||||
uint64_t n_draft_verif_steps = 0;
|
||||
|
||||
// these are absolute timestamps (in us)
|
||||
// note: must be signed - they are subtracted before the later ones are set
|
||||
int64_t t_start = 0;
|
||||
int64_t t_prompt_last = 0;
|
||||
int64_t t_gen_last = 0;
|
||||
|
||||
// can only move one direction: start -> prompt -> gen
|
||||
void update_prompt_start() {
|
||||
GGML_ASSERT(t_start == 0);
|
||||
t_start = ggml_time_us();
|
||||
}
|
||||
void set_prompt_last(int64_t t_us) {
|
||||
GGML_ASSERT(t_start > 0);
|
||||
t_prompt_last = t_us;
|
||||
}
|
||||
void update_prompt_last() {
|
||||
set_prompt_last(ggml_time_us());
|
||||
}
|
||||
void update_gen_last() {
|
||||
GGML_ASSERT(t_prompt_last > 0);
|
||||
t_gen_last = ggml_time_us();
|
||||
}
|
||||
|
||||
// these are time durations
|
||||
int64_t t_elapsed_us() const {
|
||||
return ggml_time_us() - t_start;
|
||||
}
|
||||
double t_prompt_ms() const {
|
||||
if (t_prompt_last == 0) {
|
||||
return 0.0; // the prompt is not processed yet
|
||||
}
|
||||
return (t_prompt_last - t_start) / 1000.0;
|
||||
}
|
||||
int64_t t_gen_us() const {
|
||||
if (t_gen_last == 0) {
|
||||
return 0; // the generation is not started yet
|
||||
}
|
||||
// clamp to 1 us, the first token can land in the same us as t_prompt_last
|
||||
return std::max<int64_t>(1, t_gen_last - t_prompt_last);
|
||||
}
|
||||
double t_gen_ms() const {
|
||||
return t_gen_us() / 1000.0;
|
||||
}
|
||||
|
||||
// number of decode steps spent on generation
|
||||
// the first token is free, it comes from the logits of the last prompt batch
|
||||
uint64_t n_gen_steps() const {
|
||||
return n_gen > 0 ? n_gen - 1 : 0;
|
||||
}
|
||||
|
||||
// other derived metrics
|
||||
// note: all of them return 0.0 if the divisor is not known yet
|
||||
double t_prompt_per_token_ms() const {
|
||||
return n_prompt_processed > 0 ? t_prompt_ms() / n_prompt_processed : 0.0;
|
||||
}
|
||||
double t_gen_per_token_ms() const {
|
||||
return n_gen_steps() > 0 ? t_gen_ms() / n_gen_steps() : 0.0;
|
||||
}
|
||||
double n_prompt_tps() const {
|
||||
const double t_ms = t_prompt_ms();
|
||||
return t_ms > 0.0 ? 1e3 / t_ms * n_prompt_processed : 0.0;
|
||||
}
|
||||
double n_gen_tps() const {
|
||||
const double t_ms = t_gen_ms();
|
||||
return t_ms > 0.0 ? 1e3 / t_ms * n_gen_steps() : 0.0;
|
||||
}
|
||||
|
||||
// false if the slot never started, i.e. the task result carries no stats
|
||||
bool is_set() const {
|
||||
return t_start > 0;
|
||||
}
|
||||
|
||||
json to_json() const;
|
||||
};
|
||||
|
||||
// shared between server_context_impl and server_task_result_*
|
||||
// unlike server_slot_stats, server_metrics is server-global and cumulative, not tied to a slot
|
||||
struct server_metrics {
|
||||
int64_t t_start = 0;
|
||||
|
||||
struct bucket {
|
||||
uint64_t count = 0; // number of tokens
|
||||
uint64_t steps = 0; // number of decode steps,
|
||||
// this excludes first generated token (logits from prompt batch)
|
||||
uint64_t time = 0; // in microseconds
|
||||
|
||||
// the rate uses the decode steps, so that "free" tokens do not inflate it
|
||||
double n_per_second() const {
|
||||
return time > 0 ? (double) steps / (double) time * 1e6 : 0.0;
|
||||
}
|
||||
|
||||
void add(uint64_t n, uint64_t n_steps, uint64_t t_us) {
|
||||
count += n;
|
||||
steps += n_steps;
|
||||
time += t_us;
|
||||
}
|
||||
};
|
||||
|
||||
// these are reset by reset_bucket(), only the rate is read from them
|
||||
bucket prompt_bucket;
|
||||
bucket predict_bucket;
|
||||
|
||||
// metrics below are cumulative since the server started
|
||||
bucket prompt; // only processed tokens, cached ones are counted separately below
|
||||
bucket predict;
|
||||
|
||||
// tokens reused from the cache need no decode, so they only have a count
|
||||
uint64_t n_prompt_cached = 0;
|
||||
|
||||
uint64_t n_tokens_max = 0;
|
||||
|
||||
uint64_t n_decode = 0;
|
||||
uint64_t n_busy_slots = 0;
|
||||
|
||||
uint64_t n_draft_tokens = 0; // Total draft tokens generated
|
||||
uint64_t n_draft_accepted = 0; // Draft tokens actually accepted
|
||||
uint64_t n_draft_verif_steps = 0; // Total draft token verification steps by the target model
|
||||
std::vector<uint64_t> n_accepted_per_pos; // Accepted tokens per draft position
|
||||
|
||||
void init() {
|
||||
t_start = ggml_time_us();
|
||||
}
|
||||
|
||||
void reset_bucket() {
|
||||
prompt_bucket = {};
|
||||
predict_bucket = {};
|
||||
}
|
||||
|
||||
void add_prompt(uint64_t n_tokens, uint64_t t_us) {
|
||||
prompt .add(n_tokens, n_tokens, t_us);
|
||||
prompt_bucket.add(n_tokens, n_tokens, t_us);
|
||||
}
|
||||
|
||||
void add_prompt_cached(uint64_t n_tokens) {
|
||||
n_prompt_cached += n_tokens;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// other utils
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user