Skip to content

Commit 8baa0d4

Browse files
feat(sdk): support batched prompts in geniex-bench via --- separator
A --prompt-file may now hold multiple prompts separated by a line that is exactly `---`; each segment runs as its own prompt with the KV cache reset between segments, and stdout marks boundaries with `[sep ] prompt i/n`. A file without a `---` line stays a single prompt, so timing and --accuracy runs share one code path. Signed-off-by: RemiliaForever <remilia@koumakan.cc>
1 parent d153362 commit 8baa0d4

1 file changed

Lines changed: 137 additions & 46 deletions

File tree

sdk/benchmark/benchmark.c

Lines changed: 137 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ typedef struct {
9595
int32_t audio_count;
9696

9797
int32_t n_prompt; /* LLM random-ids prefill length (llama-bench -p), used when prompt_buf is NULL */
98-
char* prompt_buf; /* heap-owned text prompt loaded via --prompt-file; NULL = use random-ids */
98+
char* prompt_buf; /* heap-owned text prompt loaded via --prompt-file; NULL = use random-ids.
99+
* Split into multiple prompts on lines that are exactly "---". */
99100
int32_t max_new_tokens;
100101
float temperature;
101102
int32_t seed;
@@ -228,6 +229,12 @@ static void usage(const char* argv0) {
228229
" For qairt, `pp` and prefill tok/s are reported over\n"
229230
" the padded length ceil(pp/128)*128, matching the\n"
230231
" engine's 128-token prefill chunking (#1194).\n"
232+
" Batch prompts by separating them with a line that\n"
233+
" is exactly `---`; each segment runs as its own\n"
234+
" prompt (KV cache reset between segments), delimited\n"
235+
" in stdout by a `[sep ] prompt i/n` marker. A file\n"
236+
" with no `---` line is a single prompt, so this works\n"
237+
" the same in timing and --accuracy runs.\n"
231238
" --no-reset-between-runs\n"
232239
" keep KV cache across measured runs (default is\n"
233240
" to call geniex_llm_reset() before every run so\n"
@@ -560,6 +567,9 @@ static char* resolve_local_anchor(const char* path) {
560567
return best;
561568
}
562569

570+
/* Defined near main(); used by run_llm to trim prompt separator lines. */
571+
static char* rstrip(char* s);
572+
563573
/* Load whole file into a heap buffer (caller frees). Used by --prompt-file
564574
* for plugins that don't support input_ids (qairt). */
565575
static char* slurp(const char* path) {
@@ -941,6 +951,15 @@ static void print_gen_text(const char* text) {
941951

942952
/* ----------------------------- LLM run loop ----------------------------- */
943953

954+
/* Append `seg` to the prompt list unless it is NULL or all whitespace, so
955+
* stray/leading/trailing "---" separators don't produce empty prompts. */
956+
static void append_prompt_if_nonempty(const char** prompts, int32_t* n, char* seg) {
957+
if (!seg) return;
958+
const char* s = seg;
959+
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++;
960+
if (*s) prompts[(*n)++] = seg;
961+
}
962+
944963
static void fill_sampler(geniex_SamplerConfig* s, const options_t* o) {
945964
memset(s, 0, sizeof(*s));
946965
s->temperature = o->temperature;
@@ -989,7 +1008,8 @@ static void run_llm(const options_t* o, const char* device_id, int32_t ngl, run_
9891008
/* Two prefill modes, picked by whether --prompt-file was passed:
9901009
* - prompt_buf != NULL: feed prompt_utf8 verbatim (the plugin tokenizes).
9911010
* `pp` is the tokenizer's count, NOT n_prompt. Required for plugins
992-
* that don't accept input_ids (today: qairt).
1011+
* that don't accept input_ids (today: qairt). The buffer is split into
1012+
* one prompt per "---"-delimited segment (see the loop below).
9931013
* - prompt_buf == NULL: random-ids mode (mirrors llama-bench
9941014
* test_prompt) — query vocab + BOS via geniex_llm_get_model_info,
9951015
* fill n_prompt positions with rand() % vocab_size, overwrite pos 0
@@ -1032,61 +1052,132 @@ static void run_llm(const options_t* o, const char* device_id, int32_t ngl, run_
10321052
fill_sampler(&sampler, o);
10331053
fill_gen_config(&gconfig, &sampler, o, /*with_media=*/false);
10341054

1035-
int32_t total = o->warmup + o->repeat;
1036-
for (int32_t i = 0; i < total; ++i) {
1037-
bool is_warmup = (i < o->warmup);
1038-
int32_t run_idx = is_warmup ? i : (i - o->warmup);
1039-
1040-
if (o->reset_between_runs) {
1041-
check(geniex_llm_reset(llm), "geniex_llm_reset");
1042-
}
1043-
1044-
geniex_LlmGenerateInput gin;
1045-
geniex_LlmGenerateOutput gout;
1046-
memset(&gin, 0, sizeof(gin));
1047-
memset(&gout, 0, sizeof(gout));
1048-
if (o->prompt_buf) {
1049-
gin.prompt_utf8 = o->prompt_buf;
1050-
} else {
1051-
gin.input_ids = tokens;
1052-
gin.input_ids_count = o->n_prompt;
1055+
/* Prompt list for the outer loop:
1056+
* - random-ids mode (prompt_buf == NULL): a single NULL entry.
1057+
* - --prompt-file: split prompt_buf on lines that are exactly "---"
1058+
* (a "\n---\n" separator, ignoring leading/trailing whitespace on that
1059+
* line). No separator => the whole file is one prompt, so the same
1060+
* split works for both accuracy and timing runs without branching on
1061+
* the mode. Multiple prompts reset the KV cache between segments.
1062+
* Each separator line is overwritten with a NUL so the preceding segment
1063+
* ends there; `prompts` points into prompt_buf and is freed here. */
1064+
const char** prompts = NULL;
1065+
int32_t n_prompts = 1;
1066+
if (o->prompt_buf) {
1067+
/* Upper bound: one more segment than newlines. */
1068+
int32_t cap = 1;
1069+
for (char* p = o->prompt_buf; *p; ++p)
1070+
if (*p == '\n') cap++;
1071+
prompts = (const char**)malloc((size_t)cap * sizeof(char*));
1072+
n_prompts = 0;
1073+
1074+
char* seg = o->prompt_buf; /* start of the current segment, NULL at EOF */
1075+
for (char* line = o->prompt_buf; line;) {
1076+
char* nl = strchr(line, '\n');
1077+
if (nl) *nl = '\0'; /* isolate this line for the separator test */
1078+
1079+
/* Is `line` exactly "---" once surrounding whitespace is ignored? */
1080+
char* t = line;
1081+
while (*t == ' ' || *t == '\t' || *t == '\r') t++;
1082+
rstrip(t);
1083+
bool is_sep = (strcmp(t, "---") == 0);
1084+
1085+
if (is_sep) {
1086+
/* End the preceding segment. When it spans real lines (seg <
1087+
* line) terminate at the newline before this separator so the
1088+
* trailing "\n" is dropped; an empty segment (seg == line, e.g.
1089+
* a leading or back-to-back separator) collapses to "". */
1090+
if (line > seg)
1091+
line[-1] = '\0';
1092+
else if (seg)
1093+
*seg = '\0';
1094+
append_prompt_if_nonempty(prompts, &n_prompts, seg);
1095+
seg = nl ? nl + 1 : NULL;
1096+
} else if (nl) {
1097+
*nl = '\n'; /* not a separator: restore so segment text is intact */
1098+
}
1099+
line = nl ? nl + 1 : NULL;
10531100
}
1054-
gin.config = &gconfig;
1055-
gin.on_token = on_token;
1056-
1057-
int32_t rc = geniex_llm_generate(llm, &gin, &gout);
1058-
if (rc != GENIEX_SUCCESS) {
1059-
const char* msg = geniex_get_error_message((geniex_ErrorCode)rc);
1060-
fprintf(stderr, "ERROR: geniex_llm_generate run %d failed: %s (%d)\n", run_idx, msg ? msg : "?", rc);
1101+
/* Trailing segment (the whole file when there is no "---"). */
1102+
append_prompt_if_nonempty(prompts, &n_prompts, seg);
1103+
if (n_prompts == 0) {
1104+
fprintf(stderr, "ERROR: --prompt-file has no non-empty prompts\n");
1105+
free(prompts);
10611106
free(tokens);
10621107
geniex_llm_destroy(llm);
10631108
exit(1);
10641109
}
1110+
} else {
1111+
prompts = (const char**)malloc(sizeof(char*));
1112+
prompts[0] = NULL; /* random-ids */
1113+
}
10651114

1066-
if (!is_warmup) {
1067-
run_result_t* r = &out[run_idx];
1068-
memset(r, 0, sizeof(*r));
1069-
r->run_idx = run_idx;
1070-
r->ttft_us = gout.profile_data.ttft;
1071-
r->prompt_time_us = gout.profile_data.prompt_time;
1072-
r->decode_time_us = gout.profile_data.decode_time;
1073-
r->prompt_tokens = gout.profile_data.prompt_tokens;
1074-
r->gen_tokens = gout.profile_data.generated_tokens;
1075-
r->prefill_tps = gout.profile_data.prefill_speed;
1076-
r->decode_tps = gout.profile_data.decoding_speed;
1077-
r->stop_reason = gout.profile_data.stop_reason;
1078-
r->status = 0;
1079-
normalize_prefill_metrics(r, o->plugin);
1115+
int32_t total = o->warmup + o->repeat;
1116+
for (int32_t pi = 0; pi < n_prompts; ++pi) {
1117+
const char* cur_prompt = prompts[pi];
1118+
if (n_prompts > 1) {
1119+
fprintf(stdout, "[sep ] prompt %d/%d\n", pi + 1, n_prompts);
10801120
}
1121+
for (int32_t i = 0; i < total; ++i) {
1122+
bool is_warmup = (i < o->warmup);
1123+
int32_t run_idx = is_warmup ? i : (i - o->warmup);
1124+
1125+
/* Reset before each run (llama-bench semantics) OR always at the
1126+
* start of a new prompt segment, so batched prompts never inherit
1127+
* the previous segment's KV cache even under
1128+
* --no-reset-between-runs. */
1129+
if (o->reset_between_runs || (n_prompts > 1 && i == 0)) {
1130+
check(geniex_llm_reset(llm), "geniex_llm_reset");
1131+
}
10811132

1082-
if (!is_warmup && o->accuracy && gout.full_text) {
1083-
print_gen_text(gout.full_text);
1084-
}
1085-
if (gout.full_text) {
1086-
geniex_free(gout.full_text);
1133+
geniex_LlmGenerateInput gin;
1134+
geniex_LlmGenerateOutput gout;
1135+
memset(&gin, 0, sizeof(gin));
1136+
memset(&gout, 0, sizeof(gout));
1137+
if (cur_prompt) {
1138+
gin.prompt_utf8 = cur_prompt;
1139+
} else {
1140+
gin.input_ids = tokens;
1141+
gin.input_ids_count = o->n_prompt;
1142+
}
1143+
gin.config = &gconfig;
1144+
gin.on_token = on_token;
1145+
1146+
int32_t rc = geniex_llm_generate(llm, &gin, &gout);
1147+
if (rc != GENIEX_SUCCESS) {
1148+
const char* msg = geniex_get_error_message((geniex_ErrorCode)rc);
1149+
fprintf(stderr, "ERROR: geniex_llm_generate run %d failed: %s (%d)\n", run_idx, msg ? msg : "?", rc);
1150+
free(tokens);
1151+
geniex_llm_destroy(llm);
1152+
exit(1);
1153+
}
1154+
1155+
if (!is_warmup) {
1156+
run_result_t* r = &out[run_idx];
1157+
memset(r, 0, sizeof(*r));
1158+
r->run_idx = run_idx;
1159+
r->ttft_us = gout.profile_data.ttft;
1160+
r->prompt_time_us = gout.profile_data.prompt_time;
1161+
r->decode_time_us = gout.profile_data.decode_time;
1162+
r->prompt_tokens = gout.profile_data.prompt_tokens;
1163+
r->gen_tokens = gout.profile_data.generated_tokens;
1164+
r->prefill_tps = gout.profile_data.prefill_speed;
1165+
r->decode_tps = gout.profile_data.decoding_speed;
1166+
r->stop_reason = gout.profile_data.stop_reason;
1167+
r->status = 0;
1168+
normalize_prefill_metrics(r, o->plugin);
1169+
}
1170+
1171+
if (!is_warmup && o->accuracy && gout.full_text) {
1172+
print_gen_text(gout.full_text);
1173+
}
1174+
if (gout.full_text) {
1175+
geniex_free(gout.full_text);
1176+
}
10871177
}
10881178
}
10891179

1180+
free(prompts);
10901181
free(tokens);
10911182
check(geniex_llm_destroy(llm), "geniex_llm_destroy");
10921183
}

0 commit comments

Comments
 (0)