@@ -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). */
565575static 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+
944963static 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