11diff --git a/common/common.cpp b/common/common.cpp
2- index 8f13217..fc0a164 100644
2+ index 8f13217..fc584e1 100644
33--- a/common/common.cpp
44+++ b/common/common.cpp
5- @@ -1591,7 +1591,9 @@ struct llama_context_params common_context_params_to_llama(const common_params &
5+ @@ -1591,8 +1591,10 @@ struct llama_context_params common_context_params_to_llama(const common_params &
66 auto cparams = llama_context_default_params();
77
88 cparams.n_ctx = params.n_ctx;
99- cparams.n_seq_max = params.n_parallel;
10+ - cparams.n_rs_seq = params.speculative.need_n_rs_seq();
1011+ // score-task forks need seq ids (and recurrent-state cells) of their
1112+ // own beyond the parallel slots
1213+ cparams.n_seq_max = params.n_parallel + params.n_seq_score_forks;
13- cparams.n_rs_seq = params.speculative.need_n_rs_seq();
14+ + cparams.n_rs_seq = std::max( params.speculative.need_n_rs_seq(), (uint32_t) std::max(0, params.n_rs_seq) );
1415 cparams.n_outputs_max = std::max(params.n_outputs_max, 0);
1516 cparams.n_batch = params.n_batch;
17+ cparams.n_ubatch = params.n_ubatch;
1618diff --git a/common/common.h b/common/common.h
17- index 1535317..8cdd664 100644
19+ index bffc176..e313bd6 100644
1820--- a/common/common.h
1921+++ b/common/common.h
20- @@ -454 ,6 +454,8 @@ struct common_params {
22+ @@ -455 ,6 +455,9 @@ struct common_params {
2123 int32_t n_keep = 0; // number of tokens to keep from initial prompt
2224 int32_t n_chunks = -1; // max number of chunks to process (-1 = unlimited)
2325 int32_t n_parallel = 1; // number of parallel sequences to decode
2426+ int32_t n_seq_score_forks = 0; // extra seq ids beyond n_parallel, reserved for server score-task forks
27+ + int32_t n_rs_seq = 0; // recurrent-state rollback snapshots per seq (hybrid models cannot rewind without them; lets score tasks reuse a cached prompt across probe changes)
2528+ bool score_enabled = false; // reserve server resources for the Score task type
2629 int32_t n_sequences = 1; // number of sequences to decode
2730 int32_t n_outputs_max = 0; // max outputs in a batch (0 = n_batch)
2831 int32_t grp_attn_n = 1; // group-attention factor
32+ diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
33+ index 780df32..1d2fe8f 100644
34+ --- a/tools/CMakeLists.txt
35+ +++ b/tools/CMakeLists.txt
36+ @@ -41,3 +41,4 @@ else()
37+ add_subdirectory(fit-params)
38+ add_subdirectory(results)
39+ endif()
40+ + add_subdirectory(grpc-server)
2941diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp
30- index 98d0cca..ac9bcac 100644
42+ index 715477e..de5bed8 100644
3143--- a/tools/server/server-context.cpp
3244+++ b/tools/server/server-context.cpp
3345@@ -49,7 +49,16 @@ static uint32_t server_n_outputs_max(const common_params & params) {
@@ -48,7 +60,7 @@ index 98d0cca..ac9bcac 100644
4860
4961 return std::max<uint32_t>(1, std::min<uint64_t>(n_batch, n_outputs));
5062 }
51- @@ -202,6 +206 ,26 @@ struct server_slot {
63+ @@ -202,6 +211 ,26 @@ struct server_slot {
5264
5365 std::vector<completion_token_output> generated_token_probs;
5466
@@ -75,7 +87,7 @@ index 98d0cca..ac9bcac 100644
7587 bool has_next_token = true;
7688 bool has_new_line = false;
7789 bool truncated = false;
78- @@ -317 ,6 +341 ,10 @@ struct server_slot {
90+ @@ -311 ,6 +340 ,10 @@ struct server_slot {
7991 }
8092 generated_tokens.clear();
8193 generated_token_probs.clear();
@@ -86,7 +98,7 @@ index 98d0cca..ac9bcac 100644
8698 json_schema = json();
8799
88100 // clear speculative decoding stats
89- @@ -2208 ,6 +2236 ,229 @@ private:
101+ @@ -2205 ,6 +2238 ,229 @@ private:
90102 queue_results.send(std::move(res));
91103 }
92104
@@ -316,17 +328,17 @@ index 98d0cca..ac9bcac 100644
316328 //
317329 // Functions to process the task
318330 //
319- @@ -2324 ,6 +2575 ,7 @@ private:
331+ @@ -2341 ,6 +2597 ,7 @@ private:
320332 case SERVER_TASK_TYPE_INFILL:
321333 case SERVER_TASK_TYPE_EMBEDDING:
322334 case SERVER_TASK_TYPE_RERANK:
323335+ case SERVER_TASK_TYPE_SCORE:
324336 {
325337 // special case: if input is provided via CLI, tokenize it first
326338 // otherwise, no need to tokenize as it's already done inside the HTTP thread
327- @@ -2796,6 +3048,13 @@ private:
339+ @@ -2832,6 +3089,13 @@ private:
340+ break; // stop any further processing
328341 }
329-
330342 }
331343+
332344+ try {
@@ -338,7 +350,7 @@ index 98d0cca..ac9bcac 100644
338350 }
339351
340352 void pre_decode() {
341- @@ -3118 ,6 +3377 ,16 @@ private:
353+ @@ -3154 ,6 +3418 ,16 @@ private:
342354 n_past = std::min(n_past, slot.alora_invocation_start - 1);
343355 }
344356
@@ -355,7 +367,7 @@ index 98d0cca..ac9bcac 100644
355367 const auto n_cache_reuse = slot.task->params.n_cache_reuse;
356368
357369 const bool can_cache_reuse =
358- @@ -3359 ,8 +3628 ,12 @@ private:
370+ @@ -3395 ,8 +3669 ,12 @@ private:
359371
360372 bool do_checkpoint = params_base.n_ctx_checkpoints > 0;
361373
@@ -370,7 +382,7 @@ index 98d0cca..ac9bcac 100644
370382
371383 // make a checkpoint of the parts of the memory that cannot be rolled back.
372384 // checkpoints are created only if:
373- @@ -3427 ,10 +3700 ,17 @@ private:
385+ @@ -3463 ,10 +3741 ,17 @@ private:
374386 // embedding requires all tokens in the batch to be output;
375387 // MTP also wants logits at every prompt position so the
376388 // streaming hook can mirror t_h_nextn into ctx_dft.
@@ -389,7 +401,7 @@ index 98d0cca..ac9bcac 100644
389401 slot.prompt.tokens.push_back(cur_tok);
390402
391403 slot.n_prompt_tokens_processed++;
392- @@ -3445 ,6 +3725,25 @@ private:
404+ @@ -3481 ,6 +3766,32 @@ private:
393405 }
394406 }
395407
@@ -399,8 +411,15 @@ index 98d0cca..ac9bcac 100644
399411+ // point where this task diverged from the previous cache: after a
400412+ // forced re-prefill a checkpoint there serves the next scoring call
401413+ // over the same stable prefix (e.g. a classifier's option list).
414+ + // The caller-declared stable-prefix boundary is the strongest of
415+ + // these: a checkpoint there is at or before every future task's
416+ + // divergence within the same option list, so it always survives
417+ + // and always restores.
402418+ if (do_checkpoint && slot.task->type == SERVER_TASK_TYPE_SCORE &&
403419+ (slot.prompt.n_tokens() == slot.task->n_score_prompt - 1 ||
420+ + (slot.task->n_stable_prompt > 0 &&
421+ + slot.prompt.n_tokens() == slot.task->n_stable_prompt &&
422+ + slot.prompt.n_tokens() < slot.task->n_score_prompt - 1) ||
404423+ (slot.prompt.n_tokens() == slot.score_divergence &&
405424+ slot.prompt.n_tokens() < slot.task->n_score_prompt - 1))) {
406425+ bool have_ckpt = false;
@@ -415,7 +434,7 @@ index 98d0cca..ac9bcac 100644
415434 // process the last few tokens of the prompt separately in order to allow for a checkpoint to be created.
416435 // create checkpoints that many tokens before the end of the prompt:
417436 // - 4 + n_ubatch
418- @@ -3477 ,6 +3776,13 @@ private:
437+ @@ -3513 ,6 +3824,15 @@ private:
419438 const bool is_user_start = spans.is_user_start(n_tokens_start);
420439 const bool is_last_user_message = n_tokens_start == last_user_pos;
421440
@@ -424,12 +443,14 @@ index 98d0cca..ac9bcac 100644
424443+ // and every candidate / next scoring call would re-process the prompt
425444+ const bool is_score_boundary = slot.task->type == SERVER_TASK_TYPE_SCORE &&
426445+ (n_tokens_start == slot.task->n_score_prompt - 1 ||
446+ + (slot.task->n_stable_prompt > 0 &&
447+ + n_tokens_start == slot.task->n_stable_prompt) ||
427448+ n_tokens_start == slot.score_divergence);
428449+
429450 // entire prompt has been processed
430451 if (slot.prompt.n_tokens() == slot.task->n_tokens()) {
431452 slot.state = SLOT_STATE_DONE_PROMPT;
432- @@ -3492 ,8 +3798 ,8 @@ private:
453+ @@ -3528 ,8 +3848 ,8 @@ private:
433454 slot.init_sampler();
434455 } else {
435456 // skip ordinary mid-prompt checkpoints, unless the batch starts a user
@@ -440,18 +461,20 @@ index 98d0cca..ac9bcac 100644
440461 do_checkpoint = false;
441462 }
442463 }
443- @@ -3510,8 +3816,8 @@ private:
464+ @@ -3546,10 +3866,10 @@ private:
444465 // do not checkpoint after mtmd chunks
445466 do_checkpoint = do_checkpoint && !has_mtmd;
446467
447468- // no need to create checkpoints that are too close together, unless it's the last user message
448- - do_checkpoint = do_checkpoint && (slot.prompt.checkpoints.empty() || is_last_user_message || n_tokens_start > slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step);
449469+ // no need to create checkpoints that are too close together, unless it's the last user message or the score boundary
450- + do_checkpoint = do_checkpoint && (slot.prompt.checkpoints.empty() || is_last_user_message || is_score_boundary || n_tokens_start > slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step);
470+ do_checkpoint = do_checkpoint && (
471+ slot.prompt.checkpoints.empty() ||
472+ - is_last_user_message || near_prompt_end ||
473+ + is_last_user_message || near_prompt_end || is_score_boundary ||
474+ n_tokens_start > slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step);
451475 SLT_DBG(slot, "main/do_checkpoint = %s, pos_min = %d, pos_max = %d\n", do_checkpoint ? "yes" : "no", pos_min, pos_max);
452476
453- // note: we create the checkpoint before calling llama_decode(), so the current batch is not
454- @@ -3683,6 +3989,13 @@ private:
477+ @@ -3703,6 +4023,13 @@ private:
455478 }
456479 }
457480
@@ -465,7 +488,7 @@ index 98d0cca..ac9bcac 100644
465488 if (!is_inside_view(slot.i_batch)) {
466489 // the required token not in this sub-batch, skip
467490 return;
468- @@ -3704 ,6 +4017 ,25 @@ private:
491+ @@ -3724 ,6 +4051 ,25 @@ private:
469492 return;
470493 }
471494
@@ -492,7 +515,7 @@ index 98d0cca..ac9bcac 100644
492515
493516 // prompt evaluated for next-token prediction
494517diff --git a/tools/server/server-task.h b/tools/server/server-task.h
495- index dc6b2da..935ead1 100644
518+ index c3eea2e..fb3c178 100644
496519--- a/tools/server/server-task.h
497520+++ b/tools/server/server-task.h
498521@@ -13,10 +13,25 @@
@@ -521,7 +544,7 @@ index dc6b2da..935ead1 100644
521544 SERVER_TASK_TYPE_INFILL,
522545 SERVER_TASK_TYPE_CANCEL,
523546 SERVER_TASK_TYPE_CONTROL,
524- @@ -153,6 +168,13 @@ struct server_task {
547+ @@ -153,6 +168,18 @@ struct server_task {
525548 task_params params;
526549 server_tokens tokens;
527550
@@ -531,19 +554,24 @@ index dc6b2da..935ead1 100644
531554+ // tokens beyond the shared prefix ride a forked sequence.
532555+ int32_t n_score_prompt = 0;
533556+ std::vector<llama_tokens> score_suffixes;
557+ + // token index where the caller-declared stable prompt prefix ends
558+ + // (0 = no hint): the option-list system prompt that repeats across
559+ + // scoring calls. A context checkpoint is forced there so models that
560+ + // cannot rewind state re-process only the per-call tail next time.
561+ + int32_t n_stable_prompt = 0;
534562+
535563 // only used by CLI, this allow tokenizing CLI inputs on server side
536564 // we need this because mtmd_context and vocab are not accessible outside of server_context
537565 bool cli = false;
538- @@ -197,6 +219 ,7 @@ struct server_task {
566+ @@ -197,6 +224 ,7 @@ struct server_task {
539567 switch (type) {
540568 case SERVER_TASK_TYPE_COMPLETION:
541569 case SERVER_TASK_TYPE_INFILL:
542570+ case SERVER_TASK_TYPE_SCORE:
543571 return true;
544572 default:
545573 return false;
546- @@ -494,6 +517 ,25 @@ struct server_task_result_rerank : server_task_result {
574+ @@ -494,6 +522 ,25 @@ struct server_task_result_rerank : server_task_result {
547575 virtual json to_json() override;
548576 };
549577
0 commit comments