227 lines
8.2 KiB
C
227 lines
8.2 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/* True per-user capacity bench — REALISTIC steady-state with prefix caching ON.
|
|
*
|
|
* The old synthetic bench cold-prefilled a fixed ~100-token prompt every step.
|
|
* This measures what the campaign is actually about: a SHARED/GROWING context
|
|
* per user (a fixed system prompt + a context that grows each turn) served
|
|
* through the real batched path with prefix caching ON, so per-turn prefill is
|
|
* a cheap DELTA against the session's warm slot (delta-prefill, warm KV+SSM) —
|
|
* i.e. steady-state per-user speed, not cold re-prefill.
|
|
*
|
|
* K users run concurrently through the batch scheduler, each with its own
|
|
* session_id, each doing WARM_TURNS growing turns. Turn 0 is a cold prefill
|
|
* (untimed warm-up); turns 1..WARM_TURNS-1 are timed steady-state (delta
|
|
* prefill + decode). Reports per-user tok/s (total decoded tokens / wall time,
|
|
* averaged over the K concurrent users) and aggregate tok/s (K users' summed
|
|
* decode throughput over the wall clock).
|
|
*
|
|
* Build (CUDA host): make USE_CUDA=1 tests/bench_serve_prefix_cache
|
|
* Run: QWEN35_BATCH_SLOTS=1 QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=0 \
|
|
* QWEN35_GPU_DIE_B=1 QWEN35_FUSE=0x37 \
|
|
* ./tests/bench_serve_prefix_cache <model.gguf> <K> [gen_per_turn] [warm_turns]
|
|
*/
|
|
#include "../include/pantheon.h"
|
|
#include "../src/core/engine.h"
|
|
#include "../src/serve/batch_sched.h"
|
|
#include "../src/serve/serve_generation.h"
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#ifndef USE_CUDA
|
|
int main(void) { fprintf(stderr, "built without USE_CUDA\n"); return 1; }
|
|
#else
|
|
|
|
#define MAX_K 8
|
|
#define MAX_TURNS 8
|
|
#define SYS_PROMPT "You are a concise, helpful assistant answering follow-up questions in a running conversation."
|
|
|
|
static double now_ms(void)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1e3 + (double)ts.tv_nsec / 1e6;
|
|
}
|
|
|
|
/* A generic follow-up that keeps the context growing turn over turn. */
|
|
static const char *turn_line(int t)
|
|
{
|
|
static const char *lines[MAX_TURNS] = {
|
|
"Explain what a hash table is in two sentences.",
|
|
"Now give one real-world use for it.",
|
|
"What is its average lookup complexity?",
|
|
"And the worst case?",
|
|
"How does chaining differ from open addressing?",
|
|
"Which is more cache-friendly?",
|
|
"Summarize the trade-off in one line.",
|
|
"Give a one-word verdict.",
|
|
};
|
|
return lines[t % MAX_TURNS];
|
|
}
|
|
|
|
typedef struct {
|
|
pn_model *model;
|
|
pn_batch_sched *sched;
|
|
int user;
|
|
int gen_per_turn;
|
|
int warm_turns; /* total turns; turn 0 untimed warm-up */
|
|
char sid[32];
|
|
/* results */
|
|
int timed_tokens; /* decode tokens over timed turns */
|
|
double timed_ms; /* wall time over timed turns (this user) */
|
|
int rc;
|
|
} bctx;
|
|
|
|
/* Growing conversation held as an sg_message array (system + user/assistant
|
|
* pairs). */
|
|
typedef struct {
|
|
sg_message msgs[1 + 2 * MAX_TURNS];
|
|
int n;
|
|
char *owned[2 * MAX_TURNS];
|
|
int n_owned;
|
|
} convo;
|
|
|
|
static void convo_init(convo *c)
|
|
{
|
|
memset(c, 0, sizeof(*c));
|
|
c->msgs[0].role = "system";
|
|
c->msgs[0].content = SYS_PROMPT;
|
|
c->n = 1;
|
|
}
|
|
static void convo_user(convo *c, const char *line)
|
|
{
|
|
c->msgs[c->n].role = "user"; c->msgs[c->n].content = line; c->n++;
|
|
}
|
|
static void convo_asst(convo *c, const char *reply)
|
|
{
|
|
char *cp = strdup(reply ? reply : "");
|
|
c->owned[c->n_owned++] = cp;
|
|
c->msgs[c->n].role = "assistant"; c->msgs[c->n].content = cp; c->n++;
|
|
}
|
|
static void convo_free(convo *c)
|
|
{
|
|
for (int i = 0; i < c->n_owned; i++) free(c->owned[i]);
|
|
}
|
|
|
|
static void *worker(void *arg)
|
|
{
|
|
bctx *b = arg;
|
|
convo c;
|
|
convo_init(&c);
|
|
sg_gen_params gp;
|
|
memset(&gp, 0, sizeof(gp));
|
|
gp.temperature = 0.0f; gp.top_p = 1.0f; gp.top_k = 0;
|
|
gp.max_tokens = b->gen_per_turn;
|
|
|
|
b->timed_tokens = 0;
|
|
b->timed_ms = 0.0;
|
|
|
|
for (int t = 0; t < b->warm_turns; t++) {
|
|
convo_user(&c, turn_line(t));
|
|
sg_result res;
|
|
memset(&res, 0, sizeof(res));
|
|
int slot = bs_slot_acquire_biased(b->sched, b->sid);
|
|
if (slot < 0) { b->rc = -100; convo_free(&c); return NULL; }
|
|
double t0 = now_ms();
|
|
int rc = sg_run_completion_batched(b->model, b->sched, slot, c.msgs,
|
|
c.n, &gp, NULL, NULL, &res, b->sid);
|
|
double dt = now_ms() - t0;
|
|
if (rc != 0) { b->rc = rc; sg_result_free(&res); convo_free(&c); return NULL; }
|
|
/* Turn 0 is the cold prefill warm-up — not timed. Turns 1.. are the
|
|
* steady-state warm-cache turns the bench reports. */
|
|
if (t > 0) {
|
|
b->timed_tokens += res.completion_tokens;
|
|
b->timed_ms += dt;
|
|
}
|
|
convo_asst(&c, res.content);
|
|
sg_result_free(&res);
|
|
}
|
|
convo_free(&c);
|
|
b->rc = 0;
|
|
return NULL;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
if (argc < 3) {
|
|
fprintf(stderr, "usage: %s <model.gguf> <K> [gen_per_turn] [warm_turns]\n",
|
|
argv[0]);
|
|
return 1;
|
|
}
|
|
pn_model *model = pn_load(argv[1]);
|
|
if (!model) { fprintf(stderr, "load failed\n"); return 1; }
|
|
|
|
int K = atoi(argv[2]);
|
|
if (K < 1) K = 1;
|
|
if (K > MAX_K) K = MAX_K;
|
|
int gen_per_turn = argc > 3 ? atoi(argv[3]) : 64;
|
|
if (gen_per_turn < 4) gen_per_turn = 4;
|
|
int warm_turns = argc > 4 ? atoi(argv[4]) : 4;
|
|
if (warm_turns < 2) warm_turns = 2;
|
|
if (warm_turns > MAX_TURNS) warm_turns = MAX_TURNS;
|
|
|
|
pn_batch_sched *sched = bs_create(model, K < MAX_K ? K : MAX_K);
|
|
if (!sched) { fprintf(stderr, "bs_create failed\n"); return 1; }
|
|
|
|
fprintf(stderr, "bench: K=%d gen/turn=%d warm_turns=%d (turn0 untimed) "
|
|
"driver_sample=%d\n", K, gen_per_turn, warm_turns,
|
|
bs_driver_sampling(sched) ? 1 : 0);
|
|
|
|
pthread_t th[MAX_K];
|
|
bctx b[MAX_K];
|
|
memset(b, 0, sizeof(b));
|
|
double wall0 = now_ms();
|
|
for (int u = 0; u < K; u++) {
|
|
b[u].model = model; b[u].sched = sched; b[u].user = u;
|
|
b[u].gen_per_turn = gen_per_turn; b[u].warm_turns = warm_turns;
|
|
snprintf(b[u].sid, sizeof(b[u].sid), "bench-%d", u);
|
|
pthread_create(&th[u], NULL, worker, &b[u]);
|
|
}
|
|
for (int u = 0; u < K; u++) pthread_join(th[u], NULL);
|
|
double wall_ms = now_ms() - wall0;
|
|
|
|
int total_tokens = 0;
|
|
double sum_per_user_tps = 0.0;
|
|
int ok = 1;
|
|
for (int u = 0; u < K; u++) {
|
|
if (b[u].rc != 0) { fprintf(stderr, "user %d rc=%d\n", u, b[u].rc); ok = 0; }
|
|
double u_tps = b[u].timed_ms > 0 ? b[u].timed_tokens * 1000.0 / b[u].timed_ms : 0.0;
|
|
sum_per_user_tps += u_tps;
|
|
total_tokens += b[u].timed_tokens;
|
|
fprintf(stderr, " user %d: %d timed tok, %.0f ms, %.2f tok/s\n",
|
|
u, b[u].timed_tokens, b[u].timed_ms, u_tps);
|
|
}
|
|
if (!ok) { bs_destroy(sched); return 1; }
|
|
|
|
/* Per-user tok/s = mean of the K concurrent users' steady-state rates.
|
|
* Aggregate tok/s = all timed decode tokens over the concurrent wall time
|
|
* of the timed turns. Wall time here spans all turns incl. turn 0; for a
|
|
* clean aggregate use the summed per-user rate (the users run concurrently
|
|
* so their steady-state rates sum), which is the standard batched-agg
|
|
* definition and matches the T4 A/B table's "agg tok/s". */
|
|
double per_user_tps = sum_per_user_tps / K;
|
|
double agg_tps = sum_per_user_tps; /* K concurrent users' summed rate */
|
|
|
|
/* Cost /1M tok: per-card active power 0.33 kW, rate $0.12491/kWh.
|
|
* $/1M = (1e6 / agg_tps) s / 3600 * 0.33 kW * $0.12491. */
|
|
double cost_per_1m = agg_tps > 0
|
|
? (1.0e6 / agg_tps) / 3600.0 * 0.33 * 0.12491 : 0.0;
|
|
|
|
fprintf(stderr, "\n=== K=%d per-user %.2f tok/s aggregate %.2f tok/s "
|
|
"cost/1M $%.4f (wall %.0f ms, %d timed tok) ===\n",
|
|
K, per_user_tps, agg_tps, cost_per_1m, wall_ms, total_tokens);
|
|
/* Machine-parseable line for the fill script. */
|
|
printf("PCBENCH K=%d per_user=%.2f agg=%.2f cost1m=%.4f\n",
|
|
K, per_user_tps, agg_tps, cost_per_1m);
|
|
|
|
bs_destroy(sched);
|
|
fflush(stdout); fflush(stderr);
|
|
if (!getenv("QWEN35_EXIT_CLEAN"))
|
|
_Exit(0);
|
|
return 0;
|
|
}
|
|
#endif
|